この記事はYoYoGames公式ブログに記載された「BEST PRACTICES WHEN CODING IN GAMEMAKER STUDIO 2」を独自に翻訳した記事となります。
意味が取れない部分は意訳が入っていますので、正確な意味が取りたい場合は、原文を読むことをおすすめします。
この記事では、GameMaker Language(略してGML)を使用してゲームをコーディングする場合の「ベストプラクティス」を取り上げ、同時にGameMaker Studio 2の内部動作について少し説明します。
ただし、この記事を読みすすめる前に、2つの非常に重要な点に注意してください。
- これはガイドであり、ゲームを作成するための100%完璧な方法ではありません! ここで言及されていることは、一般的にはゲームの設計・データ構造および部分的な最適化であり、あなたがGMLに慣れた上で、適切と考える場面でコーディングに組み込むよい習慣を提示しているだけです
- あなたのゲームが正常に動作し現状に満足している場合は、この記事の内容に無理に合わせて修正する必要は必ずしもありません。ほんのすこしのFPS(ゲームの動作速度)を上昇させるために急いですべてを修正しないでください。既存のコードを可読性の高い柔軟なモジュール式コードにするための修正コストは大きいです。修正のためには必要な時間とエネルギー、そして最終的には全体的な利益のバランスをとる必要があります。ですので、基本的に破綻したゲームでない場合は、修正せずに次のプロジェクトのためにここで学んだことを覚えておいてください
それでは、次に進んで、いつでも適用できる優れたGMLコードを作成するためのいくつかの一般的なヒントを見てみましょう...
プログラミング・スタイル
プログラムコードの書き方は千差万別です。ブラケット []
の位置、行のインデントの位置、変数を宣言するときの名前を付ける方法など……。一貫したルールで記述されたコードは、他の人(および将来の自分)が読みやすいコードにするために必要不可欠です。たとえ自分しか読まないとしても、一定の期間が経過した後、または別プロジェクトに時間を費やした後に、このプロジェクトに戻る際、読み返して理解するために必要な時間を短縮できます。
プログラミング・スタイルは様々で、特定のスタイルが最良であると主張する人もいますが、実際には使用方法に一貫性と明確なルールがある限り、ほとんどすべてのスタイルで問題ないです。
/// @function play_sound(sound, loop, pitch);
/// @param {id} sound The sound resource ID to play
/// @param {bool} loop OPTIONAL! Whether to loop the sound or not
/// @param {real} pitch OPTIONAL! The pitch of the sound to play
/// @description Play a sound and return the ID for it
if global.Sound
{
var _loop = false;
var _pitch = 1;
if argument_count > 1
{
_loop = argument[1];
if argument_count > 2
{
_pitch = argument[2];
}
}
var _s = audio_play_sound_on(global.Sound_Emitter, argument[0], _loop, irandom(100));
audio_sound_pitch(_s, _pitch);
return _s;
}
return -1;
上記のコードは、ポイントを説明するためのサンプルスクリプトです。 JSDocスタイルのコメントを使用して、すべての機能を明確に説明しており、コーディングスタイルは一貫しており、4つのスペースインデント、ローカル変数にアンダースコアなどが使用されています。
また、#region
と#endregion
を使用してコードの一部のまとまりを作り、読みやすさを大幅に向上させることもできます。リージョンもコメント化できます(ScriptEditorのマニュアル)
コードを書くときに考慮すべきこととして、ゲームをコンパイルする際に GameMaker Studio 2 はコメントを取り除き、不要な改行と空白を削除し、マクロ(#macro
) / 列挙値(enum
) をリテラル値に置き換え、コードを圧縮します。
つまり、コメントを短くしたり控えめにする必要はなく、必要なだけコメントや空白を使用することができます。
ローカル変数を使う
プログラミング・スタイルに関する上記のポイントから続けて、多くの初心者がやりがちな間違いは、できるだけ多くのコードを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);
これらのローカル変数を作成するために必要なメモリとリソースはごくわずかです。それよりも、このコードの読みやすさ・理解しやすさ・後々このコードを読み返したときのストレスからの開放、というメリットの方が遥かに重要です。同じ考えを関数にも適用する必要があります。入力変数にわかりやすい名前を割り当て、必要に応じて明確なフォーマットとローカル変数を使用して、できるだけ読みやすくする必要があります。
ローカル変数はゲーム内で高速に処理されるため処理速度を気にせず使用できます。同じ式がコードブロックまたは関数に2回以上出現する場合は、ローカル変数を作成することを検討してください。 YoYo Compilerターゲットを使用する場合、スクリプトまたはコードブロックでグローバル(global)変数またはインスタンス(instance)変数を何度も参照する場合は、コードの最初でローカル変数に割り当ててから、そのローカル変数を参照すると特に効果的です。ローカル変数を使用することではるかに優れたパフォーマンスを出すことが可能となります。
配列(Array)を使う
配列は高速に使用でき、必要なメモリはデータ構造(data-structures)よりも少なくなりますが、さらに最適化が可能です。配列を作成すると、そのサイズに基づいてメモリに割り当てられるため、配列で確保した領域をすべて使う予定がない場合でも、最初に最大サイズに初期化する必要があります。 たとえば、最大100個の値を保持する配列が必要であることがわかっている場合は、array_create()
を使用して、以下の記述で配列を100の要素に初期化します。
// 100の要素を確保。初期値は "0"
array = array_create(100, 0);
これにより、メモリが1つの「チャンク(訳注:複数のデータが1つに集まっているデータのこと)」に割り当てられ、配列のすべての値が0に初期化され、高速に利用できます。もし後々のコードで配列サイズを動的に増やす(100より大きくする)場合は、新しい値を配列に追加するたびに、メモリ全体の再割り当てが発生し、低速な処理となります。
注意:HTML5ターゲットでは、このような配列の割り当ては適用されず、配列はこのターゲットに対して0から初期化する必要があります。 これは、
os_browser
変数をチェックすることで簡単に処理できます。次に例を示します。
if(os_browser == browser_not_a_browser) {
// HTML5ターゲットでないので array_create() を使用する
array_create(100, 0);
}
else {
// HTML5の場合は手動で初期化する
for(var i = 0; i < 100; i++) {
array[i] = 0;
}
}
また、使用する変数を0に設定すると、配列に関連付けられているメモリを解放できます。さきほどのサンプルコードで生成した配列をメモリから消すには、次のように記述します。
// 配列をメモリから削除
array = 0;
また、配列は参照渡しされますが、変更が加えられると全体がコピーされることにも注意してください(この動作は、書き込み時コピー(Copy-On-Write)と呼ばれます)。
したがって、配列をスクリプトに渡すと、元の配列への参照が渡され、そこから読み取られる値はすべて元のソースから取得されます。これは素晴らしく高速ですが、配列の値を変更する必要がある場合は、配列自体が書き込みの時点で複製され、加えられた変更はスクリプトから返される必要があります。そうしないと、失われます。これははるかに遅く、より多くのメモリを消費するので、スクリプトで配列を渡す場合は注意してください。
ただし、特別な配列アクセサー @
を使用すると、このコピーオンライト動作を回避できます。これにより、基になる配列に直接アクセスできるようになります。 例えば以下のように記述します。
// スクリプトを呼び出し array を渡します
script(array);
// そのスクリプト内で以下のように記述します
var _a = argument0;
_a[0] = 100; // この場合、配列がコピーされるので "return _a;" で配列の値を返す必要があります
_a[@0] = 100; // この記述であればもとの配列の値を直接書き換えるため return は不要です
データ構造 (Data structures) を使う
GameMaker Studio 2 では、データ構造 (Data structures) が GameMaker Studio 1 よりもはるかに高速で動作するように最適化されました。
データ構造は使用しなくなった時点で、クリーンアップ(メモリから破棄)する必要があり、配列などよりも速度的に遅くなる可能性がありますが、コードがシンプルに記述できたり、複雑なデータを処理するために有効な場面がいくつかあります。速度的なペナルティはあまりないので、ゲームでそれらを使用することを恐れないでください。
すべてのデータ構造のうち、特に DS Map は読み取りと書き込みの両方で最高速の動作をするので、すべてのタイプの処理に最適な選択です。
前のセクションでは配列の**Accessor "@
"**について説明しましたが、データ構造にも似たAccessorが使用できるため、コードを整理して読みやすくすることができます。 これについては以前の技術ブログから知ることができます。
// ■リスト
var list_id = ds_list_create();
// リストの値を取り出す (4番目の要素を取り出す)
var data = list_id[| 3];
// リストへの代入 (2番目の要素に代入する)
list_id[| 1] = 123;
// ■マップ
var map_id = ds_map_create();
// マップの値取り出し (キー "abc" の値を取り出す)
var data = map_id[? "abc"]
// マップへの代入 (キー "def" に設定する)
map_id[? "def"] = 123;
// ■グリッド
var grid_id = ds_grid_create();
// グリッド (1, 2) から値を取り出す
var data = grid_id[# 1, 2];
// グリッド (10, 5) に値を代入する
grid_id[# 10, 5] = 123;
コリジョンを使う
GameMaker Studio 2 での衝突に対処する方法は複数あり、それらのほとんどでCPUへの負荷がかかります。collision_
および point_
関数、place_
関数、instance_
関数はすべて、ルーム内の特定のタイプのすべてのインスタンスのバウンディングボックスの判定に依存しており、このためにエンジンに組み込まれた最適化の方法はほとんどありません。さらにバウンディングボックスの判定(スプライトの設定など)に precise (ピクセル単位の正確な判定)
を使用するとピクセル単位のチェックも行うため、さらにパフォーマンスが悪化し、非常に重たい処理となります。これに関する古い技術ブログがあり、一読の価値があります。
これらの関数は非常に便利ですが、安易に使用すべきではありません。どの関数をどのような時に使用すべきかを知っておく必要があります。というのも、これらはすべて動作が少し異なり、処理速度も異なるためです。おおまかな目安として、place_
関数はinstance_
関数よりも速く、collision_
およびpoint_
関数よりも速いです。より詳しくは衝突に関するマニュアルを読んで、あらゆる状況に最適なものを選択してください。
または、タイルベースの衝突システムの構築を検討してください。これは、タイルマップ関数またはカスタムの2D配列またはDS Gridを使用して作成できます。これらは非常に高速で、ゲームの速度を上げるのに役立ちます。ただし、不規則な地形や、グリッドに位置合わせされていない壁やオブジェクトを使用していると、うまくいかない場合があります。
なおタイルマップの衝突の実装については、Shaun Spaldingのタイルマップの衝突に関する非常にシンプルなビデオチュートリアルが参考になります。
■Tile Collisions In GameMaker Studio 2
テクスチャスワップと頂点バッチ
debug overlayを有効にすると、デバッグ実行中の画面上部に括弧の付いた2つの数字が表示されます。
1つ目は実行中のテクスチャスワップの数で、2つ目は頂点バッチの数です。上記の画像であれば (84)(247)
がそれに該当し、テクスチャスワップが84回、頂点バッチが247回発生したことを意味します。
多くの要因がこれらの数値に影響し、エンジンが各ステップの1つまたは2つを必要とするため、この2つを (0)(0) に下げることはできませんが、これらの値をできるだけ低くすることを目指してください。
■訳注
「テクスチャスワップ」とは、テクスチャの切り替えのこと。テクスチャの切り替えは処理負荷が高いので、できるだけ1枚のテクスチャを使用して切り替えコストを発生しない作りが望ましい。
「頂点バッチ」とは、頂点データをフレームバッファに転送する処理。ドローコール(Draw Call)とほぼ同義。とてもコストが高い処理なので、一度の転送でできるだけ多くの処理をまとめて転送したほうが処理効率が良い。
テクスチャの入れ替えの場合、これを行う最も効率的な方法は、スプライトと背景がテクスチャページに保存される方法を最適化することです。これは sprite properties から行われ、 Texture Group Editor でテクスチャページを作成できます。例えばメインメニューでのみ使用される画像が多数ある場合は、それらを別のテクスチャページにまとめます。 レベル固有の画像、またはプレーヤーと敵などがある場合も同様です。基本的には、スワップ回数をできる限り削減するためには、その画面で使用するテクスチャができるだけ少ない枚数になるようにグループ化する必要があります。さらにVRAMの最適化を維持するために、必要に応じて、さまざまな プリフェッチ(事前読み込み)とフラッシュ(バッファからの削除) を使用して、メモリからテクスチャをロードおよび削除できます。
注意:この記事の冒頭で述べたように、ゲームがFPSの遅延なく正常に動作する場合……、特にデスクトップターゲットでプロジェクトを作成するときには、テクスチャのスワップについてあまり気にしないでください。これらの最適化は、大規模なゲームや低スペックのモバイルデバイスで使用する場合に最適です。誤って使用すると、パフォーマンスに悪影響を与える可能性があります。
頂点情報は、描画時に「バッチ(まとめて)」でGPUに転送されます。一般的に、バッチが大きいほど優れたパフォーマンスが出ます。ですので、描画時にバッチを「壊す」ことを避けると、GPUに転送される頂点バッチの数が増え処理コストが削減します。
バッチを「壊す」ことの原因の主なものは、ブレンドモード(blend modes)の変更、描画色の設定(draw colour)、描画アルファの設定(draw alpha)、および組み込みのシェイプとプリミティブの描画(draw_circle()
など)です。
したがって、たとえば、bm_add ブレンドモードを使用して描画する弾インスタンスが多数ある場合、弾の各Drawイベントで描画を行うと、それぞれに新しい頂点バッチを作成することになり、これは間違いなくパフォーマンスを悪化させます!
それぞれのDrawイベントで描画する代わりに、次のように、すべての弾を描画するコントローラーオブジェクトをゲームに含めます。
// 加算ブレンドを設定.
gpu_set_blendmode(bm_add);
// 弾をまとめて描画する
with (obj_BULLET) {
draw_self();
}
// 通常のアルファブレンドに戻す
gpu_set_blendmode(bm_normal);
注意:これは、
bm_add
を使用する場合にのみ発生することではありません。加算ブレンド以外にも、ブレンドモードを変更すると、バッチが中断され、パフォーマンスが低下します
このように記述すると、すべての弾が同じバッチで描画されます。この方法はアルファ(透過/alpha)、カラーを変更する場合も適用でき、次の関数も同様です。
これによりパフォーマンスが大幅に向上し、プロジェクトコード全体で必要に応じて有効/無効にすることができますが、すべての描画処理やプロジェクトに適しているとは限りません。
注意:このような描画用のコントローラを使用する際に、コントローラーインスタンス自身を描画する必要がない場合は、コメントを Drawイベント に追加してデフォルトの描画を抑制します(空のイベントを作成し
draw_self()
を抑制する)。もしくはインスタンスのvisible
プロパティを false にして非表示にすることができます(ただし、これをするとすべての Drawイベント のコードが実行されなくなります)
バッチの切り替え回数を減らすもう1つの方法は、絶対に必要な場合を除き、スプライトの Separate Texture Page (テクスチャページの分離) オプションを無効にすることです。この方法で保存された各画像は、独自のテクスチャページに送信され、異なる方法でバッチ処理されるため、これらの画像を通常のテクスチャページに配置することをおすすめします。次に、sprite_get_uvs() 関数を使用してUVS座標(テクスチャの切り出し位置)を取得し、その情報を変数に保存します。それは短いコードかもしれませんが、あなたが得るブーストはそれだけの価値があります。この方法はテクスチャのRepeatを許可しないことに注意してください!
これらのすべてのヒントは他のものと同様に、ゲームシステムを変えることがより困難になるため、現状ゲームが正常に動作する場合は、あまり心配する必要はありません...
パーティクル
パーティクルは、ゲーム内で動的なエフェクトを作成するための非常に迅速で効率的な方法を提供し、一般に優れたパフォーマンスを提供します。ただし、特にモバイルターゲットでは、パーティクルに加算ブレンディング、アルファブレンディング、カラーブレンディングを使用するとパフォーマンスが低下する可能性があるため、必要がない場合は使用しないでください。特に、加算ブレンディングは頂点バッチを大幅に増やす可能性があるため、注意して使用する必要があります。
WebGLを無効にしたHTML5ターゲットでは、マルチカラーのフェードパーティクルがあるため、大量の画像キャッシュが必要であり、非常に遅くなることに注意してください。パーティクルにスプライトを使用することでアニメーション化して、色が変化するサブイメージを持つアニメーションスプライトにすることで高速化します。このようにすれば徐々に色が変化するように見えますが、常にキャッシュイメージを作成する必要はありません。
パーティクルの詳細については、技術ブログ Guide To GameMaker Particles をご覧ください。
サーフェース
最後に、サーフェスの使用について簡単に触れます。GameMaker Studio 2.2.3ベータ版のアップデートから、ゲームでサーフェースを使用するときにかなり重要な最適化を導入しました。depth buffer (深度バッファ) のオンとオフを切り替える機能です。サーフェスを通常どおり使用する場合、GMS2は実際のサーフェスとそれに付随する深度バッファを作成し、3Dで何かを描画するときに適切な深度ソートを保証します。ただし、ほとんどの2Dゲームでは、この追加の深度バッファは必要ないため、他の処理に使用できる余分なメモリ領域と処理時間を消費します。ここで、関数 surface_depth_disable() が役立ちます...
この関数は、サーフェスを作成する前に呼び出すことで深度バッファが無効となり作成されなくなります。必要に応じてこの機能を有効/無効にしたり、ゲームの開始時に一度呼び出しておくと、その後のすべてのサーフェス呼び出し時に深度バッファを無効にできます(ほとんどの2Dゲームでは深度バッファを無効にして問題ありません)
深度バッファを無効にすることで、パフォーマンスが大幅に向上することはありませんが、ゲームがサーフェスに大きく依存していて、スペックの低いデバイスでゲームがメモリ不足になる可能性がある場合は、この関数の使用を検討する価値があります。
まとめ
この記事から、GameMakerのしくみとゲームのパフォーマンスを向上させる方法について、何か得られるものがあれば幸いです。また、他にも役立つ一般的な情報があります。
- 三角関数を使用することを恐れないでください。(一般的な考えに反して)三角関数は、特にパーティクル、コリジョン、文字列などと比較すると、かなり高速です...
- Drawイベントに描画用ではないコードを入れないでください
- 何でもかんでも Stepイベントに追加するのではなく、Alarmを使用して Stepごとに呼び出す必要がないコードをStepイベントから削除します
ただし、記事の冒頭で述べたように、これらの最適化はすべてオプションであり、60個の頂点バッチ、80個のテクスチャスワップ、加算ブレンドなどで ゲームが正常に動作する場合は、あまり気にする必要はありません。 次のゲームをプログラミングするときは、これらのことを覚えておいてください...