オレンジブログ

オレンジブログ

GameMakerのちょっとしたTipsとか

【GMS2】GameMakerの強参照/弱参照とメモリ管理について


はじめに

GMLでコンストラクタ構造体を使っていると、
deleteしたはずの構造体が残り続けるケースがあります。

  • objPlayer側のコード
// player側の処理
/// Createイベント---------------------------------
function Parameter() constructor {
    name = "Player";
    
}
// パラメータ作成
param = new Parameter();
// グローバルで参照させる
global.playerParam = param;

/// CleanUpイベント-------------------------------
// paramを破棄する
delete param;
  • UIオブジェクト側のコード
// UI側の処理(プレイヤーとは別オブジェクトのDrawイベント)
if(is_undefiend(global.playerParam) == false) {
    // プレイヤーのパラメータが存在する限り、名前を表示する
    draw_text(x,y,global.playerParam.name);
} else {
    draw_text(x,y, "Player is Destroyed");
}

プレイヤーオブジェクトと名前を表示する

上記のようなプレイヤーの名前を表示するUIがあるとします。
プレイヤーオブジェクト側でPamameterを作成し、global.playerParamに参照を持たせます。


そのglobal.playerParamを参照し、nameの値を表示します。
もちろん、プレイヤーが存在しなかったり破棄された場合は非表示にしたいため、
global.playerParamが存在するかどうかをis_undefinedを使ってチェックしています。

if(is_undefiend(global.playerParam) == false)

プレイヤー側の処理では、CleanUpイベントでparamを破棄しています。

/// CleanUpイベント-------------------------------
// paramを破棄する
delete param;


このようなコードを書いた場合、プレイヤーがDestroyされたら
global.playerParamundefinedになるのが期待されます。

しかし、このコードを実際に動作させると
プレイヤーがDestroyされてもUIは残り続けます
(global.playerParamundefinedになっていない)

プレイヤーは破棄されるが、UIは残り続ける

はて...?プレイヤーが持っているコンストラクタ(param)をdeleteしたのに
なぜまだ残っているのだろうか...??

といった疑問が出てきますが、実はこれはGameMakerでは正しい挙動となっています。
今回の記事ではこの問題について解説し、
解決策についてまとめようと思います。

ゲームを起動し続けているとメモリが増え続けてしまう(メモリリーク)が起こっている方や、
メモリ周りの仕様について知りたい方はぜひ読んでください!


GameMakerのメモリ解放について

GameMakerではGarbage Collection(以下GC)を用いてメモリ解放を行なっています。


Garbage Collectionについて

manual.gamemaker.io

GCとは、不要になったメモリを自動的に解放する仕組みのことです。*1
不要になったメモリ...つまり、誰からも参照されなくなったメモリのことです。

// PlayerのCreateイベントにて
index = 0;

if(index == 0) {
    // ローカル変数でParameterを宣言
    var param1 = new Parameter();
    // 名前の変更
    param1.name = "hogehoge";
}
// この時点でparam1はスコープを抜けたので誰からも参照されなくなった
// そのため、GCで自動的にメモリが解放される

param2 = new Parameter();
param2.name = "Player";

// param2はローカル変数ではなくインスタンス変数になるため
// プレイヤーオブジェクトが破棄されるまでは解放されない

簡単にコードで例を挙げると上記のようなコードになります。*2
このように、GameMakerではコードを書く側が明示的にメモリ解放のコードを書かなくても
メモリは自動的に解放される仕組みとなっています。


明示的にメモリを解放する方法

先ほど、メモリが解放されるには誰からも参照されなくなることが条件と言いましたが、
構造体とコンストラクタにも自信を削除する処理が存在します。

param = new Parameter();
param.name = "hogehoge";
// name : hogehoge
show_debug_message(param);
// paramをメモリ解放する
delete param;
// undefined
show_debug_message(param);

このように、deleteを使えばその瞬間メモリ解放することができます。
そうすると、さっきまで参照していた変数はundefinedを指すようになります。
さながら、構造体版のinstance_destroy()ですね。


強参照と弱参照

ここに来て、1つ疑問が生じます。
冒頭で書いたglobal.playerParamはなぜundefinedにならなかったのか?
と言った問題です。

paramdeleteしたのであれば、それを参照しているglobal.playerParamundefinedになるはず...。
と考えますが、global.playerParamがundefinedにならなかった理由はしっかりとあります。

それは、
構造体やコンストラクタは強参照だからです。

あまり聞きなれない単語かもしれませんが、参照には大きく分けて2種類の参照があります。
それが強参照弱参照です。


強参照とは

強参照とは参照(例えば構造体や配列)が別の変数に保持され続け、参照がある限りメモリ上に残る仕組みのことです。
すべての参照が解除されるまでGCの対象にならず、自動的に削除されない強力な参照となります。

強参照の場合

この状態でobjPlayerparamdeleteしてみると

delete param;

片方を破棄しても参照が存在する限り残り続ける
このように、global.playerParamは残り続けます。これが強参照です。
変数として参照を持ち続けている限り破棄されないので安全なように見えますが、
逆に言うと参照が1つでも残っていたら一生破棄されません*3

