これだけは押さえておこう!GMLベストプラクティス!

GMS2では独自言語GML(GameMaker Language)を使用します。
エンジンの特性からプログラム経験が浅い方は「この書き方で合ってるのか…?」と不安になる事も多いですよね。

公式によるベストプラクティス(こうした方がいいよ)という記事が上がってますので、自分の考えも交えつつ日本語で解説していきたいと思います。

プログラミングスタイル

日本ではコーディングスタイルと呼ばれている事が多いようです。
行のインデントや変数・関数の名前の付け方などを指します。

コーディングスタイルは多数あり、「これが最良のスタイルだ!」と言っている人は多くいますが、実際には一貫性があり、すべてが何であるかが明確であれば、どんなスタイルであっても問題はありません。

ちなみに個人的なコーディングスタイルはMicrosoft C#コーディング規約がベースです。

  • 変数名はすべて小文字で_で単語を接続する
  • 関数名は頭文字を大文字として、動詞をプレフィックスとする
  • マクロ名は全部大文字とする
  • ローカル変数はプレフィックスとして_を付ける
  • Inherited(継承オブジェクト)はI_***とする

ざっと以上を守っています。

GMLの便利な機能として、以下があります。

JSDoc

/// @description ここにはコードの詳細説明を記載します。
/// @param x 第一引数を記載します(引数名-スペース-説明)
/// @param y 第二引数を記載します(引数名-スペース-説明)

JSDocはソースコード内にコメントとして使用し、そのプログラムが何であるかを説明可能なマークアップ記法です。
例えばスクリプトに対する引数が何であるかをコードエディタ内のインフォメーション部分に表示したり、後で独自のスクリプトを見直した時にどんな内容なのかを把握しやすくなります。

Region

#region

draw_text("test text 01", x,y);
draw_text("test text 01", x+15,y);
draw_text("test text 01", x+30,y);
draw_text("test text 01", x+45,y);
draw_text("test text 01", x+60,y);

#endregion

#region#endregionで括ったコードはグループ化され、いつでも畳んだり開いたりする事が可能です。
どうしても冗長になりがちなスクリプトをまとめて最小化できると、かなり見やすいコードになります。

最終的なゲームをコンパイルするとき、GameMaker Studio 2はコメントを取り除き、不必要な改行と空白を削除し、定数/マクロ/列挙値を置換し、 一般的にプロセスの一部としてコードを圧縮することに注意する必要があります。
これは、必要なだけコードの周りに空白を追加でき、コメントを短くしたり、控えめに使用したりすることを心配はありません。

ローカル変数を使用する

多くの初心者がやりがちな事の一つは、1行で収めようとすることです。

draw_sprite(sprite_index, image_index, x + lengthdir_x(100, point_direction(x, y, mouse_x, mouse_y)), y + lengthdir_y(100, point_direction(x, y, mouse_x, mouse_y)));

これでも実行可能ですが、上記の例であればpoint_direction()は2回呼び出されているため非効率です。次のように表現する方がはるかに良いでしょう。

var p_dir = point_direction(x, y, mouse_x, mouse_y);
var local_x = x + lengthdir_x(100, p_dir);
var local_y = y + lengthdir_y(100, p_dir);
draw_sprite(sprite_index, image_index, local_x, local_y);

ローカル変数を作成するために必要なメモリとリソースはごくわずかで、 コードを後で読み直す場合や間違いを減らす時間的コストの面から得られるメリットは大きなものとなります。

同じ考え方をスクリプトにも適用する必要があります。
スクリプトの汎用変数であるargument*を多用したスクリプトは非常に混乱しやすく間違いを犯しやすいためです。

// Draw Script
draw_sprite(argument0, argument1, argument2, argument3);

上記はシンプルなので見にくい事はありませんが、以下のように修正すべきです。

// Draw Script
var _sprite = argument0;
var _frame = argument1;
var _xx = argument2;
var _yy = argument3;
draw_sprite(_sprite, _frame, _xx, _yy);

これはスクリプトが長くなればなるほど有効で、引数の増減があった場合にリファクタリングを簡単にします。

ローカル変数はゲーム内での処理が高速なため、コードブロックやスクリプトに式が2回以上現れる場合はローカル変数を使用する事を検討してください。

