关于 gamemaker-gml
面向 GameMaker 开发者的 GML 编程技能,涵盖可维护的游戏代码组织、玩法架构、内存管理、性能分析与测试。
主要功能
- 01GameMaker 事件职责划分
- 02可复用脚本与代码规范
- 03状态机和碰撞处理模式
- 04数据结构与内存清理
- 05性能分析与优化指导
- 06调试和测试实践
适用场景
- 01组织新的玩法功能
- 02重构重复的对象逻辑
- 03排查内存泄漏或性能瓶颈
- 04测试房间切换和输入
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill gamemaker-gml安装后在 Claude Code 中输入/gamemaker-gml确认已加载。
详细介绍
GameMaker Language(GML)开发
本技能涵盖如何使用 GameMaker Language(GML)编写易维护、高性能的游戏代码,包括代码组织、风格约定、游戏玩法架构,以及 GameMaker Studio 特有的性能模式。
构建 GML 游戏玩法功能的工作流
- 设计对象 — 决定对象需要哪些 Create、Step、Draw、Collision 和 Alarm 事件,以及它拥有哪些数据。
- 在 Create 中初始化 — 在 Create 事件中设置变量默认值、实例化数据结构(
ds_map、ds_list、struct),并缓存引用(图层 ID、其他实例 ID)。 - 在 Step 中模拟 — 将输入处理、物理/移动、状态机转换和碰撞响应放在 Step(或 Begin/End Step 等相邻事件)中。
- 在 Draw 中渲染 — Draw 事件仅用于渲染;不要在其中修改游戏玩法状态。
- 将可复用逻辑提取到脚本中 — 将多个对象共用的行为,或日益复杂的事件主体,移到命名脚本/函数中。
- 在 Destroy/Room End 中清理 — 销毁所有手动创建的数据结构(
ds_list_destroy等),并释放 surface,以避免内存泄漏。 - 优化前先分析性能 — 使用 GameMaker 内置的性能分析器或手动计时(
get_timer())找出实际的性能瓶颈,再调整结构以提升性能。
代码组织
- 保持对象事件代码简短;将可复用的行为移到脚本或命名函数中,而不是在多个事件或对象之间重复逻辑。
- 为脚本(
scr_)、对象(obj_)、精灵(spr_)、房间(rm_)以及全局变量/宏采用清晰一致的前缀或命名约定,并遵循项目已有的约定。 - 优先使用函数,而不是复制粘贴事件代码块——如果两个对象的 Step 事件中出现相同的五行代码,就应该将其放入脚本。
- 明确区分 Create/Step/Draw 的职责:初始化放在 Create 中,模拟和逻辑放在 Step 中,仅用于渲染的工作放在 Draw 中(HUD/UI 元素应放在 Draw GUI 中,以免随房间摄像机缩放或移动)。
GML 风格
- 使用描述性变量名(
player_speed、enemy_target);除范围很小且一目了然的循环(for (var i = 0; i < count; i++))外,避免使用单字母名称。 - 优先使用通过
var声明的局部变量(作用域为函数/事件),避免不必要的实例变量——实例变量应保存确实需要持久化或供其他位置访问的状态。 - 对重复使用的标识符、图层名称、状态 ID 和碰撞组使用常量、枚举(
enum PlayerState { IDLE, RUN, JUMP })和宏(#macro),不要为有意义的值到处散布原始数字或字符串字面量。 - 访问可选或可能已失效的实例引用变量之前,先使用
instance_exists(other_id)进行检查,因为被引用实例销毁后,其实例 ID 可能会失效。 - 尽量减少并注明全局状态(
global.*);过多的全局变量会使对象的实际依赖关系变得不可见。
示例:使用脚本和对象实现状态机
// scr_player_states.gml
function player_state_enter(_inst, _state) {
_inst.state = _state;
switch (_state) {
case PlayerState.IDLE:
_inst.sprite_index = spr_player_idle;
break;
case PlayerState.RUN:
_inst.sprite_index = spr_player_run;
break;
case PlayerState.JUMP:
_inst.sprite_index = spr_player_jump;
_inst.vspeed = -_inst.jump_force;
break;
}
}
function player_state_step(_inst) {
switch (_inst.state) {
case PlayerState.IDLE:
if (_inst.input_x != 0) {
player_state_enter(_inst, PlayerState.RUN);
} else if (!_inst.on_ground) {
player_state_enter(_inst, PlayerState.JUMP);
}
break;
case PlayerState.RUN:
_inst.hspeed = _inst.input_x * _inst.move_speed;
if (_inst.input_x == 0) {
player_state_enter(_inst, PlayerState.IDLE);
} else if (!_inst.on_ground) {
player_state_enter(_inst, PlayerState.JUMP);
}
break;
case PlayerState.JUMP:
_inst.hspeed = _inst.input_x * _inst.move_speed;
if (_inst.on_ground && _inst.vspeed >= 0) {
player_state_enter(_inst, PlayerState.IDLE);
}
break;
}
}
// obj_player - Create event
enum PlayerState { IDLE, RUN, JUMP }
move_speed = 4;
jump_force = 8;
input_x = 0;
on_ground = false;
state = PlayerState.IDLE;
player_state_enter(id, PlayerState.IDLE);
// obj_player - Step event
input_x = (keyboard_check(vk_right) - keyboard_check(vk_left));
on_ground = place_meeting(x, y + 1, obj_ground);
player_state_step(id);
move_and_collide(hspeed, vspeed, obj_ground);
游戏玩法架构
- 使用有限状态机(枚举 + switch,或基于结构体的 FSM)管理玩家、敌人、UI 和游戏流程状态,而不是堆砌布尔标志。
- 让碰撞逻辑明确且具有确定性——一致地使用
place_meeting、instance_place和碰撞事件,不要为同一种交互混用多种临时距离检测方式。 - 将输入收集(读取
keyboard_check/gamepad_*并存入input_x等意图变量)与动作执行(根据这些意图移动或攻击)分开,这样重新映射输入或由 AI 驱动控制时,无需改动游戏逻辑。 - 在项目中一致地使用警报、时间轴或显式计时器变量;对同类行为混用多种计时方式会增加调试难度。
- 使用结构化映射/结构体(
{ level: 3, hp: 80, inventory: [...] })存储存档数据,并为存档格式添加版本号(save_version字段),以便将来格式变更时迁移旧存档,而不是导致其损坏。
性能
- 避免在每个 Step 事件中执行开销较大的搜索,例如范围过广的
instance_find/instance_number循环或反复扫描整个房间进行碰撞检测;尽可能使用对象类型筛选和空间分区(网格、图层)缩小搜索范围。 - 在 Create 中缓存常用的资源 ID、图层 ID 和实例引用,而不是每个 Step 都重新获取它们(例如,反复调用
layer_get_id("Instances"))。 - 不再需要数据结构时销毁它们(
ds_list_destroy、ds_map_destroy、ds_grid_destroy)——GML 数据结构不会被垃圾回收,若不手动释放或重启游戏,就会持续占用内存。 - 对频繁生成的投射物、粒子或短暂效果使用对象池(通过
instance_deactivate_object停用并重新启用实例,或使用自定义对象池);当instance_create_layer/instance_destroy带来的开销变得显著时尤其如此。 - 优化前先进行性能分析(使用 GameMaker 内置性能分析器,或在可疑代码周围测量
get_timer()的时间差),并保持热点路径代码简洁——过早进行微优化通常会增加代码审查成本,节省的帧时间却很有限。
调试与测试
- 排查游戏玩法问题时,添加调试覆盖层(仅在启用调试标志时绘制),用于显示碰撞箱、当前状态、速度向量和 AI 决策。
- 对理论上不可能出现的状态使用断言或显式保护条件(
if (!instance_exists(target)) return;),让错误在开发期间明确暴露,而不是悄无声息地导致异常行为。 - 将房间切换、暂停/恢复、存档/读档以及手柄/键盘输入作为独立事项进行测试——它们各自都有不同的故障模式(例如,切换房间时状态未重置、暂停期间仍在处理输入等)。
- 为复杂机制保留可复现的测试房间(例如名为 "test_boss_fight" 的房间和名为 "test_platforming" 的房间),以便修改后快速重新验证特定交互。
常见错误
- 将游戏逻辑(状态变化、物理计算、生成实例)放入 Draw 事件中;Draw 事件的运行频率可能与 Step 不同,而且不应修改游戏状态。
- 创建数据结构(
ds_list_create、ds_map_create、曲面)却从不销毁,导致游戏运行期间内存泄漏。 - 关键行为依赖房间编辑器中的实例创建顺序——运行时顺序不保证与视觉/编辑器中的顺序一致;应使用显式的 Create 事件初始化或控制器对象。
- 将状态 ID 硬编码为含义不明的数字(
state = 2),而不是使用有名称的枚举值(state = PlayerState.JUMP),这会让代码难以阅读且容易出错。
评论
支持 Markdown