🙆

[Verse]切り替えの仕掛けを使った扉の開閉システム

2024/05/05に公開

はじめに

前回、扉を開く仕掛けをVerseで作成しました。

https://zenn.dev/cre8tfun_dev/articles/986f21d8e14c18

今回は、切り替えの仕掛けを使って開閉できる扉にしました。

https://youtu.be/WryZAO6uGGA

以降、本記事はバージョンv29.40 で執筆しています。

機能概要

切り替えの仕掛けと連動して、対応する扉の開閉を行います。切り替えの仕掛けと扉のペアをUEFN上で追加することで、複数の扉の開閉が可能です。

前提:
扉は建築小道具で、ゲーム開始時は閉じた状態です。
切り替えの仕掛けの状態は全体共有とし、持続データを考慮しません。

入力:
切り替えの仕掛け。トリガー

処理:

  • 扉の開閉:切り替えの仕掛けがオンになると扉を開く。オフになると扉を閉じる
  • 扉のリセット:トリガーが実行されると全ての扉を閉じる

コード & UEFN上の設定

コード

全文
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Fortnite.com/Devices/CreativeAnimation }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Fortnite.com/Devices/CreativeAnimation/InterpolationTypes }

opening_closing_door := class<concrete>:
    @editable
    MainId:type{ _X:int where 0 <= _X, _X <= 5 } = 0  # 部屋番号を設定。最小最大は任意
    @editable
    SubId:?int = false  # 同一部屋内で複数の扉がある場合に設定。扉の番号
    @editable
    DoorProp:creative_prop = creative_prop{}  # 扉のプロップ
    @editable
    InputSwitch:switch_device = switch_device{}  # 入力用スイッチ。オンオフで開閉を制御する

    var DoorId:string = array{}  # 扉のID。MainIdとSubIdを組み合わせて設定。一意
    var InitTransform:transform = transform{}  # 扉の初期位置

opening_closing_door_manager := class(creative_device):
    @editable
    RoomExits:[]opening_closing_door = array{}  # 各部屋の出口用の扉の配列
    @editable
    InputResetTrigger:trigger_device = trigger_device{}  # 入力用トリガー。全ての扉を閉じる

    RotAngle:float = 90.0  # 開く角度
    OpeningTime:float = 3.0  # 開くのにかける時間

    OnBegin<override>()<suspends>:void=
        InputResetTrigger.TriggeredEvent.Subscribe(OnResetTriggered)

        for (RoomExit:RoomExits):
            set RoomExit.DoorId = 
                if (SubId := RoomExit.SubId?) then "{RoomExit.MainId}_{SubId}"
                else "{RoomExit.MainId}"
            set RoomExit.InitTransform = RoomExit.DoorProp.GetTransform()

            spawn{ OpenCloseLoop(RoomExit) }

    OnResetTriggered(AgentOpt:?agent):void=
        if(Agent := AgentOpt?):
            spawn{ Reset(Agent) }

    # 扉の開閉
    # 対象の扉について、Switchのオンオフで開閉を制御する
    OpenCloseLoop(Door:opening_closing_door)<suspends>:void=
        loop:
            # Switchがオンになると扉を開く
            Door.InputSwitch.TurnedOnEvent.Await()
            if (AController := Door.DoorProp.GetAnimationController[]):
                OpeningKeyFrames:[]keyframe_delta = array:
                    MakeKeyFrameDelta(
                        MakeRotationFromYawPitchRollDegrees(RotAngle, 0.0, 0.0), 
                        OpeningTime)       
                AController.SetAnimation(
                    OpeningKeyFrames, ?Mode := animation_mode.OneShot)
                AController.Play()

                # Switchがオフになると、瞬時に扉を初期位置に戻す(閉じる)
                Door.InputSwitch.TurnedOffEvent.Await()
                Door.DoorProp.MoveTo(Door.InitTransform, 0.1)

            else:
                Print("OpenCloseLoop() AController Error. Door={Door.DoorId}")

    # 扉のリセット。初期位置に戻す(閉じる)
    Reset(Agent:agent)<suspends>:void=
        # 開いている扉を閉じる
        ResetDoors : []string = for:
            Door:RoomExits
            Door.InputSwitch.GetCurrentState[]

        do:        
            Door.InputSwitch.Enable()
            Door.InputSwitch.TurnOff(Agent)
            Door.DoorId

        var DoorIds : string = "["
        for (Index -> DoorId : ResetDoors):
            if (Index < ResetDoors.Length - 1) then set DoorIds += DoorId + ", "
            else set DoorIds += DoorId
        set DoorIds += + "]"
        Print("Reset() ResetDoors={ResetDoors.Length} {DoorIds}")     

    # 扉を開くアニメーションのキーフレームを作成
    MakeKeyFrameDelta(DeltaRotation : rotation, OverTime:float):keyframe_delta=
        keyframe_delta:
            DeltaLocation := vector3 { X:= 0.0, Y:= 0.0, Z:= 0.0 }
            DeltaRotation := DeltaRotation
            DeltaScale := vector3 { X:=1.0, Y:=1.0, Z:=1.0 }
            Time := OverTime
            Interpolation := EaseOut

UEFN上の設定