スクリプトまたはコードブロックで何度もglobal変数やinstance変数を参照する場合は以下のように、コードの開始時にローカル変数に代入してから使用しましょう。
これは大きくパフォーマンスに影響します。

var _grav = global.gravity;

配列

配列は高速で使用でき、データ構造よりも必要なメモリは少なくなりますが、さらに最適化できます。
配列を作成すると、そのサイズに基づいてメモリが割り当てられます。
そのため、後で追加しない場合は、最初に配列を最大サイズに初期化すると良いでしょう。

たとえば、最大100個の値を保持するために配列が必要なことが最初からわかっている場合は、array_create()関数を使用して、すぐに100スロットに初期化します。

array = array_create(100, 0);

これにより、すべての配列値がデフォルト値の0に設定された1つの「チャンク」でメモリが割り当てられ、今後の高速化に役立ちます。

注:HTML5ターゲットでは、このような配列の割り当ては適用されず、このターゲットの配列は0から初期化する必要があります!
これは、os_browser変数をチェックすることで簡単に処理できます

if (os_browser == browser_not_a_browser)
{
    // HTML5以外の場合
    array_create(100, 0);
}
else
{
    // HTML5の場合
    for (var i = 0; i < 100; ++i;)
    {
        array[i] = 0;
    }
}

配列の参照渡しの仕様

配列は基本的には参照渡しで渡されます。(スクリプトに渡すなどした場合)

しかし、渡った配列に変更が加わるとコピーされる点に注意してください。(この動作はコピーオンライトと呼ばれます)

スクリプトに配列を渡すと、元の配列を参照渡しとして渡され、そこから読み取られる値はすべて元の配列から取得されます。
これは素晴らしく、高速ですが、配列内の値のいずれかを変更する必要がある場合、書き込み時に配列自体がコピーされ、加えられた変更*はスクリプトから返す必要があります*。そうしないと失われます。これははるかに遅く、より多くのメモリを消費するため、スクリプトで配列を使用する方法に注意してください。

ただし、特別な配列アクセサ@を使用することで、書き込み時のコピー動作を回避できます。これにより、元の配列に直接アクセスできるようになります。

// スクリプトの引数として配列を渡します。
script(array);

// スクリプト内ではこんな処理になります:
var _a = argument0;
_a[0]  = 100;        // 元の配列の要素[0]がコピーされ、新しい配列の要素[0]となります。
_a[@0] = 100;        // 元の配列の要素[0]に直接アクセスして代入します。

データ構造

以前のバージョンのGameMakerよりもはるかに高速になるようにデータ構造が最適化されています。
メモリを解放するために使用されていない場合は、クリーンアップ(破棄)する必要があり、データ構造は配列よりも遅くなる可能性がありますが、データを処理するための使いやすさと追加の機能は、配列と比較しても最小限の速度差なので、データ構造を使う事を恐れないでください。

すべてのデータ構造の中で、特にDS Mapsは読み取りと書き込みの両方で超高速です。

配列のアクセサについて説明しましたが、データ構造にも利用できます。これにより、コードをクリーンアップして読みやすくすることができます。

データ構造については改めて当サイトで記事にする予定です。

衝突判定

GameMaker Studio 2には衝突を処理する方法が複数あり、それらはCPU負荷が少なからずあります。

collision_およびpoint_関数、place_およびinstance_関数は、ルーム内で接触しているすべての指定されたオブジェクトのインスタンスを判定します。

このため最適化の方法としては上記の関数を使用する事が最適であるとは言えません。
バウンディングボックスの判定ではなく、ピクセルごとの判定を行う場合はさらに悪化します。

解決案1:関数の最適な使用

しかし、これらの関数は非常に便利なため使用すべきではないということではありません。
ただし、どれを使用するか、いつ使用するかを知っておく必要があります。

それらはすべてわずかに動作が異なり、速度も異なります。遅い順に以下のようになります。

place_*** > instance_*** > collision_*** > point_***

マニュアルを読んで、状況に最も適したものを選択してください。

解決案2:タイルベースコリジョンシステム

タイルベースのコリジョンシステムの作成を検討してください。

