この記事は 新歓ブログリレー2025 13日目のものです
他の記事を見たい方は こちら↑ のリンクをクリック!
はじめに
初めましての方は初めまして、科学大デジタル創作同好会所属の学士 2 年(新3年)@Alt--erです。traP ではSysAd 班およびGame 班で主に活動しています(他にもアルゴリズム,グラフィック,サウンドに所属しています)。最近はゲームのプロジェクト進捗の日々を過ごしています。
今回の記事は個人的な備忘録。でも、人によっては技術記事になるかもしれません。
つまりどういう記事?
event Action
という便利でなんかプログラミング慣れてきた感が出る構文(実際にイベント駆動の理解につながる)を使ったときに、購読したメソッドの解除を忘れてバグらせた話です。(技術記事的にはあまりおいしくないかも... 個人の備忘録として)
event Action
とは... の前に
言語仕様に則した厳密な説明はこの記事では省略します(当方C#の言語仕様の面については未達者です)。ご了承ください
さて、今回のタイトルにもあるevent Action
ですがそもそもこれは何でしょう。その前にevent
とAction
に分けて簡単に説明します。
Action
について
記述としてはevent
の方が先になりますが、まずはAction
について説明した方が自然だったりなのでこちらから。
簡単に言葉で説明すると、メソッド(処理)を格納できる変数の型といえます。もっと噛み砕いていうのなら(実際に使うときのイメージとして)後々やることを渡しておくというイメージでしょうか。噛み砕き過ぎて分かりづらくなっているかもしれません...。
以下のように記述して使います。
// ボタンの初期設定に用いるクラス
public class ButtonSetup{
private Button _button;
private static void main(){
_button = new Button();
_button.OnClickEvent = this.OnClick();
}
// ボタンの実際の処理を書く
private void OnClickEvent(){
Console.WriteLine("Clicked");
}
}
public class Button{
public Action OnClickEvent;
// ボタンが押されたときに発火する
public void OnClick(){
OnClickEvent.Invoke();
}
}
上のスクリプトは、ボタンオブジェクトを生成するものの一例です。ButtonSetup
クラス内のmain()
内部でボタンを生成しています。今回の主役、Action
についてはButton
クラス内のpublicなフィールドとして定義されています。
ボタンが押下されてButton
クラス中のOnClick()
メソッドが発火すると、その中に書かれたOnClickEvent.Invoke()
に辿り着きますが、これはOnClickEvent
に渡されている処理を実行する、という意味となります。
今回の場合はButtonSetup
のOnClickEvent()
が渡されており、その中に書かれた処理("Clicked"と出力する)が実行されることとなります。OnClickEvent()
自体はprivateで宣言されていますが、別のクラスから操作できているのもポイントです(OnClickEvent
へのメソッドの割り当て自体はButtonSetup
クラス内で行われているためprivateでもアクセスできる)。
え、何が嬉しいの?
ってなる人も多いと思います。なぜButton
のOnClick
の中に記述しないのでしょうか。
一番のメリットはButton
そのもののクラスを使いませるというところでしょう。
例えば、汎用的に使われるボタンであれば見た目や押したときの効果音等、共通している部分が多いでしょう。Button
内部に処理を定義する形式ですと、それらの共通部分も繰り返し記述することになり手間ですし可読性も落ちてしまいます。[^1]
いうなれば、"ボタンを押したら何か起こるからくり箱の、からくりの部分だけ入れ替えて使いまわせる" といった状態に近いかも。
詳しい話
このように処理を変数として扱うときの型を総称してデリゲートと言ったりします。デリゲートを扱うには、本来定義や初期化などを複数行のコードを記述する必要があるのですが、それを逐一書いていては冗長になってしまいますし同じコードの繰り返しとなってしまいます。
C#では、16引数までのメソッドのデリゲートを予め言語仕様として定義しています。これが先ほど説明したAction
と呼ばれるものです。また、引数をセットしたい場合はAction<T1,T2,...>
のように記述します。
更に亜種として、返り値をもつメソッドを指定できるFunc<T>
というのもあります
詳しくはmicrosoftのリファレンスをご覧ください。それはそうと16引数分全部説明書いてる様子見ると壮観ですね。


event Action
について
さて、Action
の説明で長くなってしまったのでこの部分は簡潔に書きます。このようにevent
を付けることによって、代入が不可能になり、実行するメソッドの追加と削除のみが可能になります。
先ほどの説明では触れてなかったのですが、Action
には複数の処理を足していくことができます。その場合、足された順に(キューの要領で)処理が実行されていきます。なお、キューと異なる点として先頭でなくても処理を削除できる事ができます。
ところで、代入が不可能であることにどういう特異性があるのでしょうか。
代入が不可能というのを"上書きができない"と考えると分かりやすいかもしれません。Action
がpublicである限り、様々なクラスからメソッドを追加/削除することが可能になります。ここで、もし上書きが可能であった場合、各クラスそれぞれがあずかり知れないところで処理が追加/削除されるということになります。
これでは安全性が損なわれてしまうため、追加削除のみ可能なevent Action
が存在するというわけです。


んで、結局のところ何を失敗したの?
さて、ここまでevent Action
について話してきました。当時その機能を知った際には、処理を変数のように扱えること、修飾子をprivateのままにしても他のクラスから操作ができるようになること、正直スマートに書けるように見える[^2]ことからかなり多用することになりました。
これまでの話はC#の言語についてでしたが、ここからはUnity開発特有の話になります。(Unityの基本的な概念については一部端折ります。ご了承ください)
Unityでは一つのゲームを制作する際に、(概ね)ゲームの場面ごとにシーンと呼ばれるアセット毎に分けて制作を行います。シーン毎に、そのシーンでの進行を管理するゲームオブジェクト(hogeManager
と命名しがち)を設定することが多いのですが、ここで大きなミスが生じたのです。
はじめは上手くいっていた
時は冬ハッカソン(traP内で1月に開催しました。自分の班はまだですがブログ記事もあります!!)、プログラマー少数だった自分の班はかなり限界の体制だったため信頼マージ[^3]することも多くありました。

そしてはじめのうち、シーンを跨がずにいるうちは問題ない動きができていました。
シーンをつなぎ合わせるときに...
問題はシーンを行き来することによって起こりました。
ここで、基本的にはシーンを移動すると全てのゲームオブジェクトが削除されます。但し、DontDestroyOnload
を用いることでこれを回避することができます。
ただ、これを用いることで事故が発生してしまいました。
ロード時に消滅しない管理者クラスのようなものを作っていたのですが、それが大量の”missing reference error”を吐き出したのです。

つまるところ、”そんなもの無いのにアクセスしようとしているぞお前”って意味なのですが… 正直かなり困惑してました。シーンごとではちゃんと動いていたのに”繋げただけで動かなくなった”、という思考だったため。
無限エラー修正編
“参照がない”系のエラーということで値の代入忘れ等を疑いましたが違いそう、また始めのうちは正常動作していることもよくわからない度合いを増していたり。
原因は単純、”event Actionを用いて渡した処理の削除を忘れていた” でした。購読していた処理を持つオブジェクトがDestroyされているのにも関わらずそれにアクセスし続けていたということです。そりゃエラー吐くわな。
ただ、振り返ってみれば”それはそう”というような穴になぜハマったのでしょう。筆者の感覚ですが、次項のような意識の中があったのかと思ってます。
意識レベルの穴
シーンを遷移する意識が抜けていた
正直この部分が一番大きいと思います
シーンを遷移するときには基本全てのgameObjectが消されます。よくよく考えたら当たり前のことですが、案外コードを書いている間は忘れてしまうものです。テストも同一シーン内で行っていたため気づきから尚更遠ざかる結果に。
普段から"このシーンから出て行ったら、もう一度戻ってきたら"どのような挙動をするか考えたいものです。
処理への参照を渡しているだけの意識が抜けてた
他の大半の型でもそうですが、多くは参照渡しです。つまるところ、event Action
もそのメソッドへの参照を渡しているに過ぎなかったということ。
当然メソッドもそれが属するオブジェクトが消されればアクセスできなくなります。処理そのものを渡している、というように勘違いした結果オブジェクトのライフサイクルを度外視してしまったのかと。
オブジェクトが消えるときによしなにしてくれると思い込んでいた
大本になるオブジェクトが無くなっていればイベントの購読解除もしてくれるだろうと思い込んでいたために、バグの発見が遅れてしまいました。これは完全な思い込みです。というか勝手に消えたらそれはそれで怖いな...
ちゃんとOnDestroy()
の中に明示的に削除処理を書く必要がありました。後始末も大事だよっていう実生活でも役立ちそうな教訓をこんな所で。
他の種のオブジェクト等についても言えるため、この思い込みは相当危険だったでしょう... 気づけて良かった。
終わりに
全体をみてどんな動きをするか、今書いているオブジェクトが使われなくなったときどうすれば他所に迷惑をかけないか、意識しながらコードを書こうと思うきっかけになりました。
他の分野を学ぶときも気を付けたいところです。
次回は@hijoushikiさんの記事です!お楽しみに。
[^1] 継承とオーバーライド使えばそれでもできますが...
[^2] 実際には多重のデリゲーションになると追うのが大変なのでスマートかと言われれば...
[^3] 本来、共同開発なら互いにコードをよく読んでバグらないか、疑問点がないかしっかり吟味する必要がありますが、それを吹っ飛ばしてます