msw-scripting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MSW Scripting (.mlua) — Framework + File Workflow + Playtest & Debugging

MSW 脚本编写(.mlua)——框架 + 文件工作流 + Playtest 与调试

mlua is Lua-based, but it has MSW-specific annotations, a lifecycle, and an execution-space model. General Lua knowledge alone will not produce working code. All work is done by editing files in the workspace directly, and code is validated in the order build logs → runtime logs.

mlua 基于 Lua 开发,但具备 MSW 专属注解、生命周期及执行空间模型。仅掌握通用 Lua 知识无法写出可运行的代码。所有工作需直接在工作区中编辑文件完成,代码验证顺序为构建日志 → 运行时日志

1. Core Principles (must follow)

1. 核心原则(必须遵守)

1.1 Existing Script First

1.1 优先复用现有脚本

  • Before creating a new
    .mlua
    , you must search under
    ./RootDesk/MyDesk/
    for an existing script with the same or similar purpose.
  • Use glob/keyword search (file names, symbols, comment keywords) as your discovery method.
  • Duplicate implementations raise maintenance cost and conflict risk. Extending (modifying an existing file) is always the first choice.
  • 在创建新的
    .mlua
    文件之前,必须在
    ./RootDesk/MyDesk/
    目录下搜索用途相同或相似的现有脚本
  • 使用**全局/关键词搜索(文件名、符号、注释关键词)**来查找脚本。
  • 重复实现会增加维护成本和冲突风险。优先选择扩展(修改现有文件)而非创建新文件

1.2 Folder Structure for New Scripts — Never Dump Files Flat

1.2 新脚本的文件夹结构——禁止平铺文件

