電通総研 テックブログ

電通総研が運営する技術ブログ

【UEFN】はじめてのUEFN Verse 基礎編

みなさんこんにちは! 電通総研 金融ソリューション事業部の松崎です。
本記事は、Unreal Editor for Fortnite(以下UEFN)の初学者向け紹介記事の第2弾です。
前回の記事ではUEFN全体の基礎編として紹介しました。今回はUEFN特有のプログラム言語である「Verse」に注目して紹介します。

「UEFNを使い始めたけど、Verseというものがイマイチ分からない」という方は、是非最後までチェックしてください。

目次

  1. Verseとは
  2. 実装の流れ
  3. 実装例

1 Verseとは

Verseに関しては前回の記事でも軽く触れていましたが、改めて紹介します。

VerseはEpic Gamesが開発したプログラミング言語です。Verseを使用してプログラミングを行うことで、Fortniteクリエイティブモードで使う各種ロジックをカスタマイズできるようになりました。
UEでゲーム制作を行う際はBlueprint(以下、BP)やC++ を用いますが、UEFNではBPは使用できません。また、逆にUEでVerseを使用することも不可となります

2024年4月現在のVerseプログラミングでは、主に「デバイス(仕掛け)ロジック」と「NPCロジック」をカスタマイズすることが可能です。
「デバイス(仕掛け)ロジック」に関しては、既存のデバイス同士を連動させるロジックを作成したり、特定の条件(キャラクターの状態変化、時間経過など)でデバイスを起動させるロジックを作成できます。
また、「NPCロジック」に関しては、NPCスポナーデバイスからスポーンさせるNPCの挙動ロジックをカスタマイズすることが可能です。

1.1 Verseの言語的特性

Epic Games公式ドキュメントを参照に、Verseの言語的な特性は以下のように示されます。

  • 静的な型付け
  • マルチパラダイム言語(関数型・オブジェクト指向・命令型プログラミング)
  • 式で記述(文の記述はなし)
  • 失敗コンテキスト
  • 言語レベルでの並行処理

Epic Gamesは、Verseを「メタバース開発の際の共通言語」とさせることを目的としており、将来的には現在のWeb開発におけるJavaScriptの立ち位置を目指しています。
そのため、Verseは設計思想として「シンプル」「汎用的」「ハイパフォーマス」「長期的な利用可能性」などを掲げており、上記の特性はその思想が反映されたものです。

1.2 Verseの特徴的要素

1.1で示した中で、Verseの大きな特徴である「失敗コンテキスト」「並行処理」に関して解説します。

①失敗コンテキスト

失敗コンテキストとは「失敗する可能性がある式を実行できるコンテキスト」です。
失敗する可能性がある式とは、具体的には「成功して何かしらの値を生み出すか、失敗して何も値を返さない式」を意味します。つまり、失敗を前提として式を実行させることができる、ということです。

注意点として、失敗コンテキストで例外処理が走ることはありません。そもそもVerseの言語仕様として例外処理が存在しません。失敗コンテキスト内で式が失敗した場合は、コンテキスト内の処理がすべてロールバックされます。

失敗コンテキストにより、特定の式をコミットすることなく試し実行することが可能になります。
以下に、失敗コンテキストを利用した記述例を示します。

if (Element := MyArray[Index]):
    Log(Element)
  • 「:=」は変数や定数の初期化演算子
  • ifの()内にて、MyArrayt配列のインデックスが有効か否かをチェック
  • インデックスが有効であれば、ElementへアクセスしてLog出力
  • インデックスが無効の際はロールバック(ElementへのアクセスやLog出力は実行されない)

②並行処理

ここで紹介する「並行処理」に関して、Epic Games公式ドキュメントでは「並列処理」と記載されています。
プログラミングにおける一般的な認識として、「並行処理」と「並列処理」は以下のように説明されます。

  • 並行処理:
    • 処理を細かく分割し、それぞれの処理を切り替えながら実行することで複数の処理を並行で進めること。時間を「範囲」で見ると複数の処理が実行されているが「点」で見るとどれか1つの処理のみが実行されている状態。
  • 並列処理:
    • マルチコアCPUなどにより、複数の処理を同時に実行すること。時間を「点」で見た際も複数の処理が実行されている状態。

