> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/all-out-docs/docs-zh/jiao-ben-bian-xie/syntax.md).

# CSL 入门

CSL 是 All Out 的自定义脚本语言。它是静态类型的，最像 Go/Odin —— 只不过 **游戏状态会自动从服务器同步到客户端**.

{% hint style="info" %}
你不需要编写 RPC、SyncVar 或自定义复制。我们会帮你处理这些！
{% endhint %}

## 你的第一个脚本（`main.csl`)

当你创建新项目时，All Out 会生成一个 `main.csl` 在你的项目的 `scripts/` 文件夹中。

```go
import "core:ao"

// ============================================================================
// 全局生命周期
// ============================================================================

ao_before_scene_load :: proc() {
    // 注册物品定义、货币等。
    // 在空场景存在后、场景内容加载前运行。
}

ao_start :: proc() {
    // 场景开始时调用一次。
}

ao_update :: proc(dt: float) {
    // 每帧调用。
}

ao_late_update :: proc(dt: float) {
    // 在 ao_update 之后每帧调用。
}

// ============================================================================
// 玩家生命周期
// ============================================================================

Player :: class : Player_Base {
    ao_start :: method() {
    }

    ao_update :: method(dt: float) {
    }

    ao_late_update :: method(dt: float) {
    }

    ao_end :: method() {
    }
}
```

例如，在每个玩家加入时记录一条日志：

```go
Player :: class : Player_Base {
    ao_start :: method() {
        log_info("hello %", {this.get_username()});
    }
}
```

如果你想更深入了解这些函数何时运行，请参阅 [游戏/帧生命周期](/all-out-docs/docs-zh/jiao-ben-bian-xie/game-frame-lifecycle.md).

## 导入

你的 `main.csl` 应导入 `core:ao` 以及你创建的任何文件夹（例如 `ui/`, `abilities/`，等等）。

```go
import "core:ao"
import "ui"
```

导入指向文件夹，而不是单个文件。文件夹导入会包含该文件夹中的 `.csl` 文件。该文件夹内部的导入会按相对路径解析：

```go
// main.csl
import "core:ao"
import "abilities"

// abilities/projectiles.csl
import "helpers"
```

{% hint style="warning" %}
在大多数项目中， **在……中导入 `main.csl` 仅**。不要把导入散落在很多文件中——否则最终会遇到令人困惑的顺序/可见性问题。
{% endhint %}

## 声明（变量和常量）

声明将一个名称绑定到一个值。

### 变量

```go
// 通用形式
<name>: <type> = <expression>;
```

可以 `<type>` 或 `<expression>` 省略：

```go
// 显式类型
my_value: int = 42;

// 类型推断
my_value := 42; // 推断为 int

// 零初始化（与 my_value := 0; 相同）
my_value: int;
```

### 常量

常量使用 `::` 并且必须是编译期常量。它们可以是标量、字符串、类型、过程值、数组或复合字面量：

```go
MAX_PLAYERS :: 12;

Spawn_Desc :: struct {
    name: string;
    pos: v2;
}

DEFAULT_SPAWNS: []Spawn_Desc : {
    {"Blue", {-4, 0}},
    {"Red", {4, 0}},
};
```

这是无效的（因为 `a` 不是编译期常量）：

```go
a := 123;
b :: a; // 编译错误
```

全局变量初始化值也必须是编译期常量。请使用 `ao_before_scene_load` 或 `ao_start` 进行运行时初始化。

### 全局变量

全局变量使用与局部变量相同的声明语法。它们可以保持零初始化，或使用编译期常量进行初始化，包括结构体、数组、过程值以及 `typeid` 值：

```go
score_total: int;
spawn_names: []string = {"Blue", "Red"};
default_type: typeid = int;

start_round :: proc() {
}

on_match_start: proc() = start_round;
```

全局变量是可变的，并会在脚本实例的整个生命周期内持续存在。避免将它们用于每个玩家的游戏状态。

## 类型

### 原始类型

