👻

【Verse】アニメーションコントローラーで扉を開く

2024/05/04に公開

はじめに

先日Cre8tFunで公開した [HORROR] Rainbow Academy(島コード:5202-5247-4984) の実装に携わりました。

部屋を1つずつ進んでいくのですが、各部屋の出口には施錠された扉があります。
フォートナイトのアセットであればロックの仕掛けを使って扉を開けることができますが、Rainbow Academyでは、外部アセットの扉をインポートして使用しているため、ロックの仕掛けで開くことはできません。そこで扉を開くVerseの仕掛けを作りました。

以降、バージョンはv29.40で確認しています。

作成するVerseデバイスの仕様

  • 部屋ごとにトリガーと出口用扉を設置
  • トリガーが実行されたときに、その部屋の出口用扉(押戸)を開く
  • 一度開いた扉は閉じない
  • 補足:トリガーは部屋をクリアしたとき(※)に実行するように、UEFN側で設定
     ※(例)条件付きボタンが起動したとき、ボタンがインタラクトされたとき

扉の準備

事前に、扉のスタティックメッシュをインポートしたり、モデリングモードで作成したりします。

ピボットポイントの編集

スタティックメッシュのピボットポイントを回転させたい軸に移動します。
モデリングモードを使用し、Xform > Edit Pivot で、ピボットポイントを移動することができます。使用する扉は左側にドアノブがあるので、右側を回転の軸にして押戸にします。
※ 2024.05.03時点、モデリングモードのギズモが表示が動かないようです。回転の軸にする位置の値を直接入力して「承諾」することで、ビューポート上の扉と、コンテンツブラウザのスタティックメッシュのピボットポイントが移動します。

建築小道具の作成

今回アニメーションコントローラーを使用して回転させます。それには、スタティックメッシュを建築小道具にする必要があります。
建築小道具のブループリントを作成して、扉のスタティックメッシュを設定します。

アニメーションコントローラーで扉を開く

まずは扉を1つ動かしてみます。

https://youtu.be/upuGqQQsLEQ

コード

コード:opening_door_device
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_door_device := class(creative_device):

    @editable
    InputTrigger:trigger_device = trigger_device{}  # 扉を開くトリガー
    @editable
    DoorProp:creative_prop = creative_prop{}  # 扉のプロップ

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

    OnBegin<override>()<suspends>:void=
        InputTrigger.TriggeredEvent.Await()
        OpenTheDoor()

    OpenTheDoor()<suspends>:void=
        if (AController := DoorProp.GetAnimationController[]):
            MovementKeyFrames:[]keyframe_delta = array:
                MakeKeyFrameDelta(
                    MakeRotationFromYawPitchRollDegrees(RotationAngle, 0.0, 0.0))
            AController.SetAnimation(MovementKeyFrames, ?Mode := animation_mode.OneShot)

            AController.Play()
            AController.AwaitNextKeyframe()

    MakeKeyFrameDelta(DeltaRotation : rotation):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

解説

トリガーされるのを待機して、トリガー後に扉を開く処理を実行します

OnBegin<override>()<suspends>:void=
    InputTrigger.TriggeredEvent.Await()
    OpenTheDoor()

キーフレームを作成し、DoorPropから取得したアニメーションコントローラーにセットします。そのアニメーションを再生し、再生終了まで待機します。 DoorPropが存在しない場合はアニメーションコントローラーを取得できません。

OpenTheDoor()<suspends>:void=
    if (AController := DoorProp.GetAnimationController[]):
        MovementKeyFrames:[]keyframe_delta = array:
            MakeKeyFrameDelta(
                MakeRotationFromYawPitchRollDegrees(RotationAngle, 0.0, 0.0))
        AController.SetAnimation(MovementKeyFrames, ?Mode := animation_mode.OneShot)

        AController.Play()
        AController.AwaitNextKeyframe()

MakeRotationFromYawPitchRollDegreesはFortnite.digest.verseにあります。以下はコメント内容を日本語訳したものです。本記事では、YawRightDegreesに「RotationAngle」(=90.0)をセットすることで、90度扉を回転させるrotationを取得します。

        # `YawRightDegrees`、`PitchUpDegrees`、`RollClockwiseDegrees`の順に適用される`rotation`を作成します:
        #  * 最初に、Z軸を中心にして*yaw*を行います。正の角度は、上から見た場合に時計回りの回転を示します。
        #  * 次に、新しいY軸を中心にして*pitch*を行います。正の角度は、'nose up'を示します。
        #  * 最後に、新しいX軸を中心にして*roll*を行います。正の角度は、+X方向から見た場合に時計回りの回転を示します。
        # これらの規則は`MakeRotation`とは異なりますが、`ApplyYaw`、`ApplyPitch`、および`ApplyRoll`と一致します。
        MakeRotationFromYawPitchRollDegrees<native><public>(YawRightDegrees:float, PitchUpDegrees:float, RollClockwiseDegrees:float)<varies>:rotation

keyframe_deltaのDeltaLocation, DeltaRotation, DeltaScaleには、現在の位置、回転、スケールからアニメーションさせる変化量を設定します。今回、回転のみのアニメーションにするため、DeltaLocationは0.0、DeltaScaleは1.0倍にします。
Timeにはアニメーションさせる時間を設定します。
Interpolationは、補間モードを表し、cubic_bezier_parameters型の設定です。InterpolationTypesモジュールに、標準的な補間として、Linear, Ease, EaseIn, EaseOut, EaseInOutがあります。デフォルトはLinearです。cubic_bezier_parameters型(struct型)のX0, Y0, X1, Y1を編集して、カスタムすることも可能です。