Verseでは言語レベルにて並行処理の制御が可能であり、これはノンプリエンプティブマルチタスクで動作させていることを意味します。(逆にVerseからマルチコアCPUを制御できるわけではないため、本記事では「並列処理」とは呼称しません)

次に、Verseにおける並行処理の制御方法を説明します。
Verseのすべての式は、「immediate式」と「async式(以下、非同期式)」に分けられます。
Epic Games公式ドキュメントにて、それぞれの式は以下のように説明されています。

  • immediate式:遅延なく評価され、現在の「シミュレーションアップデート」内にて評価が完了する
  • 非同期式:評価が後回しになる可能性があり、現在または後続の「シミュレーションアップデート」で完了することもあれば、完了しないこともある

※ 「シミュレーションアップデート」という概念が出てきていますが、これはVerseにおける最小実行単位です。3DCGにおけるレンダリングの最小単位は「1フレーム」ですので、基本的にはこれと同じになります。
(ただし、「シミュレーションアップデート」と「フレーム」の時間を意図的にズラすことは可能です。また、オンラインのゲームサーバーが同期されなくなった場合など、「フレーム」の更新前に「シミュレーションアップデート」が完了することはあります。詳細はこちらをご参照ください)

Verseでは、上記の「immediate式」と「非同期式」を使い分けることによって、並行処理を言語側から制御することが可能です。

1.3 基本概念と記述方法

Verseはマルチパラダイム言語として設計されており、主に以下のパラダイム要素を取り入れています。

Epic Games公式ドキュメントによると、Verseにおけるマルチパラダイムは「決定論的プログラミング」を可能な限り追求する形で取り入れられているとのことです。

Verseプログラミング言語としての基本的な構文方法や機能は、Epic Games公式ドキュメント(Verse言語のクイックリファレンス)に記載されております。実装する際はご参照ください。

2 実装の流れ

Verseファイルを実装する際の流れを紹介します。

2.1 クラスの定義

Verseファイルを作成する際は、最初にクラスの定義を行います。
この際、Verseファイルの目的によって継承するクラスが変わります。

① デバイス(仕掛け)ロジック

バイス(仕掛け)ロジックを作成する場合は、[creative_device]クラスを継承させます。
このクラスは、カスタムしたデバイス(仕掛け)ロジック作成用にEpic Gamesが用意したクラスです。
[creative_device]クラスが継承されたクラスでVerseファイルを作成すると、コンパイルされる際に自動でUEFNコンテンツブラウザに表示されます。また、Verseファイル(クラス)のインスタンスはUEFNにてコンテンツブラウザからビューポート上にドラッグすることで配置されます。

なお、[creative_device]クラスは[/Fortnite.com/Devices]モジュールにて定義されているため、事前にこのモジュールをインポートする必要があります。

  • インポート式
using { /Fortnite.com/Devices }
  • クラス定義式例(継承含む)
hello_world_device := class(creative_device):

NPCロジック

NPCロジックを作成する場合は、[npc_behavior]クラスを継承します。
このクラスはカスタムNPC動作の作成用にEpic Gamesが用意したクラスです。
また、[npc_behavior]クラスを継承したクラスでは、CharacterDefinitionアセットかnpc_spawner_deviceで生み出されるキャラクターに対しての挙動ロジックを定義できます。

なお、[npc_behavior]クラスは[/Fortnite.com/AI]モジュールにて定義されているため、事前にこのモジュールをインポートする必要があります。

  • インポート式
using { /Fortnite.com/AI }
  • クラス定義式例(継承含む)
npc_custom_action := class(npc_behavior):


Verseにてインポート可能なモジュール一覧はこちらをご参照ください。

2.2 デバイス参照の定義

バイス間の連動ロジック作成時などに、Verse内で既存デバイス(仕掛け)を参照する必要があるときは、そのデバイス型の変数を「UEFNから編集可能な形」で定義する必要があります。
「UEFNから編集可能な形」で定義する理由としては、実際のデバイスインスタンスとの紐づけをUEFNで行う必要があるためです。
Verseでは、紐づけ用の箱を作っておくイメージになります。

バイス用の変数を作成する際に用いる各種デバイス型名はこちらをご参照ください。

@editable
MyDevice:timer_device = timer_device{}

