📑

[UEFN][Verse]グローバル変数を使いたい[1]シングルトンハックによる共有変数へのアクセス方法(ただし現在はもう使えません)

2023/06/19に公開

今回はVerseのユーザーコミュニティの中で「発見」され、しかし現在は使えなくなったテクニックである「シングルトンハック」を紹介します。なお「シングルトンハック」は土屋の造語です。

注意

この記事で紹介するテクニックはVerseの言語仕様変更により使えなくなりました。なので本来は書く必要が無いんですが、記録として残す意味がある気がしたのと、Verseでシングルトンを書きたい人がいた時のためにまとめておくことにしました。

共有変数にアクセスしたい

Verseでは、現時点ではグローバル空間に変数を定義できません(定数は定義出来ます)。「現時点では」と付記しているのは、将来的には対応予定があるっぽいからなのですが、ひとまず出来ないという前提で話を進めます。

OOP的にはグローバル空間に変数を持つのは行儀の良い事とはされませんが、さりとて小さなコードを書く時に長大なオブジェクトツリーの中で変数への参照を伝播させるより、変数一個をピッとグローバル空間に配置した方がコードの見通しが良くなる事はあるわけです[1]

Verseで言うと、島のゲームギミックを構築する際に、獲得ポイントや残り人数などの「カウントした値」を変数に格納しておき、任意のデバイスから参照したいという需要があります。グローバル変数定義が封じられているとこれを手軽に出来ないわけです。

verseコミュニティはこのグローバル変数定義封じの回避方法を探し、やがてシングルトンパターンをハックした手法を発見しました。以下これを「シングルトンハック」と呼ぶ事にします。

シングルトンパターン概要

先にシングルトンパターンについて概要を説明しておきます。ふんわりな説明で正確ではないのでご注意ください。

シングルトンパターンはGoF本で提案されたデザインパターンの1つで、プログラム空間全体でただ1つインスタンス化される事が保証される非staticのオブジェクトを用意するコードパターンの事を指します。

具体的にはオブジェクトのコンストラクタをprivate化し、用意されたエントリポイントからのみオブジェクトを参照させるようにしています。エントリポイントではオブジェクトが未生成であれば生成して保管し、既に保管されているならそれを返します。これにより、ユーザーはエントリポイントを介しての単一のインスタンスにのみアクセス出来るわけです。

余談ですが、GoF本の発刊(1994年)から30年近く経った現在、シングルトンパターンは「グローバル空間が汚染される」「ライフサイクルが管理しにくい」などの課題があり、むしろアンチパターンと考えられています。シングルトンが必要になる場合はDIパターン(DIコンテナ)の採用も検討した方が良いでしょう(今のVerseでDIコンテナを実装するのは難しいと思うけど)。

シングルトンパターンをハックする

話を戻して、Verseコミュニティが発見したシングルトンハックは「Verseではグローバル空間で変数が定義出来ないが、『変数を持つクラス』なら(何故か)定義出来るので、それを使おう」というシンプルな手法でした。

書き方は色々あると思われますが、公式フォーラムで知られているのは以下の様なコードです。見て分かる通り厳密なシングルトンパターンではなく、あくまでシングルトン風なコードです[2]

#①シングルトンクラスの宣言
global := class<varies><concrete>(): 
    #モジュール空間全体で共有したい変数
    var MaybeMainDevice : ?main_device = false

#②シングルトンオブジェクトの生成
Global : global = global{}

#③グローバル関数
GetMainDevice():main_device = 
    if:
        MainDevice := Global.MaybeMainDevice?
    then:
        MainDevice 
    else:
        main_device{} #こちらのフローが実行される事は想定していない。

以下コードの解説です。
①:globalシングルトンクラスを宣言しています。main_deviceが任意参照したいクラスです。ここではcreative_device派生クラスを想定しています。creative_device派生クラスはUEFN上でレベルに配置される物なので、ここでは生成せず、option型化してfalseで初期化しています。
②:global型のインスタンスGlobalを定義します。globalクラスは変数(MaybeMainDevice)を持っていますが、<varies>エフェクトを指定してるので定義が可能です。
③:MaybeMainDeviceへのアクセス関数です。global.MaybeMainDeviceはinternalなので、本来このような関数を経由する必要はないんですが、MaybeMainDeviceがoption型のため、直接参照する場合、毎回if式を使う必要があるため、その処理をカプセル化しています。ちなみに、if式のelse句のmain_device{}という処理は、コンパイルエラーを回避する為のハックで、実行される事は想定していません。

main_deviceの初期化部(OnBegin()など)では以下の様なコードを書いておきます。

#自分自身をシングルトンオブジェクトに登録する
set Global.MaybeMainDevice = option{Self}

以降は、任意のスコープにおいて、下記のようなコードでMainDeviceを取得できます。

#シングルトンオブジェクトからMainDeviceへの参照を取得
targetDevice := GetMainDevice()

loophole(抜け穴)の閉鎖

シングルトンハックはシンプルな割に強力だったため、コミュニティで広く使われたようです[3]。しかし、この手法はフォーラム上で公式スタッフからloophole(抜け穴)であると指摘があり、かつ、今後のアップデートで使えなくなると通達されました。

グローバル空間にミュータブルなオブジェクトを配置出来ない事には、それなりの理由があり[4]、将来的にはVerseの言語仕様を拡張して対応される予定でした[5]。にもかかわらず、シングルトンハックによって対応出来てしまうのは公式の想定外、まさしくloophole(抜け穴)だったわけです。

予告された通り、あるタイミングのアップデートで上記のコードはコンパイル出来なくなりました。恐らくですが、ミュータブルオブジェクトを持つクラスの宣言には自動的に<transacts>が指定されるようになり、そのためにグローバル空間でシングルトンクラスのインスタンスが定義出来なくなりました[6]

この仕様変更について、数人[7]がSNSで抗議していましたが、予め予告されていた事もあり、大きな問題にはならなかったようです。

終わりに&次回予告:Tagsを使おう。

というわけでシングルトンハックは使えなくなりました。ではデバイス間の変数共有はどうすればいいのかと言いますと、公式ではTagsの利用を推奨しています。

次回はこのTagsについて見ていきます。

参考リンク

記事内のコードはこちらを参考にしています。公式スタッフの反応などもこのスレッドに含まれています。

https://forums.unrealengine.com/t/i-came-up-with-a-way-to-make-singletons-in-verse/1139453

最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

脚注
  1. だからやっていいのかという議論はひとまず置く ↩︎

  2. ハックなので ↩︎

  3. 実際にVerse使いこなしてるクリエイターが何人いるのかは正直疑問だけども ↩︎

  4. 多分。恐らくメモリ管理上の課題があるのだと思われる。 ↩︎

  5. 多分。とはいえ無くてもなんとかなると思われる。 ↩︎

  6. 以前はvariesが指定されていたのでハックが通っていた。 ↩︎

  7. 土屋が確認した限りでは2人。 ↩︎

Discussion