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 Studio に特有のパフォーマンスパターンなど、保守しやすく高性能なゲームコードを GameMaker Language(GML)で記述する方法を扱います。
GML ゲームプレイ機能の開発ワークフロー
- オブジェクトを設計する — オブジェクトに必要な Create、Step、Draw、Collision、Alarm イベントと、オブジェクトが保持するデータを決めます。
- Create で初期化する — 既定の変数値を設定し、データ構造(
ds_map、ds_list、struct)をインスタンス化し、参照(レイヤー ID、ほかのインスタンス ID)を Create イベントでキャッシュします。 - Step でシミュレーションする — 入力処理、物理演算/移動、ステートマシンの遷移、衝突への応答を Step(または Begin Step/End Step などの Step に関連するイベント)に配置します。
- Draw で描画する — Draw イベントは描画のみに限定し、ゲームプレイの状態を変更してはいけません。
- 再利用可能なロジックをスクリプトに切り出す — 複数のオブジェクトで使用する動作や、複雑になったイベント本体は、名前付きスクリプト/関数に移します。
- Destroy/Room End でクリーンアップする — 手動で作成したデータ構造(
ds_list_destroyなど)を破棄し、メモリリークを防ぐためにサーフェスを解放します。 - 最適化の前にプロファイリングする — GameMaker の組み込みプロファイラーまたは手動計測(
get_timer())を使用し、パフォーマンス向上のために構成を変更する前に、実際に処理負荷の高い箇所を特定します。
コードの整理
- オブジェクトのイベントコードは短く保ちます。再利用可能な動作は、イベントやオブジェクト間でロジックを重複させず、スクリプトまたは名前付き関数に移します。
- プロジェクトですでに確立されている規約に合わせて、スクリプト(
scr_)、オブジェクト(obj_)、スプライト(spr_)、ルーム(rm_)、グローバル変数/マクロに、明確で一貫したプレフィックスや命名規則を使用します。 - イベントのコードをコピー&ペーストするより、関数を優先します。同じ 5 行が 2 つのオブジェクトの Step イベントにあるなら、スクリプトにまとめます。
- Create/Step/Draw の役割を分けます。初期化は Create、シミュレーションとロジックは Step、描画のみの処理は Draw に配置します(ルームのカメラに合わせて拡大・移動させない HUD/UI 要素には Draw GUI を使用します)。
GML のスタイル
- 説明的な変数名(
player_speed、enemy_target)を使用し、範囲が明らかな小さなループ(for (var i = 0; i < count; i++))以外では 1 文字の名前を避けます。 - 不要なインスタンス変数より、
varで宣言するローカル変数(関数/イベント内のスコープ)を優先します。インスタンス変数は、本当に状態を保持する必要がある場合や、ほかの場所からアクセスする必要がある場合に使用します。 - 繰り返し使用する識別子、レイヤー名、状態 ID、衝突グループには、定数、enum(
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);
ゲームプレイのアーキテクチャ
- プレイヤー、敵、UI、ゲームフローの状態には、複雑に絡み合ったブール値フラグではなく、有限状態機械(列挙型+switch、または構造体ベースのFSM)を使用します。
- 衝突判定のロジックは明示的かつ決定論的に保ちます。同じインタラクションに対して複数の場当たり的な距離チェックを混在させず、
place_meeting、instance_place、衝突イベントを一貫して使用します。 - 入力の収集(
keyboard_check/gamepad_*を読み取り、input_xなどの意図を表す変数に格納する処理)と、アクションの実行(その意図を移動や攻撃に適用する処理)を分離します。これにより、入力の再割り当てやAIによる制御のためにゲームプレイのロジックに手を加える必要がなくなります。 - プロジェクト内では、アラーム、タイムライン、明示的なタイマー変数のいずれかを一貫して使用します。同じ種類の動作に複数のタイミング手法を混在させると、デバッグが難しくなります。
- セーブデータは構造化されたマップ/構造体(
{ level: 3, hp: 80, inventory: [...] })で保存し、セーブ形式にはバージョン(save_versionフィールド)を付けます。これにより、今後形式を変更しても、古いセーブデータを破損させずに移行できます。
パフォーマンス
instance_find/instance_numberを広範囲に使ったループや、すべてのStepイベント内で部屋全体の衝突判定を繰り返すなど、コストの高い検索は避けます。可能な場合は、オブジェクト型のフィルターや空間分割(グリッド、レイヤー)を使って検索範囲を絞ります。- 頻繁に使うアセットID、レイヤーID、インスタンス参照はCreateイベントでキャッシュし、Stepごとに再取得(例:
layer_get_id("Instances"))しないようにします。 - 不要になったデータ構造は破棄します(
ds_list_destroy、ds_map_destroy、ds_grid_destroy)。GMLのデータ構造はガベージコレクションされず、手動で解放するかゲームを再起動するまでメモリリークが続きます。 - 頻繁に生成される弾、パーティクル、一時的なエフェクトでは、
instance_create_layer/instance_destroyの頻発が測定可能なコストになったら、オブジェクトプーリング(instance_deactivate_objectでインスタンスを無効化して再利用する方法、または独自のプール)を使用します。 - 最適化の前にプロファイリングを行います(GameMaker内蔵のプロファイラー、または問題のあるコードの前後で
get_timer()の差分を計測します)。負荷の高い処理経路のコードはシンプルに保ちます。早すぎる細かな最適化は、フレーム時間の節約よりコードレビューでのコストのほうが大きくなることがほとんどです。
デバッグとテスト
- ゲームプレイの不具合を調査する際は、デバッグフラグで条件付き表示するデバッグオーバーレイを追加し、衝突ボックス、現在の状態、速度ベクトル、AIの判断を表示します。
- あってはならない状態には、アサーションまたは明示的なガード節(
if (!instance_exists(target)) return;)を使います。開発中に不具合を検出したとき、動作がひそかにおかしくなるのではなく、明確にエラーが発生するようにします。 - 部屋の遷移、一時停止/再開、セーブ/ロード、コントローラー/キーボード入力は、それぞれ独立した観点としてテストします。それぞれ固有の障害パターンがあります(部屋の切り替え時に状態がリセットされない、一時停止中も入力が処理されるなど)。
- 複雑な仕組みには再現可能なテスト用の部屋を用意します(「test_boss_fight」部屋、「test_platforming」部屋など)。変更後に特定のインタラクションをすばやく再検証できます。
よくある間違い
- 状態の変更、物理処理、生成などのゲームロジックをDrawイベント内に記述すること。DrawイベントはStepイベントとは異なる頻度で実行されることがあり、ゲームプレイの状態を変更するべきではありません。
- データ構造(
ds_list_create、ds_map_create、サーフェス)を作成しても破棄しないこと。プレイ中にメモリリークが発生します。 - 重要な動作を部屋エディターでのインスタンス作成順に依存させること。実行時の順序が、ビジュアルエディター上の順序と一致する保証はありません。代わりに、Createイベントで明示的に初期化するか、コントローラーオブジェクトを使用します。
- 名前付きの列挙型の値(
state = PlayerState.JUMP)ではなく、マジックナンバーの状態ID(state = 2)をハードコードすること。コードが読みづらくなり、変更時にエラーが起きやすくなります。
コメント
Markdown 対応