上記では、「タイマーの仕掛け」紐づけ用の箱を作成しています。
「@editable」を記述することでUEFNから編集可能な形にしています。

上記にモジュールインポートとクラス定義の式を加え、以下のように記述します。

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

teckblog_device := class(creative_device):

    @editable
    MyDevice:timer_device = timer_device{}

試しにこの記述のVerseファイルをコンパイルしてUEFNのビューポート内に配置すると、インスタンスの詳細から「タイマーの仕掛けインスタンス」との紐づき設定が行えるようになっていることを確認できます。

2.3 処理の記述

バイス参照の定義まで完了しましたら、Verseで行う処理を記述します。
ここでは、Verse特有の機能をいくつか紹介します。

機能の網羅的な詳細に関しては、Verse モジュールリストから対象となるクラスを探してご参照ください。

(例:creative_classの機能 → /Fortnite.com/Devices/creative_device/Functions)

① creative_device クラス

  • OnBegin/OnEnd:クリエイティブモードのゲーム終了/開始時に呼び出されるメソッド
  • Show/Hide:デバイスインスタンスのオブジェクトをゲーム内で表示/非表示
  • GetPlayspace:fort_playspaceインタフェースへアクセス

npc_behavier クラス

  • OnBegin/OnEnd:NPCの追加/削除時に呼び出されるメソッド
  • GetAgent:ゲーム内の全NPC情報を取得(agentクラス)

③ fort_playspace インタフェース

  • GetPlayers:ゲーム内の全プレイヤー情報を取得(playerクラス)
  • PlayerAddedEvent/PlayerRemovedEvent:ゲーム内にプレイヤーが参加/離脱した際に信号を送信

④ player クラス(agent の子クラス)

  • GetFortCharacter:fort_characterインタフェーズへアクセス

⑤ fort_charcter インタフェース

  • GetAgent:キャラクターに紐づいたエージェントの状態を取得
  • EliminatedEvent:キャラクターが試合から除外された際に信号を送信

2.4 イベント処理

Verseでは、イベント処理を定義することが可能です。イベント処理とは、「特定のイベントが発生した際に動きだす」処理のことを指します。

イベント処理の際に使用する機能を紹介します。

① Awaitable インタフェース

  • Await:信号が送信されるまで実行中のタスクを待機

以下は、プレイヤー追加時に特定のメッセージが出される処理例です。
プレイヤー追加時にPlayerAddedEventによって信号が送信され、Await関数が解除されることによって後続処理(Print)が動き出します。

PlayerAddedEvent.Await()
Print("プレイヤーが追加されました")

② Subscribable インタフェース

  • Subscribe(t):信号が送信された際に(t)に登録したメソッドを呼びだす

以下は、プレイヤー追加時にAddWeaponメソッドが呼び出される処理例です。
プレイヤー追加時にPlayerAddedEventによって信号が送信され、Subscribe関数がAddWeaponメソッドを呼び出します。
(AddWeaponメソッドは別で定義されている前提になります)

PlayerAddedEvent.Subscribe(AddWeapon)


①のAwait関数を用いたイベント処理では、「待機状態を解除する」というトリガーを用いているため、1回処理が完了すると2回に信号が送信されても起動しません。(「ループ処理を組んで再度待機状態に戻す」などで、再利用すること自体は可能です)
一方、②のSubscribe関数を用いたイベント処理では信号が送信される度にイベント処理が起動します。
そのため、起動されるイベント処理の用途に合わせて使い分けが必要です。

なお、上記で紹介しているイベント処理用インタフェースは、PlayAddedEventのように特定条件で信号を送信してくれる機能を使うことが前提になります。このような機能が用意されていない条件をトリガーに設定したい際は、[event(t)]クラスを用いて、「信号を送信するロジック」から実装する必要があります。
こちらに関しては別記事にて紹介予定です。

3 実装例

2章で紹介した各種実装方法などを用いて、例となるロジックを実装していきます。
今回はデバイス間の連携ロジックを作成します。

具体的には、タイマーのカウントダウンが0になるごとに、1つずつバリアを消すロジックを実装します。
このロジック内で連携するデバイスは「バリアの仕掛け」と「タイマーの仕掛け」です。

