[UEFN][Verse] デリゲートの実装(カスタムイベントハンドラクラス)
公式フォーラムで教えてもらって感銘を受けたので記事にしておきます。以下用語が統一出来ていないので各自ニュアンスで読んで下さい。
ボタンウィジェットにイベントハンドラを設定する関数
ボタンウィジェットというのは、canvas上にUIとして表示するボタンの事です。
3D空間上に配置されているポリゴンのボタンは「ボタンデバイス[1]」、スクリーン平面上に描画される黄色の四角形が「ボタンウィジェット」です。
ボタンウィジェットをユーザーがクリックすると、予め登録(subscribe)しておいたメソッドが実行されます。このような処理を一般に「イベント」、登録するメソッドを「イベントハンドラ」と呼びます。
さて、ボタンウィジェットは「ボタン」なのですから、クリックしたらなにかが起きて貰わないと困ります。なので、通常ボタンウィジェットの生成とメソッドの登録はセットになる筈です。そこで「ボタンウィジェットを生成してメソッド登録する」までを1個にした関数を実装したいと考えました。
失敗例
しかし、色々試したんですがこの関数の実装が上手く行きませんでした。以下のコードはコンパイルエラーになってしまいます。
#このメソッドはコンパイルエラー
CreateLoudButton(Name:string, Handler: widget_message->void):button_loud =
Button:button_loud = button_loud:
DefaultText := StringToMessage(Name)
Button.OnClick().Subscribe(Handler)
return Button
コードについて少し説明しておきます。
widget_message->void
は「引数にwidget_message型を受け取り、戻り値がvoidの関数」を表すfunction型です。
なお、アロー演算子->
を使ってfunction型を宣言する方法は公式ドキュメントにはありません。ドキュメント通りだとtype{_(:widget_message):void}
になると思います。どちらでも構いませんが、型システム的にもアロー演算子の方が自然な記述になります。
StringToMessage()関数は文字列をstring型からmessage型に変換する以下のような関数です。
#string型をmessage型に変換する
StringToMessage<localizes>(value:string)<computes> : message = "{value}"
DefaultTextがmessage型を必要とするのでこのようなコンバータが必要になります。
デリゲートを定義する方法が無い
さて、前述した通りこのメソッドはコンパイルを通らず、以下の様なエラーが出ます。
正直言ってこのエラーメッセージは意味がわからないのですが[2]、エラー自体の理由は明白で、Subscribe()関数が引数として受けとれるのは「関数のfunction型」ではなく、「インスタンスメソッドのfunction型」だからです。
関数(ここではグローバル関数の事)とインスタンスメソッドの何が違うのかというと、「関数(メソッド)自体を参照するのに必要な要素」の数が異なります。
グローバル関数の参照には、その関数が配置されているメモリのアドレス(C系言語風に言えばポインタ)が1個必要です。それに対し、インスタンスメソッドの場合は関数のアドレスの他に、そのメソッドの処理対象となるインスタンスのアドレスも必要になるのです。
そのため、インスタンスメソッドをユニークに指定するには、メソッドへの参照(function型)の他に、インスタンスへの参照(C#で言うthis)が必要です。つまり、パラメータが2個必要なのです。これはC#で言うデリゲートに相当します。
ところが、現在のVerseではユーザーがデリゲートを直接宣言する方法が用意されていません。すなわち、Subscribe()の引数に渡す変数は、ユーザーには定義出来ないのです。
カスタムイベントハンドラクラス
ではどうすればいいのかと言いますと、以下のように「関数ハンドラを保存するオブジェクト」を経由させるハックを使います。
#関数ハンドラを保存するクラス
my_custom_handler := class:
#コールバックしたい関数ハンドラを格納する定数
#「引数にwidget_messageを取り、戻り値がvoid」の関数のハンドラを保存する。
Callback: widget_message->void
#subscribeに登録するコールバック関数
HandleOnClickEvent(Payload:widget_message):void =
#格納しているコールバック関数を実行する
Callback(Payload)
#create_loudウィジェットを生成し、関数ハンドラを設定して返す
CreateLoudButton(Name:string, Handler:widget_message->void):button_loud =
#ボタンウィジェットを生成
Button := button_loud:
DefaultText := StringToMessage(Name) #ボタン表示名
#関数ハンドラを保存するオブジェクトを生成
CustomHandler := my_custom_handler{Callback := Handler}
#OnClickイベント発生時に呼びだす関数ハンドラを登録する
Button.OnClick().Subscribe(CustomHandler.HandleOnClickEvent)
#設定済みのボタンウィジェットを返す
return Button
my_custom_handlerクラスは生成時にイベントハンドラを登録します。ボタンがクリックされるとCustomHandler.HandleOnClickEvent()が実行され、その中で登録されていたイベントハンドラが正しく実行されます。
内部実装がどうなっているかは分かりませんが、恐らくインスタンスを格納/参照するための専用処理が生成されています[3]。とはいえ、Epicスタッフが提案しているスタイルなので問題無い筈です。
参考リンク
今回の記事は、フォーラムでの自分の質問に頂いた回答を元に作成しました(Thank you, Fenzy.)。
公式スタッフによる説明はこちらです。
お知らせ
verse言語とUEFNの記事を他にも書いているので御覧下さい。
最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。
#Verse #UEFN #Fortnite #Verselang #UnrealEngine
宣伝
「Unityシェーダープログラミングの教科書」シリーズ1~5をBOOTHで頒布中です。
Discussion
C言語を勉強していないのでアロー演算子->の意味が分かりません。どういう意味を持つ演算子か解説してもらえないでしょうか。
アロー演算子は
function
型(引数と戻り値を持つ型)を定義する為の記法です。C/C++で使うアロー演算子とは機能が異なるので注意が必要です。分類としてはJavaやrubyのラムダ式で使うアロー演算子と同じになります。「ラムダ式 アロー演算子」でググると良いかもしれません。ここでは、
widget_message->void
とtype{_(:widget_message):void}
は同じ意味になります。演算子の右辺/左辺には型を指定します。解説ありがとうございました。もう少し自分でも調べたいと思います。