【UEFN】オブジェクトへのインタラクションをVerseを使ってカスタマイズする

# まえがき
この記事では、ゲーム内でのボタンを操作する、アイテムを取得するといったインタラクションとそれに伴う処理がどのようにして実装されているのかを解説していきます。
未だベータ版ということもあり、ネット上のサンプルコードや解説記事は非常に少ないと感じます。
UEFNは元々存在していたクリエイティブモードの拡張でもあるため、クリエイティブモードの情報とUEFNの情報が混在しやすいというつらさもあります。
これからUEFNを触ってみたい、UEFNのダウンロードが完了し、島を作成したものの何をすればいいのか分からないといった方の力になれば幸いです。


執筆時点でのUnreal Editor Fortniteのバージョンは　5.2.0-26171015+++Fortnite+Release-25.11　です。
この記事はUEFNがすでにダウンロード済みであり、プロジェクトの作成ができる状態であることを前提にしております。[UEFNのセットアップ方法](https://dev.epicgames.com/documentation/ja-jp/uefn/launching-in-unreal-editor-for-fortnite)は公式をご参照ください。


# 準備
解説するに当たって新たにプロジェクトを作成し、必要なコードを追加していくのもいいのですが「プロジェクトブラウザ」には「機能例」というタブにすでに一通りの機能が実装されたプロジェクトがあります。
こちらのプロジェクトはゲーム中の仕掛けがすでにVerseで実装されており、コードのほぼすべての行にコメントが記載されているためわかりやすく、非常にためになったと感じております。
今回はこの中のパルクールステージを例に進めていきます。

![uefn-project-top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/c9cb87ce-7858-9b3b-b47e-4cec98bd3c22.png)

プロジェクトを開き、Verseを起動するとすでにparkour_race_device.verseというファイルの中にゲームの仕掛けを動かすためのコードが記載されています。

https://dev.epicgames.com/documentation/ja-jp/uefn/user-interface-reference-for-unreal-editor-for-fortnite

<details><summary>今回使用するサンプルコード</summary>

    using { /Fortnite.com/Devices }
    using { /Fortnite.com/Characters }
    using { /Verse.org/Simulation}
    using { /UnrealEngine.com/Temporary/Diagnostics }
    
    log_parkour := class(log_channel){}
    
    # This is a Verse-authored creative device that can be placed in a level.
    # This script example is a take on Parkour where the objective is to gather 4 batteries before the timer expires.
    # 
    # Getting started:
    #   https://dev.epicgames.com/community/fortnite/getting-started/verse
    #
    parkour_race_device := class(creative_device):
        # Logger for the parkour experience channel.
        Logger : log = log{Channel:=log_parkour}
    
        # Device reference to the player spawner.
        @editable
        PlayerSpawnDevice:player_spawner_device = player_spawner_device{}
    
        # End Game Device for a Victory.
        @editable
        EndGameVictoryDevice:end_game_device = end_game_device{}
    
        # Timer for this game mode.
        @editable
        TimerDevice:timer_device = timer_device{}
    
        # Hud message feedback for the player when they get a battery.
        @editable
        HUDMessageBattery:hud_message_device = hud_message_device{}
    
        # An array of the Item Spawners we have.
        @editable
        BatteryItemSpawners:[]item_spawner_device = array{}
    
        # Spawner used to create secret batteries that can give the player additional time.
        @editable
        SecretBatteryItemSpawner:item_spawner_device = item_spawner_device{}
    
        # Tunable amount of time to add when the player picks up the secret battery.
        @editable
        SecretBatteryTimeReward:float = 10.0
            
        # Declaration for an integer that represents the number of batteries we've collected so far.
        var BatteriesCollected:int = 0
        
        # Messages to display when a battery is collected.
        BatteryCollectedMessage<localizes>(Amount:int):message = "You collected {Amount} battery"
        BatteriesCollectedMessage<localizes>(Amount:int):message = "You collected {Amount} batteries"
    
        # Messages for collecting the secret battery and the completion message.
        AllBatteriesCollectedMessage<localizes>:message = "You collected all of the batteries!"
        SecretBatteryCollectedMessage<localizes>:message = "You collected the secret battery, additional time added!"
        
        # Runs when this device_script is started in a running game.
        OnBegin<override>()<suspends>:void=
            # You can output to the log like this to determine what your script is up to.
            Logger.Print("Parkour Race Script Started!")
    
            # Subscribing to the AgentSpawnedEvent.
            # When the player spawns, the function "HandleAgentSpawned" is called.
            PlayerSpawnDevice.SpawnedEvent.Subscribe(HandleAgentSpawned)
    
            # Same for the Timer. We subscribe to the time expired event.
            TimerDevice.FailureEvent.Subscribe(HandleTimerExpired)
    
            # We go through our array of BatteryItemSpawners and for each Item Spawner, we subscribe to the ItemPickupEvent.
            # We do this for each Item Spawner and we don't save the handle like we did for the AgentSpawnedSubscription and TimerExpiredSubscription above.
            # You don't have to save the handle, but without it, you won't be able to cancel the subscription so it will fire every time an item is picked up. 
            for (BatterySpawner : BatteryItemSpawners):
                BatterySpawner.ItemPickedUpEvent.Subscribe(HandleBatteryPickedUp)
            
            <# An alternate way to do the for loop above that uses an integer to walk through the array.
               for (i:int := 0..BatteryItemSpawners.Length - 1):
                    if (Battery := BatteryItemSpawners[i]):
                        Battery.ItemPickedUpEvent.Subscribe(HandlebatteryPickedUp) #>
            
            SecretBatteryItemSpawner.ItemPickedUpEvent.Subscribe(HandleSecretBatteryPickedUp)
    
        
        # A function that is called when the Agent Spawns from the Player Spawn Pad.
        HandleAgentSpawned(Agent:agent):void=
            Logger.Print("Agent Spawned!")
    
            # Reset the Timer Device to ensure respawning players get the full amount of time.
            TimerDevice.Reset(Agent)
            TimerDevice.Start(Agent)
    
        
        # Function that is called when a battery item is picked up from the Item Spawners.
        HandleBatteryPickedUp(Agent:agent):void=
            # Increment the number of batteries collected.
            set BatteriesCollected = BatteriesCollected + 1
    
            # This is how you can output the number of batteries to the log. Useful for debugging.
            Logger.Print("Number of batteries picked up: {BatteriesCollected}")
    
            # Check to see if there are enough batteries collected to the end the game.
            if:
                BatteriesCollected >= BatteryItemSpawners.Length
            then:
                # Check to see if we've collected 4 (or more) batteries. If so, we have won, call the EndGame function.
                spawn { EndGame(Agent) }
            else:
                # This code runs if the number of batteries is less than 4. Show a Hud Message to update the player on their progress.
                # HUD message is "battery" if only 1 battery is collect. Becomes "batteries" if more than 1 is collected
                if:
                    BatteriesCollected = 1
                then:
                    HUDMessageBattery.SetText(BatteryCollectedMessage(BatteriesCollected))
                    HUDMessageBattery.Show(Agent)
                else:
                    HUDMessageBattery.SetText(BatteriesCollectedMessage(BatteriesCollected))
                    HUDMessageBattery.Show(Agent)
    
                # Then we get the next Item Spawner in our array we set up.
                # We do this by "indexing" into the array. It is inside the "if" statement to ensure that NextBatterySpawner is referenced correctly.
                if: 
                    NextBatterySpawner := BatteryItemSpawners[BatteriesCollected]
                then:
                    # If we got the next item spawner, call SpawnItem which will activate the next battery to get.
                    NextBatterySpawner.SpawnItem()
    
    
        HandleSecretBatteryPickedUp(Agent:agent):void=
            Logger.Print("Picked up secret battery")
    
            # Get the time remaining so we can add additional time to it.
            var TimeRemaining:float = TimerDevice.GetActiveDuration( Agent )
            var TimeToAdd:float = (TimeRemaining + SecretBatteryTimeReward)
    
            # Add additional time to the Timer Device, but don't go over the initial starting time.
            TimerDevice.SetActiveDuration(Min(TimeToAdd, TimerDevice.GetMaxDuration()), Agent )
    
            HUDMessageBattery.SetText(SecretBatteryCollectedMessage)
            HUDMessageBattery.Show(Agent)
    
    
        # Function that is called when the timer expires.
        HandleTimerExpired(MaybeAgent:?agent):void=
            Logger.Print("Timer Ended")
    
            if (Agent := MaybeAgent?):
                # Eliminate the player
                if: 
                    FortCharacter:fort_character = Agent.GetFortCharacter[]
                then:
                    FortCharacter.Damage(500.0)
    
        
        # Asynchronous function that handles the end of the game.
        EndGame(Agent:agent)<suspends>:void=
            HUDMessageBattery.SetText(AllBatteriesCollectedMessage)
            HUDMessageBattery.Show(Agent)
    
            # Wait for three seconds before ending the game.
            Sleep(3.0)
            EndGameVictoryDevice.Activate(Agent)
    
    
        # Runs when this device script is stopped or the game ends.
        OnEnd<override>():void=
            Logger.Print("Verse device stopped!")    

</details>

このサンプルはマップ上に配置されているアイテムの取得のイベントを作成しています。
parkour_race_deviceの中で該当の仕組みがどこかというのを追っていくかたちで確認していきます。
その動作を実現させているアイテムスポナーという仕掛けの該当部分を追ってみます。

# 全体の流れ
1. Verseに仕掛けのリファレンスを配置する
1. 仕掛けのイベントを設定する
1. creative deviceから仕掛けを参照させる

図にするとこんな感じ
![スクリーンショット 2023-07-05 220926.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/ce74409e-30db-9cc5-6d8d-4bb30b80e177.png)


## 1. Verseに仕掛けのリファレンスを配置する
Verseから仕掛けを参照するには、仕掛けのタイプに対応したプロパティを追加します。
アイテムスポナーを使用したい場合はアイテムスポナー仕掛けのタイプのクラスを使って初期化する必要があります。
初期化されたプロパティを用意することによってエディタで表示されるようになります。

該当のコードは以下です。

    using { /Fortnite.com/Devices }
    .
    .
    parkour_race_device := class(creative_device):
        .
        .
        # An array of the Item Spawners we have.
        @editable
        BatteryItemSpawners:[]item_spawner_device = array{}

後に、エディタからレベルに配置されたアイテムスポナーとVerseによって定義されたアイテムスポナーの参照箇所も確認していきます。

## 2. 仕掛けのイベントを設定する
アイテムスポナーにより設置されたアイテムの取得時のイベントを記述します。

該当のコードは以下です。
    
    OnBegin<override>()<suspends>:void=
        .
        .
        .
        for (BatterySpawner : BatteryItemSpawners):
                BatterySpawner.ItemPickedUpEvent.Subscribe(HandleBatteryPickedUp)

`OnBegin`はゲーム開始時にcreative deviceから呼び出されます。
BatterySpawnerが持つItemPickedUpEventのSubscribeにHandlerを渡すことによってイベント発生時に呼び出す関数を指定できます。

HandleBatteryPickedUpの中ではざっくりと
- 取得したアイテムの合計を加算する
- アイテム取得数のメッセージを送信する
- 取得したアイテムの総数がスポナーの総数以上ならゲームを終了する

等の処理を行っています。

## 3. Creative deviceから仕掛けを参照させる
Verseに記述したコードはビルドすることにより、使用可能になります。（今回はすでにビルド済み）

![verse-build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/f161453b-f079-0956-fb3b-35cd7d2d551b.png)

ビルドが正常に完了している場合、コンテンツブラウザより作成したcreative deviceが利用可能になります。ドラッグオンドロップでdeviceを配置できます。（今回はすでに配置されています）

![creative_device.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/239f3680-a659-d807-2a71-a574a150a146.png)

設置されたcreative deviceを選択すると詳細タブにVerseで設定したリファレンスの一覧が表示されていますね。

![ref-device.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/9f7e80a1-453d-7cb5-bfd1-49fb6b2137e2.png)

参照先の仕掛けをあらかじめ設置しておきcreative deviceからその仕掛けを選択することで参照先の仕掛けがイベントを持つようになります。

![スクリーンショット 2023-07-05 185718.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3112051/27eb2124-c089-9b06-4e6b-8c1ec625bb40.png)

今回Verse上で記述されていたBatteryItemSpawnersの一つ目として参照している仕掛けです。


以上がレベル上に配置されたオブジェクトをVerseを使ってカスタマイズする流れとなります。


# おわりに
筆者自身も初学者ですので間違い等あればご指摘いただけますと幸いです。
また拙作で恐縮ではございますが、UEFNのゲームの公開もしておりますので、お暇があれば遊んでみてください。
早打ちゲーム　8011-3489-5685
クイズゲーム　8678-6360-7074
ボックスファイト　4027-3011-6768


### 参考
https://dev.epicgames.com/documentation/ja-jp/uefn/user-interface-reference-for-unreal-editor-for-fortnite

https://dev.epicgames.com/documentation/ja-jp/uefn/unreal-editor-for-fortnite-glossary

https://dev.epicgames.com/documentation/ja-jp/uefn/customize-device-properties-in-verse

https://dev.epicgames.com/documentation/ja-jp/uefn/coding-device-interactions-in-verse