feature image

2025年3月19日 | ブログ記事

後始末には気を付けようという話 ~event Action失敗談~

この記事は 新歓ブログリレー2025 13日目のものです
他の記事を見たい方は こちら↑ のリンクをクリック!

はじめに

初めましての方は初めまして、科学大デジタル創作同好会所属の学士 2 年(新3年)@Alt--erです。traP ではSysAd 班およびGame 班で主に活動しています(他にもアルゴリズム,グラフィック,サウンドに所属しています)。最近はゲームのプロジェクト進捗の日々を過ごしています。

今回の記事は個人的な備忘録。でも、人によっては技術記事になるかもしれません。

つまりどういう記事?

event Actionという便利でなんかプログラミング慣れてきた感が出る構文(実際にイベント駆動の理解につながる)を使ったときに、購読したメソッドの解除を忘れてバグらせた話です。(技術記事的にはあまりおいしくないかも... 個人の備忘録として)

event Action とは... の前に

言語仕様に則した厳密な説明はこの記事では省略します(当方C#の言語仕様の面については未達者です)。ご了承ください

さて、今回のタイトルにもあるevent Actionですがそもそもこれは何でしょう。その前にeventActionに分けて簡単に説明します。

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に渡されている処理を実行する、という意味となります。

今回の場合はButtonSetupOnClickEvent()が渡されており、その中に書かれた処理("Clicked"と出力する)が実行されることとなります。OnClickEvent()自体はprivateで宣言されていますが、別のクラスから操作できているのもポイントです(OnClickEventへのメソッドの割り当て自体はButtonSetupクラス内で行われているためprivateでもアクセスできる)。

え、何が嬉しいの?

ってなる人も多いと思います。なぜButtonOnClickの中に記述しないのでしょうか。

一番のメリットはButtonそのもののクラスを使いませるというところでしょう。
例えば、汎用的に使われるボタンであれば見た目や押したときの効果音等、共通している部分が多いでしょう。Button内部に処理を定義する形式ですと、それらの共通部分も繰り返し記述することになり手間ですし可読性も落ちてしまいます。[^1]

いうなれば、"ボタンを押したら何か起こるからくり箱の、からくりの部分だけ入れ替えて使いまわせる" といった状態に近いかも。

詳しい話

このように処理を変数として扱うときの型を総称してデリゲートと言ったりします。デリゲートを扱うには、本来定義や初期化などを複数行のコードを記述する必要があるのですが、それを逐一書いていては冗長になってしまいますし同じコードの繰り返しとなってしまいます。

C#では、16引数までのメソッドのデリゲートを予め言語仕様として定義しています。これが先ほど説明したActionと呼ばれるものです。また、引数をセットしたい場合はAction<T1,T2,...>のように記述します。

更に亜種として、返り値をもつメソッドを指定できるFunc<T>というのもあります

詳しくはmicrosoftのリファレンスをご覧ください。それはそうと16引数分全部説明書いてる様子見ると壮観ですね。

Action<T> Delegate (System)
Encapsulates a method that has a single parameter and does not return a value.
公式ドキュメントさん 1個1個ちゃんとページ作っています

event Actionについて

さて、Actionの説明で長くなってしまったのでこの部分は簡潔に書きます。このようにeventを付けることによって、代入が不可能になり、実行するメソッドの追加と削除のみが可能になります。

先ほどの説明では触れてなかったのですが、Actionには複数の処理を足していくことができます。その場合、足された順に(キューの要領で)処理が実行されていきます。なお、キューと異なる点として先頭でなくても処理を削除できる事ができます。

ところで、代入が不可能であることにどういう特異性があるのでしょうか。

代入が不可能というのを"上書きができない"と考えると分かりやすいかもしれません。Actionがpublicである限り、様々なクラスからメソッドを追加/削除することが可能になります。ここで、もし上書きが可能であった場合、各クラスそれぞれがあずかり知れないところで処理が追加/削除されるということになります。

これでは安全性が損なわれてしまうため、追加削除のみ可能なevent Actionが存在するというわけです。

event Action では+=,-=などしか使えず既存の物を上書きできない
event抜きなら問題ない

んで、結局のところ何を失敗したの?

さて、ここまでevent Actionについて話してきました。当時その機能を知った際には、処理を変数のように扱えること、修飾子をprivateのままにしても他のクラスから操作ができるようになること、正直スマートに書けるように見える[^2]ことからかなり多用することになりました。

これまでの話はC#の言語についてでしたが、ここからはUnity開発特有の話になります。(Unityの基本的な概念については一部端折ります。ご了承ください)

Unityでは一つのゲームを制作する際に、(概ね)ゲームの場面ごとにシーンと呼ばれるアセット毎に分けて制作を行います。シーン毎に、そのシーンでの進行を管理するゲームオブジェクト(hogeManagerと命名しがち)を設定することが多いのですが、ここで大きなミスが生じたのです。

はじめは上手くいっていた

時は冬ハッカソン(traP内で1月に開催しました。自分の班はまだですがブログ記事もあります!!)、プログラマー少数だった自分の班はかなり限界の体制だったため信頼マージ[^3]することも多くありました。

2024年度 冬ハッカソンを開催しました!!
2025年 1/20~26の一週間にかけて部内冬ハッカソンを実施しました例年は12月に開催している冬ハッカソンですが、ICTSCやISUCON、会場の都合などもあり、1月に開催となりました。21チーム98名の参加となり、冬ハッカソンとしては、過去最大級の規模となりました。 テーマを“unite”と”ひろがる”と設定し、後述するテーマ賞に見られるように上手く絡めた作品も多く誕生しました。 現在も製作に取り組んでいる班もあり、続々と完成報告が届いています。受賞作品の中にも製作進行中といった作品もあるので続報もお待ちください!!(一部作品は部内限定公開となっております) なお、今回のハッカソンは”プログラミング能力検定協会”様の協賛を受けております。この場を借りてお礼申し上げます。 【公式】プログラミング能力検定(プロ検)|受験者数No.1のプログラミング検定(小学生・中学生・高校生・大学生/専門学校生・社会人対象)プログラミング検定(資格・試験)なら、受験者数No.1の「プログラミング能力検定(プロ検)」。小学生・中学生・高校生・大学生(専門学校生)

そしてはじめのうち、シーンを跨がずにいるうちは問題ない動きができていました。

シーンをつなぎ合わせるときに...

問題はシーンを行き来することによって起こりました。
ここで、基本的にはシーンを移動すると全てのゲームオブジェクトが削除されます。但し、DontDestroyOnloadを用いることでこれを回避することができます。

ただ、これを用いることで事故が発生してしまいました。

ロード時に消滅しない管理者クラスのようなものを作っていたのですが、それが大量の”missing reference error”を吐き出したのです。

当時の記録がこちら エラーログ長すぎ&なぜか定期的に消えてtraQ上に残してました

つまるところ、”そんなもの無いのにアクセスしようとしているぞお前”って意味なのですが… 正直かなり困惑してました。シーンごとではちゃんと動いていたのに”繋げただけで動かなくなった”、という思考だったため。

無限エラー修正編

“参照がない”系のエラーということで値の代入忘れ等を疑いましたが違いそう、また始めのうちは正常動作していることもよくわからない度合いを増していたり。

原因は単純、”event Actionを用いて渡した処理の削除を忘れていた” でした。購読していた処理を持つオブジェクトがDestroyされているのにも関わらずそれにアクセスし続けていたということです。そりゃエラー吐くわな。

ただ、振り返ってみれば”それはそう”というような穴になぜハマったのでしょう。筆者の感覚ですが、次項のような意識の中があったのかと思ってます。

意識レベルの穴

シーンを遷移する意識が抜けていた

正直この部分が一番大きいと思います

シーンを遷移するときには基本全てのgameObjectが消されます。よくよく考えたら当たり前のことですが、案外コードを書いている間は忘れてしまうものです。テストも同一シーン内で行っていたため気づきから尚更遠ざかる結果に。

普段から"このシーンから出て行ったら、もう一度戻ってきたら"どのような挙動をするか考えたいものです。

処理への参照を渡しているだけの意識が抜けてた

他の大半の型でもそうですが、多くは参照渡しです。つまるところ、event Actionもそのメソッドへの参照を渡しているに過ぎなかったということ。

当然メソッドもそれが属するオブジェクトが消されればアクセスできなくなります。処理そのものを渡している、というように勘違いした結果オブジェクトのライフサイクルを度外視してしまったのかと。

オブジェクトが消えるときによしなにしてくれると思い込んでいた

大本になるオブジェクトが無くなっていればイベントの購読解除もしてくれるだろうと思い込んでいたために、バグの発見が遅れてしまいました。これは完全な思い込みです。というか勝手に消えたらそれはそれで怖いな...

ちゃんとOnDestroy()の中に明示的に削除処理を書く必要がありました。後始末も大事だよっていう実生活でも役立ちそうな教訓をこんな所で。

他の種のオブジェクト等についても言えるため、この思い込みは相当危険だったでしょう... 気づけて良かった。

終わりに

全体をみてどんな動きをするか、今書いているオブジェクトが使われなくなったときどうすれば他所に迷惑をかけないか、意識しながらコードを書こうと思うきっかけになりました。

他の分野を学ぶときも気を付けたいところです。

次回は@hijoushikiさんの記事です!お楽しみに。

[^1] 継承とオーバーライド使えばそれでもできますが...
[^2] 実際には多重のデリゲーションになると追うのが大変なのでスマートかと言われれば...
[^3] 本来、共同開発なら互いにコードをよく読んでバグらないか、疑問点がないかしっかり吟味する必要がありますが、それを吹っ飛ばしてます

Alt--er icon
この記事を書いた人
Alt--er

23B_ゲーム,Sysad,サウンド,アルゴに興味あり。 なんか色々頑張りたい

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年11月21日
School Breakin' Tag -新感覚おにごっこ-
s9 icon s9
2023年9月3日
タイピング&アクション『TypeTheCode』作りました
wal icon wal
2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2025年2月4日
冬ハッカソン2024 22班 「Queen Bee」
YHz_ikiri icon YHz_ikiri
2025年3月5日
2024年度冬ハッカソン12班「Hero Girl」
gurukun41 icon gurukun41
2025年2月3日
ユナ (You and) / 𝑩𝑰𝑮 𝑳𝑶𝑽𝑬 feat. 初音ミク 【2024年冬ハッカソン1班】
hijoushiki icon hijoushiki
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記