When extending an existing script is not possible and a new
.mlua
must be created, organize it under a feature/category subfolder. Do not drop scripts directly into
./RootDesk/MyDesk/
.
Required path shape:
./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua
(or a deeper nested path when the feature has sub-systems).
  • Reuse an existing subfolder if one already fits (e.g.,
    Player/
    ,
    UI/
    ,
    Combat/
    ,
    Tower/
    ,
    Inventory/
    ). Run a glob/list on
    ./RootDesk/MyDesk/
    first to see what's already there.
  • If no fitting subfolder exists, create one named for the feature/system (PascalCase, matching the surrounding project's casing). Examples:
    • ./RootDesk/MyDesk/Inventory/InventoryManager.mlua
    • ./RootDesk/MyDesk/Combat/MeleeAttackComponent.mlua
    • ./RootDesk/MyDesk/UI/Popup/RewardPopupLogic.mlua
  • Group related scripts together — Component, Logic, custom Event, and helper Struct for the same feature should sit in the same folder so the system is discoverable as a unit.
  • A single-file feature still gets its own folder. Even one script belongs in
    <FeatureFolder>/
    , not at the root, so future additions have a home.
  • Naming: folder = feature noun (
    TowerDefense/
    ,
    Quest/
    ); file = role-specific name reusing the feature noun where helpful (
    TowerSpawnerLogic.mlua
    ,
    QuestTrackerComponent.mlua
    ).
  • Do not create generic catch-all folders like
    Scripts/
    ,
    Misc/
    ,
    Common/
    ,
    New/
    ,
    temp/
    . Pick a real feature name. If you genuinely have a project-wide utility, place it under a specific utility folder such as
    Util/
    or
    Shared/
    only if that pattern already exists in the project.
Why: a flat
MyDesk/
quickly becomes unsearchable and makes the "search before creating" rule (1.1) impossible to follow. A consistent feature-folder layout is what lets future agents (and humans) discover what already exists.
当无法扩展现有脚本而必须创建新
.mlua
文件时,需将其组织在功能/类别子文件夹下。不得直接将脚本放入
./RootDesk/MyDesk/
根目录。
必填路径格式
./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua
(若功能包含子系统,可使用更深层级的嵌套路径)。
  • 如果已有合适的子文件夹,复用现有子文件夹(例如
    Player/
    UI/
    Combat/
    Tower/
    Inventory/
    )。先对
    ./RootDesk/MyDesk/
    执行全局搜索/列表查看,确认已有文件夹。
  • 如果没有合适的子文件夹,创建一个以功能/系统命名的文件夹(采用 PascalCase 命名,与项目现有命名规则一致)。示例:
    • ./RootDesk/MyDesk/Inventory/InventoryManager.mlua
    • ./RootDesk/MyDesk/Combat/MeleeAttackComponent.mlua
    • ./RootDesk/MyDesk/UI/Popup/RewardPopupLogic.mlua
  • 将相关脚本分组存放——同一功能的 Component、Logic、自定义 Event 和辅助 Struct 应放在同一文件夹中,以便作为一个单元被发现。
  • 单文件功能也需单独建文件夹。即使只有一个脚本,也应放入
    <FeatureFolder>/
    ,而非根目录,为未来新增内容预留位置。
  • 命名规则:文件夹 = 功能名词(
    TowerDefense/
    Quest/
    );文件 = 特定角色名称,必要时复用功能名词(
    TowerSpawnerLogic.mlua
    QuestTrackerComponent.mlua
    )。
  • 禁止创建通用的兜底文件夹,如
    Scripts/
    Misc/
    Common/
    New/
    temp/
    。选择真实的功能名称。如果确实需要项目级别的工具类脚本,仅当项目中已存在该模式时,才可将其放入
    Util/
    Shared/
    等特定工具文件夹。
原因:平铺的
MyDesk/
目录会迅速变得难以搜索,导致“创建前先搜索”规则(1.1)无法执行。一致的功能文件夹布局能让后续的智能体(及人类开发者)轻松发现已有的内容。

1.3 Never Guess APIs or Syntax — Verify Before Writing Code

1.3 切勿猜测 API 或语法——编写代码前务必验证

If you guess an MSW API name, parameter, or return type, the call will silently fail at runtime. Before writing code, verify the spec using one of the methods below.
Method 1 — Read the
.d.mlua
definition file directly
(exact signatures): The full engine API is defined as
.d.mlua
files under
./Environment/NativeScripts/
:
FolderContentsFilesExample
Component/
Engine components104
TransformComponent.d.mlua
Service/
System services46
SpawnService.d.mlua
Event/
Event types202
HitEvent.d.mlua
Logic/
Built-in logic9
TweenLogic.d.mlua
Enum/
Enumerations118
BodyType.d.mlua
Misc/
Utility types135
Vector2.d.mlua
You know the API name → Read ./Environment/NativeScripts/{folder}/{name}.d.mlua
You don't know the name → Grep keywords in the NativeScripts folder
Method 2 —
msw-search
skill
(detailed descriptions, examples, implementation guides): When
.d.mlua
only contains signatures and lacks explanation, search for parameter semantics, code examples, related APIs, and implementation guides.
Required order: confirm signature in
.d.mlua
→ (if needed) look up details via
msw-search
→ write code → LSP diagnose runs automatically (PostToolUse hook).
如果猜测 MSW API 的名称、参数或返回类型,调用会在运行时静默失败。编写代码前,请通过以下方法之一验证规范。
方法 1 — 直接读取
.d.mlua
定义文件
(精确签名): 完整的引擎 API 定义为
./Environment/NativeScripts/
下的
.d.mlua
文件:
文件夹内容文件数量示例
Component/
引擎组件104
TransformComponent.d.mlua
Service/
系统服务46
SpawnService.d.mlua
Event/
事件类型202
HitEvent.d.mlua
Logic/
内置逻辑9
TweenLogic.d.mlua
Enum/
枚举类型118
BodyType.d.mlua
Misc/
工具类型135
Vector2.d.mlua
已知 API 名称 → 读取 ./Environment/NativeScripts/{folder}/{name}.d.mlua
未知 API 名称 → 在 NativeScripts 文件夹中搜索关键词
方法 2 — 使用
msw-search
技能
(详细说明、示例、实现指南): 当
.d.mlua
仅包含签名而缺乏解释时,可搜索参数语义、代码示例、相关 API 及实现指南
必填顺序:在
.d.mlua
中确认签名 →(若需要)通过
msw-search
查询细节 → 编写代码 → LSP 诊断自动运行(PostToolUse 钩子)。

1.4 Lint (LSP diagnostics) — required after every script change

1.4 代码检查(LSP 诊断)——每次脚本修改后必须执行

  • Whenever you create or modify a
    .mlua
    file, the
    mlua-diagnose
    hook runs LSP
    diagnose
    automatically and feeds errors back.
  • Repeat fix → re-edit until error-severity diagnostics reach zero.
  • 每当创建或修改
    .mlua
    文件时,
    mlua-diagnose
    钩子会自动运行 LSP
    diagnose
    并反馈错误。
  • 重复执行修复 → 重新编辑,直到错误级别的诊断结果为零

1.5
.codeblock
files

1.5
.codeblock
文件

  • .codeblock
    files are generated automatically by Maker Refresh.
  • The agent must never create, edit, or delete them manually.
  • .codeblock
    文件由 Maker Refresh 自动生成
  • 智能体不得手动创建、编辑或删除此类文件。

1.6 Refresh Timing

1.6 刷新时机

  • After creating, modifying, renaming, or deleting a
    .mlua
    , you must call Maker MCP
    refresh
    .
  • refresh
    cannot run during play mode
    — first call
    stop
    to return to edit mode.
  • 创建、修改、重命名或删除
    .mlua
    文件后,必须调用 Maker MCP 的
    refresh
    命令。
  • refresh
    无法在游戏模式下运行
    ——需先调用
    stop
    返回编辑模式。

1.7 MSW ≠ Unity — Do Not Reason From Intuition
[VERIFIED]

1.7 MSW ≠ Unity — 切勿凭直觉推断
[VERIFIED]

MSW uses Lua + ECS + an MSW-specific execution-space model. Applying Unity or generic game-engine patterns directly will compile fine but silently fail at runtime. Common misconceptions:
Unity / generic intuitionMSW realityCorrect pathSource
MonoBehaviour.gameObject
/
this.transform
to access the owning entity
@Logic
has no
self.Entity
(that is
@Component
-only)
Inject via
property Entity x = "uuid"
on the Logic, or use
_EntityService:GetEntityByPath(...)
Logic.d.mlua
,
Component.d.mlua:16
,
mod/.../MODLogic.cs
OnMouseDown
/
BoxCollider2D
is enough to receive clicks/touches
Physics colliders and Rigidbody do not emit
TouchEvent
World:
TouchReceiveComponent
/ UI:
ButtonComponent
or
UITouchReceiveComponent
EmitTouchEvent
in
TouchReceiveComponent.d.mlua
, §10
OnCollisionEnter
+ Rigidbody for collision callbacks
Entity-to-entity collisions use a separate
TriggerComponent
+
TriggerEnter/Leave/Stay
Attach
TriggerComponent
, then
ConnectEvent(TriggerEnterEvent, ...)
TriggerComponent.d.mlua:56-62
Attach multiple Rigidbody/Collider freelyOne Body per map type (MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody)Custom models include only the Body that matches the map type
msw-general/references/platform.md §4
.
DefaultPlayer
has all three with engine auto-activation.
Reference/modify UI objects from server codeUI entities exist only on the client — referencing them from server
@ExecSpace
returns nil
Server→UI must go through an
@ExecSpace("Client")
RPC
msw-general/references/ui.md:445-446
Instantiate(prefab)
callable anywhere
_SpawnService:SpawnByModelId(id, name, pos, parent)
parent
is required
, server-only
Inside
ServerOnly
/
Server
RPC, pass
CurrentMap
as parent
SpawnService.d.mlua:22
(no default for parent)
static
classes / hand-rolled singletons
@Logic
is itself an engine singleton
Call from other scripts as
_ScriptName:Method()
(e.g.,
_TweenLogic
,
_UtilLogic
,
_ScreenMessageLogic
) — never instantiate
§3.2
Rule: any time you catch yourself thinking "Unity does it this way, so MSW probably does too", stop and verify against
Environment/NativeScripts/*.d.mlua
or the
mod/
engine source before writing code.

MSW 使用 Lua + ECS + MSW 专属的执行空间模型。直接套用 Unity 或通用游戏引擎的模式,代码可能编译通过但在运行时静默失败。常见误区:
Unity / 通用直觉MSW 实际情况正确做法来源
使用
MonoBehaviour.gameObject
/
this.transform
访问所属实体
@Logic
没有
self.Entity
(仅
@Component
拥有该属性)
在 Logic 中通过
property Entity x = "uuid"
注入,或使用
_EntityService:GetEntityByPath(...)
Logic.d.mlua
,
Component.d.mlua:16
,
mod/.../MODLogic.cs
使用
OnMouseDown
/
BoxCollider2D
即可接收点击/触摸事件
物理碰撞器和 Rigidbody 不会触发
TouchEvent
世界实体:
TouchReceiveComponent
/ UI:
ButtonComponent
UITouchReceiveComponent
TouchReceiveComponent.d.mlua
中的
EmitTouchEvent
,第10节
使用
OnCollisionEnter
+ Rigidbody 处理碰撞回调
实体间碰撞需使用独立的
TriggerComponent
+
TriggerEnter/Leave/Stay
附加
TriggerComponent
,然后调用
ConnectEvent(TriggerEnterEvent, ...)
TriggerComponent.d.mlua:56-62
可自由附加多个 Rigidbody/Collider每种地图类型对应一个 Body(MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody)自定义模型仅包含与地图类型匹配的 Body
msw-general/references/platform.md §4
DefaultPlayer
包含全部三种 Body,由引擎自动激活。
从服务器代码中引用/修改 UI 对象UI 实体仅存在于客户端——从服务器
@ExecSpace
引用会返回 nil
服务器→UI 通信必须通过
@ExecSpace("Client")
RPC
msw-general/references/ui.md:445-446
可在任意位置调用
Instantiate(prefab)
_SpawnService:SpawnByModelId(id, name, pos, parent)
parent
为必填参数
,仅服务器可调用
ServerOnly
/
Server
RPC 中,传入
CurrentMap
作为父对象
SpawnService.d.mlua:22
(parent 无默认值)
使用
static
类 / 手动实现单例
@Logic
本身就是引擎单例
从其他脚本中通过
_ScriptName:Method()
调用(例如
_TweenLogic
_UtilLogic
_ScreenMessageLogic
)——切勿实例化
第3.2节
规则:每当你觉得“Unity 是这么做的,所以 MSW 可能也是如此”时,请停止操作,先对照
Environment/NativeScripts/*.d.mlua
mod/
引擎源码验证,再编写代码。

2. Paths and File Roles

2. 路径与文件角色

TargetPathAgent action
User scripts
./RootDesk/MyDesk/**/*.mlua
Create / read / modify / delete directly
Auto-generated artifacts
*.codeblock
Do not touch (Refresh manages them)
Engine API definitions
./Environment/NativeScripts/**
Read-only (do not modify)
Models (component lists)
./RootDesk/MyDesk/*.model
,
./Global/*.model
, etc.
Edit
Components
when attaching scripts
Map instances
./map/*.map
Edit when attaching scripts to entities that exist only inside a map

目标路径智能体操作
用户脚本
./RootDesk/MyDesk/**/*.mlua
直接创建 / 读取 / 修改 / 删除
自动生成的工件
*.codeblock
禁止修改(由 Refresh 管理)
引擎 API 定义
./Environment/NativeScripts/**
只读(禁止修改)
模型(组件列表)
./RootDesk/MyDesk/*.model
,
./Global/*.model
附加脚本时编辑
Components
地图实例
./map/*.map
当需要为仅存在于地图内的实体附加脚本时编辑

3. Script Types and Declarations

3. 脚本类型与声明

3.1 Component scripts (
@Component
)

3.1 组件脚本(
@Component

Scripts attached to an Entity. Use
self.Entity
to access the owning entity.
lua
@Component
script MyScript extends Component

    property number Speed = 5.0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- initialization
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- per-frame logic
    end

    @ExecSpace("ServerOnly")
    method void OnEndPlay()
        -- cleanup
    end

end
Allowed parents:
  • Component
    — generic component
  • AttackComponent
    — attack system (Shape, AttackFast, OnAttack)
  • HitComponent
    — hit system (OnHit, HandleHitEvent)
附加到实体的脚本。可使用
self.Entity
访问所属实体。
lua
@Component
script MyScript extends Component

    property number Speed = 5.0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- 初始化
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- 每帧逻辑
    end

    @ExecSpace("ServerOnly")
    method void OnEndPlay()
        -- 清理
    end

end
允许的父类
  • Component
    — 通用组件
  • AttackComponent
    — 攻击系统(Shape, AttackFast, OnAttack)
  • HitComponent
    — 受击系统(OnHit, HandleHitEvent)

3.2 Logic scripts (
@Logic
)

3.2 逻辑脚本(
@Logic

Global singletons. Run independently without an Entity. Use for game managers, UI managers, utilities, etc.
lua
@Logic
script GameManager extends Logic

    @Sync property integer Score = 0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- global initialization
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- per-frame logic
    end

end
  • One per world (singleton)
  • Accessed from other scripts as
    _GameManager
    (underscore + script name)
  • Supports
    @Sync
    properties — server→client sync behaves the same way
⚠️ Warning —
@Logic
does NOT have
self.Entity
[VERIFIED]
The public members of the
Logic
parent class (
Environment/NativeScripts/Logic/Logic.d.mlua
) are only
ConnectEvent
/
DisconnectEvent
/
IsClient
/
IsServer
/
SendEvent
. There is no
Entity
property,
GetOwner
, or
Owner
. The engine source (
mod/.../MODLogic.cs
) confirms this.
self.Entity
is
@Component
-exclusive
(
readonly property Entity Entity
in
Component.d.mlua
).
Code like
self.Entity.xxx
inside a Logic compiles but produces a nil-access at runtime and silently fails. If a Logic needs to operate on a specific world entity, use one of these:
lua
@Logic
script GameManager extends Logic
    property Entity spawnPoint = "uuid-string"        -- inject UUID directly
    property EntityRef bossEntity = ""                -- survives map transitions

    method void OnBeginPlay()
        local map = _EntityService:GetCurrentMap()    -- current map
        local e   = _EntityService:GetEntityByPath("/maps/Main/Entities/Boss")
    end
end
  • Property injection (recommended): hard-code the UUID of an entity already placed in the map into the property default.
  • Service lookup:
    _EntityService:GetCurrentMap()
    /
    GetEntityByPath(...)
    /
    FindEntityByName(...)
    .
Decision: @Component vs @Logic — behavior attached to an entity →
@Component
; global singleton manager →
@Logic
. A Logic's
OnUpdate
runs before Components'
OnUpdate
.
全局单例脚本。无需实体即可独立运行。适用于游戏管理器、UI 管理器、工具类等场景。
lua
@Logic
script GameManager extends Logic

    @Sync property integer Score = 0

    @ExecSpace("ServerOnly")
    method void OnBeginPlay()
        -- 全局初始化
    end

    @ExecSpace("ServerOnly")
    method void OnUpdate(number delta)
        -- 每帧逻辑
    end

end
  • 每个世界仅存在一个实例(单例)
  • 从其他脚本中通过
    _GameManager
    (下划线 + 脚本名称)访问
  • 支持
    @Sync
    属性——服务器→客户端同步行为一致
⚠️ 警告 —
@Logic
没有
self.Entity
[VERIFIED]
Logic
父类(
Environment/NativeScripts/Logic/Logic.d.mlua
)的公共成员仅包括
ConnectEvent
/
DisconnectEvent
/
IsClient
/
IsServer
/
SendEvent
。不存在
Entity
属性、
GetOwner
Owner
。引擎源码(
mod/.../MODLogic.cs
)已确认这一点。
self.Entity
@Component
专属
Component.d.mlua
中的
readonly property Entity Entity
)。
在 Logic 中编写
self.Entity.xxx
这类代码会编译通过,但运行时会产生空引用并静默失败。如果 Logic 需要操作特定的世界实体,请使用以下方式之一:
lua
@Logic
script GameManager extends Logic
    property Entity spawnPoint = "uuid-string"        -- 直接注入 UUID
    property EntityRef bossEntity = ""                -- 可跨地图保留引用

    method void OnBeginPlay()
        local map = _EntityService:GetCurrentMap()    -- 当前地图
        local e   = _EntityService:GetEntityByPath("/maps/Main/Entities/Boss")
    end
end
  • 属性注入(推荐):将已放置在地图中的实体 UUID 硬编码到属性默认值中。
  • 服务查找
    _EntityService:GetCurrentMap()
    /
    GetEntityByPath(...)
    /
    FindEntityByName(...)
决策:@Component vs @Logic — 与实体绑定的行为 → 使用
@Component
;全局单例管理器 → 使用
@Logic
。Logic 的
OnUpdate
先于 Components 的
OnUpdate
执行。

3.3 Extend scripts (extending an existing component)

3.3 扩展脚本(扩展现有组件)

lua
@Component
script PlayerAttack extends AttackComponent
    -- Override AttackComponent's methods
    -- Call parent via __base:MethodName()
end
lua
@Component
script PlayerAttack extends AttackComponent
    -- 重写 AttackComponent 的方法
    -- 通过 __base:MethodName() 调用父类方法
end

3.4 Other script types

3.4 其他脚本类型

AnnotationPurposeNotes
@Event
Define a custom event typeDeclare event parameters
@Item
Define an item typeInventory system
@BTNode
Behaviour Tree nodeAI behavior trees
@State
Define a state typeState machines
@Struct
Struct / user typeComposite data types

注解用途说明
@Event
定义自定义事件类型声明事件参数
@Item
定义物品类型用于 inventory 系统
@BTNode
行为树节点用于 AI 行为树
@State
定义状态类型用于状态机
@Struct
结构体 / 用户自定义类型用于复合数据类型

4. mlua Language Extensions (vs. plain Lua)

4. mlua 语言扩展(与标准 Lua 的差异)

mlua is based on Lua 5.3 but differs in the following ways.
mlua 基于 Lua 5.3,但存在以下差异。

Added keywords / operators

新增关键字 / 运算符

FeatureSyntaxNotes
continue
continue
Skip to next iteration in a loop (not in standard Lua)
Compound assignment
+=
,
-=
,
*=
,
/=
,
//=
,
%=
,
^=
,
..=
Multi-assign (
a, b += 1, 2
) is invalid; cannot be used as a function arg (
print(a += 1)
)
Bitwise operators
&
,
|
,
<<
,
>>
Compound forms also valid:
&=
,
|=
,
<<=
,
>>=
特性语法说明
continue
continue
跳过循环的下一次迭代(标准 Lua 中无此关键字)
复合赋值
+=
,
-=
,
*=
,
/=
,
//=
,
%=
,
^=
,
..=
不支持多变量赋值(
a, b += 1, 2
无效);不能作为函数参数使用(
print(a += 1)
无效)
位运算符
&
, `
, 
<<
, 
>>`

Restrictions

限制

RestrictionDescription
No global variablesThe
global
keyword is not allowed. Values shared across scripts must be declared as Properties.
No coroutinesLua's
coroutine.create/resume/yield
is not available.
__base
instead of
super
Call parent methods with
__base:MethodName()
, not
super
.
限制描述
禁止全局变量不允许使用
global
关键字。跨脚本共享的值必须声明为 Properties。
禁止协程Lua 的
coroutine.create/resume/yield
不可用。
使用
__base
而非
super
通过
__base:MethodName()
调用父类方法,而非
super

Built-in utility functions

内置工具函数

FunctionSignaturePurpose
log()
log(any... args)
Info-level log output
log_warning()
log_warning(any... args)
Warning-level log output
log_error()
log_error(any... args)
Error-level log output
wait()
wait(number seconds)
Pause script execution for the given seconds
isvalid()
isvalid(any object) → boolean
Validity check (handles deletion / nil)
enum()
enum(table t) → table
Swap a table's keys and values
beginscope()
/
endscope()
beginscope(string name)
/
endscope()
User profiling scopes

函数签名用途
log()
log(any... args)
信息级日志输出
log_warning()
log_warning(any... args)
警告级日志输出
log_error()
log_error(any... args)
错误级日志输出
wait()
wait(number seconds)
暂停脚本执行指定秒数
isvalid()
isvalid(any object) → boolean
有效性检查(处理对象删除 / nil 情况)
enum()
enum(table t) → table
交换表的键和值
beginscope()
/
endscope()
beginscope(string name)
/
endscope()
用户性能分析作用域

5. Lifecycle

5. 生命周期

Component and Logic share the same lifecycle.
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
Plus, on map transitions:
OnMapEnter
/
OnMapLeave
.
MethodWhen it firesPurpose
OnInitialize
Right after creationInitialize internal variables (rarely used)
OnBeginPlay
Game start / activationWire up events, start timers, initial setup
OnUpdate(delta)
Every frameMovement, animation, input handling
OnMapEnter
Entering a mapPer-map initialization
OnMapLeave
Leaving a mapPer-map cleanup
OnEndPlay
Game end / deactivationDisconnect events, clear timers (mandatory!)
OnDestroy
On removalFinal cleanup (rarely used)
Required pattern: anything connected in
OnBeginPlay
must be released in
OnEndPlay
.
lua
property any eventHandler = nil   -- store the EventHandlerBase returned by ConnectEvent
property integer timerId = 0

method void OnBeginPlay()
    self.eventHandler = self.Entity:ConnectEvent(SomeEvent, self.OnSomeEvent)
    self.timerId = _TimerService:SetTimerRepeat(self.Tick, 1/60)
end

method void OnEndPlay()
    if self.eventHandler then
        self.Entity:DisconnectEvent(SomeEvent, self.eventHandler)
    end
    if self.timerId then
        _TimerService:ClearTimer(self.timerId)
    end
end
ConnectEvent
return type
: an
EventHandlerBase
object. When storing it as a property, declare it as
any
. If you declare it as
integer
,
DisconnectEvent
will fail to detach due to a type mismatch.

Component 和 Logic 共享相同的生命周期。
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy
此外,地图切换时会触发:
OnMapEnter
/
OnMapLeave
方法触发时机用途
OnInitialize
创建后立即触发初始化内部变量(很少使用)
OnBeginPlay
游戏启动 / 实体激活时绑定事件、启动计时器、初始设置
OnUpdate(delta)
每帧触发移动、动画、输入处理
OnMapEnter
进入地图时按地图初始化
OnMapLeave
离开地图时按地图清理
OnEndPlay
游戏结束 / 实体停用解绑事件、清除计时器(必须执行!)
OnDestroy
实体被移除时最终清理(很少使用)
必填模式:在
OnBeginPlay
中绑定的内容必须在
OnEndPlay
中释放。
lua
property any eventHandler = nil   -- 存储 ConnectEvent 返回的 EventHandlerBase 对象
property integer timerId = 0

method void OnBeginPlay()
    self.eventHandler = self.Entity:ConnectEvent(SomeEvent, self.OnSomeEvent)
    self.timerId = _TimerService:SetTimerRepeat(self.Tick, 1/60)
end

method void OnEndPlay()
    if self.eventHandler then
        self.Entity:DisconnectEvent(SomeEvent, self.eventHandler)
    end
    if self.timerId then
        _TimerService:ClearTimer(self.timerId)
    end
end
ConnectEvent
返回类型
EventHandlerBase
对象。将其存储为属性时,需声明为
any
。如果声明为
integer
DisconnectEvent
会因类型不匹配而无法解绑。

6. Execution Space (ExecSpace)

6. 执行空间(ExecSpace)

MSW is a server-client architecture. Every method must declare where it runs.
ExecSpaceRuns onDirectionUse case
ServerOnly
ServerServer-internal onlyDamage calc, state changes, spawning
ClientOnly
ClientClient-internal onlyUI updates, effects, sounds
Server
ServerClient→Server RPCClient requesting the server (attack, item use)
Client
ClientServer→Client RPCServer notifying a client (result UI, effects)
Multicast
All clientsServer→all clientsGlobal events (announcements, boss spawn)
(unspecified)Caller sideServer→Server, Client→ClientShared functions executed locally on either side
MSW 采用服务器-客户端架构。每个方法必须声明其运行位置。
ExecSpace运行位置方向使用场景
ServerOnly
服务器仅服务器内部伤害计算、状态变更、实体生成
ClientOnly
客户端仅客户端内部UI 更新、特效、音效
Server
服务器客户端→服务器 RPC客户端向服务器发起请求(攻击、物品使用)
Client
客户端服务器→客户端 RPC服务器通知客户端(结果 UI、特效)
Multicast
所有客户端服务器→所有客户端全局事件(公告、Boss 生成)
(未指定)调用方所在端服务器→服务器、客户端→客户端在本地执行的共享函数

ExecSpace constraints on lifecycle methods

生命周期方法的 ExecSpace 限制

MethodAllowed ExecSpace
OnSyncProperty
ClientOnly
only
OnInitialize
,
OnBeginPlay
,
OnUpdate
,
OnEndPlay
,
OnDestroy
,
OnMapEnter
,
OnMapLeave
ServerOnly
,
ClientOnly
, or unspecified
All event handlers
ServerOnly
,
ClientOnly
, or unspecified
Custom user methodsAny of
Server
,
Client
,
ServerOnly
,
ClientOnly
,
Multicast
方法允许的 ExecSpace
OnSyncProperty
ClientOnly
OnInitialize
,
OnBeginPlay
,
OnUpdate
,
OnEndPlay
,
OnDestroy
,
OnMapEnter
,
OnMapLeave
ServerOnly
,
ClientOnly
未指定
所有事件处理器
ServerOnly
,
ClientOnly
未指定
自定义用户方法
Server
,
Client
,
ServerOnly
,
ClientOnly
,
Multicast
中的任意一种

Key rules

核心规则

lua
-- Calling a ServerOnly function from the client → silently ignored (no error!)
@ExecSpace("ServerOnly")
method void TakeDamage(number amount)
    self.Hp = self.Hp - amount  -- runs only on the server
end

-- Server RPC: client calls → runs on the server
@ExecSpace("Server")
method void RequestAttack()
    -- client calls self:RequestAttack()
    -- actual execution happens on the server (network latency applies)
end

-- Client RPC: server calls → runs on that client
@ExecSpace("Client")
method void ShowDamageEffect(number damage)
    -- server calls self:ShowDamageEffect(50)
    -- runs on the targeted user's client
end
lua
-- 从客户端调用 ServerOnly 函数 → 会被静默忽略(无错误提示!)
@ExecSpace("ServerOnly")
method void TakeDamage(number amount)
    self.Hp = self.Hp - amount  -- 仅在服务器运行
end

-- Server RPC:客户端调用 → 在服务器执行
@ExecSpace("Server")
method void RequestAttack()
    -- 客户端调用 self:RequestAttack()
    -- 实际执行在服务器(存在网络延迟)
end

-- Client RPC:服务器调用 → 在目标客户端执行
@ExecSpace("Client")
method void ShowDamageEffect(number damage)
    -- 服务器调用 self:ShowDamageEffect(50)
    -- 在目标用户的客户端执行
end

Typical server-client pattern

典型服务器-客户端模式

[Client]                    [Server]
  Detect input (ClientOnly)
       └─── RequestAction() ──→ Validate + handle (ServerOnly)
                                     ├─ State auto-syncs via @Sync
       ←── ShowResult() ────────────┘  (Client RPC)
  Update UI (ClientOnly)
[客户端]                    [服务器]
  检测输入(ClientOnly)
       └─── RequestAction() ──→ 验证 + 处理(ServerOnly)
                                     ├─ 状态通过 @Sync 自动同步
       ←── ShowResult() ────────────┘  (Client RPC)
  更新 UI(ClientOnly)

senderUserId
— verifying the requester on the server

senderUserId
— 在服务器验证请求发起方

When an
@ExecSpace("Server")
method is called from the client, the server side can read the caller's UserId from the local
senderUserId
variable. This is required for security checks.
lua
@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
    -- Verify the requester: confirm the call came from the local client
    if senderUserId ~= self.Entity.PlayerComponent.UserId then
        log_warning("blocked request from a different client")
        return
    end
    self:ProcessPurchase(itemId)
end
@ExecSpace("Server")
方法从客户端调用时,服务器端可通过本地变量
senderUserId
获取调用者的 UserId。这是安全检查的必要步骤。
lua
@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
    -- 验证请求发起方:确认调用来自本地客户端
    if senderUserId ~= self.Entity.PlayerComponent.UserId then
        log_warning("拦截来自其他客户端的请求")
        return
    end
    self:ProcessPurchase(itemId)
end

Sending a Client RPC to a specific client only

仅向特定客户端发送 Client RPC

When the server invokes an
@ExecSpace("Client")
function, adding a UserId as the last argument at the call site routes execution to that user's client only.
lua
@ExecSpace("Client")
method void ShowReward(string itemName)
    log("reward earned: " .. itemName)
end

@ExecSpace("ServerOnly")
method void GiveReward(string playerId, string itemName)
    -- Show UI only on playerId's client
    self:ShowReward(itemName, playerId)
end
Do not add the UserId parameter to the function declaration. Add it only as the final argument at the call site.
当服务器调用
@ExecSpace("Client")
函数时,在调用时添加 UserId 作为最后一个参数,可将执行路由到该用户的客户端。
lua
@ExecSpace("Client")
method void ShowReward(string itemName)
    log("获得奖励: " .. itemName)
end

@ExecSpace("ServerOnly")
method void GiveReward(string playerId, string itemName)
    -- 仅在 playerId 的客户端显示 UI
    self:ShowReward(itemName, playerId)
end
请勿在函数声明中添加 UserId 参数。仅在调用时将其作为最后一个参数添加。

Allowed parameter types across exec spaces

跨执行空间允许的参数类型

When functions are called across server↔client boundaries:
  • Allowed:
    string
    ,
    integer
    ,
    number
    ,
    boolean
    ,
    table
    ,
    Vector2
    ,
    Vector3
    ,
    Vector4
    ,
    Color
    ,
    Entity
    ,
    Component
    ,
    EntityRef
    ,
    ComponentRef
  • Not allowed:
    any
  • SyncTable generic parameters (k, v) must also be one of the allowed types above.

当函数跨服务器↔客户端调用时:
  • 允许
    string
    ,
    integer
    ,
    number
    ,
    boolean
    ,
    table
    ,
    Vector2
    ,
    Vector3
    ,
    Vector4
    ,
    Color
    ,
    Entity
    ,
    Component
    ,
    EntityRef
    ,
    ComponentRef
  • 不允许
    any
  • **SyncTable 泛型参数(k, v)**也必须是上述允许的类型之一。

7. Property System

7. 属性系统

Basic property types

基础属性类型

lua
property number Speed = 5.0              -- floating point (float/double)
property integer Count = 0               -- integer
property string Name = "Player"          -- string
property boolean IsAlive = true          -- boolean
property Vector2 Direction = Vector2(0, 0)
property Vector3 Position = Vector3(0, 0, 0)
property Color Tint = Color(1, 1, 1, 1)
property any CustomData = nil            -- arbitrary type
Type-name reminder: integers are
integer
, floats are
number
.
lua
property number Speed = 5.0              -- 浮点数(float/double)
property integer Count = 0               -- 整数
property string Name = "Player"          -- 字符串
property boolean IsAlive = true          -- 布尔值
property Vector2 Direction = Vector2(0, 0)
property Vector3 Position = Vector3(0, 0, 0)
property Color Tint = Color(1, 1, 1, 1)
property any CustomData = nil            -- 任意类型
类型名称提示:整数类型为
integer
,浮点数类型为
number

Entity / Component reference properties

实体 / 组件引用属性

lua
property Entity targetEntity = ""                    -- linked by UUID string
property Entity popup = "94a274e4-4111-40f1-924d-c95a3a1f14d5"
property ButtonComponent btnOk = "uuid-string"       -- specific component reference
property TextComponent txtScore = "uuid-string"
AI automation principle — inject UUID strings directly
The default value of an Entity/Component/EntityRef/ComponentRef property is a UUID string. The AI must NOT push the work onto the user (e.g., "drag it in the Maker editor"). Instead:
  1. Look up the target entity's
    id
    (UUID) in the
    .map
    /
    .ui
    file.
  2. Hard-code that UUID as a string literal into the
    .mlua
    property default.
  3. Apply the same pattern to multiple-slot references (e.g.,
    wp0~wp7
    array-style references) — inject each as a string.
lua
@Logic
script WaypointPath extends Logic
    property Entity wp0 = "a1b2c3d4-...-000000000000"
    property Entity wp1 = "a1b2c3d4-...-000000000001"
    -- ... no drag required. Inject UUIDs from the map file as strings.
end
Note: drag-binding in the Maker editor is a convenience for human authors only. In an AI automation flow, UUID-string injection is the default path.
lua
property Entity targetEntity = ""                    -- 通过 UUID 字符串关联
property Entity popup = "94a274e4-4111-40f1-924d-c95a3a1f14d5"
property ButtonComponent btnOk = "uuid-string"       -- 特定组件引用
property TextComponent txtScore = "uuid-string"
AI 自动化原则 — 直接注入 UUID 字符串
Entity/Component/EntityRef/ComponentRef 属性的默认值为 UUID 字符串。AI 不得将此工作推给用户(例如“在 Maker 编辑器中拖拽绑定”)。正确做法:
  1. .map
    /
    .ui
    文件中查找目标实体的
    id
    (UUID)。
  2. 将该 UUID 作为字符串字面量硬编码到
    .mlua
    的属性默认值中。
  3. 多槽引用(例如
    wp0~wp7
    数组式引用)也采用相同模式——将每个引用作为字符串注入。
lua
@Logic
script WaypointPath extends Logic
    property Entity wp0 = "a1b2c3d4-...-000000000000"
    property Entity wp1 = "a1b2c3d4-...-000000000001"
    -- ... 无需拖拽。直接从地图文件注入 UUID 字符串。
end
注意:在 Maker 编辑器中拖拽绑定仅为人类开发者提供便利。在 AI 自动化流程中,UUID 字符串注入是默认方式。

Entity vs EntityRef

Entity vs EntityRef

TypeAfter map transitionUse case
Entity
Reference is dropped (nil)References within the same map
EntityRef
Reference persistsWhen the reference must survive a map transition
Component
Reference is droppedReferences within the same map
ComponentRef
Reference persistsWhen the reference must survive a map transition
Multi-map games should prefer
EntityRef
/
ComponentRef
.
Entity
/
Component
is sufficient for single-map games.
类型地图切换后使用场景
Entity
引用失效(变为 nil)同一地图内的引用
EntityRef
引用保持有效需要跨地图保留的引用
Component
引用失效同一地图内的引用
ComponentRef
引用保持有效需要跨地图保留的引用
多地图游戏应优先使用
EntityRef
/
ComponentRef
。单地图游戏使用
Entity
/
Component
即可。

Sync annotations

同步注解

AnnotationBehavior
@Sync
Server → all clients
@TargetUserSync
Server → only that user's client
Both take no arguments.
lua
@Sync
property number CurrentHp = 100
-- Server changes the value → automatically reflected on all clients

@TargetUserSync
property number PrivateScore = 0
-- Synced to the owning user's client only
Core rules:
  • Server → client, one direction only. Changing a
    @Sync
    value on the client does NOT propagate back to the server.
  • Sync has network latency — not instantaneous.
  • Cannot be synced:
    any
    ,
    table
    (use
    SyncTable
    instead).
@TargetUserSync
caveat
: only meaningful on a component attached to a PlayerEntity. If attached to any other entity, it behaves like a regular
@Sync
. It pays off (saving bandwidth) for information that other users do not need to see, such as personal currency, achievements, or consumable counts.
注解行为
@Sync
服务器 → 所有客户端
@TargetUserSync
服务器 → 仅目标用户的客户端
两者均无参数
lua
@Sync
property number CurrentHp = 100
-- 服务器修改值 → 自动同步到所有客户端

@TargetUserSync
property number PrivateScore = 0
-- 仅同步到所属用户的客户端
核心规则
  • 仅服务器→客户端单向同步。在客户端修改
    @Sync
    值不会同步回服务器。
  • 同步存在网络延迟——并非即时生效。
  • 不可同步
    any
    ,
    table
    (如需同步表,使用
    SyncTable
    )。
@TargetUserSync
注意事项
:仅附加到 PlayerEntity 的组件上才有意义。如果附加到其他实体,其行为与普通
@Sync
相同。适用于其他用户无需查看的信息(如个人货币、成就、消耗品数量),可节省带宽。

SyncTable type

SyncTable 类型

A table that can be synced. Supports both array and dictionary forms.
lua
-- Array form: SyncTable<ValueType>
@Sync
property SyncTable<number> Scores = {}

-- Dictionary form: SyncTable<KeyType, ValueType>
@Sync
property SyncTable<string, number> Stats = {}
  • Use together with
    @Sync
    .
  • Different from a plain
    table
    — only
    SyncTable
    is synchronized.
可同步的表类型。支持数组和字典形式。
lua
-- 数组形式:SyncTable<ValueType>
@Sync
property SyncTable<number> Scores = {}

-- 字典形式:SyncTable<KeyType, ValueType>
@Sync
property SyncTable<string, number> Stats = {}
  • 需与
    @Sync
    配合使用。
  • 与普通
    table
    不同——仅
    SyncTable
    可被同步。

Temporary properties (
_T
)

临时属性(
_T

self._T
exposes non-synced temporary properties created on the fly. No
property
declaration is required.
lua
-- Use for non-synced, frame-local state
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false
  • Cannot be
    @Sync
    'd — server and client each keep their own values.
  • Convenient because no property declaration is needed, but it does NOT show up in the editor inspector.
self._T
用于存储动态创建的非同步临时属性。无需声明
property
lua
-- 用于非同步、帧局部状态
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false
  • 不可被
    @Sync
    ——服务器和客户端各自保留独立的值。
  • 使用方便,无需声明属性,但不会显示在编辑器检查器中

OnSyncProperty
callback

OnSyncProperty
回调

A callback automatically invoked on the client when a
@Sync
property changes on the server.
lua
@ExecSpace("ClientOnly")
method void OnSyncProperty(string name, any value)
    if name == "CurrentHp" then
        self:UpdateHpBar(value)
    elseif name == "IsDead" and value == true then
        self:PlayDeathEffect()
    end
end
Rules:
  • Fixed to
    ClientOnly
    — ExecSpace cannot be changed.
  • name
    : the changed property's name.
  • value
    : the new value.
  • Available on both Component and Logic.
当服务器上的
@Sync
属性发生变化时,客户端会自动调用此回调。
lua
@ExecSpace("ClientOnly")
method void OnSyncProperty(string name, any value)
    if name == "CurrentHp" then
        self:UpdateHpBar(value)
    elseif name == "IsDead" and value == true then
        self:PlayDeathEffect()
    end
end
规则
  • 固定为
    ClientOnly
    ——无法更改 ExecSpace。
  • name
    :发生变化的属性名称。
  • value
    :属性的新值。
  • Component 和 Logic 均支持此回调。

Property editor attributes

属性编辑器属性

Control how the property is shown in the Maker editor inspector:
lua
@DisplayName("Display Name")           -- Override the name shown in the editor
property string InternalName = ""

@Description("Used for ~")             -- Inspector tooltip
property number Damage = 10

@MinValue(0)                           -- Min limit (number/integer)
@MaxValue(999)                         -- Max limit (number/integer)
@Delta(5)                              -- Step value for the mobile editor's +/- buttons
property integer Score = 0

@MaxLength(20)                         -- Max string length
property string Nickname = ""

@HideFromInspector                     -- Hide from the inspector
property any InternalState = nil

控制属性在 Maker 编辑器检查器中的显示方式:
lua
@DisplayName("显示名称")           -- 覆盖编辑器中显示的名称
property string InternalName = ""

@Description("用于 ~")             -- 检查器提示信息
property number Damage = 10

@MinValue(0)                           -- 最小值限制(number/integer)
@MaxValue(999)                         -- 最大值限制(number/integer)
@Delta(5)                              -- 移动端编辑器 +/- 按钮的步长值
property integer Score = 0

@MaxLength(20)                         -- 字符串最大长度
property string Nickname = ""

@HideFromInspector                     -- 在检查器中隐藏
property any InternalState = nil

8. Event System / RPC

8. 事件系统 / RPC

Static handler declaration (statically subscribed events)

静态处理器声明(静态订阅事件)

lua
@EventSender("Self")
handler HandleHitEvent(HitEvent event)
    local damage = event.TotalDamage
    -- Receives events emitted by my own entity
end
lua
@EventSender("Self")
handler HandleHitEvent(HitEvent event)
    local damage = event.TotalDamage
    -- 接收自身实体触发的事件
end

@EventSender
parameters

@EventSender
参数

1st parameter2nd parameterPurpose
"Self"
noneEvents from my own entity
"LocalPlayer"
noneEvents from the local player entity
"Entity"
entity ID (string)Events from a specific entity
"Model"
model ID (string)Events from a specific model
"Service"
service type name (e.g.,
"InputService"
)
Service events
"Logic"
logic type nameLogic events
lua
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
    -- Receives key events emitted by InputService
end
第一个参数第二个参数用途
"Self"
自身实体触发的事件
"LocalPlayer"
本地玩家实体触发的事件
"Entity"
实体 ID(字符串)指定实体触发的事件
"Model"
模型 ID(字符串)指定模型触发的事件
"Service"
服务类型名称(例如
"InputService"
服务触发的事件
"Logic"
逻辑类型名称逻辑脚本触发的事件
lua
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
    -- 接收 InputService 触发的按键事件
end

Dynamic event connect / disconnect

动态事件绑定 / 解绑

lua
-- Connect (in OnBeginPlay) — return type is an EventHandlerBase object
local eventHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)
-- Disconnect (mandatory in OnEndPlay)
entity:DisconnectEvent(ButtonClickEvent, eventHandler)
⚠️
ConnectEvent
is called on Entity / Logic / Service — NOT on a Component
[VERIFIED]
Only three types expose
ConnectEvent
/
DisconnectEvent
:
  • Entity
    (
    Misc/Entity.d.mlua:96-104
    )
  • Logic
    (
    Logic/Logic.d.mlua:7-15
    )
  • Service
    (
    Service/Service.d.mlua:8-16
    )
The public members of the
Component
parent (
Component.d.mlua
) are only
Enable
/
Entity
/
IsClient
/
IsServer
. So no Component — including
ButtonComponent
and
TriggerComponent
— has its own
ConnectEvent
. Components only emit events; the entity they are attached to is what receives them.
lua
-- ❌ Wrong — Component has no ConnectEvent. Runtime nil access.
self.Entity.ButtonComponent:ConnectEvent(ButtonClickEvent, self.OnClick)

-- ✅ Correct — subscribe via the entity that owns the Component
self.clickHandler = self.Entity:ConnectEvent(ButtonClickEvent, self.OnClick)

-- ✅ Service events: subscribe on the service
self.keyHandler = _InputService:ConnectEvent(KeyDownEvent, self.OnKeyDown)
⚠️
handler
vs
method void
— do not mix them up
[VERIFIED]
Declaration keywordUseWiring
handler Name(Ev event)
Static subscription — paired with the
@EventSender(...)
annotation. Engine wires it automatically.
Wired by declaration alone
method void Name(Ev event)
Dynamic callback — wired at runtime via
ConnectEvent(EvType, self.Name)
self.Entity:ConnectEvent(...)
or
_InputService:ConnectEvent(...)
Passing a
handler
as the callback to
ConnectEvent
compiles but never fires (E-V1-5). Conversely, putting a
method void
underneath an
@EventSender
won't get statically wired. Rules:
  • If
    @EventSender
    is also present → use
    handler
    .
  • If you will subscribe with
    ConnectEvent(...)
    → use
    method void
    .
lua
-- 绑定(在 OnBeginPlay 中)——返回类型为 EventHandlerBase 对象
local eventHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)
-- 解绑(必须在 OnEndPlay 中执行)
entity:DisconnectEvent(ButtonClickEvent, eventHandler)
⚠️
ConnectEvent
需在 Entity / Logic / Service 上调用 — 不可在 Component 上调用
[VERIFIED]
只有三种类型暴露
ConnectEvent
/
DisconnectEvent
  • Entity
    Misc/Entity.d.mlua:96-104
  • Logic
    Logic/Logic.d.mlua:7-15
  • Service
    Service/Service.d.mlua:8-16
Component
父类(
Component.d.mlua
)的公共成员仅包括
Enable
/
Entity
/
IsClient
/
IsServer
。因此任何 Component(包括
ButtonComponent
TriggerComponent
)都没有自己的
ConnectEvent
。Component 仅负责触发事件;它们所附加的实体负责接收事件。
lua
-- ❌ 错误 — Component 没有 ConnectEvent。运行时会产生空引用。
self.Entity.ButtonComponent:ConnectEvent(ButtonClickEvent, self.OnClick)

-- ✅ 正确 — 通过 Component 所属的实体订阅
self.clickHandler = self.Entity:ConnectEvent(ButtonClickEvent, self.OnClick)

-- ✅ 服务事件:在服务上订阅
self.keyHandler = _InputService:ConnectEvent(KeyDownEvent, self.OnKeyDown)
⚠️
handler
vs
method void
— 请勿混淆
[VERIFIED]
声明关键字用途绑定方式
handler Name(Ev event)
静态订阅 — 与
@EventSender(...)
注解配合使用。引擎自动完成绑定。
仅通过声明即可完成绑定
method void Name(Ev event)
动态回调 — 在运行时通过
ConnectEvent(EvType, self.Name)
绑定
self.Entity:ConnectEvent(...)
_InputService:ConnectEvent(...)
handler
作为
ConnectEvent
的回调参数会编译通过,但永远不会触发(E-V1-5)。反之,在
@EventSender
下方定义
method void
也不会被静态绑定。规则:
  • 如果同时存在
    @EventSender
    → 使用
    handler
  • 如果将通过
    ConnectEvent(...)
    订阅 → 使用
    method void

Defining a CustomEvent — typed class style

定义 CustomEvent — 类型化类风格

The only way to author a CustomEvent is the typed-class form:
@Event
+
extends EventType
with
property
fields. There is no inline factory like
CustomEvent("Name", { ... })
— that signature does not exist in mlua. Always declare an event class.
Define the event:
lua
@Event
script UserLogEvent extends EventType
    property string userId = ""
    property number logTime = 0
end
Dispatch from a Logic via
SendEvent
:
lua
@Logic
script UserLogService extends Logic
    method void LogIn(string userId)
        local userLog = UserLogEvent()
        userLog.userId = userId
        userLog.logTime = DateTime.UtcNow.Elapsed
        _UserService:SendEvent(userLog)
    end
end
Receive via
ConnectEvent
on the entity. The first argument is the event Type (the class itself), and the callback must reference an existing method:
lua
@Component
script UserLogComponent extends Component
    method void OnBeginPlay()
        self.Entity:ConnectEvent(UserLogEvent, self.OnUserLogEvent)
    end

    method void OnUserLogEvent(UserLogEvent event)
        log("User Id: " .. event.userId .. ", Login Time: " .. tostring(event.logTime))
    end
end
编写 CustomEvent 的唯一方式是使用类型化类形式:
@Event
+
extends EventType
并定义
property
字段。mlua 中不存在类似
CustomEvent("Name", { ... })
的内联工厂方法——该签名不存在。请始终声明事件类。
定义事件:
lua
@Event
script UserLogEvent extends EventType
    property string userId = ""
    property number logTime = 0
end
从 Logic 中通过
SendEvent
触发:
lua
@Logic
script UserLogService extends Logic
    method void LogIn(string userId)
        local userLog = UserLogEvent()
        userLog.userId = userId
        userLog.logTime = DateTime.UtcNow.Elapsed
        _UserService:SendEvent(userLog)
    end
end
通过实体上的
ConnectEvent
接收。第一个参数是事件类型(类本身),回调必须引用已存在的方法:
lua
@Component
script UserLogComponent extends Component
    method void OnBeginPlay()
        self.Entity:ConnectEvent(UserLogEvent, self.OnUserLogEvent)
    end

    method void OnUserLogEvent(UserLogEvent event)
        log("用户 ID: " .. event.userId .. ", 登录时间: " .. tostring(event.logTime))
    end
end

Sending events

发送事件

Instantiate the typed event class, set its
property
fields, and pass the instance to
SendEvent
:
lua
@Event
script DamageDealtEvent extends EventType
    property number amount = 0
end

-- Send to my own entity
local dmg = DamageDealtEvent()
dmg.amount = 50
self.Entity:SendEvent(dmg)

-- Send to another entity
local heal = HealEvent()
heal.amount = 20
targetEntity:SendEvent(heal)
实例化类型化事件类,设置其
property
字段,然后将实例传递给
SendEvent
lua
@Event
script DamageDealtEvent extends EventType
    property number amount = 0
end

-- 发送给自身实体
local dmg = DamageDealtEvent()
dmg.amount = 50
self.Entity:SendEvent(dmg)

-- 发送给其他实体
local heal = HealEvent()
heal.amount = 20
targetEntity:SendEvent(heal)

NativeEvent vs CustomEvent

NativeEvent vs CustomEvent

NativeEventCustomEvent
DefinitionBuilt into the engine (
.d.mlua
)
User-defined via
@Event ... extends EventType
ExamplesHitEvent, ButtonClickEvent, StateChangedEventUserLogEvent, DamageDealtEvent (any class you declare)
ParametersFixed (see per-event spec)
property
fields you declare on the class
Reference
Environment/NativeScripts/Event/
User code
NativeEventCustomEvent
定义内置在引擎中(
.d.mlua
用户通过
@Event ... extends EventType
定义
示例HitEvent, ButtonClickEvent, StateChangedEventUserLogEvent, DamageDealtEvent(你声明的任意类)
参数固定(查看每个事件的规范)你在类上声明的
property
字段
引用位置
Environment/NativeScripts/Event/
用户代码

Common NativeEvent parameters

常见 NativeEvent 参数

lua
-- HitEvent
event.TotalDamage      -- number: total damage
event.AttackerEntity   -- Entity: attacker
event.DamageType       -- DamageType: damage type

-- ButtonClickEvent
-- (no parameters; emitted by the entity)

-- StateChangedEvent
event.PrevState        -- string: previous state
event.CurState         -- string: current state

-- PlayerActionEvent
event.ActionName       -- string: action name

lua
-- HitEvent
event.TotalDamage      -- number: 总伤害
event.AttackerEntity   -- Entity: 攻击者
event.DamageType       -- DamageType: 伤害类型

-- ButtonClickEvent
-- 无参数;由实体触发

-- StateChangedEvent
event.PrevState        -- string: 之前的状态
event.CurState         -- string: 当前状态

-- PlayerActionEvent
event.ActionName       -- string: 动作名称

9. Validity Checks and Method Override

9. 有效性检查与方法重写

Validity checks

有效性检查

lua
-- Entity validity (deletion / inactive check)
if isvalid(entity) then
    -- safe to access
end

-- Confirm a component exists
local comp = self.Entity.SomeComponent
if isvalid(comp) then
    -- only when the component is present
end
Caution: accessing a deleted entity is a runtime error. Always use
isvalid()
.
lua
-- 实体有效性(删除 / 非激活检查)
if isvalid(entity) then
    -- 可安全访问
end

-- 确认组件存在
local comp = self.Entity.SomeComponent
if isvalid(comp) then
    -- 仅当组件存在时执行
end
注意:访问已删除的实体会导致运行时错误。请始终使用
isvalid()

Method override

方法重写

  • Inside an
    extends
    -ing script, declaring a
    method
    with the same signature as the parent overrides it.
  • Built-in engine components allow override only for methods without
    ---@sealed
    .
  • Call the parent original via
    __base:MethodName(args)
    (optional, position is flexible).

  • 在继承脚本中,声明与父类签名相同
    method
    即可重写该方法。
  • 内置引擎组件仅允许重写没有
    ---@sealed
    标记的方法。
  • 通过
    __base:MethodName(args)
    调用父类原方法(可选,调用位置灵活)。

10. Input / Click Events — World vs UI (Do Not Confuse)

10. 输入 / 点击事件 — 世界实体 vs UI(请勿混淆)

World touch — two approaches

世界触摸 — 两种方式

ApproachReceiverEvent (+ Hold/Release variants)Connect on
Entity touchAn entity with
TouchReceiveComponent
TouchEvent
entity:ConnectEvent(...)
Screen touchThe whole screen (no component required)
ScreenTouchEvent
_InputService:ConnectEvent(...)
  • Both events carry
    TouchId
    (int32) +
    TouchPoint
    (Vector2, screen coords).
  • Entity touch: control the touch area via
    TouchReceiveComponent
    's
    TouchArea
    /
    Offset
    /
    AutoFitToSize
    . Suited for per-object interactions (NPCs, items).
  • Screen touch: suited for coordinate-based interactions (tower placement, move target). For world coordinates, convert via
    _UILogic:ScreenToWorldPosition(event.TouchPoint)
    . Filter UI clicks with
    _InputService:IsPointerOverUI()
    .
  • If a map touch is not picked up by
    TouchEvent
    : it may be a
    TouchArea
    or raycast-priority issue. Combining
    ScreenTouchEvent
    +
    ScreenToWorldPosition()
    is more robust without configuration.
⚠️ Warning —
BoxCollider2D
/ physics colliders do NOT emit
TouchEvent
[VERIFIED]
If you reason from Unity's
OnMouseDown
/
OnPointerClick
and just attach
BoxCollider2D
/
Rigidbody
/
TriggerComponent
to an entity, no touch input will arrive. In MSW, world-entity touch reception is owned exclusively by
TouchReceiveComponent
(
Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua
EmitTouchEvent
/
EmitTouchHoldEvent
/
EmitTouchReleaseEvent
exist only on this component).
ComponentRoleTouchEvent
BoxCollider2D
,
CircleCollider2D
, Rigidbody/Kinematicbody, etc.
Physics collision / raycast
TriggerComponent
Entity-entity overlap callbacks (
TriggerEnter/Exit
)
TouchReceiveComponent
Touch-input reception
Required setup (
TouchReceiveComponent
):
  • AutoFitToSize = true
    — auto-fits
    TouchArea
    to the
    SpriteRenderer
    /
    AvatarRenderer
    scale. Skips manual math.
  • TouchArea = Vector2(w, h)
    — when set manually, leave 10–20% slack beyond the sprite size (e.g., 1×1 sprite → 1.2×1.2). Too small leads to misaligned hit detection.
  • Offset
    — adjust only when the sprite pivot is not at center.
  • RelayEventToBehind = true
    (default) — forwards the event to entities behind. Set
    false
    only for standalone objects you want to block from passing through.
Symptoms → diagnosis order:
  1. Is
    TouchReceiveComponent
    actually attached to the target entity? (Check
    .map
    /
    .model
    .)
  2. Is
    TouchArea
    zero, or is the entity outside the rendered area?
  3. Is a front-most
    TouchReceiveComponent
    blocking with
    RelayEventToBehind = false
    ?
  4. Did you call
    entity:ConnectEvent(TouchEvent, handler)
    in
    OnBeginPlay
    and store the handler in
    property any
    ? (If unstored, GC will collect it.)
Selection rule: "Which entity was touched" →
TouchEvent
; "Where on the screen was touched (coords)" →
ScreenTouchEvent
.
方式接收者事件(含长按/释放变体)绑定位置
实体触摸附加了
TouchReceiveComponent
的实体
TouchEvent
entity:ConnectEvent(...)
屏幕触摸整个屏幕(无需组件)
ScreenTouchEvent
_InputService:ConnectEvent(...)
  • 两种事件均携带
    TouchId
    (int32) +
    TouchPoint
    (Vector2,屏幕坐标)。
  • 实体触摸:通过
    TouchReceiveComponent
    TouchArea
    /
    Offset
    /
    AutoFitToSize
    控制触摸区域。适用于逐对象交互(NPC、物品)。
  • 屏幕触摸:适用于基于坐标的交互(塔防放置、移动目标)。如需转换为世界坐标,使用
    _UILogic:ScreenToWorldPosition(event.TouchPoint)
    。可通过
    _InputService:IsPointerOverUI()
    过滤 UI 点击。
  • 如果地图触摸未被
    TouchEvent
    捕获
    :可能是
    TouchArea
    或射线检测优先级问题。结合使用
    ScreenTouchEvent
    +
    ScreenToWorldPosition()
    无需配置,更可靠。
⚠️ 警告 —
BoxCollider2D
/ 物理碰撞器不会触发
TouchEvent
[VERIFIED]
如果你按照 Unity 的
OnMouseDown
/
OnPointerClick
逻辑,仅为实体附加
BoxCollider2D
/
Rigidbody
/
TriggerComponent
不会收到任何触摸输入。在 MSW 中,世界实体的触摸接收完全由
TouchReceiveComponent
负责(
Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua
EmitTouchEvent
/
EmitTouchHoldEvent
/
EmitTouchReleaseEvent
仅存在于该组件)。
组件作用是否触发 TouchEvent
BoxCollider2D
,
CircleCollider2D
, Rigidbody/Kinematicbody 等
物理碰撞 / 射线检测
TriggerComponent
实体间重叠回调(
TriggerEnter/Exit
TouchReceiveComponent
触摸输入接收
必填设置
TouchReceiveComponent
):
  • AutoFitToSize = true
    — 自动将
    TouchArea
    适配到
    SpriteRenderer
    /
    AvatarRenderer
    的缩放尺寸。无需手动计算。
  • TouchArea = Vector2(w, h)
    — 手动设置时,需在精灵尺寸基础上预留 10–20% 的余量(例如 1×1 精灵 → 1.2×1.2)。尺寸过小会导致点击检测错位。
  • Offset
    — 仅当精灵 pivot 不在中心时调整。
  • RelayEventToBehind = true
    (默认)——将事件转发到后方实体。仅当你希望阻止事件穿透时,设置为
    false
症状 → 诊断顺序
  1. 目标实体是否确实附加了
    TouchReceiveComponent
    ?(检查
    .map
    /
    .model
    。)
  2. TouchArea
    是否为零,或实体是否在渲染区域外?
  3. 最前方的
    TouchReceiveComponent
    是否设置了
    RelayEventToBehind = false
    并阻止了事件?
  4. 是否在
    OnBeginPlay
    中调用了
    entity:ConnectEvent(TouchEvent, handler)
    并将处理器存储在
    property any
    中?(如果未存储,会被 GC 回收。)
选择规则:需要知道“触摸了哪个实体” → 使用
TouchEvent
;需要知道“触摸了屏幕的哪个位置(坐标)” → 使用
ScreenTouchEvent

Clicks in UI

UI 中的点击

  • For UI entities, use the
    ButtonComponent
    +
    ButtonClickEvent
    pattern.
  • UI lives under
    ./ui/*.ui
    and the
    ui
    tree of the hierarchy.
  • 对于 UI 实体,使用
    ButtonComponent
    +
    ButtonClickEvent
    模式。
  • UI 位于
    ./ui/*.ui
    及层级结构中的
    ui
    分支下。

What goes wrong if you mix them up

混淆两者会导致的问题

  • Putting only UI button events on a world object, or only world-touch components on UI, results in nothing happening.
  • Even if the requirement says "button," first decide whether it is a world object or a UI panel button.

  • 在世界对象上仅设置 UI 按钮事件,或在 UI 上仅设置世界触摸组件,会导致无任何响应
  • 即使需求是“按钮”,也要先确定它是世界对象还是 UI 面板按钮

11. Map Context and Entity Spawning

11. 地图上下文与实体生成

Prefer
Entity.CurrentMap

优先使用
Entity.CurrentMap

  • For map-dependent logic,
    Entity.CurrentMap
    is safer and more readable.
  • 对于依赖地图的逻辑,
    Entity.CurrentMap
    更安全且可读性更高。

Runtime entity spawning needs a model

运行时生成实体需要模型

  • To create an entity at runtime, use
    _SpawnService
    (
    SpawnByModelId
    /
    SpawnByEntity
    , etc.). A model (template) to spawn from must already exist.
  • If a brand-new kind of object is needed, follow this order: design a
    .model
    → place it in a map or write spawn code
    .
  • 要在运行时创建实体,使用
    _SpawnService
    SpawnByModelId
    /
    SpawnByEntity
    等方法)。必须已存在用于生成的模型(模板)
  • 如果需要全新类型的对象,请按照以下顺序操作:设计
    .model
    → 将其放置在地图中或编写生成代码

parent
parameter caveats

parent
参数注意事项

  • SpawnByModelId
    's
    parent
    is required
    — there is no default, so you must pass a map entity. Passing
    nil
    leaves the entity orphaned (not parented), and the engine logs
    NativeIssue_NotRecommendedValue
    .
  • In contrast,
    SpawnByEntity
    defaults
    parent = nil
    and may be omitted — the two methods have different signatures.
  • Get the map entity via
    self.Entity.CurrentMap
    or
    _EntityService:GetEntitiesByPath("/maps/MapName")
    .
  • SpawnByModelId
    parent
    是必填参数
    ——无默认值,因此必须传入地图实体。传入
    nil
    会导致实体成为孤儿(无父对象),引擎会记录
    NativeIssue_NotRecommendedValue
  • 相比之下,
    SpawnByEntity
    的默认
    parent = nil
    ,可省略——两种方法的签名不同
  • 通过
    self.Entity.CurrentMap
    _EntityService:GetEntitiesByPath("/maps/MapName")
    获取地图实体。

Body components and direct Position writes

Body 组件与直接写入 Position

  • On entities with a Body component (Kinematicbody/Rigidbody/Sideviewbody), setting
    TransformComponent.WorldPosition
    directly will be overwritten by the physics engine on the next frame. This is a top cause of "movement doesn't work."
  • Per-frame movement:
    MovementComponent:MoveToDirection(direction, deltaTime)
    .
  • Instant teleport:
    MovementComponent:SetPosition(pos)
    or the corresponding Body's
    SetPosition(pos)
    .
  • Direct
    TransformComponent.WorldPosition
    writes are limited to entities without a Body (decorations, effects, etc.).
  • Do NOT remove the Body component as a workaround — tile collision and enter/leave events all become disabled, and the engine logs
    NativeIssue_MissingComponent
    .

  • 对于附加了 Body 组件(Kinematicbody/Rigidbody/Sideviewbody)的实体,直接设置
    TransformComponent.WorldPosition
    在下一帧被物理引擎覆盖。这是“移动不生效”的主要原因之一。
  • 逐帧移动:
    MovementComponent:MoveToDirection(direction, deltaTime)
  • 瞬间传送:
    MovementComponent:SetPosition(pos)
    或对应 Body 的
    SetPosition(pos)
  • 直接写入
    TransformComponent.WorldPosition
    仅适用于无 Body 的实体(装饰、特效等)。
  • 请勿删除 Body 组件作为 workaround——瓦片碰撞和进入/离开事件都会失效,引擎会记录
    NativeIssue_MissingComponent

12. Frequently Used Services / Logic

12. 常用服务 / 逻辑

All services and logic are accessed via
_Name
(underscore + type name). Only the most common ones are listed.
Service / LogicPurpose
_SpawnService
Spawn / despawn entities (
SpawnByModelId
,
SpawnByEntity
,
Despawn
)
_TimerService
Timers (
SetTimer
,
SetTimerRepeat
,
ClearTimer
)
_EntityService
Entity lookup (
GetEntity
,
GetEntities
,
GetEntitiesByPath
)
_InputService
Input state queries; receives
ScreenTouchEvent
_ResourceService
Look up resource RUIDs
_DataStorageService
Persistent data (player saves) — ⚠️ Credit-billed. Do not call in
OnUpdate
/ short timers; use
Batch*
in loops. Details: references/datastorage.md
_UtilLogic
Random, time, string, and math utilities
_TweenLogic
Tween animations (MoveTo, ScaleTo, RotateTo)
_UILogic
UI coordinate conversions (e.g., ScreenToWorldPosition) — ClientOnly
For the full list, read the
.d.mlua
files directly:
./Environment/NativeScripts/Service/
(46 files) and
./Environment/NativeScripts/Logic/
(9 files). For domain details, search via
msw-search
.

所有服务和逻辑均通过
_Name
(下划线 + 类型名称)访问。以下仅列出最常用的部分。
服务 / 逻辑用途
_SpawnService
生成 / 销毁实体(
SpawnByModelId
,
SpawnByEntity
,
Despawn
_TimerService
计时器(
SetTimer
,
SetTimerRepeat
,
ClearTimer
_EntityService
实体查找(
GetEntity
,
GetEntities
,
GetEntitiesByPath
_InputService
输入状态查询;接收
ScreenTouchEvent
_ResourceService
查找资源 RUID
_DataStorageService
持久化数据(玩家存档) — ⚠️ 按调用次数计费。请勿在
OnUpdate
/ 短计时器中调用;循环中使用
Batch*
方法。详情:references/datastorage.md
_UtilLogic
随机数、时间、字符串和数学工具
_TweenLogic
补间动画(MoveTo, ScaleTo, RotateTo)
_UILogic
UI 坐标转换(例如 ScreenToWorldPosition) — 仅客户端可用
完整列表请直接阅读
.d.mlua
文件:
./Environment/NativeScripts/Service/
(46个文件)和
./Environment/NativeScripts/Logic/
(9个文件)。如需领域细节,使用
msw-search
搜索。

13. Math, Utilities, Reserved Words, Type Annotations

13. 数学、工具、保留字、类型注解

Math / utility examples

数学 / 工具示例

lua
-- Random
local rand = _UtilLogic:RandomDouble()              -- 0.0 ~ 1.0
local randInt = _UtilLogic:RandomIntegerRange(1, 10) -- 1 ~ 10

-- Time
local elapsed = _UtilLogic.ElapsedSeconds           -- elapsed game time

-- Trig
local radian = math.rad(angle)
local x = math.cos(radian) * distance
local y = math.sin(radian) * distance

-- Distance
local diff = targetPos - myPos
local dist = math.sqrt(diff.x * diff.x + diff.y * diff.y)
lua
-- 随机数
local rand = _UtilLogic:RandomDouble()              -- 0.0 ~ 1.0
local randInt = _UtilLogic:RandomIntegerRange(1, 10) -- 1 ~ 10

-- 时间
local elapsed = _UtilLogic.ElapsedSeconds           -- 游戏运行时间

-- 三角函数
local radian = math.rad(angle)
local x = math.cos(radian) * distance
local y = math.sin(radian) * distance

-- 距离计算
local diff = targetPos - myPos
local dist = math.sqrt(diff.x * diff.x + diff.y * diff.y)

mlua utility classes

mlua 工具类

Collections / utility types beyond the Lua standard library:
ClassPurposeNotes
List
Dynamic array (1-based index)Add / remove / search / sort
ReadOnlyList
Read-only arrayFor data protection
SyncList
Network-synced arrayAuto-sync server↔client
Dictionary
Hash table (key-value)Fast lookup by unique key
ReadOnlyDictionary
Read-only hash tableFor data protection
SyncDictionary
Network-synced hash tableAuto-sync server↔client
DateTime
Date/timeFormat-string support
TimeSpan
Time spanDays/hours/minutes/seconds/milliseconds
Regex
Regular expressionMatch / search / replace
Translator
LocalizationCurrent language, translated text lookup
Quaternion
3D rotationAvoids gimbal lock; smooth rotations
Vector2Int
Integer 2D vectorUseful for grid coords
FastVector2/3
,
FastColor
High-performance vector / colorIn-place ops without new objects (when perf matters)
Item
Inventory itemQuantity, icon RUID, data-table linkage
For detailed APIs, browse
Environment/NativeScripts/
or query the
msw-search
skill.
Lua 标准库之外的集合 / 工具类型:
用途说明
List
动态数组(1-based 索引)添加 / 删除 / 搜索 / 排序
ReadOnlyList
只读数组用于数据保护
SyncList
网络同步数组自动同步服务器↔客户端
Dictionary
哈希表(键值对)通过唯一键快速查找
ReadOnlyDictionary
只读哈希表用于数据保护
SyncDictionary
网络同步哈希表自动同步服务器↔客户端
DateTime
日期/时间支持格式字符串
TimeSpan
时间间隔天/小时/分钟/秒/毫秒
Regex
正则表达式匹配 / 搜索 / 替换
Translator
本地化当前语言、翻译文本查找
Quaternion
3D 旋转避免万向节锁;平滑旋转
Vector2Int
整数 2D 向量适用于网格坐标
FastVector2/3
,
FastColor
高性能向量 / 颜色性能要求高时,可直接操作对象而非创建新对象
Item
物品数量、图标 RUID、数据表关联
详细 API 请浏览
Environment/NativeScripts/
或使用
msw-search
技能查询。

Type annotations (code hints)

类型注解(代码提示)

---@
-style annotations help editors with autocomplete and type inference. They have no runtime effect — editor assist only.
lua
---@type string
local name = GetPlayerName()

---@type table<string, Entity>
local entityMap = {}

---@param target Entity
---@param damage integer
---@return boolean
local function ApplyDamage(target, damage)
    return target ~= nil
end
---@
风格的注解可帮助编辑器实现自动补全和类型推断。无运行时效果——仅用于编辑器辅助。
lua
---@type string
local name = GetPlayerName()

---@type table<string, Entity>
local entityMap = {}

---@param target Entity
---@param damage integer
---@return boolean
local function ApplyDamage(target, damage)
    return target ~= nil
end

Reserved words

保留字

Using mlua keywords as local variable names produces a parse error.
Forbidden as variable names:
handler
,
property
,
method
,
script
,
end
,
extends
,
self
,
nil
,
true
,
false
.
lua
-- Wrong
local handler = entity:ConnectEvent(...)    -- 'handler' is a reserved word

-- Correct (ConnectEvent returns: EventHandlerBase object)
local eventHandler = entity:ConnectEvent(...)
local connHandler = entity:ConnectEvent(...)

将 mlua 关键字用作局部变量名会导致解析错误。
禁止用作变量名
handler
,
property
,
method
,
script
,
end
,
extends
,
self
,
nil
,
true
,
false
lua
-- 错误
local handler = entity:ConnectEvent(...)    -- 'handler' 是保留字

-- 正确(ConnectEvent 返回:EventHandlerBase 对象)
local eventHandler = entity:ConnectEvent(...)
local connHandler = entity:ConnectEvent(...)

14. External Tooling

14. 外部工具

NeedSkill
Maker MCP (
refresh
,
logs
,
play
,
stop
,
screenshot
, etc.)
msw-general
MCP wiring,
.mcp.json
, API key setup
msw-env-setup
Descriptions, examples, and implementation guides not in
.d.mlua
msw-search
Core debug order: build logs first → play → logs → stop → fix → diagnose → refresh → repeat.

需求技能
Maker MCP(
refresh
,
logs
,
play
,
stop
,
screenshot
等)
msw-general
MCP 配置、
.mcp.json
、API 密钥设置
msw-env-setup
.d.mlua
中未包含的说明、示例和实现指南
msw-search
核心调试顺序:先检查构建日志 → play → logs → stop → 修复 → 诊断 → refresh → 重复

15. Script Authoring Workflow (essentials)

15. 脚本编写工作流(要点)

  1. Search: scan
    ./RootDesk/MyDesk/**/*.mlua
    → if a similar script exists, modify it first; do not create new.
  2. Verify spec: Read the
    .d.mlua
    → if insufficient, use
    msw-search
    .
  3. Decide path (see §1.2 — folder structure is mandatory): pick a feature/category subfolder. Reuse one if it already exists; otherwise create a new feature-named folder. Final path must look like
    ./RootDesk/MyDesk/<FeatureFolder>/<Name>.mlua
    never write directly to
    ./RootDesk/MyDesk/
    root
    .
  4. Write: create the file at the path chosen in step 3.
  5. Validate:
    mlua-diagnose
    hook auto-runs on save — fix until errors hit zero.
  6. Refresh: Maker MCP
    refresh
    (if MCP is not connected, point the user to the button).
  7. (If needed)
    play
    → reproduce →
    logs
    stop
    .
Delete / rename: still requires
refresh
after the file change. Also clean up references in
.model
/
.map
.

  1. 搜索:扫描
    ./RootDesk/MyDesk/**/*.mlua
    → 如果存在相似脚本,优先修改现有脚本;不要创建新脚本。
  2. 验证规范:阅读
    .d.mlua
    → 如果信息不足,使用
    msw-search
  3. 确定路径(见第1.2节 — 文件夹结构为必填项):选择功能/类别子文件夹。如果已有合适的文件夹则复用;否则创建新的功能命名文件夹。最终路径必须为
    ./RootDesk/MyDesk/<FeatureFolder>/<Name>.mlua
    绝对禁止直接写入
    ./RootDesk/MyDesk/
    根目录
  4. 编写:在步骤3确定的路径下创建文件。
  5. 验证:保存时
    mlua-diagnose
    钩子自动运行 — 修复直到错误数为零。
  6. 刷新:调用 Maker MCP 的
    refresh
    (如果未连接 MCP,引导用户点击按钮)。
  7. (如有需要)
    play
    → 复现问题 →
    logs
    stop
删除 / 重命名:文件修改后仍需调用
refresh
。同时清理
.model
/
.map
中的引用。

16. Attaching Scripts (Components) to Entities

16. 为实体附加脚本(Components)

  • Attach to a model template (
    .model
    ) — recommended
    : add the script-component entry to the
    Components
    array. Map instances inherit it automatically (must follow the
    .model
    JSON schema).
  • Only for a specific map instance: add the component to the matching entity definition inside the
    .map
    file.
  • Global models (e.g.,
    DefaultPlayer
    under
    ./Global/
    ): affect the entire project — confirm the blast radius before changing.
In all cases, do NOT edit
.model
/
.map
JSON directly — use the personal builder.

  • 推荐附加到模型模板(
    .model
    :将脚本组件条目添加到
    Components
    数组中。地图实例会自动继承(必须遵循
    .model
    JSON schema)。
  • 仅针对特定地图实例:在
    .map
    文件中对应的实体定义中添加组件。
  • 全局模型(例如
    ./Global/
    下的
    DefaultPlayer
    ):会影响整个项目 — 修改前确认影响范围。
无论哪种情况,请勿直接编辑
.model
/
.map
JSON — 使用个人构建工具。

17. Playtesting and Debugging

17. 游戏测试与调试

The procedure for verifying behavior in play mode in Maker, then narrowing down bugs with runtime logs, screenshots, and simulated input.
For the MCP tool list, play-mode constraints, and refresh rules, see
msw-general
.
在 Maker 的游戏模式中验证行为,然后通过运行时日志、截图和模拟输入定位 bug 的流程。
关于 MCP 工具列表、游戏模式限制和刷新规则,请查看
msw-general

17.1 Always Check Build Logs First (the first step of every playtest)

17.1 始终先检查构建日志(每次测试的第一步)

Before entering play mode, you MUST inspect the build console (build logs) first. If there are build errors, scripts will not load or will behave unexpectedly, making runtime debugging meaningless.
1. After refresh → call logs(category="build")
2. Are there build errors?
   ├─ YES → from the error messages, identify file and line → edit the .mlua → refresh → recheck build logs (repeat until errors are zero)
   └─ NO → enter play → run runtime tests
If you run play with build errors:
  • Scripts with errors fail to load entirely — it behaves as if the component/logic isn't there.
  • Build errors may not appear in the runtime logs, making the cause extremely hard to find.
  • Most "the code looks correct but it doesn't work" reports come from missed build errors.
This step is mandatory in every workflow pattern (general test, regression test, error analysis).
进入游戏模式前,必须先检查构建控制台(构建日志)。如果存在构建错误,脚本将无法加载或行为异常,导致运行时调试毫无意义。
1. 刷新后 → 调用 logs(category="build")
2. 是否存在构建错误?
   ├─ 是 → 根据错误信息定位文件和行号 → 编辑 .mlua → 刷新 → 重新检查构建日志(重复直到错误数为零)
   └─ 否 → 进入游戏 → 运行测试
如果带着构建错误进入游戏模式
  • 存在错误的脚本完全无法加载——表现为组件/逻辑不存在。
  • 构建错误可能不会出现在运行时日志中,导致问题原因极难查找
  • 大多数“代码看起来正确但无法运行”的反馈都源于遗漏了构建错误。
此步骤在所有工作流模式中都是必填项(常规测试、回归测试、错误分析)。

17.2 Error Classification Table (by log / symptom)

17.2 错误分类表(按日志 / 症状)

When reading
logs
(and the script stack), do a first-pass classification with the table below.
ClassCommon signsWhere to look
Script errorStack trace with file name and line numberThe exact line in the
.mlua
; event/timing order
nil reference
attempt to index a nil value
, crash right before a field access
Init order,
isvalid
, 1-frame timing right after Spawn
component missingComponent field is nil;
GetComponent
fails
Components
array in
.model
; typos in name/path
network / syncOnly client breaks; values mismatch; values converge after a delay
@Sync
, only-on-server changes,
ExecSpace
, RPC flow
To narrow down a cause: if logs alone are inconclusive, add
log()
outputs to the
.mlua
to inspect the relevant entity / component / property state.
查看
logs
(及脚本堆栈)时,先通过下表进行初步分类
类别常见迹象排查方向
脚本错误堆栈跟踪包含文件名和行号
.mlua
中的具体行;事件/时序顺序
空引用
attempt to index a nil value
,字段访问前崩溃
初始化顺序、
isvalid
、生成后的1帧时序问题
组件缺失组件字段为 nil;
GetComponent
失败
.model
中的
Components
数组;名称/路径拼写错误
网络 / 同步问题仅客户端出错;值不匹配;值延迟后一致
@Sync
、仅服务器端的修改、
ExecSpace
、RPC 流程
定位原因:如果仅靠日志无法确定,可在
.mlua
中添加
log()
输出,检查相关实体 / 组件 / 属性的状态。

17.3 Test-Result Report Format

17.3 测试结果报告格式

When a playtest ends, summarize briefly in this format.
  1. Scenario name: one-line description of what was verified.
  2. Environment: map / mode (if known); whether a
    refresh
    happened before playing.
  3. Steps: a summary of the execution order — input simulation, Lua runs, etc.
  4. Result: Pass / Fail / Blocked (e.g., couldn't enter play).
  5. Evidence: one or two key lines quoted from
    logs
    ; whether a screenshot exists (only if the user asked for one).
  6. Next action: candidate files to edit, repro conditions, whether to use
    clear_logs
    on the next run.
测试结束后,按以下格式简要总结。
  1. 场景名称:一行描述验证的内容。
  2. 环境:地图 / 模式(如已知);测试前是否执行了
    refresh
  3. 步骤:执行顺序总结——输入模拟、Lua 运行等。
  4. 结果通过 / 失败 / 阻塞(例如无法进入游戏模式)。
  5. 证据:从
    logs
    中引用一到两行关键内容;是否存在截图(仅当用户要求时)。
  6. 下一步操作:候选修改文件、复现条件、下次运行是否需要使用
    clear_logs

17.4 Workflow Patterns

17.4 工作流模式

1) General playtest

1) 常规游戏测试

  1. Prepare scripts / maps / models in edit mode.
  2. If the workspace was changed, call MCP
    refresh
    .
  3. Check build errors with
    logs(category="build")
    → if any, fix and refresh until clean.
  4. MCP
    play
    → enter play mode.
  5. As needed, use
    keyboard_input
    /
    mouse_input
    to reproduce input or UI clicks.
  6. As needed, use
    logs
    to inspect runtime state.
  7. As needed, use
    logs(category="runtime")
    to inspect runtime logs.
  8. MCP
    stop
    → return to edit mode.
  1. 在编辑模式下准备脚本 / 地图 / 模型。
  2. 如果工作区有变更,调用 MCP
    refresh
  3. 使用
    logs(category="build")
    检查构建错误 → 如有错误,修复并刷新直到无错误。
  4. 调用 MCP
    play
    → 进入游戏模式。
  5. 根据需要,使用
    keyboard_input
    /
    mouse_input
    复现输入或 UI 点击。
  6. 根据需要,使用
    logs
    检查运行时状态。
  7. 根据需要,使用
    logs(category="runtime")
    检查运行时日志。
  8. 调用 MCP
    stop
    → 返回编辑模式。

2) Regression test loop (fix loop)

