LoginSignup
4
4

Verse言語へ提言。「Awaitable Property」のアイデアと実装を紹介

Last updated at Posted at 2023-11-30

こんにちは、XRエンジニアのイワケンです。

これはIwaken Lab.アドベントカレンダー2023の1日目の記事です。トップバッターとして頑張っていきます!

「好きな技術/今触っている技術」についてなんでもOKということで、私はUEFNのVerse言語をテーマに1つ記事を書きたいと思います。

[前提] UEFN/Verseとは

UEFN(Unreal Engine For Fortnite)は、2023年3月より導入された、Fortnite用のUnreal Engineベースのエディターを通じてオリジナルゲームを実装するためのシステムです。

Verse言語とは、Epic Game社が開発した、UEFN上で唯一動作するオリジナルのプログラミング言語です。

この記事の対象者

  • Verse言語の活用法に興味がある人

ということで、単なるUEFNでマップ作りたい!というだけの人にはあまり刺さらないかもしれません。プログラミング言語が好きで、Verse言語の可能性に魅力を感じている一部の人に刺さるかもしれない記事になります。

[背景] Verse言語でReactive Propertyを使いたかったが...

例えば、UnityのライブラリであるUniRxは、C#をベースにしてRx (Reactive Extensions)のアプローチを取り入れています。
その中にReactive Propertyという機能があり 「値の変更を通知してくれる変数」 のようなものです。

これがあると、ロジックの実装とUI実装が分離しやすくなるので便利です。また、毎フレーム(1秒間に30回など)UIを更新するのではなく、変数が変更されたときのみUIを更新するといった効率の良い実装が実現可能になります。

興味ある方はこちらの
"Web出身のUnityエンジニアによる大規模ゲームの基盤設計"の記事にある、MVPアーキテクチャの考え方に触れてもらえればと思います。

例えば、

var property = new ReactiveProperty<int>();
property.Value = 100;
property.Subscribe(x => {
    view.SetText("Height:" + x.ToString() + "m")
});

のように、実装することができます。

同じようなReactive Propertyの機能をVerse言語でも作りたいと思いました。

しかし、私自身の力ではこの機能を実現することはできませんでした。

私の理解では、実装に必要な機能がVerse言語に備わっていないという理解です。これについては後日別記事で書ければと思います。

もう一つ、Reactive Property実装チャレンジをやめた理由として、Epicとして 「Subscribeを活用する実装を推奨していない」というのがあります。
代わりに「イベントのためにAwait()を推奨します」という主張があります。

その根拠となる講演については、こちらのZenn Scrapにまとめましたので、みていただけると嬉しいです。

ということで、私は「Reactive Propertyは作れないかもしれないけど、Awaitable Propertyなら作れるぞ、むしろVerse言語ではこちらの方が主流になるかもしれない」と思いました。Verse言語でこの表現を言葉にしたのは、おそらく私が初めてです。

Awaitable Propertyとは 「値の変更をAwaitで通知してくれる変数」 です。

こちらのアイデアと実装をしましたので紹介します。

ただし、現時点で実装部分は不完全なものになります。アイデアを共有しつつ、実装の課題については皆さまのアドバイスの下改善していきたいと思います。

この記事でのアウトプットサンプル

このように、自分の位置の高さに応じて、黒板に文字を表示する実装を考えます。

awaitable2.gif

Awaitable Propertyによる実装イメージ

以下のように計算の実装では、int_awaitable_property型で変数を定義して計算を扱っています。
ポイントとしては、UIの更新の実装が一切されていないことです。

height_calculation_device.verse

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Concurrency}
using {IwakenVerseToolkit.awaitable_property_module}

height_calculation_device := class(creative_device):

    # Awaitable Propertyの変数定義
    PointProperty:int_awaitable_property = int_awaitable_property{} 
    
    # 外のクラスからアクセスするためのProperty
    GetPointProperty<public>():awaitable(int)=PointProperty
    @editable
    FloorHeight:float = 0.0 #基準となる床の高さ

    @editable
    EnterTrigger:mutator_zone_device = mutator_zone_device{}
    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        PointProperty.SetValue(0) # 初期値は0
        Agent := EnterTrigger.AgentEntersEvent.Await() #プレイヤーがエリアに入るまで待つ
        loop:
            Sleep(0.1) # 0.1秒空ける
            IntHeight := GetPlayerHeight(Agent) #高さを取得
            PointProperty.SetValue(IntHeight) #変数に代入

    ## 以下省略