※バリアとはUEFNがデフォルトで提供しているデバイスの一つで、主に「キャラクターや銃弾を通さない半透明な壁」として利用されます。

最初に、デバイスロジックテンプレートからVerseファイルを作成します。


以下のようにVerseを記述しました。

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


timer_barrier_connect_device := class<concrete>(creative_device):
    @editable
    Barriers : []barrier_device = array{barrier_device{}}
    @editable
    Timers : []timer_device = array{timer_device{}}


    OnBegin<override>()<suspends>:void=
        for(Index -> Timer : Timers):
            Timer.SuccessEvent
            .Subscribe(event_handler{Device := Self,Timer_num := Index}
            .TimerCompleatedHandler)


    DropBarriers(Index: int):void=
        if(Barrier :=  Barriers[Index]):
            Barrier.Disable()
        
        
event_handler := class:
    Device : timer_barrier_connect_device
    Timer_num : int
            
    TimerCompleatedHandler(PlayerAgent:?agent): void =
        Device.DropBarriers(Timer_num)

処理の流れをまとめると以下になります。

  1. Onbeginメソッド内でタイマーカウントダウンの完了イベント(SuccessEvent)を、待つ
  2. タイマー完了イベントが発生したら、Subscribe関数に登録されている[event_handler]クラスのTimerCompleatedHandlerメソッドが呼び出される。
    (この際、完了した「タイマーの仕掛け」のIndex数値が[event_handler]クラスへ渡される)
  3. TimerCompleatedHandlerメソッドの中で、メインクラスのDropBarriersメソッドが呼び出される。
    (「2.」で渡されたIndex数値をDropBarriersメソッド呼び出しの引数に用いる)
  4. DropBarriersメソッドにて、Index数値に対応する「バリアの仕掛け」を無効化


以下、各部に分けて説明します。

最初はモジュールのインポートです。

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

次に、メインクラスとなる[timer_barrier_connect_device]クラスを定義します。
[creative_device]クラスを継承する形で定義しています。

timer_barrier_connect_device := class<concrete>(creative_device):

メインクラスの中に、「バリアの仕掛け」と「タイマーの仕掛け」用の変数を定義します。
今回はそれぞれの仕掛けを複数個用いるので、配列で定義しています。

    @editable
    Barriers : []barrier_device = array{barrier_device{}}
    @editable
    Timers : []timer_device = array{timer_device{}}

OnBeginメソッドの中身を記述します。(OnBeginはゲーム開始と同時に動きだすメソッドです)
for文を利用し、「タイマーの仕掛け」の個数分だけイベント処理が登録されるようにしています。
Subscribe関数にて呼び出されているのは[event_handler]クラスのTimerCompleatedHandlerメソッドです。(後で定義します)

    OnBegin<override>()<suspends>:void=
        for(Index -> Timer : Timers):
            Timer.SuccessEvent
            .Subscribe(event_handler{Device := Self,Timer_num := Index}
            .TimerCompleatedHandler)

DropBarriersメソッドです。名前の通り「バリアの仕掛け」を無効化するためのメソッドです。
引数のIndexに対応した「バリアの仕掛け」を無効化します。
無効化の処理がif文に入っている理由は、配列インデックスの存在チェックのためです。(1.2 ①で紹介した失敗コンテキストを利用しています)

    DropBarriers(Index: int):void=
        if(Barrier :=  Barriers[Index]):
            Barrier.Disable()

メインクラスとは別で、[event_handler]クラスを定義します。
クラス内のTimerCompleatedHandlerメソッドでは、メインクラスのDropBarriersメソッドが呼び出されるように定義しています。

event_handler := class:
    Device : timer_barrier_connect_device
    Timer_num : int
            
    TimerCompleatedHandler(PlayerAgent:?agent): void =
        Device.DropBarriers(Timer_num)

各部の説明は以上となりますが、ここで
「Subscribe関数から、なぜ直接DropBarriersメソッドを呼び出していないのか?」を説明します。

[event_handler]という別クラスを経由してDropBarriersメソッドを呼び出している理由は、
「Subscribe関数からメソッド呼び出しを行う際は、呼び出しメソッドに対してパラメータ(Index数値)を渡すことができないから」です。
Subscribe関数からメソッドを呼び出す際、ほとんどの場合はagentパラメータしか渡すことができません。
渡せるパラメータは、Subscribe関数に信号を送っているイベント関数に依存します。
(今回の場合は[timer_device]クラスのSuccessEventが該当し、以下の通りagentパラメータしか渡せません)