* 有符号整数： `s8`, `s16`, `s32`, `s64`
* 无符号整数： `u8`, `u16`, `u32`, `u64`
* 布尔值： `bool`
* 浮点数： `f32`, `f64`
* 别名：
  * `int` == `s64`
  * `uint` == `u64`
  * `float` == `f32`
* 向量： `v2`, `v3`, `v4`
* `string`
* `typeid`
* `任意`

### 向量类型

`v2` 具有 `.x`, `.y`; `v3` 添加 `.z`; `v4` 添加 `.w` ——全部为 float 字段：

```go
pos: v2 = {10, 20};        // x=10, y=20
color: v4 = {1, 0, 0, 1};  // 带 alpha=1 的红色
offset := v3{1, 4, 9};     // 类型推断
```

## 结构体和类

结构体是 **值类型** （赋值时会复制）。类是 **引用类型** （你用 `new`).

### 结构体（值类型）

```go
Food_Definition :: struct {
    name: string;
    food_value: int;
}

food: Food_Definition;
food.name = "Apple";
food.food_value = 10;
```

### 类（引用类型）

```go
Foo :: class {
    value: int = 10;
    position: v2 = {12, 34};
}

foo := new(Foo);
```

类字段可以有默认值。派生类可以覆盖继承的默认值，而无需重新声明该字段：

```go
Enemy :: class {
    health: int = 100;
    speed: float = 3.0;
}

Boss :: class : Enemy {
    health = 500;
    speed = 1.5;
}
```

### 继承

结构体/类可以从其他结构体/类继承：

```go
Animal :: class {
    name: string;
    age: int;
}

Dog :: class : Animal {
    breed: string;
}

dog := new(Dog);
dog.name = "Buddy";
dog.age = 5;
dog.breed = "Labrador";
```

## 过程和方法

### 过程（`proc`)

```go
add :: proc(a: int, b: int) -> int {
    return a + b;
}

result := add(2, 4); // 6
```

过程是普通值，可以像任何其他值一样被赋值/存储：

```go
op := proc(a: int, b: int) -> int { return a + b; };
op = proc(a: int, b: int) -> int { return a * b; };
```

### 方法（`method`)

使用 `method()` 在结构体/类内部。方法具有一个隐式的 `this` 引用参数。

```go
Dog :: class {
    name: string;

    bark :: method() {
        log_info("% says bark!", {name});
    }
}

dog := new(Dog);
dog.name = "Buddy";
dog.bark();
```

### 字段访问与方法调用

使用 `.` 对字段和方法都适用：

```go
hp := player.health;        // 字段访问
player.respawn();          // 方法调用
entity.set_local_scale({2, 2});
```

{% hint style="info" %}
如果某个过程的第一个参数匹配接收者类型，那么它就可以像“方法”一样被调用。真正的方法以及类型上的过程值字段会优先于 CSL 回退到匹配的自由过程。
{% endhint %}

## 数组

CSL 有几种你会经常使用的“类数组”类型：

* **固定数组**: `[4]int`
* **切片 / 托管数组**: `[]T` （通常用作数组的“只读视图”）
* **动态数组**: `[..]T` （可调整大小的列表）
* **非托管数组**: `[^]T` （用于内置 API 签名，例如 `format_string`, `log_info`，等等——将值作为 `{a, b, c}`)

动态数组暴露 `.data`, `.count`，以及 `.capacity`，并对操作使用方法调用语法：

```go
numbers: [..]int;
numbers.append(10);
numbers.append(20);

log_info("count: %", {numbers.count});
log_info("first: %", {numbers[0]});
```

完整指南（包括移除模式），请参阅 [数组和集合](/all-out-docs/docs-zh/jiao-ben-bian-xie/arrays-and-collections.md).

## 控制流

### 如果 / 否则

```go
if hp <= 0 {
    die();
} else if hp < 25 {
    Notifier.notify(player, "生命值过低！");
} else {
    // 一切正常
}
```

### Switch

使用 `default:` 用于默认分支。分支支持 **多个值** （以逗号分隔）以及 **区间**。没有 C 风格的贯穿。

