Move the language server into devtools, wire the VS Code extension, and show anonymous closures (eve_update = function) by frame prototype instead of a shifted CallGraph zip. Co-authored-by: Cursor <cursoragent@cursor.com>
30 KiB
EveScript 完整教程
EveScript 是 EVEngine 的游戏脚本语言。它以 Squirrel 为基础,保留 Squirrel 的对象模型、闭包、
协程和动态类型能力,同时增加脚本模块、渐进类型、空值运算、命名参数、Inspector 元数据、热重载
持久变量、单位字面量、模式匹配以及 async / await。
所有游戏脚本仍使用 .nut 扩展名。项目不需要语言标记,也没有“启用 EveScript”的开关:
config.nut、main.nut、导入的脚本、REPL、调试器和 MCP 执行的代码都使用同一套语法。
没有使用扩展语法的标准 Squirrel 文件可以直接运行。
本教程面向使用 SDK 制作游戏的开发者。模块 API(图形、物理、UI、ECS 等)请配合 模块使用手册查阅。
1. 第一个 EveScript 游戏
一个游戏至少包含 config.nut 和 main.nut:
my-game/
├── config.nut
├── main.nut
└── scripts/
└── movement.nut
config.nut 是普通 Squirrel 配置脚本。模块需求是整个项目的属性,因此在这里统一声明:
config <- {
width = 960
height = 540
title = "EveScript Tutorial"
hotReload = true
modules = ["gfx", "keyboard", "timer"]
optionalModules = ["audio"]
}
modules中的模块必须存在,否则引擎会在执行main.nut前停止并报告缺失项;- 写出
modules或optionalModules后,引擎只构造名单里的槽位,再加上启动循环需要的win/gfx/timer/platform_event/fs/hot。未列出的模块不会实例化,has_module()为 false;需要时可用ensure_module("audio")再加载; - 原生类的方法绑定也是按需的:第一次读取
eve.Graphics/eve.Window等名字时才 注册该类的全部方法(以及它在同一次expose里挂上的嵌套类型,如WindowSettings)。 Squirrel 的in不会触发这次绑定,因此不要用"Graphics" in eve判断模块是否在构建里, 请用eve.moduleList/ensure_module(); optionalModules在 SDK 里有则加载,没有则跳过,使用前仍要调用has_module();- 名称是脚本中的 root slot,例如图形模块写
gfx,不是 CMake 名称graphics; - 这两个字段必须是字符串数组字面量,语言服务器和打包器不会执行任意配置代码来推断模块。
- 不写这两个字段的旧项目保持原来的行为:构造当前构建里的全部模块。
scripts/movement.nut:
export const PLAYER_SPEED = 180.0
export function wrap_x(x: float, width: float) -> float {
return x > width ? -48.0 : x
}
main.nut:
import { PLAYER_SPEED, wrap_x } from "./scripts/movement.nut"
persist playerX: float = 100.0
eve_init <- function() {
gfx.setBackgroundColor(0.08, 0.10, 0.16, 1.0)
}
eve_update <- function(dt: float) {
playerX = wrap_x(playerX + PLAYER_SPEED * dt, config.width)
}
eve_render <- function() {
gfx.clear()
gfx.drawSolidRect(playerX, 220.0, 48.0, 48.0, 0.3, 0.75, 1.0, 1.0)
}
运行:
eve run my-game
保存脚本后,persist playerX 会在热重载时保留当前位置;函数和常量则由新代码替换。
从旧项目批量迁移 root 持久状态时,可先预览再写入:
python scripts/migrate_evescript.py --check
python scripts/migrate_evescript.py --write
迁移器只改写可证明等价的 root-table 判断和 persist(name, initializer) 样板;普通 Squirrel、
动态 root-table 操作和兼容性 dofile() 不会被猜测式重写。
2. 仍然需要掌握的 Squirrel 基础
EveScript 是 Squirrel 的兼容扩展,不是另一套对象模型。以下写法仍是日常代码的主体。
2.1 源文件、注释和语句
源码采用 UTF-8。标识符、字符串、数字和运算符沿用 Squirrel;语言关键字区分大小写。支持行注释 与块注释:
// 行注释
/*
* 块注释
*/
分号与标准 Squirrel 一样可以作为语句终止符。项目代码建议依靠换行和大括号,不混用多种风格; 同一行放多条语句时必须用分号分隔。
常用运算符仍包括算术 + - * / %、比较 == != < <= > >=、逻辑 && || !、三元表达式
condition ? yes : no、成员访问 . / [] 和赋值运算。EveScript 新增的 ?.、??、??=
见第 5 节。
import、export、async、await、persist 和 match 在对应语法位置具有语言含义;from
和 as 只在 import 子句中按关键字解析,因此旧脚本仍可把它们用作普通标识符。类型名称和单位
名称也主要在类型/字面量上下文中解析。
2.2 值、局部变量和 root slot
local lives = 3
local speed = 120.0
local title = "Arena"
local enabled = true
local target = null
local spawn = { x = 20.0, y = 40.0 }
local enemies = ["slime", "bat"]
local 创建词法作用域局部变量。<- 在表中创建新 slot,= 修改已有 slot:
gameState <- { score = 0 } // 在 root table 创建 gameState
gameState.score = 10 // 修改已有字段
优先使用 local、模块内私有值和显式 export。只有引擎生命周期回调等约定入口才需要放在 root
table。不要用 const 表达运行时只读状态:Squirrel 的 const 是编译期常量声明,适合固定数值和
导出的常量;普通可变游戏状态使用变量。
2.3 函数、闭包和默认参数
function clamp_health(value, maximum = 100) {
return value < 0 ? 0 : (value > maximum ? maximum : value)
}
local multiplier = 2
local scale = function(value) {
return value * multiplier // 闭包捕获外层变量
}
2.4 表、数组和遍历
local inventory = {
potion = 2
key = 1
}
inventory.potion += 1
inventory["coin"] <- 20
foreach (name, count in inventory)
print(name + ": " + count + "\n")
local actors = ["hero", "merchant"]
actors.append("guard")
foreach (index, actor in actors)
print(index + ": " + actor + "\n")
2.5 类、继承和 this
class Actor {
name = ""
health = 100
constructor(name) {
this.name = name
}
function damage(amount) {
health -= amount
}
}
class Player extends Actor {
coins = 0
}
local player = Player("Ada")
player.damage(10)
类字段的默认值属于 Squirrel 类语义。引擎原生对象也通过相同的成员调用形式使用,但可调用的接口 以模块手册和绑定契约为准,并非 C++ 类的所有 public 方法。
2.6 控制流、异常和 generator
if (player.health <= 0) {
print("game over\n")
} else {
player.damage(1)
}
for (local i = 0; i < 3; ++i) {
// ...
}
try {
load_save()
} catch (error) {
print("load failed: " + error + "\n")
}
function dialogue_lines() {
yield "Hello"
yield "Welcome to EVEngine"
}
yield 和 Squirrel generator 仍然保留,适合由调用者主动推进的序列。需要等待 Promise、计时器或
主线程事件时,优先使用 EveScript 的 async function。
3. 脚本模块:import 与 export
新代码使用模块,而不是用 dofile() 把多个文件写入同一个全局表。
3.1 导出
模块 root scope 可以导出常量、函数和类:
local nextId = 1 // 模块私有
export const DEFAULT_HEALTH = 100
export class ActorId {
value = 0
constructor(value) { this.value = value }
}
export function allocate_actor_id() {
return ActorId(nextId++)
}
export 只能用于模块顶层的 const、function 或 class 声明。局部变量不会泄漏给导入方。
3.2 导入
import { ActorId, allocate_actor_id } from "game:/scripts/ids.nut"
import { allocate_actor_id as allocate_id } from "./ids.nut"
import * as ids from "./ids.nut"
local first = allocate_actor_id()
local second = allocate_id()
local third = ids.allocate_actor_id()
模块说明符必须是字符串字面量:
game:/...指向游戏包中的脚本;engine:/...指向 SDK 标准脚本库;plugin:<id>/...指向插件公开的脚本模块;./...和../...相对于当前模块解析,但不能越出所属根目录。
同一规范 URI 在一次 Runtime generation 中只实例化一次。多处导入不会重复执行顶层代码。 循环导入会在编译依赖图时被拒绝;把共享类型或常量提取到第三个无环模块即可。
模块 URI 与物理文件格式无关,所以同一条 import 在开发目录、.eve ZIP、Android APK、iOS
bundle 和 Web 预加载文件系统中保持一致。打包器会扫描依赖图;动态拼接路径不是合法 import。
3.3 与 dofile 的区别
dofile(path) 仍为旧 Squirrel 脚本保留,但它表达的是“读取并执行文件”,没有私有命名空间、
显式导出、单次实例化缓存或静态依赖图。新项目应使用 import;只有迁移旧脚本或确实需要传统
共享 root-table 副作用时才使用 dofile。
4. 渐进类型
类型标注是可选的。未标注代码继续使用 Squirrel 动态语义;标注不会把值包装成新的运行时对象。
local speed: float = 180.0
local name: string = "Ada"
local targets: Array<string> = []
local scores: Table<string, int> = {}
function damage(amount: int, critical: bool) -> int {
return critical ? amount * 2 : amount
}
基础类型为 null、bool、int、float、string 和 dynamic。容器使用 Array<T>、
Table<K, V>;引擎绑定类使用文档中的脚本类名。
类型是“渐进式静态检查”,不是完整的运行时类型系统:
- 字面量与已知函数调用会在编译期检查;
- 原生方法依赖 SDK 生成的 Binding Contract 检查参数名、类型和单位;
dynamic或无法推断的旧代码仍可能在运行时失败;- 类型标注在生成字节码前擦除,普通局部值仍是原始 Squirrel 值。
例如:
local count: int = "many" // 编译错误
function set_count(value: int) {}
set_count(1.5) // 编译错误
5. Nullable 与空值运算
T? 表示值可以为 null:
local target: Actor? = find_target()
常用运算符:
target?.damage(10) // target 为 null 时不调用,结果为 null
local label = target?.name ?? "None" // 左侧为 null 才计算右侧
target ??= find_fallback_target() // 仅在 target 为 null 时赋值
接收者和左操作数只求值一次,?? 具有短路语义:
local target = cached ?? expensive_lookup()
当 cached 非空时,expensive_lookup() 不会执行。成员 slot 的 ??= 也受支持:
local state = { checkpoint = null }
state.checkpoint ??= make_checkpoint()
非 nullable 类型不能直接赋 null。来自旧代码的动态值仍应在原生资源、场景查询等边界做好错误
处理。
6. 字符串取值类型
EVEngine 的模式、类型和策略名称在 C++ 边界大量使用字符串。EveScript 用字符串字面量联合约束 合法值,而不强制把它们改成整数枚举:
local movement: "idle" | "run" | "jump" = "idle"
function set_surface(mode: "opaque" | "mask" | "blend") {
material.setSurfaceMode(mode)
}
好处是补全、拼写检查、match 穷尽检查和 Inspector 下拉选项都能共享同一组值;传入 C++、
JSON 或日志时仍是普通字符串。
set_surface("transparent") // 已知字面量不在集合中,编译错误
7. 命名参数
调用时用 name: value 指明参数:
local world = physics.newWorld(
gravityY: 980.0,
sleep: true,
gravityX: 0.0
)
编译器根据脚本函数声明或原生 Binding Contract 还原成位置调用,因此不会创建参数表,也没有额外 逐帧分配。参数可以重排;未知、重复或缺失参数是编译错误。
function blend(from: float, to: float, weight: float) -> float {
return from + (to - from) * weight
}
local value = blend(weight: 0.25, from: 0.0, to: 1.0)
参数表达式仍按源码出现顺序求值,而不是按声明顺序求值。不要依赖重排后的隐藏副作用:
compose(third: log_and_return(3), first: log_and_return(1), second: log_and_return(2))
// 调用日志顺序仍是 3、1、2;传给函数的槽位顺序则是 first、second、third。
无法取得可靠 Binding Contract 的动态原生函数不能使用命名参数,改用位置参数。
SDK 构建会从当前 profile 中实际参与编译的 SimpleSquirrel addFunc 声明生成契约。无法解析真实
参数名的绑定会直接使生成步骤失败,不会退化成 arg0、arg1。生成结果在 ScriptCompiler
创建时注册到 SimpleSquirrel 的 named-argument resolver,因此编译检查、补全、Hover 与运行时
使用的是同一份契约。C++ 默认实参不会自动成为脚本默认值;不同类上同名且签名有歧义的方法应
继续使用位置参数。契约类型、字符串选项和单位检查只约束参数表达式边界上的直接字面量;合法的
复合表达式(例如 ui.setText("status", "count " + (count + 1)))会按正常表达式语义编译,
不会把内部数值字面量误判成字符串参数。
8. Inspector 属性注解
注解是 Squirrel attribute 的短写,放在类字段之前:
class CharacterData {
@editor("slider", min: 0, max: 100, step: 1)
@unit("hp")
health: int = 100
@editor("combo")
job: "warrior" | "mage" | "rogue" = "warrior"
@editor("text")
displayName: string = "Ada"
}
它等价于已有的 Squirrel attribute 元数据,Inspector、反射和 MCP 读取的仍是同一份信息。字符串
取值类型配合 @editor("combo") 时会自动生成选项。
内置注解拼写错误会在编译期报告。插件注解必须先由插件注册,例如插件可能提供
@plugin_asset("texture");未注册的未知注解不能静默忽略。
9. 热重载持久变量:persist
普通变量在脚本重载后没有保留承诺。需要保留的模块/root 状态显式声明为 persist:
persist score: int = 0
persist session: Table<string, dynamic> = {}
初始化表达式只在该名称第一次出现时执行:
persist world = create_world() // 后续热重载不会再次调用 create_world()
规范:
persist只允许在文件 root scope;- 名称在 root table 中必须唯一;
- 未写
persist的变量就是普通临时脚本状态,不需要transient; - 优先保存普通数据;无法安全快照的原生句柄应由对应引擎模块的状态提供器管理;
- 修改持久数据结构时,要在
eve_reload()中做显式版本迁移或回退。
旧式写法仍兼容:
score <- persist("score", function() { return 0 })
新代码使用声明形式,避免名称字符串与变量名不一致。
10. 单位字面量
时间、角度和空间量可以直接携带单位:
local fade: seconds = 250ms // 0.25
local turn: radians = 90deg // π / 2
local tileSize: pixels = 32px // 32.0
local height: meters = 1.8m // 1.8
v1 支持的量纲和后缀:
| 量纲 | 目标类型 | 字面量后缀 | 说明 |
|---|---|---|---|
| 时间 | seconds、milliseconds |
ms |
250ms 转为 seconds 时是 0.25 |
| 角度 | radians、degrees |
deg |
90deg 转为 radians 时约为 1.5708 |
| 像素 | pixels |
px |
保持浮点标量 |
| 距离 | meters |
m |
保持浮点标量 |
原生 Binding Contract 声明目标单位后,调用处会自动进行确定的换算:
camera.setYaw(90deg) // 若参数契约为 radians,编译为弧度标量
asyncSleep(250ms) // asyncSleep 接收毫秒,数值为 250.0
没有目标类型或绑定契约时,字面量采用自身的基础单位数值;不同量纲不能混传。单位只参与编译期 检查与常量换算,运行时仍是普通 float。
11. match 模式匹配
v1 的 match 是语句,不是返回值的表达式:
function update_animation(mode: "idle" | "run" | "jump") {
match mode {
"idle" => animation.play("idle")
"run" => animation.play("run")
"jump" => animation.play("jump")
}
}
分支可以是单条语句或代码块:
match eventName {
"damage" => {
flash_red()
play_hit_sound()
}
"heal" => show_heal_effect()
else => print("unknown event: " + eventName + "\n")
}
规则:
- 字符串取值联合的所有选项都覆盖时,可以省略
else; - 未穷尽联合类型必须提供剩余分支,否则编译错误;
- 动态值无法静态证明穷尽,因此必须有
else; else只能出现一次且必须是最后一个分支;- 被匹配的表达式只求值一次。
需要产生值时,在各分支中赋给外部局部变量或直接 return;不要写成表达式赋值。
12. 异步编程:async function 与 await
EveScript 异步建立在 EVEngine 现有 Promise、计时器、事件队列和主线程 pump 上:
async function intro_sequence() -> string {
await asyncSleep(250ms)
ui.setText("message", "Ready")
local values = await Promise.all([
load_profile_async(),
load_inventory_async()
])
return values[0].name
}
intro_sequence().then(
function(name) { print("welcome " + name + "\n") },
function(error) { print("intro failed: " + error + "\n") }
)
语义:
- 调用
async function立即返回 Promise; await value会用Promise.resolve(value)统一普通值和 Promise;- Promise 完成后,continuation 在游戏线程的异步队列恢复;
try/catch可以捕获被拒绝 Promise 的原因;return value完成外层 Promise,未捕获异常会拒绝它;await只能出现在async function内;- 异步函数不会把 Squirrel VM 移到 worker 线程。
循环、局部变量、this 和异常状态会跨 await 保存:
class Counter {
offset = 4
async function compute(limit: int) -> int {
local total = this.offset
for (local i = 0; i < limit; ++i)
total += await Promise.resolve(i)
try {
await Promise.reject("expected")
} catch (error) {
total += 10
}
return total
}
}
引擎帧循环会自动调用 async_pump(),游戏代码通常不应手动 pump。脚本热重载会取消旧 generation
尚未恢复的 continuation,防止旧闭包在新代码和新状态上继续运行;需要长期可恢复的任务时,应把
进度保存为普通数据,并在重载后重新启动任务。
Promise 不等于后台线程。CPU 密集任务应使用 thread 模块提供的受支持 worker 操作,再通过事件、 Channel 或 Promise 回到主线程;worker 不得访问 Squirrel VM、窗口、GPU 或脚本对象。
当文件监听器发现已导入模块变化时,Runtime 会重编译该模块及反向依赖它的全部缓存 generation, 并在整组实例化成功后一次性提交。任一依赖编译或实例化失败都会恢复旧模块和旧依赖图;未进入 模块图的兼容性 root 脚本仍沿用软重载路径。
13. 与响应式事件配合
async / await 适合有明确开始、等待和结束的流程;事件或 Rx 适合持续数据流。不要把所有异步
逻辑都写成永久等待的任务:
- 一次加载、延迟、过场流程:
async function; - 多个监听者的离散游戏事件:event 模块;
- 输入、状态或事件的持续组合与过滤:rx 模块;
- 跨线程工作:thread + Channel/Event,最终回到主线程。
14. 生命周期、模块与推荐组织方式
生命周期回调仍使用引擎约定的 root slot:
eve_init <- function() {}
eve_update <- function(dt) {}
eve_render <- function() {}
eve_reload <- function() {}
eve_asset_reload <- function(path) {}
eve_quit <- function() {}
推荐把 main.nut 保持为编排层:导入系统、创建顶层持久状态、连接生命周期。具体玩法逻辑放进
各自模块:
scripts/
├── player.nut
├── combat.nut
├── ui/
│ └── hud.nut
└── world/
└── dungeon.nut
模块建议:
- export 少量稳定的类和函数,不导出可随意修改的内部表;
- 避免模块顶层产生不可重复的外部副作用,把初始化放进显式函数;
- 模块间保持有向无环依赖;共享定义放到更底层的小模块;
- 需要跨重载的数据用
persist,可重建缓存则保持普通变量; - 必需引擎模块写进
config.modules,可选功能同时使用optionalModules和has_module()。
可选模块示例:
if (has_module("audio")) {
// audio 是当前构建注入的全局 Audio 实例;Source 由它创建并播放。
audio.stopAll()
}
15. 错误处理和编码规范
推荐规范:
- 文件名使用小写
snake_case.nut,模块 URI 使用/; - 局部变量和函数使用
snake_case,类使用PascalCase,导出常量使用UPPER_SNAKE_CASE; - 默认使用
local,跨文件接口使用export,不要把普通状态随意写入 root table; - 公共函数、引擎边界和持久状态优先写类型;短小内部动态代码可以不标注;
- 字符串模式使用联合类型,不为方便而引入脚本整数枚举;
- 多个相邻数字或布尔参数优先使用命名参数;
- 时间、角度和空间参数优先携带单位;
- 对可恢复的资源错误使用
try/catch,不要吞掉编程错误; - 每帧热路径避免临时大数组、动态 import 和不必要的跨 C++/Squirrel 细粒度调用;
- 异步任务必须考虑失败、退出和热重载取消。
原生绑定遇到非法字符串选项、参数类型错误、空对象或资源不存在时通常会抛 Squirrel 异常。对可 恢复错误:
try {
load_level("levels/forest.json")
} catch (error) {
print("cannot load forest: " + error + "\n")
load_level("levels/fallback.json")
}
16. EveScript 与标准 Squirrel 的区别
| 能力 | 标准 Squirrel | EveScript |
|---|---|---|
| 文件 | .nut |
仍为 .nut,全项目默认启用 |
| 模块 | 通常用 dofile 和共享 root table |
import / export、私有环境、缓存和静态依赖图 |
| 类型 | 动态类型 | 可选渐进类型,编译后擦除 |
| 空值 | 手写 x == null |
T?、?.、??、??= |
| 有限选项 | 字符串或整数常量 | 字符串字面量联合,运行时仍是字符串 |
| 调用参数 | 位置参数 | 已知签名支持 name: value |
| 字段元数据 | </ ... /> attribute |
@editor(...)、@unit(...) 短写 |
| 热重载状态 | 手写 root 判断或 persist(name, fn) |
persist name = initializer 声明 |
| 单位 | 裸数字 | 250ms、90deg、32px、1.8m |
| 分支 | if / switch |
额外提供可穷尽检查的 match 语句 |
| 异步 | generator、thread、Promise 链 | 额外提供 Promise 状态机 async / await |
| VM 与对象模型 | Squirrel VM | 不变,继续使用同一 VM 和原生绑定 |
EveScript 没有增加以下概念:
- 没有文件级语言版本标记;
- 没有默认不可变变量,也没有
let/var分裂; - 没有要求所有变量写类型;
- 没有把普通变量标成
transient;未写persist就是普通变量; - 没有隐藏 ECS 的新 DSL,ECS 继续通过
eve.Component、eve.Entity、eve.System和模块 API; - 没有统一的 Signal 关键字,事件、Rx、Promise 和状态绑定各自承担适合的职责;
- 没有独立虚拟机或 TypeScript 式外部转译运行时。
17. 从现有 Squirrel 项目迁移
迁移可以逐文件进行,因为标准 Squirrel 是 EveScript 的兼容子集。
第一步:直接运行并建立基线
不要先机械重写。升级 SDK 后先运行现有游戏,修复真实的不兼容或绑定变化。原有类、闭包、
generator、table、array、attribute 和 dofile 都仍然可用。
第二步:声明项目模块
把所有脚本无条件使用的模块写入 config.modules;有降级路径的模块写入
config.optionalModules 并保留 has_module()。写出任一场后,未点名的模块不再在启动时构造。
第三步:用 import/export 替换文件级共享副作用
旧代码:
// player.nut
Player <- class {}
// main.nut
dofile("scripts/player.nut")
local player = Player()
新代码:
// scripts/player.nut
export class Player {}
// main.nut
import { Player } from "./scripts/player.nut"
local player = Player()
第四步:只给稳定边界补类型
先标注导出函数、持久状态、Inspector 字段和原生 API 边界,不必一次给所有局部变量加类型。
第五步:替换重复样板
- root-table 热重载判断改成
persist; - 判空链改成
?./??; - 多个同类型参数改成命名参数;
- 毫秒、角度和距离裸数改成单位字面量;
- Promise 链较深且有顺序流程时改成
async/await。
每一步都保持游戏可运行,并单独验证热重载和打包结果。
18. 工具、诊断和调试
桌面 SDK 提供 EveScript Language Server:
eve language-server --root /path/to/my-game
它通过标准输入输出使用 LSP 协议,供编辑器插件启动;不要在同一 stdio 通道输出额外日志。当前 能力包括:
- 语法和结构化诊断;
config.modules/optionalModules检查;- 已知脚本符号、EveScript 关键字、模块 slot 与 Binding Contract 补全(含
gfx.成员); - 原生方法签名和文档 Hover;
- 调用处 signature help;
- 文档符号大纲;
- 跨文件
import/export定义跳转; - 项目级引用查找;
- 导出符号和 import alias 的安全重命名;
- 按花括号 / 方括号缩进的文档格式化(及范围格式化);
- 多行
{ }、[ ]与/* */代码折叠; - 语义高亮(类 / 函数 / 方法 / 变量 / 模块 slot);
- 文档打开、增量
contentChanges和关闭同步。
进程内宿主(测试、VS Code 扩展)可以直接使用 eve::dev::LanguageServer 的 typed API,不必走 stdio。
VS Code / Cursor 安装仓库内 tools/vscode-eve-debug/ 后,打开 .nut 即会启动该语言服务。
运行时调试:
eve run --debug --dap-port=4711 my-game
EveScript 编译器为扩展语法生成 Source Map,断点、停止位置、调用栈和异常会映射回原始 .nut
源码,而不是显示内部状态机位置。打包前可运行:
eve zip my-game
打包器会验证脚本模块图,缺失模块、循环依赖或越界 URI 会在生成归档前失败,并在包内写入脚本 模块清单。
常见诊断处理:
await is only allowed inside async function:给所在函数加async,或改用 Promise 链;named arguments require a known function or Binding Contract:改用位置参数,或确认 SDK 绑定契约;non-exhaustive match:补齐字符串联合选项,或添加最终else;outside the allowed choices:使用联合类型或绑定契约列出的字符串;persist is only allowed at root scope:把声明移到模块顶层;required module is missing:更换包含该模块的 SDK/profile,或移入optionalModules并实现降级;- 模块循环:提取共享定义,保持依赖图有向无环。
19. 完整示例
下面把模块、类型、nullable、持久状态、单位、match、命名参数和异步流程组合在一起。
scripts/player.nut:
export class PlayerState {
@editor("slider", min: 0, max: 100, step: 1)
health: int = 100
@editor("combo")
mode: "idle" | "run" | "hurt" = "idle"
x: float = 100.0
}
export function update_player(
player: PlayerState,
dt: float,
speed: float
) {
if (keyboard.isDown("right")) {
player.x += speed * dt
player.mode = "run"
} else {
player.mode = "idle"
}
}
export function render_player(player: PlayerState) {
match player.mode {
"idle" => gfx.drawSolidRect(player.x, 220.0, 48.0, 48.0, 0.3, 0.75, 1.0, 1.0)
"run" => gfx.drawSolidRect(player.x, 220.0, 48.0, 48.0, 0.2, 1.0, 0.5, 1.0)
"hurt" => gfx.drawSolidRect(player.x, 220.0, 48.0, 48.0, 1.0, 0.2, 0.2, 1.0)
}
}
main.nut:
import { PlayerState, update_player, render_player } from "./scripts/player.nut"
persist player: PlayerState = PlayerState()
local notice: string? = null
async function show_ready_message() {
notice = "Get ready"
await asyncSleep(750ms)
notice = "Go!"
await asyncSleep(500ms)
notice = null
}
eve_init <- function() {
gfx.setBackgroundColor(0.08, 0.10, 0.16, 1.0)
show_ready_message().then(
function(_) {},
function(error) { print("message sequence failed: " + error + "\n") }
)
}
eve_update <- function(dt: float) {
update_player(player: player, speed: 180.0, dt: dt)
}
eve_render <- function() {
gfx.clear()
render_player(player)
// 文本绘制需要先通过 font 模块创建字体;这里用色条表示异步提示仍在显示。
if (notice != null)
gfx.drawSolidRect(32.0, 32.0, 180.0, 8.0, 1.0, 0.8, 0.2, 1.0)
}
实际项目中的绘制和输入函数请以当前 SDK 的模块使用手册为准。学习顺序建议是:
先掌握 Squirrel 的 table、array、class 和闭包,再使用模块与类型建立代码边界,最后在确有等待流程
时引入 async / await。