これが冒頭で起こった問題ですね。
破棄するには、全ての参照を切る必要があります。

delete global.playerParam;

全ての参照をdeleteするとメモリ解放される


冒頭で起こった問題の解説

再度、冒頭で貼ったコードを載せます。

// player側の処理
// Createイベント---------------------------------
function Parameter() constructor {
    name = "Player";
    
}
// パラメータ作成
param = new Parameter();
// グローバルで参照させる
global.playerParam = param;

// CleanUpイベント-------------------------------
// paramを破棄する
delete param;

//----------------------------------------------

// UI側の処理(プレイヤーとは別オブジェクトのDrawイベント)
if(is_undefiend(global.playerParam) == false) {
    // プレイヤーのパラメータが存在する限り、名前を表示する
    draw_text(x,y,global.playerParam.name);
} else {
    draw_text(x,y, "Player is Destroyed");
}

このコードでプレイヤーオブジェクトで生成したparamを破棄してもglobal.playerParamが破棄されなかった問題は、
他でもないglobal.playerParamが元のparamを参照し続けているからですね。

構造体やコンストラクタは強参照のため、1つでも参照が残っていた場合はGC管理にならず、破棄されません
プレイヤーが生成したparamは破棄されても、それをglobal.playerParamが参照していたため、
結果的に破棄されない形となりました。

このコードの問題点は
すでに不要となったparamが破棄されず、メモリに残り続けていることです

これは強参照特有の問題でして、メモリリークの原因の1つです。
自身のコードで少しでもあり得そうなコードがあったら疑ってください。


弱参照とは

弱参照とはそのデータが参照されていたとしても、GCの削除対象になるかどうかに影響を与えない弱い参照のことです。*4
強参照と違い、弱参照のみされているデータはGCの対象になって破棄されます

弱参照の場合
この状態でobjPlayerのparamをdeleteすると

// 参照元の構造体をデリートをすると
delete param;

if(is_undefined(global.playerParam)) {
    // 参照先の構造体もundefinedになるイメージ
    show_debug_message("global.playerParam = undefined");
}

1つでも破棄処理が通るとメモリ解放される
図のように、参照先のglobal.playerParamundefinedになるイメージです。
と言っても、この説明だけだといまいちピンときませんね。

さらに、普通に書いてもこのような弱参照を使うことはできません。
構造体やコンストラクタは強制的に強参照になるからです。


しかし、GameMakerには能動的に弱参照を作り出す方法があります。
そこで実際に冒頭の問題を解決するコードを弱参照を使って書いてみましょう。


冒頭の問題の解決方法

実際に冒頭の問題の解決を弱参照を使って解決してみます。


強参照を弱参照で受け取る方法

弱参照を使うためには、まずは強参照の構造体を弱参照で受け取る必要があります。
weak_ref_create関数を使うことで、強参照の構造体やコンストラクタ、配列を
弱参照として受け取ることができます。

manual.gamemaker.io

// パラメーターを生成
param = new Parameter();
// 弱参照で渡す
global.playerParam = weak_ref_create(param);

処理はこれだけです。
参照が生きているか確認するには、weak_ref_aliveを使います。

if(weak_ref_alive(global.playerParam) == true){
    // 参照元がまだ生存している時の処理
} else {
    // 参照元が破棄された時の処理
}

注意が必要な点として、
参照元にアクセスするには直接ドット演算子(.)で繋ぐのではなく、
受け取った弱参照のrefパラメータにアクセスする必要があります。

// この書き方だとエラーが起きる
global.playerParam.name = "hogehoge";
// こっちならOK
global.playerParam.ref.name = "hogehoge";

非常に間違えやすいので、ここだけ注意してください


弱参照の挙動

さて、構造体を弱参照で受け取れたところで
実際に弱参照の挙動について確認してみましょう。

/// PlayerのCreateイベント
// paramを作成
param = new Parameter();
// 弱参照でグローバルに渡す
global.playerParamWeakRef = weak_ref_create(param);
/// UI表示オブジェクトのDrawイベント
// 弱参照の存在チェック
if(weak_ref_alive(global.playerParamWeakRef) == true) {
    // 存在していたら alive value を表示
    draw_text(x,y,"alive value");
} else {
    // 存在していなかったら undefined!! を表示
    draw_text(x,y,"undefined!!");
}

このコードだと、Parameterを参照しているのは
paramglobal.playerParamWeakRefの2つになります。
ただ、このうちglobal.playerParamWeakRefは弱参照です。

存在しているので alive value と表示されている

この状態でPlayerオブジェクト側のparamdeleteしてみましょう。

/// Playerオブジェクト側のコード
delete param;

すると...??

破棄された!

テキストがUndefined!!になりました!
これは正しくparamが破棄された証拠ですね。

このように、global.playerParamWeakRefに参照されていても
deleteをすることでparamはしっかりGC管理になり破棄されました。
これはglobal.playerParamWeakRefが弱参照をしていたことを表しています。