2) 回归测试循环(修复循环)

  1. Edit files based on the previous failure cause.
  2. refresh
    .
  3. Check build errors with
    logs(category="build")
    → if any, fix and refresh until clean.
  4. For a clean repro, call
    clear_logs
    then
    play
    .
  5. Replay the same scenario via input / Lua.
  6. Use
    logs(category="runtime")
    to confirm regression status.
  7. stop
    , then edit again if needed.
  1. 根据上一次失败原因编辑文件。
  2. refresh
  3. 使用
    logs(category="build")
    检查构建错误 → 如有错误,修复并刷新直到无错误。
  4. 为了干净复现,调用
    clear_logs
    然后
    play
  5. 通过输入 / Lua 重放相同场景。
  6. 使用
    logs(category="runtime")
    确认回归状态。
  7. stop
    ,如有需要再次编辑。

3) Error-analysis workflow

3) 错误分析工作流

  1. First check build errors with
    logs(category="build")
    — fix them before any runtime analysis.
  2. clear_logs
    (optional) →
    play
    .
  3. Reproduce the issue via
    keyboard_input
    /
    mouse_input
    / in-game manipulation.
  4. Collect
    logs(category="runtime")
    and map them to the error classification table above.
  5. If logs are insufficient, add
    log()
    outputs in the
    .mlua
    to inspect entity / property state.
  6. After
    stop
    , fix the code /
    .model
    / sync.
  7. refresh
    recheck build logs
    play
    to re-verify.
  1. 首先使用
    logs(category="build")
    检查构建错误
    — 在进行任何运行时分析前修复这些错误。
  2. clear_logs
    (可选)→
    play
  3. 通过
    keyboard_input
    /
    mouse_input
    / 游戏内操作复现问题。
  4. 收集
    logs(category="runtime")
    并对照上述错误分类表分析。
  5. 如果日志信息不足,在
    .mlua
    中添加
    log()
    输出,检查实体 / 属性状态。
  6. stop
    后,修复代码 /
    .model
    / 同步逻辑。
  7. refresh
    重新检查构建日志
    play
    重新验证。