Subscribe関数に上記の制約があるため、
イベントハンドラ用のクラスを別で作成し、(メソッド呼び出しではなく)クラス呼び出しの部分でパラメータを渡す」
という手段を取っています。詳細にはUEFNフォーラム投稿もご参照ください。


Verseの実装が完了したので、ビルドした後にVerseクラスをビューポートへドラッグ&ドロップし、Verseインスタンスを作成します。


UEFNの「Fortnite/Devices」から、「タイマーの仕掛け」と「バリアの仕掛け」を3つずつ配置します。
この際、「タイマーの仕掛け」は以下のように設定します。

  • 持続時間:30秒(タイマー1) / 60秒(タイマー2) / 90秒(タイマー3)
  • ゲーム開始時にスタート:オン

また、「バリアの仕掛け」は見分けがつくよう「バリアの素材」を変化させています。

Verseインスタンスの詳細から、「バリアの仕掛け」と「タイマーの仕掛け」をVerseに紐づけます。
アウトライナからVerseインスタンスを選択し、詳細画面でデバイス名の欄を開きます。(今回は「Timer Barrier Connect Device」)
BarriersとTimersの欄を開き、「+」を押してインデックスの配列要素を3つに増やします。
ビューポートの左から順に対応するよう、各インデックス配列要素に「バリアの仕掛け」「タイマーの仕掛け」を設定(紐付け)しました。


Fortniteにてテストプレイを行います。

ゲーム開始時

30秒経過後

60秒経過後

90秒経過後

想定通りにロジックが動いていることを確認できました。

おわりに

本記事では、UEFNにおけるVerseの基礎となる概念を実装例と共に紹介しました。
細かい構文方法やAPIに関しては、本文中に紹介したEpic Games公式ドキュメント(これとかこれ)にまとまっております。実際にVerseのコードを書く際はそれらをじっくり参照しながら記述すると、深い学びになると思います。

VerseではNPCの挙動ロジックをカスタマイズすることも可能ですので、次はそちらの実装例を紹介したいと思います。

現在、電通総研はweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間をお待ちしております!
電通総研の採用ページ

参考文献

Epic Games公式ドキュメント Verse 言語のリファレンス
Epic Games公式ドキュメント Verse 言語のクイック リファレンス
Epic Games公式ドキュメント Verse を使用したプログラミングの詳細
Epic Games公式ドキュメント モジュールとパス
Epic Games公式ドキュメント Verse API Reference
Epic Games公式ドキュメント Verse の仕掛けのプロパティをカスタマイズする
Epic Games公式ドキュメント 3.プレイヤー イベントをサブスクライブする
Epic Games公式ドキュメント クラス
Epic Games公式ドキュメント 失敗
Epic Games公式ドキュメント 並列処理の概要
Epic Games公式ドキュメント frame
Epic Gamesフォーラム 追加パラメーターを使用したイベント サブスクライブのガイド (ハンドラー関数)
Epic Games公式ドキュメント 4. 仕掛けをリンクする
Verse Concurrency—Time Flow: Everything, Everywhere in UEFN, All at Once | Unreal Fest 2023
UEデザイナーがUEFNで遊んでみた
verse言語の設計思想を読み解きたい(2)失敗コンテキストとは何か
Verse言語の設計思想を読み解きたい(9)非同期処理① 並行処理と並列処理
Verse言語の設計思想を読み解きたい(10)非同期処理② 非同期式と非同期コンテキスト
[UEFN][verse] UEFNのカスタムイベント実装を考える[1] Await()の使い方
[UEFN][verse] UEFNのカスタムイベント実装を考える[2] 「イベントリスナ」と「イベントハンドラ」についておさらい
【UEFN】 VerseでDeviceを連携させる(Verseの学習①)
【UEFN】Verseでイベント発生時に追加でパラメータを渡す
【UEFN】Verseでcreative_deviceクラスの多用をやめてみる

執筆:@matsuzaki.shota、レビュー:@nakamura.toshihiro
Shodoで執筆されました