このようにweak_ref_createを使用すると意図しない参照が残るケースを防げるため、
基本的に構造体やコンストラクタを外部に渡す時にはweak_ref_createを使うことをオススメします。
強参照の方がありがたいパターンの方が少ないのも理由ですが、メモリリークは規模が大きくなればなるほど原因がわかりにくくなるため、
少しでもメモリリークを防ぐ対策を事前にしておくのが良いです。


冒頭の問題を解決するコード

では、弱参照を使って冒頭の問題を解決するコードについて記述します。

// player側の処理
/// Createイベント---------------------------------
function Parameter() constructor {
    name = "Player";
    
}
// パラメータ作成
param = new Parameter();
// メモリリークを防ぐため、グローバルで弱参照を渡す
global.playerWeakParam = weak_ref_create(param);

/// CleanUpイベント-------------------------------
// paramを破棄する
// 弱参照で渡したため、global.playerWeakParam.refもundefinedになる
delete param;

//----------------------------------------------

// UI側の処理(プレイヤーとは別オブジェクトのDrawイベント)
if(weak_ref_alive(global.playerWeakParam) == true){ // weak_ref_aliveで参照元が生存しているかチェック
    // プレイヤーのパラメータの参照が存在する限り、名前を表示する
    draw_text(x,y,global.playerWeakParam.ref.name);
} else {
    draw_text(x,y, "Player is Destroyed");
}

破棄成功!

長くなりましたが、ついに解決しました!!


番外編 : オブジェクトインスタンスは何参照?

ちょっと気になるのが、オブジェクトインスタンスを受け取った場合はどっちの参照なのかという話ですね。

// ここで受け取ったparamは何参照なのか?
param = instance_create_layer(x, y, layer, objPlayerParameter);

結論を申し上げますと、これは強参照でも弱参照でもなく、
ハンドルと呼ばれるものです


 ハンドルとは?

強参照や弱参照は構造体等のメモリ上のアドレスを指していますが、
ハンドルはオブジェクトインスタンス識別するIDのことを指します

ハンドルと参照の違い

つまり、instance_create系の関数で受け取った変数には
そのオブジェクトのIDが入っているということです。

確かに、オブジェクトインスタンスを渡す時はIdを指定して渡しますよね。

paramId = self.id;

これをハンドルと呼びます。


 ハンドルの挙動


ハンドルへのアクセス

.(ドット演算子)を使ったアクセス

objPlayer.x = 20;

with文を使ったアクセス

with(objPlayer){
    x = 20;
}

ハンドルの破棄

自信を破棄する方法

// 自信を破棄
instance_destroy();

idを指定して破棄する方法

// instance_destroyの引数にハンドルのIdを入れる
instance_destroy(playerId);

// with文で自信を破棄させる
with(playerId){
    instance_destroy();
}

ハンドルの寿命

オブジェクトインスタンスのメモリについては、
instance_create_○○を呼べばメモリが確保され、
instance_destroy()を呼べばメモリ解放が行われます。

// PlayerのCreateイベントにて
index = 0;

if(index == 0) {
    // instance_create_depthを呼んでメモリ確保
    var obj = instance_create_depth(100,100, 0, objPlayer);
    obj.name = "Player";
}
// この時点でobjはスコープを抜けたが、instance_destroyはされていないのでメモリ解放はされていない

// instance_destroyを呼んでメモリ解放
instance_destroy();
// この時点でobjPlayerが確保したメモリはGC管理にされ、メモリ解放される

となっています。
その他にも、下記のような特徴を持ちます。

  • オブジェクトが削除されるとIDは無効になる
  • 他の変数に同じIDを持っていても破棄には影響しない
    • 強参照と違い、別の参照があっても破棄される
  • ほぼ弱参照のような動きに近い
    • 所有権を持たず、参照先が消えると無効
    • 厳密に同じものではないが、挙動は似ていると覚えておくと楽

ハンドルと参照の比較

種類 所有権 参照先の削除時 参照方法
ハンドル id = instance_create_layer(...) なし IDは無効化(アクセス不可) .演算子 / with
強参照 param = new Parameter() あり 参照がある限り削除されない param.hp
弱参照 weakRef = weak_ref_create(param) なし 内部のrefundefinedになる ref 経由

といった感じです。
難しいなぁと思う方は、とりあえず弱参照と似ていると覚えておけば大丈夫です。
逆に弱参照は、オブジェクトインスタンスと似たような動作をすると覚えても問題ありません。


まとめ

今回はGCについてと、それに伴った強参照、弱参照についてまとめました。
メモリ周りはイメージがつきにくいため、最初は苦戦するかもしれません。

ただ、強参照によるメモリリークは非常に危険且つよく発生してしまう不具合のケースなので、
しっかりと理解して扱うのが良いでしょう。

メモリ周りについてよく学べたところで、今回の記事はここまでです。
メモリについてはもっと調べると面白い情報がいっぱいなので調べると楽しいですよ...!!

*1:解放 = メモリを空にして再利用できるようにすること

*2:実際はもっと複雑なロジックでメモリ解放が行われていますが

*3:C++のshared_ptrと同等

*4:std::weak_ptrのこと