mcpField
G
gamemaker-gml

gamemaker-gml

github.com/mindrally开发28767 次安装3 周前更新
代码仓库

关于 gamemaker-gml

面向 GameMaker 开发者的 GML 编程技能,涵盖可维护的游戏代码组织、玩法架构、内存管理、性能分析与测试。

主要功能

  1. 01GameMaker 事件职责划分
  2. 02可复用脚本与代码规范
  3. 03状态机和碰撞处理模式
  4. 04数据结构与内存清理
  5. 05性能分析与优化指导
  6. 06调试和测试实践

适用场景

  1. 01组织新的玩法功能
  2. 02重构重复的对象逻辑
  3. 03排查内存泄漏或性能瓶颈
  4. 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 游戏玩法功能的工作流

  1. 设计对象 — 决定对象需要哪些 Create、Step、Draw、Collision 和 Alarm 事件,以及它拥有哪些数据。
  2. 在 Create 中初始化 — 在 Create 事件中设置变量默认值、实例化数据结构(ds_map、ds_list、struct),并缓存引用(图层 ID、其他实例 ID)。
  3. 在 Step 中模拟 — 将输入处理、物理/移动、状态机转换和碰撞响应放在 Step(或 Begin/End Step 等相邻事件)中。
  4. 在 Draw 中渲染 — Draw 事件仅用于渲染;不要在其中修改游戏玩法状态。
  5. 将可复用逻辑提取到脚本中 — 将多个对象共用的行为,或日益复杂的事件主体,移到命名脚本/函数中。
  6. 在 Destroy/Room End 中清理 — 销毁所有手动创建的数据结构(ds_list_destroy 等),并释放 surface,以避免内存泄漏。
  7. 优化前先分析性能 — 使用 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