以下がUI部分の実装です。別クラスに実装します。
これにより、変数が更新されたときだけに、UIを更新します。

height_view_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

height_view_device := class(creative_device):

    @editable
    Calculater:height_calculation_device = height_calculation_device{}

    @editable
    TextUI:billboard_device = billboard_device{}

    StringToMessage<localizes>(Text:string):message = "{Text}"

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        Print("")
        loop:
            Point := Calculater.GetPointProperty().Await() # プロパティが変更されるまで待つ
            TextUI.SetText(StringToMessage("Height:{Point}m")) # UIを更新

Awaitable Propertyの中身 (int型のみ)

こちらUniRxのReactive Propertyの実装を参考にしつつ、実装しました。

Githubにもあげていますので、参考にしてください (Star嬉しいです!)

awaitable_property_module (一部抜粋)
using { /Verse.org/Concurrency}

awaitable_property_module<public> := module:
    read_only_awaitable_property_interface<public>(t: type) := interface(awaitable(t)):
        GetValue<public>()<transacts><decides>:t
    awaitable_property_interface<public>(t: type) := interface(read_only_awaitable_property_interface(t)):
        SetValue<public>(t2:t):void

    int_awaitable_property<public> := class(awaitable_property_interface(int)):
        var Value :int = 0
        ChangeEvent:event(int) = event(int){}
        SetValue<override>(InputValue:int):void=
            if(not Value = InputValue):
                set Value = InputValue
                ChangeEvent.Signal(InputValue)
        GetValue<override>()<decides><transacts>:string=
            return Value
        Await<override>()<suspends>:string=
            return ChangeEvent.Await()

Verse言語の中で使用した機能として

  • awaitable(t)
  • event

があります。
生のソースコードを見ると

Verse.digest.verse
awaitable<public>(payload:type)<computes> := interface:
    Await<public>()<suspends>:payload
awaitable<public>()<computes> := awaitable(void)

event<native><public>(t:type)<computes> := class(signalable(t), awaitable(t)):
    Await<native><override>()<suspends>:t
    Signal<native><override>(Val:t):void

とあります。awaitableはinterfaceです。パラメトリック型により、明示的な型引数を指定しています。
eventはclassですが、実際の実装は<native>とあるように隠蔽されています。しかし、classであるおかげで、Signalメソッドを呼ぶと、Await()が実行されるという機構を持ってくれています。これにより、Awaitable Propertyを実装することができます。

実装したかったけどできなかったこと / 迷っていること

  • C#のProperty機能
  • awaitable_property(t: type)は実装不可?
  • SubscribeできるReactive Propertyは実装不可?
  • 初期化をコンストラクタで実装できないか?
  • GetValueは<decides><transacts>にすべきか?

C#のProperty機能

UniRxであるように、Property.Value = 2のように、直接的に値を代入してもイベントが送られるようにしたかったのですが、C#のようにPropertyとしてsetterの中に実装を書き込むことができないため、メソッドのSetValue()を呼び出す形にしています。

awaitable_property(t: type)は実装不可?

今回
int_awaitable_property<public> := class(awaitable_property_interface(int)):

というint型のproperty実装をinterfaceを実装することで行っていますが、UniRxのように
awaitable_property(t: type)という抽象クラスを作って、それを継承するint型のクラスを実装したかたったのですが実装不可能とみています。

理由として

  • Mutable memberはパラメトリッククラスでは実装できない
  • 汎用的な型に対する等値比較子が言語に備わっていない
awaitable_propertyを実装するとエラーがでる
awaitable_property(t: type) := class(awaitable_property_interface(t)):
    var Value:t # Mutable members in parametric classes are not yet implemented.
    ChangeEvent:event(t) = event(t){}
    SetValue<override>(InputValue:t):void=
        if(not Value = InputValue): #This function parameter expects a value of type tuple(comparable,comparable), but this argument is an incompatible value of type tuple(t,t).
            set Value = InputValue
            ChangeEvent.Signal(InputValue)
        ChangeEvent.Signal(InputValue)
    GetValue<override>()<decides><transacts>:t=
        return Value
    Await<override>()<suspends>:t=
        return ChangeEvent.Await()