opening closing door manager を配置して、RoomExits, InputResetTrigger を設定します。
切り替えの仕掛けは、インタラクトしてオンしても、ほかのデバイスからオンにしてもよいです。動画では各デバイスのイベント時にオンにしています。


解説

OnBegin<override>()<suspends>:void=
    InputResetTrigger.TriggeredEvent.Subscribe(OnResetTriggered)

    for (RoomExit:RoomExits):
        set RoomExit.DoorId = 
            if (SubId := RoomExit.SubId?) then "{RoomExit.MainId}_{SubId}"
            else "{RoomExit.MainId}"
        set RoomExit.InitTransform = RoomExit.DoorProp.GetTransform()

        spawn{ OpenCloseLoop(RoomExit) }

InputResetTrigger.TriggeredEvent.Subscribe(OnResetTriggered)

InputResetTriggerがトリガーされた時にOnResetTriggeredを実行します。

for (RoomExit:RoomExits):

RoomExits配列の各要素に対して次の処理を行います。
DoorId に MainIdとSubIdから成る文字列をセットします。SubIdがfalseの場合は、MainIdのみ文字列にします。こちらは扉のリセット処理の際、ログ出力に使用しています。
InitTransform に DoorPropの初期位置(transform)をセットします。
開閉処理(OpenCloseLoop)を実行します。開閉処理は、非同期関数なのでspawnしています。

OnResetTriggered(AgentOpt:?agent):void=
    if(Agent := AgentOpt?):
        spawn{ Reset(Agent) }
Reset(Agent:agent)<suspends>:void=
    # 開いている扉を閉じる
    ResetDoors : []string = for:
        Door:RoomExits
        Door.InputSwitch.GetCurrentState[]

    do:        
        Door.InputSwitch.Enable()
        Door.InputSwitch.TurnOff(Agent)
        Door.DoorId

    var DoorIds : string = "["
    for (Index -> DoorId : ResetDoors):
        if (Index < ResetDoors.Length - 1) then set DoorIds += DoorId + ", "
        else set DoorIds += DoorId
    set DoorIds += + "]"
    Print("Reset() ResetDoors={ResetDoors.Length} {DoorIds}")

OnResetTriggeredは、トリガーされたときに呼び出されるので、引数は?agent型です。agent情報がある場合に、リセット処理(Reset)を実行します。

for:
  Door:RoomExits
  Door.InputSwitch.GetCurrentState[]

RoomExits配列の各要素に対して、InputSwitchがオンの場合に次の処理を行います。
InputSwitchを有効化し、InputSwitchをオフにします。
ResetDoors に DoorId をセットします。
(補足)InputSwitchは、ほかのデバイスからの入力を検知して扉を閉じるのがこのVerseデバイスでの役割としています。ただし、ResetTriggerがトリガーされたときは、特別にオフにする操作をしています。扉を閉じることとInputSwitchをオフにすることはペアなので、UEFN上で各InputTriggerの「オフにする」にResetTriggerのトリガーイベントを紐づけるよりも、ここで一緒に行ったほうがよさそうです。

Print("Reset() ResetDoors={ResetDoors.Length} {DoorIds}")

閉じた扉を確認するため、ResetDoors の数と DoorId をログ出力します。
(例)
0件の場合、「Reset() ResetDoors=0 []」
DoorId="1" と DoorId="2_1" の2件の場合、「Reset() ResetDoors=2 [1, 2_1]」

OpenCloseLoop(Door:opening_closing_door)<suspends>:void=
    loop:
        # Switchがオンになると扉を開く
        Door.InputSwitch.TurnedOnEvent.Await()
        if (AController := Door.DoorProp.GetAnimationController[]):
            OpeningKeyFrames:[]keyframe_delta = array:
                MakeKeyFrameDelta(
                    MakeRotationFromYawPitchRollDegrees(RotAngle, 0.0, 0.0), 
                    OpeningTime)       
            AController.SetAnimation(
                OpeningKeyFrames, ?Mode := animation_mode.OneShot)
            AController.Play()

            # Switchがオフになると、瞬時に扉を初期位置に戻す(閉じる)
            Door.InputSwitch.TurnedOffEvent.Await()
            Door.DoorProp.MoveTo(Door.InitTransform, 0.1)

        else:
            Print("OpenCloseLoop() AController Error. Door={Door.DoorId}")

loop:

何度でも繰り返し開閉できるようにloop式に処理を入れます。

if (AController := Door.DoorProp.GetAnimationController[]):

else:
  Print("OpenCloseLoop() AController Error. Door={Door.DoorId}")

InputSwitchがオンになると、DoorPropからアニメーションコントローラーを取得し、扉を開きます。取得失敗した場合、 DoorId をログに出力します。

扉を開く処理については、以下の記事をご参照ください。

https://zenn.dev/cre8tfun_dev/articles/986f21d8e14c18#アニメーションコントローラーで扉を開く

Door.DoorProp.MoveTo(Door.InitTransform, 0.1)

InputSwitchがオンになったあと、InputSwitchがオフになると0.1秒かけて扉を初期位置に戻します(扉を閉じます)。
アニメーションコントローラーではなくMoveToを使用しているのは、初期位置に戻せればよかったことと、扉を閉じる動作は素早く行いたかったのでアニメーションコントローラーである必要がなかったことが理由です。

以上、完成です。
お読みいただきありがとうございました!

Cre8tfun 技術ブログ

Discussion