4) Runtime Lua debugging

4) 运行时 Lua 调试

  1. Add
    log()
    calls in the
    .mlua
    for the values you want to inspect.
  2. If you don't know the API, look it up first: search
    .d.mlua
    msw-search
    .
  3. refresh
    play
    to enter play mode.
  4. Collect output via
    logs
    and analyze.
  5. After analysis,
    stop
    → edit → repeat.
  1. .mlua
    中添加
    log()
    调用,检查需要查看的值。
  2. 如果不知道 API,先查找:搜索
    .d.mlua
    → 使用
    msw-search
  3. refresh
    play
    进入游戏模式。
  4. 通过
    logs
    收集输出并分析。
  5. 分析完成后,
    stop
    → 编辑 → 重复。

17.5 Final Verification Before Completion (PASS/FAIL)

17.5 完成前的最终验证(通过/失败)

Before reporting "done" to the user, you must pass the following checklist:
Principle: "No errors ≠ Pass." You need positive
log()
-based evidence
that the intended logic actually executed.
Full checklist: references/verify-checklist.md (Step 1 Runtime Execution → Step 2 Code Review → Step 3 Log Evidence → Step 4 PASS/FAIL Verdict)
向用户报告“完成”前,必须通过以下检查清单:
原则:“无错误 ≠ 通过”。你需要基于
log()
的积极证据
证明预期逻辑确实已执行。
完整检查清单:references/verify-checklist.md (步骤1 运行时执行 → 步骤2 代码审查 → 步骤3 日志证据 → 步骤4 通过/失败 verdict)

17.6 Related Skills

17.6 相关技能

  • msw-general
    : MCP tools, screenshot/logs policy, refresh rules, workspace and hierarchy.
  • msw-general
    :MCP 工具、截图/日志策略、刷新规则、工作区和层级结构。