# Mutable members in parametric classes are not yet implemented. とあるように、ミュータブル(変更可能)な変数は、パラメトリッククラスではまだ実装できない。とエラーが出ます。

これはのちに解決できると期待しています。

もう一つ、同じ値を代入したときに、通知されないように「等値比較子」で同じ値かどうか判定する必要があるのですが、汎用的な型に対して「等値比較子」が適応できないため、実装不可能となります。(間違っていたら教えてください)

This function parameter expects a value of type tuple(comparable,comparable), but this argument is an incompatible value of type tuple(t,t).

SubscribeできるReactive Propertyは実装不可?

Subscribeをメソッドに持つinterfaceとしてsubscribableがあります。しかし、Await()と違って、eventといった内部実装されたクラスが存在しません。なので、自分でSubscribeの機構を実装することができます。

    subscribable<public>(t:type)<computes> := interface:
        Subscribe<public>(Callback:type {__(:t):void})<transacts>:cancelable
    subscribable<public>()<computes> := subscribable(tuple())

これを内部実装する場合、関数を変数化し、配列で管理する必要があります。C#ではdelegateという仕組みがありますが、Verseには存在しません。代わりに型マクロという仕組みがあります。しかし、型マクロな変数を配列として管理するとエラーが出てクラッシュしたため、私は実装を諦めました。

こちらぜひ実装チャレンジしてみてください。

初期化をコンストラクタで実装できないか?

現在、Awaitable Propertyをインスタンス化したときに初期化の変数を入れられればよいのですが、できなくはないです。例えばこういう実装になります。

    make_int_awaitable_property<constructor><public>(Arg:int) := int_awaitable_property:
        Value := Arg

使用する時はこちら

    var PointProperty:int_awaitable_property = int_awaitable_property{}
    OnBegin<override>()<suspends>:void=
        set PointProperty = make_int_awaitable_property(0)

こちらには以下の課題点があります

  • 初期化されたときのイベントは通知されない
    • コンストラクタで変数の代入はできるが、それ以外の関数の呼び出しなどできないため
  • C#のように、同じクラス名のコンストラクタにならないので、忘れるリスクがある

なので、実装は可能だが、課題はあるという認識です。

GetValueは<decides><transacts>にすべきか?

Verseスタイルコードの4.2の「GetX 関数は<decides><transacts>にする」の法則にしたがって、<decides><transacts>を付けたのですが、失敗する可能性がないと考えれば不要かもしれません。
これをつける弊害として

  • GetValue[]の呼び出しが面倒くさくなる

というのがあります。例えば「0.1秒ごとに1増えるポイント」を実装する場合、以下のように毎回if文を書かないといけなくなります。

    PointProperty:int_awaitable_property = int_awaitable_property{}
    
    OnBegin<override>()<suspends>:void=
        PointProperty.SetValue(0) # 初期値は0
        loop:
            Sleep(0.1) # 0.1秒空ける
            if(CurrentValue := PointProperty.GetValue[])
                PointProperty.SetValue(CurrentValue + 1)

これだと使う気がなくなってしまうかもしれないので、入れるか悩んでいます。みなさんいかがでしょうか?

まとめ

Verse言語の活用は、2023年3月から始まり、今世界中で開拓されている分野になります。現在はFortnite向けのみですが、将来的にはUnreal Engineや、その他のメタバースプラットフォームで使えるようにするという目標をEpic Games社から聞いています。ということで私もVerse言語の活用方法を広げていきたいと思います。

今回「Awaitable Property」というアイデアと実装の紹介でしたが、実装部分はまだ不完全です。皆様のお力でよいものを作って行きたいと思います。

こちらのGithubにissueを投げていただけるとありがたいです。

アドカレ次回は...?

次のIwaken Lab.アドベントカレンダー2日目きくはな@VRさんです!お楽しみに!

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4