MakeKeyFrameDelta(DeltaRotation : rotation):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

備考: InterpolationTypes(Fortnite.digest.verseの日本語訳)

補間モード 説明
Linear 一定の速度で移動
Ease ゆっくり始まり、加速し、ゆっくり終わる。終わりの方がわずかに遅くなる
EaseIn ゆっくり始まり、終わりに向かって加速
EaseOut 速く始まり、終わりに向かって減速
EaseInOut Ease に似ているが 開始と終了のアニメーション速度が対称

複数の扉の管理(完成)

パターン① 部屋の扉を順番に開ける

ひと続きの5つの部屋があり、その出口の扉を順番に開いていくと想定します。

コード:opening_door, opening_door_manager
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_door := class<concrete>():
    @editable
    InputTrigger:trigger_device = trigger_device{}  # 扉を開くトリガー
    @editable
    DoorProp:creative_prop = creative_prop{}  # 扉のプロップ           

opening_door_manager := class(creative_device):
    @editable
    Room1Exit:opening_door = opening_door{}
    @editable
    Room2Exit:opening_door = opening_door{}
    @editable
    Room3Exit:opening_door = opening_door{}
    @editable
    Room4Exit:opening_door = opening_door{}
    @editable
    Room5Exit:opening_door = opening_door{}

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

    OnBegin<override>()<suspends>:void=
        Room1Exit.InputTrigger.TriggeredEvent.Await()
        OpenTheDoor(Room1Exit.DoorProp)

        Room2Exit.InputTrigger.TriggeredEvent.Await()
        OpenTheDoor(Room2Exit.DoorProp)

        Room3Exit.InputTrigger.TriggeredEvent.Await()
        OpenTheDoor(Room3Exit.DoorProp)

        Room4Exit.InputTrigger.TriggeredEvent.Await()
        OpenTheDoor(Room4Exit.DoorProp)

        Room5Exit.InputTrigger.TriggeredEvent.Await()
        OpenTheDoor(Room5Exit.DoorProp)

    OpenTheDoor(DoorProp:creative_prop)<suspends>:void=
        if (AController := DoorProp.GetAnimationController[]):
            MovementKeyFrames:[]keyframe_delta = array:
                MakeKeyFrameDelta(
                    MakeRotationFromYawPitchRollDegrees(RotationAngle, 0.0, 0.0))
            AController.SetAnimation(MovementKeyFrames, ?Mode := animation_mode.OneShot)

            AController.Play()
            AController.AwaitNextKeyframe()

    MakeKeyFrameDelta(DeltaRotation : rotation):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

https://youtu.be/0fxyuNxt1As

5つの扉を順番に開いていくので確実ですが、どの部屋からでも開始できるようにすると各部屋のテストもしやすいと思います。
以下では、順番を問わず、実行されたトリガーと紐づく扉を開けるようにします。

パターン② 順番不問で扉を開ける

コード:opening_door, opening_door_manager
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_door := class<concrete>():
    @editable
    InputTrigger:trigger_device = trigger_device{}  # 扉を開くトリガー
    @editable
    DoorProp:creative_prop = creative_prop{}  # 扉のプロップ           

opening_door_manager := class(creative_device):
    @editable
    Room1Exit:opening_door = opening_door{}
    @editable
    Room2Exit:opening_door = opening_door{}
    @editable
    Room3Exit:opening_door = opening_door{}
    @editable
    Room4Exit:opening_door = opening_door{}
    @editable
    Room5Exit:opening_door = opening_door{}

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

    OnBegin<override>()<suspends>:void=
        # ゲームの設計上、扉は順番に開いていくこととなるが、テストではいずれの部屋からでも
        # スタートできるようにしたい。loopとraceで、順番を無視して扉を開けるようにする。
        loop:
            race:
                OpenTheDoor(Room1Exit)
                OpenTheDoor(Room2Exit)
                OpenTheDoor(Room3Exit)
                OpenTheDoor(Room4Exit)
                OpenTheDoor(Room5Exit)

    OpenTheDoor(Door:opening_door)<suspends>:void=
        Door.InputTrigger.TriggeredEvent.Await()

        if (AController := Door.DoorProp.GetAnimationController[]):
            MovementKeyFrames:[]keyframe_delta = array:
                MakeKeyFrameDelta(
                    MakeRotationFromYawPitchRollDegrees(RotationAngle, 0.0, 0.0))
            AController.SetAnimation(MovementKeyFrames, ?Mode := animation_mode.OneShot)

            AController.Play()
            AController.AwaitNextKeyframe()

        # 一度開いた扉は再度開かないようにするため、トリガーを無効化する。
        Door.InputTrigger.Disable()

    MakeKeyFrameDelta(DeltaRotation : rotation):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

ほかのデバイスからトリガーを実行するように連携して完成です。

https://youtu.be/ds8RzG4Eym4

おわりに

以上、Rainbow Academyの実装で学んだことでした。お読みいただきありがとうございました。

[HORROR] Rainbow Academy
https://youtu.be/l1j5lk0l_Q8?si=wCMyzhERVjTwHT3Y

Cre8tfun 技術ブログ

Discussion