```go
Item_Tier :: enum {
    COMMON;
    UNCOMMON;
    RARE;
    EPIC;
    LEGENDARY;
}

get_tier_color :: proc(tier: Item_Tier) -> v4 {
    switch tier {
        case .COMMON:              return {0.7, 0.7, 0.7, 1.0};
        case .UNCOMMON:            return {0.3, 0.8, 0.3, 1.0};
        case .RARE, .EPIC:         return {0.3, 0.5, 1.0, 1.0};
        case .LEGENDARY:           return {1.0, 0.8, 0.2, 1.0};
        default:                   return {1, 1, 1, 1};
    }
}

// 区间分支
get_difficulty :: proc(level: int) -> string {
    switch level {
        case 1..5:          return "Easy";
        case 6..<11:        return "Medium";
        case 11..20, 25:    return "Hard";
        default:            return "Unknown";
    }
}
```

`..` 包括两个端点。 `..<` 不包含上限端点。

### While / for

```go
while condition {
    // ...
}

// 闭区间范围：0..9 包括 9
for i: 0..9 {
    log_info("i=%", {i});
}

// 左闭右开范围：0..<count 不包含 count
for i: 0..<items.count {
    log_info("item %", {items[i]});
}

// 逆向迭代
for i: 0..<items.count #reverse {
    log_info("reverse item %", {items[i]});
}

// 遍历数组/切片元素
for item: my_items {
    // ...
}

// 带索引变量
for item, i: my_items {
    log_info("item % at index %", {item, i});
}

// 遍历自定义迭代器（实体/组件中常见）
for enemy: component_iterator(Enemy) {
    enemy.update_ai();
}
```

基于自定义迭代器的 `for` 循环需要一个 `next :: method() -> bool` 以及一个 `current` 字段。

## 类型转换

使用 `expr.(T)` 或 `cast(T)expr` 进行类型转换：

```go
a := 123.4;
b := a.(int);   // 123
c := cast(float)b; // 123.0
```

当目标类型已知时，你可以让 CSL 推断它：

```go
i: int = a.();
j: int = cast a;
```

## 按引用传递： `ref` （推荐）

当你需要修改参数时， **优先使用 `ref`** 而不是原始指针。

```go
update_position :: proc(pos: ref v2, velocity: v2, dt: float) {
    pos.x += velocity.x * dt;
    pos.y += velocity.y * dt;
}

my_pos := v2{0, 0};
update_position(ref my_pos, {10, 5}, 0.16);
```

## 回调：函数指针 + userdata（无闭包）

CSL 没有闭包。内联 `proc(...) { ... }` 无法捕获周围变量。

要携带上下文，请将回调与一个 `userdata: Object` 字段：

```go
Player :: class : Player_Base {
    on_death_userdata: Object;
    on_death: proc(player: Player, userdata: Object);

    die :: method() {
        if on_death != null {
            on_death(this, on_death_userdata);
        }
    }
}

Death_Tracker :: class : Component {
    count: int;

    ao_start :: method() {
        player := entity.get_component(Player);
        player.on_death_userdata = this;
        player.on_death = proc(player: Player, userdata: Object) {
            tracker := userdata.(Death_Tracker);
            tracker.count += 1;
        };
    }
}
```

## 类型信息（将类型作为值）

`typeid` 值可以传递给多态过程：

```go
default_of :: proc($T: typeid) -> T {
    t: T;
    return t;
}

a := default_of(int);      // 0
b := default_of(string);   // ""
c := default_of([4]int);   // {0, 0, 0, 0}
```

## 最佳实践（All Out 中的 CSL）

* **避免全局游戏状态。** 多个玩家连接时——将每个玩家的状态存储在 `Player` 其上，而不是全局变量中。
* **所有玩家 UI 都应绘制自 `Player.ao_late_update`.** 将其包装在 `is_local_or_server()`.
* **使用 `is_local()` 中，仅用于玩家特定的视觉覆盖。** 不要在其中更改游戏玩法或 UI 状态。
* **默认以移动端优先。** 除非你的游戏明确面向 PC，否则不要依赖键盘/鼠标输入。
* **如果你不确定语法或 API**，请打开对应的 `.csl_engine` 文件，位于 `api_references/` 在你的项目中。
