msw-scripting
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMSW 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 , you must search under
.mluafor an existing script with the same or similar purpose../RootDesk/MyDesk/ - 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 must be created, organize it under a feature/category subfolder. Do not drop scripts directly into .
.mlua./RootDesk/MyDesk/Required path shape: (or a deeper nested path when the feature has sub-systems).
./RootDesk/MyDesk/<FeatureFolder>/<ScriptName>.mlua- Reuse an existing subfolder if one already fits (e.g., ,
Player/,UI/,Combat/,Tower/). Run a glob/list onInventory/first to see what's already there../RootDesk/MyDesk/ - 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 , not at the root, so future additions have a home.
<FeatureFolder>/ - Naming: folder = feature noun (,
TowerDefense/); file = role-specific name reusing the feature noun where helpful (Quest/,TowerSpawnerLogic.mlua).QuestTrackerComponent.mlua - Do not create generic catch-all folders like ,
Scripts/,Misc/,Common/,New/. Pick a real feature name. If you genuinely have a project-wide utility, place it under a specific utility folder such astemp/orUtil/only if that pattern already exists in the project.Shared/
Why: a flatquickly 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.MyDesk/
当无法扩展现有脚本而必须创建新 文件时,需将其组织在功能/类别子文件夹下。不得直接将脚本放入 根目录。
.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/
原因:平铺的目录会迅速变得难以搜索,导致“创建前先搜索”规则(1.1)无法执行。一致的功能文件夹布局能让后续的智能体(及人类开发者)轻松发现已有的内容。MyDesk/
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 definition file directly (exact signatures):
The full engine API is defined as files under :
.d.mlua.d.mlua./Environment/NativeScripts/| Folder | Contents | Files | Example |
|---|---|---|---|
| Engine components | 104 | |
| System services | 46 | |
| Event types | 202 | |
| Built-in logic | 9 | |
| Enumerations | 118 | |
| Utility types | 135 | |
You know the API name → Read ./Environment/NativeScripts/{folder}/{name}.d.mlua
You don't know the name → Grep keywords in the NativeScripts folderMethod 2 — skill (detailed descriptions, examples, implementation guides):
When only contains signatures and lacks explanation, search for parameter semantics, code examples, related APIs, and implementation guides.
msw-search.d.mluaRequired order: confirm signature in→ (if needed) look up details via.d.mlua→ write code → LSP diagnose runs automatically (PostToolUse hook).msw-search
如果猜测 MSW API 的名称、参数或返回类型,调用会在运行时静默失败。编写代码前,请通过以下方法之一验证规范。
方法 1 — 直接读取 定义文件(精确签名):
完整的引擎 API 定义为 下的 文件:
.d.mlua./Environment/NativeScripts/.d.mlua| 文件夹 | 内容 | 文件数量 | 示例 |
|---|---|---|---|
| 引擎组件 | 104 | |
| 系统服务 | 46 | |
| 事件类型 | 202 | |
| 内置逻辑 | 9 | |
| 枚举类型 | 118 | |
| 工具类型 | 135 | |
已知 API 名称 → 读取 ./Environment/NativeScripts/{folder}/{name}.d.mlua
未知 API 名称 → 在 NativeScripts 文件夹中搜索关键词方法 2 — 使用 技能(详细说明、示例、实现指南):
当 仅包含签名而缺乏解释时,可搜索参数语义、代码示例、相关 API 及实现指南。
msw-search.d.mlua必填顺序:在中确认签名 →(若需要)通过.d.mlua查询细节 → 编写代码 → LSP 诊断自动运行(PostToolUse 钩子)。msw-search
1.4 Lint (LSP diagnostics) — required after every script change
1.4 代码检查(LSP 诊断)——每次脚本修改后必须执行
- Whenever you create or modify a file, the
.mluahook runs LSPmlua-diagnoseautomatically and feeds errors back.diagnose - Repeat fix → re-edit until error-severity diagnostics reach zero.
- 每当创建或修改 文件时,
.mlua钩子会自动运行 LSPmlua-diagnose并反馈错误。diagnose - 重复执行修复 → 重新编辑,直到错误级别的诊断结果为零。
1.5 .codeblock
files
.codeblock1.5 .codeblock
文件
.codeblock- files are generated automatically by Maker Refresh.
.codeblock - The agent must never create, edit, or delete them manually.
- 文件由 Maker Refresh 自动生成。
.codeblock - 智能体不得手动创建、编辑或删除此类文件。
1.6 Refresh Timing
1.6 刷新时机
- After creating, modifying, renaming, or deleting a , you must call Maker MCP
.mlua.refresh - cannot run during play mode — first call
refreshto return to edit mode.stop
- 在创建、修改、重命名或删除 文件后,必须调用 Maker MCP 的
.mlua命令。refresh - 无法在游戏模式下运行——需先调用
refresh返回编辑模式。stop
1.7 MSW ≠ Unity — Do Not Reason From Intuition [VERIFIED]
[VERIFIED]1.7 MSW ≠ Unity — 切勿凭直觉推断 [VERIFIED]
[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 intuition | MSW reality | Correct path | Source |
|---|---|---|---|
| | Inject via | |
| Physics colliders and Rigidbody do not emit | World: | |
| Entity-to-entity collisions use a separate | Attach | |
| Attach multiple Rigidbody/Collider freely | One Body per map type (MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody) | Custom models include only the Body that matches the map type | |
| Reference/modify UI objects from server code | UI entities exist only on the client — referencing them from server | Server→UI must go through an | |
| | Inside | |
| | Call from other scripts as | §3.2 |
Rule: any time you catch yourself thinking "Unity does it this way, so MSW probably does too", stop and verify against or the engine source before writing code.
Environment/NativeScripts/*.d.mluamod/MSW 使用 Lua + ECS + MSW 专属的执行空间模型。直接套用 Unity 或通用游戏引擎的模式,代码可能编译通过但在运行时静默失败。常见误区:
| Unity / 通用直觉 | MSW 实际情况 | 正确做法 | 来源 |
|---|---|---|---|
使用 | | 在 Logic 中通过 | |
使用 | 物理碰撞器和 Rigidbody 不会触发 | 世界实体: | |
使用 | 实体间碰撞需使用独立的 | 附加 | |
| 可自由附加多个 Rigidbody/Collider | 每种地图类型对应一个 Body(MapleTile→Rigidbody, RectTile→Kinematicbody, SideViewRectTile→Sideviewbody) | 自定义模型仅包含与地图类型匹配的 Body | |
| 从服务器代码中引用/修改 UI 对象 | UI 实体仅存在于客户端——从服务器 | 服务器→UI 通信必须通过 | |
可在任意位置调用 | | 在 | |
使用 | | 从其他脚本中通过 | 第3.2节 |
规则:每当你觉得“Unity 是这么做的,所以 MSW 可能也是如此”时,请停止操作,先对照 或 引擎源码验证,再编写代码。
Environment/NativeScripts/*.d.mluamod/2. Paths and File Roles
2. 路径与文件角色
| Target | Path | Agent action |
|---|---|---|
| User scripts | | Create / read / modify / delete directly |
| Auto-generated artifacts | | Do not touch (Refresh manages them) |
| Engine API definitions | | Read-only (do not modify) |
| Models (component lists) | | Edit |
| Map instances | | Edit when attaching scripts to entities that exist only inside a map |
| 目标 | 路径 | 智能体操作 |
|---|---|---|
| 用户脚本 | | 直接创建 / 读取 / 修改 / 删除 |
| 自动生成的工件 | | 禁止修改(由 Refresh 管理) |
| 引擎 API 定义 | | 只读(禁止修改) |
| 模型(组件列表) | | 附加脚本时编辑 |
| 地图实例 | | 当需要为仅存在于地图内的实体附加脚本时编辑 |
3. Script Types and Declarations
3. 脚本类型与声明
3.1 Component scripts (@Component
)
@Component3.1 组件脚本(@Component
)
@ComponentScripts attached to an Entity. Use to access the owning entity.
self.Entitylua
@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
endAllowed parents:
- — generic component
Component - — attack system (Shape, AttackFast, OnAttack)
AttackComponent - — hit system (OnHit, HandleHitEvent)
HitComponent
附加到实体的脚本。可使用 访问所属实体。
self.Entitylua
@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 - — 攻击系统(Shape, AttackFast, OnAttack)
AttackComponent - — 受击系统(OnHit, HandleHitEvent)
HitComponent
3.2 Logic scripts (@Logic
)
@Logic3.2 逻辑脚本(@Logic
)
@LogicGlobal 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 (underscore + script name)
_GameManager - Supports properties — server→client sync behaves the same way
@Sync
⚠️ Warning —does NOT have@Logicself.Entity[VERIFIED]The public members of theparent class (Logic) are onlyEnvironment/NativeScripts/Logic/Logic.d.mlua/ConnectEvent/DisconnectEvent/IsClient/IsServer. There is noSendEventproperty,Entity, orGetOwner. The engine source (Owner) confirms this.mod/.../MODLogic.csisself.Entity-exclusive (@Componentinreadonly property Entity Entity).Component.d.mluaCode likeinside 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:self.Entity.xxxlua@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 →; global singleton manager →@Component. A Logic's@Logicruns before Components'OnUpdate.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
⚠️ 警告 —没有@Logicself.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 中编写这类代码会编译通过,但运行时会产生空引用并静默失败。如果 Logic 需要操作特定的世界实体,请使用以下方式之一:self.Entity.xxxlua@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会先于 Components 的OnUpdate执行。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()
endlua
@Component
script PlayerAttack extends AttackComponent
-- 重写 AttackComponent 的方法
-- 通过 __base:MethodName() 调用父类方法
end3.4 Other script types
3.4 其他脚本类型
| Annotation | Purpose | Notes |
|---|---|---|
| Define a custom event type | Declare event parameters |
| Define an item type | Inventory system |
| Behaviour Tree node | AI behavior trees |
| Define a state type | State machines |
| Struct / user type | Composite data types |
| 注解 | 用途 | 说明 |
|---|---|---|
| 定义自定义事件类型 | 声明事件参数 |
| 定义物品类型 | 用于 inventory 系统 |
| 行为树节点 | 用于 AI 行为树 |
| 定义状态类型 | 用于状态机 |
| 结构体 / 用户自定义类型 | 用于复合数据类型 |
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
新增关键字 / 运算符
| Feature | Syntax | Notes |
|---|---|---|
| continue | | Skip to next iteration in a loop (not in standard Lua) |
| Compound assignment | | Multi-assign ( |
| Bitwise operators | | Compound forms also valid: |
| 特性 | 语法 | 说明 |
|---|---|---|
| continue | | 跳过循环的下一次迭代(标准 Lua 中无此关键字) |
| 复合赋值 | | 不支持多变量赋值( |
| 位运算符 | | |
Restrictions
限制
| Restriction | Description |
|---|---|
| No global variables | The |
| No coroutines | Lua's |
| Call parent methods with |
| 限制 | 描述 |
|---|---|
| 禁止全局变量 | 不允许使用 |
| 禁止协程 | Lua 的 |
使用 | 通过 |
Built-in utility functions
内置工具函数
| Function | Signature | Purpose |
|---|---|---|
| | Info-level log output |
| | Warning-level log output |
| | Error-level log output |
| | Pause script execution for the given seconds |
| | Validity check (handles deletion / nil) |
| | Swap a table's keys and values |
| | User profiling scopes |
| 函数 | 签名 | 用途 |
|---|---|---|
| | 信息级日志输出 |
| | 警告级日志输出 |
| | 错误级日志输出 |
| | 暂停脚本执行指定秒数 |
| | 有效性检查(处理对象删除 / nil 情况) |
| | 交换表的键和值 |
| | 用户性能分析作用域 |
5. Lifecycle
5. 生命周期
Component and Logic share the same lifecycle.
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroyPlus, on map transitions: / .
OnMapEnterOnMapLeave| Method | When it fires | Purpose |
|---|---|---|
| Right after creation | Initialize internal variables (rarely used) |
| Game start / activation | Wire up events, start timers, initial setup |
| Every frame | Movement, animation, input handling |
| Entering a map | Per-map initialization |
| Leaving a map | Per-map cleanup |
| Game end / deactivation | Disconnect events, clear timers (mandatory!) |
| On removal | Final cleanup (rarely used) |
Required pattern: anything connected in must be released in .
OnBeginPlayOnEndPlaylua
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
endConnectEventEventHandlerBaseanyintegerDisconnectEventComponent 和 Logic 共享相同的生命周期。
OnInitialize → OnBeginPlay → OnUpdate(delta) → OnEndPlay → OnDestroy此外,地图切换时会触发: / 。
OnMapEnterOnMapLeave| 方法 | 触发时机 | 用途 |
|---|---|---|
| 创建后立即触发 | 初始化内部变量(很少使用) |
| 游戏启动 / 实体激活时 | 绑定事件、启动计时器、初始设置 |
| 每帧触发 | 移动、动画、输入处理 |
| 进入地图时 | 按地图初始化 |
| 离开地图时 | 按地图清理 |
| 游戏结束 / 实体停用 | 解绑事件、清除计时器(必须执行!) |
| 实体被移除时 | 最终清理(很少使用) |
必填模式:在 中绑定的内容必须在 中释放。
OnBeginPlayOnEndPlaylua
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
endConnectEventEventHandlerBaseanyintegerDisconnectEvent6. Execution Space (ExecSpace)
6. 执行空间(ExecSpace)
MSW is a server-client architecture. Every method must declare where it runs.
| ExecSpace | Runs on | Direction | Use case |
|---|---|---|---|
| Server | Server-internal only | Damage calc, state changes, spawning |
| Client | Client-internal only | UI updates, effects, sounds |
| Server | Client→Server RPC | Client requesting the server (attack, item use) |
| Client | Server→Client RPC | Server notifying a client (result UI, effects) |
| All clients | Server→all clients | Global events (announcements, boss spawn) |
| (unspecified) | Caller side | Server→Server, Client→Client | Shared functions executed locally on either side |
MSW 采用服务器-客户端架构。每个方法必须声明其运行位置。
| ExecSpace | 运行位置 | 方向 | 使用场景 |
|---|---|---|---|
| 服务器 | 仅服务器内部 | 伤害计算、状态变更、实体生成 |
| 客户端 | 仅客户端内部 | UI 更新、特效、音效 |
| 服务器 | 客户端→服务器 RPC | 客户端向服务器发起请求(攻击、物品使用) |
| 客户端 | 服务器→客户端 RPC | 服务器通知客户端(结果 UI、特效) |
| 所有客户端 | 服务器→所有客户端 | 全局事件(公告、Boss 生成) |
| (未指定) | 调用方所在端 | 服务器→服务器、客户端→客户端 | 在本地执行的共享函数 |
ExecSpace constraints on lifecycle methods
生命周期方法的 ExecSpace 限制
| Method | Allowed ExecSpace |
|---|---|
| |
| |
| All event handlers | |
| Custom user methods | Any of |
| 方法 | 允许的 ExecSpace |
|---|---|
| 仅 |
| |
| 所有事件处理器 | |
| 自定义用户方法 | |
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
endlua
-- 从客户端调用 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)
-- 在目标用户的客户端执行
endTypical 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
senderUserIdsenderUserId
— 在服务器验证请求发起方
senderUserIdWhen an method is called from the client, the server side can read the caller's UserId from the local variable. This is required for security checks.
@ExecSpace("Server")senderUserIdlua
@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当 方法从客户端调用时,服务器端可通过本地变量 获取调用者的 UserId。这是安全检查的必要步骤。
@ExecSpace("Server")senderUserIdlua
@ExecSpace("Server")
method void RequestBuyItem(integer itemId)
-- 验证请求发起方:确认调用来自本地客户端
if senderUserId ~= self.Entity.PlayerComponent.UserId then
log_warning("拦截来自其他客户端的请求")
return
end
self:ProcessPurchase(itemId)
endSending a Client RPC to a specific client only
仅向特定客户端发送 Client RPC
When the server invokes an function, adding a UserId as the last argument at the call site routes execution to that user's client only.
@ExecSpace("Client")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)
endDo not add the UserId parameter to the function declaration. Add it only as the final argument at the call site.
当服务器调用 函数时,在调用时添加 UserId 作为最后一个参数,可将执行路由到该用户的客户端。
@ExecSpace("Client")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,EntityRefComponentRef - 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,EntityRefComponentRef - 不允许:
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 typeType-name reminder: integers are , floats are .
integernumberlua
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 -- 任意类型类型名称提示:整数类型为 ,浮点数类型为 。
integernumberEntity / 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:
- Look up the target entity's (UUID) in the
id/.mapfile..ui - Hard-code that UUID as a string literal into the property default.
.mlua - Apply the same pattern to multiple-slot references (e.g., array-style references) — inject each as a string.
wp0~wp7
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.
endNote: 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 编辑器中拖拽绑定”)。正确做法:
- 在 /
.map文件中查找目标实体的.ui(UUID)。id - 将该 UUID 作为字符串字面量硬编码到 的属性默认值中。
.mlua - 多槽引用(例如 数组式引用)也采用相同模式——将每个引用作为字符串注入。
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
| Type | After map transition | Use case |
|---|---|---|
| Reference is dropped (nil) | References within the same map |
| Reference persists | When the reference must survive a map transition |
| Reference is dropped | References within the same map |
| Reference persists | When the reference must survive a map transition |
Multi-map games should prefer/EntityRef.ComponentRef/Entityis sufficient for single-map games.Component
| 类型 | 地图切换后 | 使用场景 |
|---|---|---|
| 引用失效(变为 nil) | 同一地图内的引用 |
| 引用保持有效 | 需要跨地图保留的引用 |
| 引用失效 | 同一地图内的引用 |
| 引用保持有效 | 需要跨地图保留的引用 |
多地图游戏应优先使用/EntityRef。单地图游戏使用ComponentRef/Entity即可。Component
Sync annotations
同步注解
| Annotation | Behavior |
|---|---|
| Server → all clients |
| 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 onlyCore rules:
- Server → client, one direction only. Changing a value on the client does NOT propagate back to the server.
@Sync - Sync has network latency — not instantaneous.
- Cannot be synced: ,
any(usetableinstead).SyncTable
@TargetUserSync@Sync| 注解 | 行为 |
|---|---|
| 服务器 → 所有客户端 |
| 服务器 → 仅目标用户的客户端 |
两者均无参数。
lua
@Sync
property number CurrentHp = 100
-- 服务器修改值 → 自动同步到所有客户端
@TargetUserSync
property number PrivateScore = 0
-- 仅同步到所属用户的客户端核心规则:
- 仅服务器→客户端单向同步。在客户端修改 值不会同步回服务器。
@Sync - 同步存在网络延迟——并非即时生效。
- 不可同步:,
any(如需同步表,使用table)。SyncTable
@TargetUserSync@SyncSyncTable 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 — only
tableis synchronized.SyncTable
可同步的表类型。支持数组和字典形式。
lua
-- 数组形式:SyncTable<ValueType>
@Sync
property SyncTable<number> Scores = {}
-- 字典形式:SyncTable<KeyType, ValueType>
@Sync
property SyncTable<string, number> Stats = {}- 需与 配合使用。
@Sync - 与普通 不同——仅
table可被同步。SyncTable
Temporary properties (_T
)
_T临时属性(_T
)
_Tself._Tpropertylua
-- Use for non-synced, frame-local state
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false- Cannot be 'd — server and client each keep their own values.
@Sync - Convenient because no property declaration is needed, but it does NOT show up in the editor inspector.
self._Tpropertylua
-- 用于非同步、帧局部状态
self._T.accumulatedDamage = 0
self._T.lastAttackTime = 0
self._T.isCharging = false- 不可被 ——服务器和客户端各自保留独立的值。
@Sync - 使用方便,无需声明属性,但不会显示在编辑器检查器中。
OnSyncProperty
callback
OnSyncPropertyOnSyncProperty
回调
OnSyncPropertyA callback automatically invoked on the client when a property changes on the server.
@Synclua
@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
endRules:
- Fixed to — ExecSpace cannot be changed.
ClientOnly - : the changed property's name.
name - : the new value.
value - Available on both Component and Logic.
当服务器上的 属性发生变化时,客户端会自动调用此回调。
@Synclua
@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规则:
- 固定为 ——无法更改 ExecSpace。
ClientOnly - :发生变化的属性名称。
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 = nil8. 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
endlua
@EventSender("Self")
handler HandleHitEvent(HitEvent event)
local damage = event.TotalDamage
-- 接收自身实体触发的事件
end@EventSender
parameters
@EventSender@EventSender
参数
@EventSender| 1st parameter | 2nd parameter | Purpose |
|---|---|---|
| none | Events from my own entity |
| none | Events from the local player entity |
| entity ID (string) | Events from a specific entity |
| model ID (string) | Events from a specific model |
| service type name (e.g., | Service events |
| logic type name | Logic events |
lua
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
-- Receives key events emitted by InputService
end| 第一个参数 | 第二个参数 | 用途 |
|---|---|---|
| 无 | 自身实体触发的事件 |
| 无 | 本地玩家实体触发的事件 |
| 实体 ID(字符串) | 指定实体触发的事件 |
| 模型 ID(字符串) | 指定模型触发的事件 |
| 服务类型名称(例如 | 服务触发的事件 |
| 逻辑类型名称 | 逻辑脚本触发的事件 |
lua
@EventSender("Service", "InputService")
handler HandleKeyDown(KeyDownEvent event)
-- 接收 InputService 触发的按键事件
endDynamic 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)⚠️is called on Entity / Logic / Service — NOT on a ComponentConnectEvent[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-16The public members of theparent (Component) are onlyComponent.d.mlua/Enable/Entity/IsClient. So no Component — includingIsServerandButtonComponent— has its ownTriggerComponent. Components only emit events; the entity they are attached to is what receives them.ConnectEventlua-- ❌ 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)
⚠️vshandler— do not mix them upmethod void[VERIFIED]
Declaration keyword Use Wiring handler Name(Ev event)Static subscription — paired with the annotation. Engine wires it automatically.@EventSender(...)Wired by declaration alone method void Name(Ev event)Dynamic callback — wired at runtime via ConnectEvent(EvType, self.Name) orself.Entity:ConnectEvent(...)_InputService:ConnectEvent(...)Passing aas the callback tohandlercompiles but never fires (E-V1-5). Conversely, putting aConnectEventunderneath anmethod voidwon't get statically wired. Rules:@EventSender
- If
is also present → use@EventSender.handler- If you will subscribe with
→ useConnectEvent(...).method void
lua
-- 绑定(在 OnBeginPlay 中)——返回类型为 EventHandlerBase 对象
local eventHandler = entity:ConnectEvent(ButtonClickEvent, self.OnClick)
-- 解绑(必须在 OnEndPlay 中执行)
entity:DisconnectEvent(ButtonClickEvent, eventHandler)⚠️需在 Entity / Logic / Service 上调用 — 不可在 Component 上调用ConnectEvent[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。因此任何 Component(包括IsServer和ButtonComponent)都没有自己的TriggerComponent。Component 仅负责触发事件;它们所附加的实体负责接收事件。ConnectEventlua-- ❌ 错误 — 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)
⚠️vshandler— 请勿混淆method void[VERIFIED]
声明关键字 用途 绑定方式 handler Name(Ev event)静态订阅 — 与 注解配合使用。引擎自动完成绑定。@EventSender(...)仅通过声明即可完成绑定 method void Name(Ev event)动态回调 — 在运行时通过 绑定ConnectEvent(EvType, self.Name) 或self.Entity:ConnectEvent(...)_InputService:ConnectEvent(...)将作为handler的回调参数会编译通过,但永远不会触发(E-V1-5)。反之,在ConnectEvent下方定义@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: + with fields. There is no inline factory like — that signature does not exist in mlua. Always declare an event class.
@Eventextends EventTypepropertyCustomEvent("Name", { ... })Define the event:
lua
@Event
script UserLogEvent extends EventType
property string userId = ""
property number logTime = 0
endDispatch from a Logic via :
SendEventlua
@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
endReceive via on the entity. The first argument is the event Type (the class itself), and the callback must reference an existing method:
ConnectEventlua
@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 的唯一方式是使用类型化类形式: + 并定义 字段。mlua 中不存在类似 的内联工厂方法——该签名不存在。请始终声明事件类。
@Eventextends EventTypepropertyCustomEvent("Name", { ... })定义事件:
lua
@Event
script UserLogEvent extends EventType
property string userId = ""
property number logTime = 0
end从 Logic 中通过 触发:
SendEventlua
@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通过实体上的 接收。第一个参数是事件类型(类本身),回调必须引用已存在的方法:
ConnectEventlua
@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
endSending events
发送事件
Instantiate the typed event class, set its fields, and pass the instance to :
propertySendEventlua
@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)实例化类型化事件类,设置其 字段,然后将实例传递给 :
propertySendEventlua
@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
| NativeEvent | CustomEvent | |
|---|---|---|
| Definition | Built into the engine ( | User-defined via |
| Examples | HitEvent, ButtonClickEvent, StateChangedEvent | UserLogEvent, DamageDealtEvent (any class you declare) |
| Parameters | Fixed (see per-event spec) | |
| Reference | | User code |
| NativeEvent | CustomEvent | |
|---|---|---|
| 定义 | 内置在引擎中( | 用户通过 |
| 示例 | HitEvent, ButtonClickEvent, StateChangedEvent | UserLogEvent, DamageDealtEvent(你声明的任意类) |
| 参数 | 固定(查看每个事件的规范) | 你在类上声明的 |
| 引用位置 | | 用户代码 |
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 namelua
-- 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
endCaution: 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 -ing script, declaring a
extendswith the same signature as the parent overrides it.method - Built-in engine components allow override only for methods without .
---@sealed - Call the parent original via (optional, position is flexible).
__base:MethodName(args)
- 在继承脚本中,声明与父类签名相同的 即可重写该方法。
method - 内置引擎组件仅允许重写没有 标记的方法。
---@sealed - 通过 调用父类原方法(可选,调用位置灵活)。
__base:MethodName(args)
10. Input / Click Events — World vs UI (Do Not Confuse)
10. 输入 / 点击事件 — 世界实体 vs UI(请勿混淆)
World touch — two approaches
世界触摸 — 两种方式
| Approach | Receiver | Event (+ Hold/Release variants) | Connect on |
|---|---|---|---|
| Entity touch | An entity with | | |
| Screen touch | The whole screen (no component required) | | |
- Both events carry (int32) +
TouchId(Vector2, screen coords).TouchPoint - Entity touch: control the touch area via 's
TouchReceiveComponent/TouchArea/Offset. Suited for per-object interactions (NPCs, items).AutoFitToSize - Screen touch: suited for coordinate-based interactions (tower placement, move target). For world coordinates, convert via . Filter UI clicks with
_UILogic:ScreenToWorldPosition(event.TouchPoint)._InputService:IsPointerOverUI() - If a map touch is not picked up by : it may be a
TouchEventor raycast-priority issue. CombiningTouchArea+ScreenTouchEventis more robust without configuration.ScreenToWorldPosition()
⚠️ Warning —/ physics colliders do NOT emitBoxCollider2DTouchEvent[VERIFIED]If you reason from Unity's/OnMouseDownand just attachOnPointerClick/BoxCollider2D/Rigidbodyto an entity, no touch input will arrive. In MSW, world-entity touch reception is owned exclusively byTriggerComponent(TouchReceiveComponent—Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua/EmitTouchEvent/EmitTouchHoldEventexist only on this component).EmitTouchReleaseEvent
Component Role TouchEvent ,BoxCollider2D, Rigidbody/Kinematicbody, etc.CircleCollider2DPhysics collision / raycast ❌ TriggerComponentEntity-entity overlap callbacks ( )TriggerEnter/Exit❌ TouchReceiveComponentTouch-input reception ✅ Required setup ():TouchReceiveComponent
— auto-fitsAutoFitToSize = trueto theTouchArea/SpriteRendererscale. Skips manual math.AvatarRenderer — 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.TouchArea = Vector2(w, h) — adjust only when the sprite pivot is not at center.Offset (default) — forwards the event to entities behind. SetRelayEventToBehind = trueonly for standalone objects you want to block from passing through.falseSymptoms → diagnosis order:
- Is
actually attached to the target entity? (CheckTouchReceiveComponent/.map.).model- Is
zero, or is the entity outside the rendered area?TouchArea- Is a front-most
blocking withTouchReceiveComponent?RelayEventToBehind = false- Did you call
inentity:ConnectEvent(TouchEvent, handler)and store the handler inOnBeginPlay? (If unstored, GC will collect it.)property any
Selection rule: "Which entity was touched" →; "Where on the screen was touched (coords)" →TouchEvent.ScreenTouchEvent
| 方式 | 接收者 | 事件(含长按/释放变体) | 绑定位置 |
|---|---|---|---|
| 实体触摸 | 附加了 | | |
| 屏幕触摸 | 整个屏幕(无需组件) | | |
- 两种事件均携带 (int32) +
TouchId(Vector2,屏幕坐标)。TouchPoint - 实体触摸:通过 的
TouchReceiveComponent/TouchArea/Offset控制触摸区域。适用于逐对象交互(NPC、物品)。AutoFitToSize - 屏幕触摸:适用于基于坐标的交互(塔防放置、移动目标)。如需转换为世界坐标,使用 。可通过
_UILogic:ScreenToWorldPosition(event.TouchPoint)过滤 UI 点击。_InputService:IsPointerOverUI() - 如果地图触摸未被 捕获:可能是
TouchEvent或射线检测优先级问题。结合使用TouchArea+ScreenTouchEvent无需配置,更可靠。ScreenToWorldPosition()
⚠️ 警告 —/ 物理碰撞器不会触发BoxCollider2DTouchEvent[VERIFIED]如果你按照 Unity 的/OnMouseDown逻辑,仅为实体附加OnPointerClick/BoxCollider2D/Rigidbody,不会收到任何触摸输入。在 MSW 中,世界实体的触摸接收完全由TriggerComponent负责(TouchReceiveComponent—Environment/NativeScripts/Component/TouchReceiveComponent.d.mlua/EmitTouchEvent/EmitTouchHoldEvent仅存在于该组件)。EmitTouchReleaseEvent
组件 作用 是否触发 TouchEvent ,BoxCollider2D, Rigidbody/Kinematicbody 等CircleCollider2D物理碰撞 / 射线检测 ❌ TriggerComponent实体间重叠回调( )TriggerEnter/Exit❌ TouchReceiveComponent触摸输入接收 ✅ 必填设置():TouchReceiveComponent
— 自动将AutoFitToSize = true适配到TouchArea/SpriteRenderer的缩放尺寸。无需手动计算。AvatarRenderer — 手动设置时,需在精灵尺寸基础上预留 10–20% 的余量(例如 1×1 精灵 → 1.2×1.2)。尺寸过小会导致点击检测错位。TouchArea = Vector2(w, h) — 仅当精灵 pivot 不在中心时调整。Offset (默认)——将事件转发到后方实体。仅当你希望阻止事件穿透时,设置为RelayEventToBehind = true。false症状 → 诊断顺序:
- 目标实体是否确实附加了
?(检查TouchReceiveComponent/.map。).model 是否为零,或实体是否在渲染区域外?TouchArea- 最前方的
是否设置了TouchReceiveComponent并阻止了事件?RelayEventToBehind = false- 是否在
中调用了OnBeginPlay并将处理器存储在entity:ConnectEvent(TouchEvent, handler)中?(如果未存储,会被 GC 回收。)property any
选择规则:需要知道“触摸了哪个实体” → 使用;需要知道“触摸了屏幕的哪个位置(坐标)” → 使用TouchEvent。ScreenTouchEvent
Clicks in UI
UI 中的点击
- For UI entities, use the +
ButtonComponentpattern.ButtonClickEvent - UI lives under and the
./ui/*.uitree of the hierarchy.ui
- 对于 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优先使用 Entity.CurrentMap
Entity.CurrentMap- For map-dependent logic, is safer and more readable.
Entity.CurrentMap
- 对于依赖地图的逻辑,更安全且可读性更高。
Entity.CurrentMap
Runtime entity spawning needs a model
运行时生成实体需要模型
- To create an entity at runtime, use (
_SpawnService/SpawnByModelId, etc.). A model (template) to spawn from must already exist.SpawnByEntity - If a brand-new kind of object is needed, follow this order: design a → place it in a map or write spawn code.
.model
- 要在运行时创建实体,使用 (
_SpawnService/SpawnByModelId等方法)。必须已存在用于生成的模型(模板)。SpawnByEntity - 如果需要全新类型的对象,请按照以下顺序操作:设计 → 将其放置在地图中或编写生成代码。
.model
parent
parameter caveats
parentparent
参数注意事项
parent- 's
SpawnByModelIdis required — there is no default, so you must pass a map entity. Passingparentleaves the entity orphaned (not parented), and the engine logsnil.NativeIssue_NotRecommendedValue - In contrast, defaults
SpawnByEntityand may be omitted — the two methods have different signatures.parent = nil - Get the map entity via or
self.Entity.CurrentMap._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 directly will be overwritten by the physics engine on the next frame. This is a top cause of "movement doesn't work."
TransformComponent.WorldPosition - Per-frame movement: .
MovementComponent:MoveToDirection(direction, deltaTime) - Instant teleport: or the corresponding Body's
MovementComponent:SetPosition(pos).SetPosition(pos) - Direct writes are limited to entities without a Body (decorations, effects, etc.).
TransformComponent.WorldPosition - 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) - 瞬间传送:或对应 Body 的
MovementComponent:SetPosition(pos)。SetPosition(pos) - 直接写入 仅适用于无 Body 的实体(装饰、特效等)。
TransformComponent.WorldPosition - 请勿删除 Body 组件作为 workaround——瓦片碰撞和进入/离开事件都会失效,引擎会记录 。
NativeIssue_MissingComponent
12. Frequently Used Services / Logic
12. 常用服务 / 逻辑
All services and logic are accessed via (underscore + type name). Only the most common ones are listed.
_Name| Service / Logic | Purpose |
|---|---|
| Spawn / despawn entities ( |
| Timers ( |
| Entity lookup ( |
| Input state queries; receives |
| Look up resource RUIDs |
| Persistent data (player saves) — ⚠️ Credit-billed. Do not call in |
| Random, time, string, and math utilities |
| Tween animations (MoveTo, ScaleTo, RotateTo) |
| UI coordinate conversions (e.g., ScreenToWorldPosition) — ClientOnly |
For the full list, read thefiles directly:.d.mlua(46 files) and./Environment/NativeScripts/Service/(9 files). For domain details, search via./Environment/NativeScripts/Logic/.msw-search
所有服务和逻辑均通过 (下划线 + 类型名称)访问。以下仅列出最常用的部分。
_Name| 服务 / 逻辑 | 用途 |
|---|---|
| 生成 / 销毁实体( |
| 计时器( |
| 实体查找( |
| 输入状态查询;接收 |
| 查找资源 RUID |
| 持久化数据(玩家存档) — ⚠️ 按调用次数计费。请勿在 |
| 随机数、时间、字符串和数学工具 |
| 补间动画(MoveTo, ScaleTo, RotateTo) |
| UI 坐标转换(例如 ScreenToWorldPosition) — 仅客户端可用 |
完整列表请直接阅读文件:.d.mlua(46个文件)和./Environment/NativeScripts/Service/(9个文件)。如需领域细节,使用./Environment/NativeScripts/Logic/搜索。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:
| Class | Purpose | Notes |
|---|---|---|
| Dynamic array (1-based index) | Add / remove / search / sort |
| Read-only array | For data protection |
| Network-synced array | Auto-sync server↔client |
| Hash table (key-value) | Fast lookup by unique key |
| Read-only hash table | For data protection |
| Network-synced hash table | Auto-sync server↔client |
| Date/time | Format-string support |
| Time span | Days/hours/minutes/seconds/milliseconds |
| Regular expression | Match / search / replace |
| Localization | Current language, translated text lookup |
| 3D rotation | Avoids gimbal lock; smooth rotations |
| Integer 2D vector | Useful for grid coords |
| High-performance vector / color | In-place ops without new objects (when perf matters) |
| Inventory item | Quantity, icon RUID, data-table linkage |
For detailed APIs, browseor query theEnvironment/NativeScripts/skill.msw-search
Lua 标准库之外的集合 / 工具类型:
| 类 | 用途 | 说明 |
|---|---|---|
| 动态数组(1-based 索引) | 添加 / 删除 / 搜索 / 排序 |
| 只读数组 | 用于数据保护 |
| 网络同步数组 | 自动同步服务器↔客户端 |
| 哈希表(键值对) | 通过唯一键快速查找 |
| 只读哈希表 | 用于数据保护 |
| 网络同步哈希表 | 自动同步服务器↔客户端 |
| 日期/时间 | 支持格式字符串 |
| 时间间隔 | 天/小时/分钟/秒/毫秒 |
| 正则表达式 | 匹配 / 搜索 / 替换 |
| 本地化 | 当前语言、翻译文本查找 |
| 3D 旋转 | 避免万向节锁;平滑旋转 |
| 整数 2D 向量 | 适用于网格坐标 |
| 高性能向量 / 颜色 | 性能要求高时,可直接操作对象而非创建新对象 |
| 物品 | 数量、图标 RUID、数据表关联 |
详细 API 请浏览或使用Environment/NativeScripts/技能查询。msw-search
Type annotations (code hints)
类型注解(代码提示)
---@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
endReserved words
保留字
Using mlua keywords as local variable names produces a parse error.
Forbidden as variable names: , , , , , , , , , .
handlerpropertymethodscriptendextendsselfniltruefalselua
-- Wrong
local handler = entity:ConnectEvent(...) -- 'handler' is a reserved word
-- Correct (ConnectEvent returns: EventHandlerBase object)
local eventHandler = entity:ConnectEvent(...)
local connHandler = entity:ConnectEvent(...)将 mlua 关键字用作局部变量名会导致解析错误。
禁止用作变量名:, , , , , , , , , 。
handlerpropertymethodscriptendextendsselfniltruefalselua
-- 错误
local handler = entity:ConnectEvent(...) -- 'handler' 是保留字
-- 正确(ConnectEvent 返回:EventHandlerBase 对象)
local eventHandler = entity:ConnectEvent(...)
local connHandler = entity:ConnectEvent(...)14. External Tooling
14. 外部工具
| Need | Skill |
|---|---|
Maker MCP ( | |
MCP wiring, | |
Descriptions, examples, and implementation guides not in | |
Core debug order: build logs first → play → logs → stop → fix → diagnose → refresh → repeat.
| 需求 | 技能 |
|---|---|
Maker MCP( | |
MCP 配置、 | |
| |
核心调试顺序:先检查构建日志 → play → logs → stop → 修复 → 诊断 → refresh → 重复。
15. Script Authoring Workflow (essentials)
15. 脚本编写工作流(要点)
- Search: scan → if a similar script exists, modify it first; do not create new.
./RootDesk/MyDesk/**/*.mlua - Verify spec: Read the → if insufficient, use
.d.mlua.msw-search - 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 — never write directly to
./RootDesk/MyDesk/<FeatureFolder>/<Name>.mluaroot../RootDesk/MyDesk/ - Write: create the file at the path chosen in step 3.
- Validate: hook auto-runs on save — fix until errors hit zero.
mlua-diagnose - Refresh: Maker MCP (if MCP is not connected, point the user to the button).
refresh - (If needed) → reproduce →
play→logs.stop
Delete / rename: still requires after the file change. Also clean up references in / .
refresh.model.map- 搜索:扫描 → 如果存在相似脚本,优先修改现有脚本;不要创建新脚本。
./RootDesk/MyDesk/**/*.mlua - 验证规范:阅读 → 如果信息不足,使用
.d.mlua。msw-search - 确定路径(见第1.2节 — 文件夹结构为必填项):选择功能/类别子文件夹。如果已有合适的文件夹则复用;否则创建新的功能命名文件夹。最终路径必须为 — 绝对禁止直接写入
./RootDesk/MyDesk/<FeatureFolder>/<Name>.mlua根目录。./RootDesk/MyDesk/ - 编写:在步骤3确定的路径下创建文件。
- 验证:保存时 钩子自动运行 — 修复直到错误数为零。
mlua-diagnose - 刷新:调用 Maker MCP 的 (如果未连接 MCP,引导用户点击按钮)。
refresh - (如有需要) → 复现问题 →
play→logs。stop
删除 / 重命名:文件修改后仍需调用 。同时清理 / 中的引用。
refresh.model.map16. Attaching Scripts (Components) to Entities
16. 为实体附加脚本(Components)
- Attach to a model template () — recommended: add the script-component entry to the
.modelarray. Map instances inherit it automatically (must follow theComponentsJSON schema)..model - Only for a specific map instance: add the component to the matching entity definition inside the file.
.map - Global models (e.g., under
DefaultPlayer): affect the entire project — confirm the blast radius before changing../Global/
In all cases, do NOT edit / JSON directly — use the personal builder.
.model.map- 推荐附加到模型模板():将脚本组件条目添加到
.model数组中。地图实例会自动继承(必须遵循ComponentsJSON schema)。.model - 仅针对特定地图实例:在 文件中对应的实体定义中添加组件。
.map - 全局模型(例如 下的
./Global/):会影响整个项目 — 修改前确认影响范围。DefaultPlayer
无论哪种情况,请勿直接编辑 / JSON — 使用个人构建工具。
.model.map17. 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 testsIf 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 (and the script stack), do a first-pass classification with the table below.
logs| Class | Common signs | Where to look |
|---|---|---|
| Script error | Stack trace with file name and line number | The exact line in the |
| nil reference | | Init order, |
| component missing | Component field is nil; | |
| network / sync | Only client breaks; values mismatch; values converge after a delay | |
To narrow down a cause: if logs alone are inconclusive, add outputs to the to inspect the relevant entity / component / property state.
log().mlua查看 (及脚本堆栈)时,先通过下表进行初步分类。
logs| 类别 | 常见迹象 | 排查方向 |
|---|---|---|
| 脚本错误 | 堆栈跟踪包含文件名和行号 | |
| 空引用 | | 初始化顺序、 |
| 组件缺失 | 组件字段为 nil; | |
| 网络 / 同步问题 | 仅客户端出错;值不匹配;值延迟后一致 | |
定位原因:如果仅靠日志无法确定,可在 中添加 输出,检查相关实体 / 组件 / 属性的状态。
.mlualog()17.3 Test-Result Report Format
17.3 测试结果报告格式
When a playtest ends, summarize briefly in this format.
- Scenario name: one-line description of what was verified.
- Environment: map / mode (if known); whether a happened before playing.
refresh - Steps: a summary of the execution order — input simulation, Lua runs, etc.
- Result: Pass / Fail / Blocked (e.g., couldn't enter play).
- Evidence: one or two key lines quoted from ; whether a screenshot exists (only if the user asked for one).
logs - Next action: candidate files to edit, repro conditions, whether to use on the next run.
clear_logs
测试结束后,按以下格式简要总结。
- 场景名称:一行描述验证的内容。
- 环境:地图 / 模式(如已知);测试前是否执行了 。
refresh - 步骤:执行顺序总结——输入模拟、Lua 运行等。
- 结果:通过 / 失败 / 阻塞(例如无法进入游戏模式)。
- 证据:从 中引用一到两行关键内容;是否存在截图(仅当用户要求时)。
logs - 下一步操作:候选修改文件、复现条件、下次运行是否需要使用 。
clear_logs
17.4 Workflow Patterns
17.4 工作流模式
1) General playtest
1) 常规游戏测试
- Prepare scripts / maps / models in edit mode.
- If the workspace was changed, call MCP .
refresh - Check build errors with → if any, fix and refresh until clean.
logs(category="build") - MCP → enter play mode.
play - As needed, use /
keyboard_inputto reproduce input or UI clicks.mouse_input - As needed, use to inspect runtime state.
logs - As needed, use to inspect runtime logs.
logs(category="runtime") - MCP → return to edit mode.
stop
- 在编辑模式下准备脚本 / 地图 / 模型。
- 如果工作区有变更,调用 MCP 。
refresh - 使用 检查构建错误 → 如有错误,修复并刷新直到无错误。
logs(category="build") - 调用 MCP → 进入游戏模式。
play - 根据需要,使用 /
keyboard_input复现输入或 UI 点击。mouse_input - 根据需要,使用 检查运行时状态。
logs - 根据需要,使用 检查运行时日志。
logs(category="runtime") - 调用 MCP → 返回编辑模式。
stop
2) Regression test loop (fix loop)
2) 回归测试循环(修复循环)
- Edit files based on the previous failure cause.
- .
refresh - Check build errors with → if any, fix and refresh until clean.
logs(category="build") - For a clean repro, call then
clear_logs.play - Replay the same scenario via input / Lua.
- Use to confirm regression status.
logs(category="runtime") - , then edit again if needed.
stop
- 根据上一次失败原因编辑文件。
- 。
refresh - 使用 检查构建错误 → 如有错误,修复并刷新直到无错误。
logs(category="build") - 为了干净复现,调用 然后
clear_logs。play - 通过输入 / Lua 重放相同场景。
- 使用 确认回归状态。
logs(category="runtime") - ,如有需要再次编辑。
stop
3) Error-analysis workflow
3) 错误分析工作流
- First check build errors with — fix them before any runtime analysis.
logs(category="build") - (optional) →
clear_logs.play - Reproduce the issue via /
keyboard_input/ in-game manipulation.mouse_input - Collect and map them to the error classification table above.
logs(category="runtime") - If logs are insufficient, add outputs in the
log()to inspect entity / property state..mlua - After , fix the code /
stop/ sync..model - → recheck build logs →
refreshto re-verify.play
- 首先使用 检查构建错误 — 在进行任何运行时分析前修复这些错误。
logs(category="build") - (可选)→
clear_logs。play - 通过 /
keyboard_input/ 游戏内操作复现问题。mouse_input - 收集 并对照上述错误分类表分析。
logs(category="runtime") - 如果日志信息不足,在 中添加
.mlua输出,检查实体 / 属性状态。log() - 后,修复代码 /
stop/ 同步逻辑。.model - → 重新检查构建日志 →
refresh重新验证。play
4) Runtime Lua debugging
4) 运行时 Lua 调试
- Add calls in the
log()for the values you want to inspect..mlua - If you don't know the API, look it up first: search →
.d.mlua.msw-search - →
refreshto enter play mode.play - Collect output via and analyze.
logs - After analysis, → edit → repeat.
stop
- 在 中添加
.mlua调用,检查需要查看的值。log() - 如果不知道 API,先查找:搜索 → 使用
.d.mlua。msw-search - →
refresh进入游戏模式。play - 通过 收集输出并分析。
logs - 分析完成后,→ 编辑 → 重复。
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-based evidence that the intended logic actually executed.log()
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 相关技能
- : MCP tools, screenshot/logs policy, refresh rules, workspace and hierarchy.
msw-general
- :MCP 工具、截图/日志策略、刷新规则、工作区和层级结构。
msw-general