> 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 的自定义编程语言，旨在让编写多人游戏像创建单人游戏一样简单。

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

{% hint style="info" %}
你无需编写 RPC、SyncVars 或自定义复制。我们会为你处理这些！
{% 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 之后每帧调用。
}

ao_draw :: proc(dt: float) {
    // 仅用于表现效果。交互式 UI 应放在 ao_update/ao_late_update 中。
}

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

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

    ao_update :: method(dt: float) {
    }

    ao_late_update :: method(dt: float) {
    }

    ao_draw :: method(dt: float) {
        // 仅用于表现效果。绝不会有交互式 UI。
    }

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

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

```go
Player :: class : Player_Base {
    ao_start :: method() {
        log_info("你好 %", {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
// 一般形式
<名称>: <类型> = <表达式>;
```

二者之中 `<类型>` 或 `<表达式>` 可以省略：

```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`
* `any`

### 数字字面量

小数字面量可以在小数点前包含或省略 0。例如， `.52` 和 `-.52` 等价于 `0.52` 和 `-0.52`:

```go
opacity := .75;
offset := v2{-.52, 1.22};
```

### 向量类型

`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";
```

同名的派生方法会遮蔽继承的方法。调用不是虚调用：方法查找使用接收者的静态类型，因此通过基类型值进行调用时，绝不会选择派生类型中同名的方法。

派生类会暴露一个只读的 `base` 字段，它以与其直接父类型相同的方式查看同一个对象。它允许方法查找显式地从父类型开始：

```go
Animal :: class {
    speak :: method() {
        log_info("animal");
    }
}

Dog :: class : Animal {
    speak :: method() {
        base.speak();
        log_info("dog");
    }
}

dog := new(Dog);
dog.speak();        // Dog.speak
dog.base.speak();   // Animal.speak

animal: Animal = dog;
animal.speak();     // Animal.speak，不是 Dog.speak
```

`base` 不是保留字。名为 `base` 的局部声明会正常遮蔽继承字段。

## 过程和方法

### 过程（`proc`)

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

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

返回多个值的过程必须将返回类型包在括号中：

```go
get_thing :: proc() -> (Thing, bool) {
    return g_thing, true;
}

thing, ok := get_thing();
```

使用与 `=` 相同的逗号分隔形式，将结果赋给现有目标。一次赋值可以消耗一个多返回表达式，或为每个目标提供一个表达式：

```go
thing: Thing;
ok := false;
thing, ok = get_thing();
a, b = b, a;
```

目标数量必须与值的数量一致。右侧表达式会在任何目标被解析或写入之前，从左到右求值。这使得交换是安全的，并且 `_` 可以忽略一个值：

```go
a, b = make_pair(b, a);
_, ok = get_thing();
```

逗号分隔的右侧表达式仅由赋值语句支持；它们不会创建可在其他表达式中使用的元组值。

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

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

每个过程都需要一个唯一标识，以便热加载可以匹配新旧代码。命名声明（`add :: proc(...)`）会自动获得这一点。一个 `:=`/`=` proc 值保持匿名——每个作用域有一个匿名 proc 没问题，但如果一个作用域中有多个，请用 `proc "name" (...)` 为每个过程指定一个唯一的链接名，否则编译器会报告链接 ID 冲突。

### 方法（`method`)

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

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

    bark :: method() {
        log_info("% 说汪！", {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 = {10, 20};
numbers.append(30);

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:` 用于默认分支。case 支持 **多个值** （逗号分隔）以及 **范围**。不支持 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};
    }
}

// 范围 case
get_difficulty :: proc(level: int) -> string {
    switch level {
        case 1..5:          return "简单";
        case 6..<11:        return "中等";
        case 11..20, 25:    return "困难";
        default:            return "未知";
    }
}
```

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

### 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;
```

过程值不能转换为另一个过程签名，也不能与非过程值之间互相转换。若要有意进行低级签名重解释，请使用一个 unsafe 声明： `#unsafe retyped := (&callback).(*Other_Callback).*`.

类转换必须保持在同一继承层次内。向上转型不会产生运行时工作；向下转型会验证具体对象类型，并在不匹配时抛出异常。空类值在向下转型时仍保持为空。

## 按引用传递： `ref` （首选）

当你需要修改参数时， **优先使用 `ref`** 而不是原始指针。使用 `&` 获取原始地址需要 `#unsafe` 上下文。单个未加括号的 unsafe 声明会保留在外层作用域中。

```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);
```

## 闭包和回调

过程字面量可以捕获外围参数和局部变量：

```go
invoke_twice :: proc(callback: proc()) {
    callback();
    callback();
}

count_twice :: proc(initial: int) -> int {
    value := initial;
    invoke_twice(proc() {
        value += 1;
    });
    return value;
}
```

带捕获的过程字面量是有作用域的回调表达式。它们只能立即调用，或直接传递给接受捕获的过程参数。该参数可以调用该回调，或直接将其转发给另一个接受捕获的参数。带捕获的过程不能赋值给变量、返回，或放入字段、全局变量、集合或其他存储中。

未标注的过程参数可能接收到带捕获的过程，因此它也具有相同的调用或转发限制。当过程需要存储、返回或以其他方式操作它时，请将该参数标记为 `#no_captures` 。此时调用者只能传入不带捕获的过程；请将持久状态保存在显式 userdata 中：

```go
Callback_State :: class {
    value: int;
}

Callback :: proc(userdata: Object);
saved: Callback;
saved_userdata: Object;

install :: proc(userdata: Object, #no_captures callback: Callback) {
    saved_userdata = userdata;
    saved = callback;
}

increment :: proc(userdata: Object) {
    state := userdata.(Callback_State);
    state.value += 1;
}

state := new(Callback_State);
install(state, increment);
```

该注解是接收过程契约的一部分，也可以出现在过程类型中，例如 `Retainer :: proc(userdata: Object, #no_captures callback: Callback)`。带捕获的过程和普通过程仍然具有相同的双字过程/环境表示；普通过程的环境为零。如果通过低级重解释将带捕获的过程传递给一个 `#no_captures` 参数。

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

`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}
```

`#type_info(T)` 返回某个类型的反射数据。使用其 `id` 字段在记录或格式化该类型名称时：

```go
ti := #type_info(Thing);
log_info("type: %", {ti.id});
```

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

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