これは、tilemap_関数または2次元配列またはDS_Gridを使用して作成できます。
これらは*非常に*高速で、ゲームの速度を上げるのに役立ちます。

ただし、不規則な地形や壁(タイルに準じないスロープなど)およびグリッドに整列していないオブジェクトを使用している場合、それらは適切ではない可能性があります。
Brendan Waylandによる記事から、タイルマップの衝突に関する非常に簡単なチュートリアルを見つけることができます。

テクスチャスワップと頂点バッチ

show_debug_overlay(true)とすると、下図のようなステータスバーが表示されます。
リアルFPSの横に表示されている括弧で表示された二つの数字は以下を表しています。

  • テクスチャスワップ数
  • 頂点バッチ数

パフォーマンスが著しく落ちている原因はここに起因する事が多いため、よく見ておくことをお勧めします。
この二つの数値をできる限り低く保つことが重要ですが、両方とも0まで下げることはできません。

テクスチャスワップとは?最適化するには?

Unityで言うところのドローコールのようなものです。

GameMaker Studio2にはテクスチャページという概念が存在していますが、通常気にすることは少ないと思います。
BackgroundSpriteを追加すると自動的に1枚のテクスチャにしてくれる機能があるからです。

自動で次々と生成される上で限界のサイズに達すると、別のテクスチャページを作ります。
ざっくり言うと、このテクスチャページを切り替える処理がテクスチャスワップです。

最適化するには、例えばプレイキャラクターや敵キャラクターのスプライトは1つのグループとしてテクスチャページに収め、GUIや背景などをまた別のグループとしてテクスチャページを分けます。

スプライトプロパティから設定する事ができ、テクスチャグループエディタでテクスチャページを作成できます。

例えば、メインメニューでしか使われないGUI用のスプライトや、特定のステージでしか使わない背景やスプライトなどを別のテクスチャページに分けて使用します。
さらに、VRAMの最適化を維持するために、異なるプリフェッチとフラッシュを使用して、必要に応じてメモリからテクスチャをロードおよび削除することで最適化が可能です。

注意:これらはFPSを維持しているデスクトップアプリケーションでは問題にならない事が多いため、主にモバイルデバイスなどのローエンドばデバイスを対象として最適化するほうがいいでしょう。

誤って使用すると逆にパフォーマンスに悪影響になる可能性があります。

頂点バッチとは?最適化するには?

Unityで言うところのバッチングという処理です。

描画を行うために頂点情報をGPUに送信されます。送信される情報はより多くひとまとめになっているほど良いです。
したがって、ひとまとめにする情報のグループ細分化してしまうような事を避けるべきです。

主にブレンドモードの変更、描画色の設定、描画アルファの設定、組み込みの形状とプリミティブの描画など、バッチを途切れる原因になるものがいくつかあります。

例えばブレンドモードを変更するインスタンスの処理が入った時点でバッチが途切れることになります。
ではブレンドモードを変更するにはどうしたらいいでしょうか?

ゲームによって様々な対応方法になってしまうのですが、例えば弾丸インスタンスにそれぞれ加算合成(bm_add)を行いたい場合、それぞれのインスタンスで以下のように指定している事が多いと思います。

// OBJ_Bullet Draw Event

gpu_set_blendmode(bm_add);
draw_self();
gpu_set_blendmode(bm_normal);

この場合、弾丸が1つ描画されるごとにバッチが途切れる事になります。

これらの問題に対応するには、弾丸描画管理オブジェクトとしてOBJ_Bullets_Managerというオブジェクトで管理すると、以下のようになります。

// OBJ_Bullets_Manager

gpu_set_blendmode(bm_add);
with (OBJ_Bullet)
{
    draw_self();
}
gpu_set_blendmode(bm_normal);

ただし上記の管理方法の場合、描画順などの問題が別で出てくる事に注意してください。
描画の最適化は非常に効果的に負荷を減らしやすいのですが、同時に望んだ結果を生み出すのも大きな手間となります。

gpu_set_blendenable() gpu_set_alphatestref() gpu_set_alphatestenable()を使用することで大幅にパフォーマンスを向上させる可能性があります。
あなたのプロジェクトのコード全体で必要に応じて、それらは適切ではないかもしれないですが、有効/無効にすることで最適化ができる可能性があります。

パーティクル

パーティクルは、ゲーム内でダイナミックエフェクトを作成するための非常に迅速かつ効率的な方法で、優れたパフォーマンスを提供します。
ただし、特にモバイルターゲットでは、パーティクルに加算ブレンド、アルファブレンド、カラーブレンドを使用するとパフォーマンスが低下する可能性があるため、必要ない場合は使用しないでください。
特に加算ブレンドは頂点バッチを大幅に増加させる可能性があるため、注意して使用する必要があります。

これは、先述した通り粒子一つ一つでバッチが途切れるために起きる現象です。

非WebGL HTML5ターゲットでは、マルチカラーのフェードパーティクルがあるため、多くの画像キャッシュが必要になり、非常に遅くなることに注意してください。

ただし、パーティクルスプライトはアニメーション化できるため、色を変更するサブイメージを持つアニメーション化されたスプライトを作成し、代わりにそれをパーティクルに使用できます。
徐々に色が変化するように見えますが、キャッシュイメージを常に作成する必要はありません。

サーフェイス

2.2.3ベータアップデート以降、GameMaker Studio 2はゲームでそれらを使用する際にかなり重要な最適化を導入しました:深度バッファのオンとオフを切り替える機能が追加されました。

サーフェスを通常どおり使用する場合、GMS2はサーフェスと併せて深度バッファを作成し、3Dで何かを描画するときに深度ソートを適切にするために使用されます。
ただし、ほとんどの2Dゲームではこの追加の深度バッファーは不要であるため、他のことに使用できるはずのメモリスペースと処理時間を占有してしまいます。

そこでsurface_depth_disable()出番です!

この関数は、surface_create()関数が呼び出される前に呼び出して深度バッファの生成を無効にすることができます。
その後作成されるすべてのサーフェスには深度バッファが作成されません。

必要に応じてこの機能を有効/無効にすることができ、ゲームの開始時に一度呼び出して、その後のすべてのサーフェスの深度バッファを無効にすることもできます(ほとんどの2Dゲームではこれで問題ありません)

それは主要なパフォーマンスの向上につながるものではありませんが、あなたのゲームがサーフェイスに大きく依存し、低スペックデバイス上のメモリが不足して、あなたのゲームを停止する可能性がある場合は、心に留めておくべきものです。

まとめ

GameMakerの仕組みとゲームのパフォーマンスを改善する方法について、この記事から1つでも学んでいただければ幸いです。

他にも役立つ一般的なものがあります:

  • 三角関数を使用することを恐れないでください。特に粒子、衝突、文字列などの処理と比較すると、かなり高速です。
  • Drawイベントに物を描くためではないコードを入れないでください。
  • アラームを使用して、毎ステップを呼び出す必要のないコードを呼び出します。

しかし、記事の冒頭で述べたように、これらの最適化はすべてオプションであり、ゲームが60の頂点バッチ、80のテクスチャスワップ、加算ブレンドなどで正常に動作する場合、あまり心配する必要はありません。
次のゲームをプログラミングするときは、これらのことを念頭に置いてください…

Saitosより

この記事は時々更新され、新しい発見があるのでよく見ています。
「この処理はどっちの方が高速なの?」という疑問が生まれながら開発している事が時々あります。

最適化は非常に重要な事です。
遊んでくれるプレイヤーに意図しないストレスを与えかねないですし、開発者もまた非効率な部分と戦う時間が非常に多いからです。

しかし、「最適化を意識しすぎて時間的コストがかかっている」とか「最適なルールを厳守しすぎて自由度を失っている」という状況に陥りやすいのも事実ですので、そういった点に注意しながら、この記事を心にとめてもらえればうれしいです。

Related Posts

初心者でも簡単!2Dゲームエンジン『GameMaker Studio 2』の魅力
大好きなゲームを作って売ってみたいと思う人は多いと思います。 でもゲーム開発はハードルが高くて、なかなか手が出しづらいですよね。 UnityやU...
GameMaker Studio2 バージョン2.2.4が公開!
GameMaker Studio 2の2.2.4がついに公開されました。 3Qとなる今回は主にコンソールSDKの更新とモバイルOS向けのアップデートの予定でした。 GAMEMAKER STUDIO 2...