【UEFN】フォートナイトで攻撃魔法を作る (Verseの学習⑤)

フォートナイトにNiagaraで独自の攻撃魔法を作る

フォートナイトで俯瞰視点のクリエイティブマップを作っています。ただ、俯瞰視点では通常のエイムが必要な銃器による攻撃がまともにできません。 剣だけで戦うのはゲーム性が低いため、なんらか別の攻撃手段を実装する必要があります。

そこで、俯瞰視点でも使用できる攻撃魔法を作ることにしました。

今回の完成映像はこちらになります。

実装方法

以下の方法で実装していきます。

  • 視覚効果にはNiagaraの機能を使用
  • NiagaraとDamageVolumeDeviceを連動させることで、Creatureにダメージを与える

Niagaraを動作させる方法は、現状のUEFNだと以下の2つの選択かと思います。

  1. VFXSpawnerで自作したNiagaraのエフェクトを制御する
  2. LevelSequenceで自作したNiagaraのエフェクトを制御する

今回は、Niagaraによるエフェクト効果、発動した時の効果音、DamageVolumeの大きさ、照り返しの光の照明、などをまとめて制御する必要があるため、2のLevelSequenceの方式を使用することにしました。

Niagaraでエフェクトを作る

NiagaraはUnrealEngine4.20からベータテストが始まり、4.26で正式版となった、UnrealEngineとしては比較的新しいエフェクト機能になります。

そもそも、UEFNにはUnrealEngineと違い、Blueprintのノード機能が現状ないため、(おそらく将来的にはVerseがそこを担っていく?)、Niagaraの制御方法が乏しいのですが、Niagara単品での機能は、UnrealEngineと同様に使用することができます。

Niagaraを使ったShockWaveの作り方は、以下の動画が一例として非常に参考になります。

youtu.be

上記の動画に従って作成していただければ、UEFNでも同じエフェクトを作成することができます。

注意点として、UEFNでは、UnrealEngineでサンプル的に入っている素材が入っていません。

上記の動画で使用している、M_smoke_sub_uvというマテリアルはUEFNには存在せず、UnrealEngineにのみ存在しています。

UnrealEngine 5.1.1をダウンロードしていき、UnrealEngine 5.1.1からコピーしていただくのがいいかと思います。

やり方は、まず、UnrealEngineでプロジェクトを作成します。この際、スターターコンテンツをオンにしてください。

スターターコンテンツをオンにしてプロジェクトを作成してください

作成したプロジェクトのなかで、StarterContentの中にあるM_smoke_sub_UVを探して、移行(migrate)します。

このマテリアルを移行(migrate)してやります

マテリアルの作り方、Niagaraの作り方などは、UnrealEngineを基準にしたものが非常に多く情報があり、UEFNでもほぼ同様の手法が使えるため、UnrealEngineをインストールしておくのはUEFNで開発を行う上でも有益かと思います。

Material Functionというマテリアル周りの機能もUEFNでは標準で入っていないので、それらもUnrealEngineから持ってくることも可能です。

素材の移行(migrate)の仕方の詳細は、以下のドキュメントを参考にしてください。

docs.unrealengine.com

Migrate先の指定は、UEFNでは必ず以下の場所になります。

  • (プロジェクトディレクトリへのパス)\Plugins\(プロジェクト名)\Content

シーンへの配置

出来上がったNiagaraエフェクトと必要な素材をシーン上に配置していきます。

(※自分は上記の動画とは別に準備したNiagaraエフェクトを使用しています)

Outlinerの構成

  • ROOT_SHOCK_WAVE
    • Creative_propです。全体を移動させるために使用しします。
  • CSD_ShockWave
    • Cinematic Sequencer Device です。シーケンスを再生させるために使用(移動させる必要は特にないが、管理のためここに配置)
  • DVD_ShockWave
    • Damage Volume Device
  • NS Shockwave
  • PointLight
    • 照り返し用にライトを追加

シーケンサーの追加

エフェクト制御のためのシーケンサーを一つ作成して、タイムラインを以下のように構築します。

シーケンサー構成

Niagaraシーケンサーで同期させるためには、ComponentにNiagara System Life Cycle Trackを追加する必要があります。

やり方はShiotaniさんのブログで詳細に書かれているため以下を参考にしてください。

saltcanyon.hatenablog.com

DamageVolumeDeviceの大きさ、PointLightの強さに対してキーフレームをうち、音声を追加して、以下のようにしました。

タイムラインの動作

出来上がったシーケンサーを、Cinematic Sequence Device で動作するように設定します。

CSD_ShockWaveの設定

Verseでの作業

今回はコードの抜粋です。

基本的に実施するのは、

  • なんらかのトリガー(自分の場合はしゃがみ動作)で、 ROOT_SHOCK_WAVEをプレイヤーの位置に移動
  • 必要なデバイスをEnableに
  • シーケンサーの再生
  • 終了処理

ということになります。

しゃがみ動作にたいする登録作業はこのような感じです。

        # add skill event 
        Players : []player = GetPlayspace().GetPlayers()
        if(Player : player = Players[0]):
            if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                FortniteCharacter.CrouchedEvent().Subscribe(OnPlayerCrouched)
    OnPlayerCrouched(the_player:fort_character, IsCrounch: logic):true= 
        if(IsCrounch?):
            Players : []player = GetPlayspace().GetPlayers()
            if(Player : player = Players[0]):
                if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                    PlayerPosition : vector3 = FortniteCharacter.GetTransform().Translation

                    ## taqrget position
                    TargetPositionX : float = PlayerPosition.X
                    TargetPositionY : float = PlayerPosition.Y
                    TargetPositionZ : float = PlayerPosition.Z

                    TargetPosition:vector3 = vector3{X:=TargetPositionX, Y:=TargetPositionY, Z:=TargetPositionZ}
                    _SkillController.CastSkill(TargetPosition, _BaseCameraSequence)

今まで書いてきたコードとほぼ同じ形なので、特に難しい部分はありません。

スキル発動部分はこのような感じにしました。

    ### process for crouched
    CastSkill<public>(TargetPosition : vector3, BaseCameraSequence : cinematic_sequence_device) : void=
        ### teleport and cast skill
        if(_SkillRoot.TeleportTo[TargetPosition, IdentityRotation()]):
            spawn{CastSkill(BaseCameraSequence)}
    
    CastSkill(BaseCameraSequence : cinematic_sequence_device)<suspends> : void = 
        BaseCameraSequence.Stop()
        _SkillSequence.Play()
        ## start scale
        Sleep(0.0)
        _SkillDamageVolume.Enable()

        ## wait for anim end
        Sleep(_SkillTimeOffset)
        
        ## disable volume
        _SkillDamageVolume.Disable()
        
        ## stop camera and recover
        _SkillSequence.Stop()
        BaseCameraSequence.Play()
  • _SkillRootには、ROOT_SHOCK_WAVEをセットしています。
  • _SkillSequenceには、CSD_ShockWaveをセットしています。

スキル用のDeviceとプレイヤー制御のDeviceは分けて作っています。

スキル発動時はカメラなども別にする可能性があるため、カメラ制御用のシーケンスを別途、関数に渡しており、スキル発動後にもとに戻しています。

今回のサンプルはシンプルにするために、Sleepで処理を終わらせていますが、CinematicSequencerDeviceは終了時のEventがあるため、これで終了してやる方がいいかもしれません。

まとめ

完成したものはこのような形です。

このままでは強すぎるので、使用回数など制限を与えることで、ゲームとしておもしろくなりそうです。

今回は、Verseで作成したDeviceの使い方の詳細までは載せていません。

前回までの記事をみていただければ、この辺りは同じですのでわかるのではないかと思います。

少し無理やり作っている部分もありますが、現状のUEFNで出来る形はこのようなものではないかなぁと考えています。

もし、もっといいやり方を考案された方がいらっしゃれば、ぜひ教えていただけるとうれしいです。

この記事が参考になれば幸いです。

UEFN、超楽しい:)

今後もさまざまな情報を発信していきますので、Twitterフォローしていただけると嬉しいです。 twitter.com

【UEFN】 DiabloライクのTop-DownViewカメラを作る Part.2(Verseの学習④)

カメラ方向と操作方向が一致したTop-Down Viewカメラを作る

前回紹介したTop-Down View カメラはカメラ方向と操作方向が一致していない場合に、非常に操作が行いづらいという欠点がありました。

ringogames.hatenablog.com

そこで、前回のものを改修して、プレイヤーの向きにカメラが追従するように変更します。

イメージとしてはこのような形になります。

プレイヤーの向きに対応したTop-Down Viewカメラ

改修方針

  • 親となっているオブジェクト(図のNullProp)の位置をプレイヤーの位置と一致させるように動かします。(A)
  • カメラのオフセットは親からの相対座標として与えておきます。(B)
  • プレイヤーの視点移動に合わせて、親となっているオブジェクトのYaw角を動かします。(A)
  • ジャンプした際に、カメラ方向と操作方向を一致させるようにします。(常に一致させることも可能ですが、ゲーム性としてジャンプした際のみに一致させる方針で進めます)
  • 概念図

シーン準備

前回と同一のものを使用し、カメラオフセット(上記図のB)は変更しておきます。

Verse Code

改修したコードは以下のようになります。

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

# A Verse-authored creative device that can be placed in a level
track_player_manager := class(creative_device):

    @editable
    _CameraRoot: creative_prop = creative_prop{}

    @editable
    _OtherRoot: creative_prop = creative_prop{}

    @editable
    _OffsetX : float = 0.0
    @editable
    _OffsetY : float = 0.0
    @editable
    _OffsetZ : float = 0.0

    var _CurrentRotation : rotation = rotation{}

    ### OnBegin
    OnBegin<override>()<suspends>:void=
        Offset:vector3 = vector3{X:=_OffsetX, Y:=_OffsetY, Z:=_OffsetZ}

        ## add jumped event 
        Players : []player = GetPlayspace().GetPlayers()
        if(Player : player = Players[0]):
            if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                FortniteCharacter.JumpedEvent().Subscribe(OnPlayerJumped)

        UpdateRotationValue()

        ## start update loop
        spawn{UpdateTargetPosition(Offset)}

    ### process for jump
    OnPlayerJumped(the_player:fort_character):true= 
        UpdateRotationValue()

    ### update rotation value
    UpdateRotationValue():void=
        Players : []player = GetPlayspace().GetPlayers()
        if(Player : player = Players[0]):
            if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):

                ### Not Good
                #var playerRotation : rotation = FortniteCharacter.GetTransform().Rotation
                
                ### Good
                var viewRotation : rotation = FortniteCharacter.GetViewRotation()

                var angles :[]float = viewRotation.GetYawPitchRollDegrees()
                if(angleYaw := angles[0]):
                    anglePitch := 0.0
                    angleRoll := 0.0
                    targetRotation := MakeRotationFromYawPitchRollDegrees(angleYaw, anglePitch, angleRoll)
                    set _CurrentRotation = _CurrentRotation.RotateBy(MakeShortestRotationBetween(_CurrentRotation, targetRotation))

    ### Loop Process
    UpdateTargetPosition(Offset:vector3)<suspends>:void=
        loop:
            ## get player and player position
            Players : []player = GetPlayspace().GetPlayers()
            if(Player : player = Players[0]):

                if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                    PlayerPosition : vector3 = FortniteCharacter.GetTransform().Translation
                    ## add rotation
                    PlayerRotation : rotation = FortniteCharacter.GetTransform().Rotation

                    ## first rotation
                    TargetPositionX : float = PlayerPosition.X
                    TargetPositionY : float = PlayerPosition.Y
                    TargetPositionZ : float = PlayerPosition.Z

                    TargetPosition:vector3 = vector3{X:=TargetPositionX, Y:=TargetPositionY, Z:=TargetPositionZ} + Offset
                    
                    ## move target to player position
                    _CameraRoot.MoveTo(TargetPosition, _CurrentRotation, 0.5)
                    _OtherRoot.MoveTo(TargetPosition, IdentityRotation(), 0.5)
                    
                    # Not Good
                    # if(_CameraRoot.TeleportTo[TargetPosition, IdentityRotation()]):
                    #     void

            Sleep(0.5)

            # Not Good
            # Sleep(0.0)

コード解説します。 CameraRootは上記図で言うNullPropをセットする場所です。 OtherRootを今回追加しています。これは、回転を適用せずにプレイヤーに追従してほしいものを入れるために用意しました。 自分の作例では、ライトを入れています。(ライトは回転させたくないため)

    @editable
    _CameraRoot: creative_prop = creative_prop{}
    @editable
    _OtherRoot: creative_prop = creative_prop{}

_CurrentRotationに計算した回転量を保持していきます。

var _CurrentRotation : rotation = rotation{}

プレイヤーがジャンプした際に、回転量計算が発生するようにしています。

FortniteCharacter.JumpedEvent().Subscribe(OnPlayerJumped)
...
OnPlayerJumped(the_player:fort_character):true= 
        UpdateRotationValue()

UpdateRotationValueが回転量計算の部分になります。

今回の一番のポイントはここです。

                ### Not Good
                #var playerRotation : rotation = FortniteCharacter.GetTransform().Rotation
                
                ### Good
                var viewRotation : rotation = FortniteCharacter.GetViewRotation()

プレイヤーに追従させたいのでプレイヤーの回転をとってしまいそうなところですが、それではうまくいきません。実際の操作は視点の方向と一致する必要があるため、視点の回転情報を取得する必要があります。

Fortniteは3人称ゲームのため、キャラクターの方向と視点の方向が一致していません。

Yaw角だけを取り出して回転情報を作ります。現在の回転との差分を出し、時計回りと半時計回りで近い方向に回るようにしています。

                var angles :[]float = viewRotation.GetYawPitchRollDegrees()
                if(angleYaw := angles[0]):
                    anglePitch := 0.0
                    angleRoll := 0.0
                    targetRotation := MakeRotationFromYawPitchRollDegrees(angleYaw, anglePitch, angleRoll)
                    set _CurrentRotation = _CurrentRotation.RotateBy(MakeShortestRotationBetween(_CurrentRotation, targetRotation))

ここで出来た回転情報を使用してMoveTo関数を利用します。

_CameraRoot.MoveTo(TargetPosition, _CurrentRotation, 0.5)

_OtherRootは回転はせず位置だけ追従させるので前回と同様です。

バイス設定

バイス設定は以下のようになります。 _OtherRootはここではセットしていませんが、実際はライトのためのRootオブジェクトをセットしています。

まとめ

今回の結果はこのような形になります。

俯瞰カメラ ジャンプ時の回転対応

視点方向とカメラ方向が一致したため、感覚的に操作が行えるようになりました。

また、ジャンプで回転というゲーム性も出たため、なかなかいいように思います。

EpicのDevCommunityで今回のコードをSnippetとして投稿しておきました。

https://dev.epicgames.com/community/snippets/xve/fortnite-top-down-view-support-device

この情報が皆さんの参考になれば幸いです。

今後もさまざまな情報を発信していきますので、Twitterフォローしていただけると嬉しいです。 twitter.com

UEFN楽しい:)

【UEFN】 DiabloライクのTop-DownViewカメラを作る Part.1(Verseの学習③)

DiabloライクのTop-DownViewカメラを作る

Twitterでサンプルを公開したところ、反響が多かったため、Top-DownViewカメラの作り方を紹介します。 今回の完成映像はこちらになります。

DiabloLikeCamera

UEFNで俯瞰視点のゲームを作るためにトップダウンカメラを作ってみます。

実装方法

簡単な実装方法は以下の流れになります。

  1. 俯瞰カメラを作成し、LevelSequenceでゲーム開始時にそのカメラに切り替わるようにする

  2. Verseで現状動かせるオブジェクトはCreativePropになるので、俯瞰カメラをCreativePropの子供にする

  3. プレイヤーの位置をみて、CreativePropを移動させる

シーン準備

カメラの親となるCreativePropを一つシーン上に配置します。FortniteのPropsの下のものを配置してもいいですが、今回は独自にPropを準備しました。

このページに従い、破壊されない小道具(Indestructible Prop)を作成します。 この際、メッシュは特に使用する必要はないため、空にしておきます。

次に、カメラをシーン上に作成します。

先ほど作成したPropもシーン上に配置し、カメラをPropの子供にします。

カメラの座標値はすべて0に、角度は下向きにしておきます。

次に、LevelSequenceを作成し、先ほど作成したカメラをこのトラックに追加します。

シーン上にCinematic Sequence Deviceを配置します。

  • Sequecneに先ほど作成したレベルシーケンスをセット
  • LoopPlayback,AutoPlayをONにします。

これで基本準備は完了です。

Verse コード

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


# A Verse-authored creative device that can be placed in a level
track_player_manager := class(creative_device):

    @editable
    _Target : creative_prop = creative_prop{}

    @editable
    _OffsetX : float = -3000.0
    @editable
    _OffsetY : float = 0.0
    @editable
    _OffsetZ : float = 3000.0


    OnBegin<override>()<suspends>:void=
        Offset:vector3 = vector3{X:=_OffsetX, Y:=_OffsetY, Z:=_OffsetZ}

        ## start update loop
        spawn{UpdateTargetPosition(Offset)}

    UpdateTargetPosition(Offset:vector3)<suspends>:void=
        loop:
            ## get player and player position
            Players : []player = GetPlayspace().GetPlayers()
            if(Player : player = Players[0]):

                if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                    PlayerPosition : vector3 = FortniteCharacter.GetTransform().Translation

                    TargetPositionX : float = PlayerPosition.X
                    TargetPositionY : float = PlayerPosition.Y
                    TargetPositionZ : float = PlayerPosition.Z

                    TargetPosition:vector3 = vector3{X:=TargetPositionX, Y:=TargetPositionY, Z:=TargetPositionZ} + Offset
                    
                    ## move target to player position
                    _Target.MoveTo(TargetPosition, IdentityRotation(), 0.5)
                    
                    # Not Good
                    # if(_Target.TeleportTo[TargetPosition, IdentityRotation()]):
                    #     void

            Sleep(0.5)

            # Not Good
            # Sleep(0.0)

コード解説していきます。

OnBegin時に位置を更新するための非同期処理を開始しています。 このあたりの処理に関しては、alweiさんが非常に分かりやすく書いてくださっていたので、参考にされるといいと思います。

unrealengine.hatenablog.com

ここでプレイヤーと、プレイヤーの現在値を取得しています。

            Players : []player = GetPlayspace().GetPlayers()
            if(Player : player = Players[0]):

                if(FortniteCharacter : fort_character = Player.GetFortCharacter[]):
                    PlayerPosition : vector3 = FortniteCharacter.GetTransform().Translation

その位置にオフセットをつけてpropのMoveTo関数で移動しています。

## move target to player position
                    _Target.MoveTo(TargetPosition, IdentityRotation(), 0.5)
                    
                    # Not Good
                    # if(_Target.TeleportTo[TargetPosition, IdentityRotation()]):
                    #     void

            Sleep(0.5)

            # Not Good
            # Sleep(0.0)

TeleportとSleep(0.0)の部分はコメントアウトしていますが、あえて残しています。 当初、これで実装してみたのですが、かなりガタガタしてしまい、あまりいい動きとなりませんでした。 0.5秒間隔でMoveToするくらがちょうどいい塩梅かなと感じています。

ちなみにSleep(0.0)にしたときに、どれくらいレートが出ているのか、計測するためのスクリプトを公開してくれている人がいました。 参考に貼っておきます。

dev.epicgames.com

最終シーンセットアップ

ビルドして出来上がったtrack_player_manager をシーン上に配置します。

  • _Targetにカメラの親となっているPropをセット
  • 適宜オフセットをセット
  • Visible in Gameをオフにセット

まとめ

これで実行すれば、サンプルのようなプレイヤーに追従する俯瞰カメラが出来上がります。 問題として、Fortniteはそもそも俯瞰カメラに対応した操作になっていないため、プレイヤーをカメラ前方向に向けておかないと、カメラの向きとプレイヤーの操作が一致せず、非常に操作がやりにくい形になります。この辺りは少しゲーム内で工夫する必要があります。

次の記事で、カメラの向きと操作を一致させるようスクリプトを改修します。

ringogames.hatenablog.com

今後もさまざまな情報を発信していきますので、Twitterフォローしていただけると嬉しいです。 twitter.com

【UEFN】 エイム練習用のプロップを作る!(Verseの学習②)

Verseでエイム練習用プロップを作る

前回 のサンプルを発展させ、Verseを使用して、ダメージを受けた際にランダムに移動するプロップを作っていきます。

今回の最終系はこのような形になります。

Suzanneモデルが射撃を受けるたびにランダムな方向に移動していきます。

エイム練習などに使用できるのではないかと思います。

実装方法

現状のVerseでは、動きに関する機能が存在するのはCreativePropだけでした。ただ、CreativePropは、ダメージを受けるという判定ができないようです。(CreativePropでダメージをEventとして受け取れる方法がご存じの方いれば教えてください)

そこで、

  • ダメージ判定は、前回同様TriggerDeviceで
  • 移動はCreativePropで

という少しトリッキーな実装をすることにしました。

シーン準備

上記の理由から、まず、Fortniteのプロップライブラリから適当なものをシーンに一つ配置します。 自分はBearのプロップを使用しました。(どちらにしろこれは表示には使用しないためなんでも良い)

次にTriggerDeviceをシーンに配置します。 シーンに配置後、TriggerDeviceをBearの子供にします。Outliner上でTriggerDeviceをBearにドラッグアンドドロップすれば子供にできます。

このような見え方にアウトライナ上でなっていれば親子付けされています。親子付けされていれば、親のベアーが移動するとその子供のTriggerDeviceも連動して移動します。

前回と同様にそれぞれのオブジェクトは以下のように設定していきます。

  • Bear
    • アクタ->Can be Damage をOFFに(ダメージを受けて壊れないように)
    • StaticMeshComponent のレンダリング->visible をOFFに(表示しないように)
  • Trigger
    • ユーザーオプション->ゲーム中に表示をOFFに
    • StaticMeshComponentのスタティックメッシュコンポーネントを外部から読み込んだSuzanneモデルに
    • StaticMeshComponentのマテリアルはMI_Steel_Brightを今回は選択。
    • ※メッシュとマテリアルはお好みで適当なものにしてください。

基本的なシーン構成はこれで完成です。

Verseコード完成形

では、Verseのコーディングを行っていきます。 前回同様、新規のVerseコードをCreativeDeviceを継承する形で作成します。 今回のコードの完成形はこのような形となりました。

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
## vector3 や rotation に必要
using { /UnrealEngine.com/Temporary/SpatialMath }
## random に必要
using { /Verse.org/Random }

# A Verse-authored creative device that can be placed in a level
aim_game_manager := class(creative_device):
    @editable
    RootProp : creative_prop = creative_prop{}

    @editable
    TargetMoveOffset : float = 100.0

    @editable
    TargetMoveTime : float = 0.5

    @editable
    TargetTrigger : trigger_device = trigger_device{}


    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        TargetTrigger.TriggeredEvent.Subscribe(OnTriggeredEvent)
    
    OnTriggeredEvent(Player:?agent):void=
        ## 現在位置の取得
        CurrentLocation := RootProp.GetTransform().Translation
        
        ## ランダムで-TargetMoveOffsetから+TargetMoveOffsetまでの移動距離を与える
        MinV := TargetMoveOffset
        MaxV := -TargetMoveOffset 
        localX := GetRandomFloat(MinV, MaxV)
        localY := GetRandomFloat(MinV, MaxV)
        localZ := GetRandomFloat(MinV, MaxV)
        var Position : vector3 = CurrentLocation + vector3{X:=localX, Y:=localY, Z:= localZ}
        
        ## 低すぎる、もしくは、高すぎる場合は初期位置に
        if(Position.Z < 0.0 or Position.Z > 200.0):
            set Position = vector3{X:=Position.X, Y:=Position.Y, Z:=100.0}
        
        ## Rotationの仮作成
        Rotation : rotation = MakeRotationFromYawPitchRollDegrees(0.0, 0.0, 0.0)
        
        ## Propを動かす
        spawn{RootProp.MoveTo(Position, Rotation, TargetMoveTime)}

コード解説していきます。 入力パラメーターの設定になります。Bearを入力するRootProp、TriggeerDeviceを入力するTargetTriggerの他、移動量(TargetMoveOffset)、移動時間(TargetMoveTime)を設定できるようにしています。これら4つのパラメータは後程Editor上で設定することになります。

@editable
    RootProp : creative_prop = creative_prop{}
    @editable
    TargetMoveOffset : float = 100.0
    @editable
    TargetMoveTime : float = 0.5
    @editable
    TargetTrigger : trigger_device = trigger_device{}

OnBeginの部分は前回と同一のため解説しません。

次にOnTriggeredEventの解説です。

この部分では現在のCreativePropの位置をまず取得したのち

CurrentLocation := RootProp.GetTransform().Translation

現在値に対して、X,Y,Zそれぞれの方向に-TargetMoveOffsetから+TargetMoveOffsetまでの間のランダムな量をオフセット値として加算します。

その値を次に向かう位置の座標とします。

最後に、PropをCreativePropの持っているMoveTo関数で移動させていますが、ここで注意点があります。

OnTriggeredDeviceは通常の関数なのですが、MoveToは非同期関数です。

Verseのルールの中で、通常の関数内で、非同期関数はそのまま使用することができません。

(非同期の話は、Verseのマニュアルのこちらを見ていただければと思います。詳細に解説されています。ただ、読むのには少し時間がかかりますが・・・)

そのため、spawnなどをつけて実行する必要があります。

spawn{RootProp.MoveTo(Position, Rotation, TargetMoveTime)}

これで、TriggerEventでランダムな位置に動くCreativeDeviceの完成となります。

最終シーンセットアップ

出来上がったCreativeDevice(この場合は、aim_game_manager という名前)をシーン上に配置し、以下のように設定します。

  • RootPropは、Bearに
  • TargetTriggerはBearの子供になっているTriggerに
  • TargetMoveOffsetとTargetMoveTimeは適当なものを試してください。

これで実行すると、最初の画像のようにプロップが動くはずです。

まとめ

CreativePropがダメージを受けた時になんらかの処理をすることができないのかは、現在も調査中になります。これができればもっとスマートに組めるのですが・・・

次回以降でもう少しこれに色付けをして、ゲームらしくしていこうと考えています。

みなさんの何らかの参考になれば幸いです。

次回もよろしくお願いいたします。

今後もさまざまな情報を発信していきますので、Twitterフォローしていただけると嬉しいです。 twitter.com

【UEFN】 VerseでDeviceを連携させる(Verseの学習①)

ついに登場!UnrealEditor for Fortnite (UEFN)

3月23日、発表から約2年半の歳月を経て、UnrealEditor for Fortnite (以下UEFN)がついにリリースされました。

Fortnite専用のUnrealEditorとして一体どこまでのことができるのか、23日のリリースから自分も毎日試用してきます。まだまだ分からない点が多いですが、把握できたことをブログとして残しておきたいと思います。

今回は、現状でもっとも基本的な作業だと思われる、新プログラミング言語「Verse」を使用して、デバイスを連携させる基本的な方法を紹介します。

外部の3Dデータ(BlenderのSuzannne)を読み込んで、これにダメージを与えた時に画面上にメッセージ(Hello Suzanne!!) を表示します。

現状のUEFNについて

「UnrealEngineとFortniteが連携する!」ということで、UnrealEngineで出来ることが全てできるようになるのか?という期待をしてました。マップの作成という面では、かなり多くのUnrealEngineのツールが使えるようになっており、大きく可能性が広がったのですが、ゲームロジックの作成という面では、まだできることが限られてる印象です。

現状では、UnrealEngineのようなBlueprintで制御を組むことができない、また、UnrealEngineのTickや、UnityのUpdate関数などのような、作画の毎フレームでの処理を行うことができないため、あくまでも従来のFortniteCreativeの仕組みの中でゲームを作成していく必要がありそうです。

将来的には、UnrealEngineのすべての機能をUEFNに入れていく予定だとEpicは話していたので、この状況は飛躍的に変わる可能性がありますが、まだ、少し時間がかかりそうです。

そこで現状のキーとなるのが、UEFNに導入されたVerseです。(こちらはまだUnrealEngineに実装されておらず、UEFNが先行する形)

Haskellの開発者をEpicに招き入れ、TimSweeneyが鳴り物入りで開発している言語ということでいったいどんな実装になるのかわかっていませんでしたが、思ったよりも従来のプログラミング言語に近い形でした。他のプログラミング言語に親しんで来た人はそれほど苦労することなく使える気がします。

現状のVerseを使った開発の基本構成

Verseを使用した開発の現状の肝は、Creative_Deviceの基本機能を継承した独自のCreative_Deviceを作成し、その独自Creative_Deviceの中でFortniteのDeviceの制御を行うことかと思われます。現状では、Creative_Deviceを基本形とするテンプレートしか用意されていないようです。(ここも将来的には拡張される部分ではないかと思われます)

そこで今回は下図の構成のサンプルを組みます。 Verseで独自のCreativeDeviceを作成し、TriggerDeviceからのEvent(ダメージを受けた時などに情報が送出される)を受け取り、その信号を受け取った際にHUDMessageDeviceで制御を行いメッセージを出力します。

この構成が現状のUEFNの基本形かと思われ、多くのものはこの形を複数組み合わせることにより出来上がるはずですので、この基本形を理解しておくことが一番最初のステップではないでしょうか?

シーン準備

では、ここから実際に制作していきます。まず、プロジェクトを作成し、コンテンツブラウザからシーン上HUDMessageDeviceとTriggerDeviceを追加します。

そのままの見た目ではあまりにも単純なので、外部からモデルを読み込んでみることにします。別途BlenderからSuzanneのモデルをFBXフォーマットで出力しており、これを①の部分にエクスプローラからドラッグしてやると、インポート画面が表示されます。今回は特に何も設定せずに「全てインポート」を押すとエディター上でモデルを使用できるようになります。

次に、モデルを適用します。

アウトライナー上でTriggerDeviceを選択し、②詳細パネルでStaticMeshComponentを選択、③StaticMeshの項目から読み込んだSM_Suzanneを選択すると、TriggerDeviceがSuzanneで表示されます。

このモデルは質感が設定されていないので、マテリアルをEditor内に入っていたMI_Gravelのマテリアルを設定しました。

TriggerDeviceのプロパティをさらに編集し、 もとのTriggerDeviceのモデルは表示したくないので、「ゲーム中に表示」のチェックはOFF、ダメージに反応してほしいため以下のようにチェックを付けます。 このようにするだけで外部から読み込んだモデルに対してもきちんと当たり判定がつき、TriggerDeviceの一部として動作してくれるようです。

現状の見え方はこのようになってます。(位置は調整しています)

Verseコードの追加

では、いよいよこれらを制御するCreativeDeviceをVerseで作成していきます。 上部VerseメニューからVeseExplorerを開きます。

プロジェクト名を右クリックして、「Add new Verse file to project」 を押下します

Verseスクリプト作成画面が現れます。前述したようにCreativeDeviceを継承したテンプレートしか現状はないようです。

「作成」を押下するとVisualStudioCodeが起動します。(インストール済みの場合)

インストールしていない場合は、以下のリンクからダウンロードしてインストールしておくのがいいかと思われます。 VisualStudioCodeはプログラミング用のEditorとして非常に優秀で、多くのプログラマーが愛用しています。

code.visualstudio.com

Verseコード初期状態

作成した直後のコードは以下のようになっています。

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

# A Verse-authored creative device that can be placed in a level
trigger_test := class(creative_device):

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        # TODO: Replace this with your code
        Print("Hello, world!")
        Print("2 + 2 = {2 + 2}")

では、このコードを解説していきます。 先頭の3行の以下の部分は、必要なライブラリの宣言です。 自分が使用したい機能をもっているライブラリを追加しておく必要があります。

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

どういったライブラリが存在し、どういった機能があるのかは、以下を参照していく必要があります。

dev.epicgames.com

次にこの部分です。

trigger_test := class(creative_device):

ここはクラス定義となっており、簡単に言うと「creative_deviceという基本機能からそれを拡張したtrigger_testという新機能を作ります」という宣言になります。

それに続く部分が、このtrigger_testという新デバイスがcreative_deviceから拡張された新しい機能を書いていく部分です。

最も大事な部分は以下の部分になります。

OnBegin<override>()<suspends>:void=

creative_deviceの基本機能の中では、OnBeginで書かれている部分がゲームスタート時に実行されるというルールになっています。そのため、ゲーム開始時に実行したい制御をこの下に書いていきます。

これを見るとゲーム実行時に以下のことをするようです。

Print("Hello, world!")
Print("2 + 2 = {2 + 2}")

Printと書いてありますが、これは画面に出るわけではなくログ画面に出力されるものとなっています。ログを見ると以下のようになっていることが分かります。

このPrintはプログラムをDebugする際に重宝するかと思います。

Verseプログラム改修後

今回の処理を実現するためにコードを以下のように変更しました。

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

# A Verse-authored creative device that can be placed in a level
trigger_test := class(creative_device):

    @editable 
    Trigger:trigger_device = trigger_device{}

    @editable
    HudMessageDevice : hud_message_device = hud_message_device{}

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        Trigger.TriggeredEvent.Subscribe(OnTriggeredEvent)

    hudMessage<localizes> : message = "Hello Suzanne !!"
    OnTriggeredEvent(Player:?agent):void=
        HudMessageDevice.SetText(hudMessage)
        HudMessageDevice.Show()

変更後のコードを見ていきます。

まず、以下の部分です。

    @editable 
    Trigger:trigger_device = trigger_device{}

    @editable
    HudMessageDevice : hud_message_device = hud_message_device{}

このプログラムの中では、TriggerDeviceから情報を受け取り、HUDMessageDeviceの制御をしなければなりません。そのため、これを扱えるようにしています。 @editableと付けることで、UnrealEditor上で、実際にどのデバイスをターゲットとするのか設定することができるようになります。

次がOnBeginの中の以下の部分です。

Trigger.TriggeredEvent.Subscribe(OnTriggeredEvent)

Subscribeは購読するという意味になりますが、これはTriggerでイベントが発生したらOnTriggerEventという部分を実行するように登録しておくといったニュアンスになります。 現状のUEFNのVerseのプログラミングでは、このイベントを登録するというSubscribeの関数が非常に重要になっていきます。

ここでは、ゲーム開始時にトリガーイベントが発生した際に実行する部分をまず登録しています。

そのOnTriggerEvent部分です。

hudMessage<localizes> : message = "Hello Suzanne !!"
    OnTriggeredEvent(Player:?agent):void=
        HudMessageDevice.SetText(hudMessage)
        HudMessageDevice.Show()

OnBeginでゲーム開始時にSubscribeされたのち、この部分はTriggerDeviceがイベントを発行するたびに実行されます。 そのため、TriggerDeviceがダメージを受けるたびに、このOnTriggerEventは実行されます。 処理の内容は、

  • HUDMessageDeviceが表示する文字を”Hello Suzanne !!"にする
  • その文字を表示(Show)

という流れになります。

シーンにtrigger_test deviceを追加

これはまだtrigger_testデバイスという素材を作成しただけです。そのため、これを実際にシーン上に配置して、@editableで設定したパラメーターをセットしなくてはなりません。

「Verseコードをビルド」するとUnrealEditorがVerseコードからEditor上で使用可能なtrigger_test デバイスをコンテンツブラウザ上に作ってくれます。そこで、これをシーン上に配置してやります。

配置したtrigger_test deviceで@editableにしていたTrigger DeviceとHUDMessageDeviceをそれぞれシーン上のものに指定します。

これで設定としてはすべて完了したため、いよいよFortnite上で動作をさせていきます。 画面上部のメニューから 「Push Verse Changes」「Push Changes」を押下して、サーバー側にデータを送信します。

この後ゲームをスタートすると、Suzanneモデルを殴った際に、HelloSuzanne の文字が表示されるかと思います。

まとめ

今回、最も基本的なVerseを使用した制御を作成しました。Verseについて動作の流れに重点を置いて説明をしたため、細かな部分はあまり説明していません。Verseの言語仕様に関してはEpicのドキュメントを見ていただく必要があるかと思います。

dev.epicgames.com

今後もFortniteCreativeを盛り上げることを目標に、情報発信を続けていくつもりです。 Twitterフォローしていただけると嬉しいです。 twitter.com

Blender Pythonを使用して外部データを読み込みGeometry Nodeで使用する

Pythonを使用して外部データを読み込みGeometry Nodeで使用する

GeometryNodeのみでは、CSVなどの外部データを読み込むことができません。外部データを使用したい場合は、Pythonで事前にデータを読み込んだ点群を作って、それをGeometryNodeで使用する方法がいいかと思われます。

Pythonによるデータ読み込みの簡単なサンプル

Blender上に4点を作成する簡単なサンプルです。 その4点に10から40までの値(dummydata)を与えています。

import bpy
 
# make mesh
vertices = [(10, 0, 0),(0, 10, 0),(-10,0,0), (0,-10,0)]
edges = []
faces = []
plotDataMesh = bpy.data.meshes.new('plot_data')
plotDataMesh.from_pydata(vertices, edges, faces)
attr = plotDataMesh.attributes.new("dummydata", 'FLOAT', 'POINT')
dummyData = [10,20,30,40]
attr.data.foreach_set("value", dummyData)

plotDataMesh.update()

# make object from mesh
plotDataObject = bpy.data.objects.new('plot_data_object', plotDataMesh)

curScene = bpy.context.scene
curScene.collection.objects.link(plotDataObject)

このスクリプトを実行し、アトリビュートをGeometryNodeから見るとdummydataのフィールドにdummydataというフィールドがそれぞれ値が入っていることが分かります。

GeometryNodeからはNamedAttributeのノードを使用してデータを活用します。

世界の人口数をビジュアライズしてみる

次に以下のページで公開されていた世界の人口データをPython+GeometryNodeで映像化します。

City, Latitude, Longitude, Country, Population · GitHub

コードはこのような形になります。先ほどのものにCSVを読み込む部分を追加した形です。

import bpy
import os

class population_data(object):
    city = ""
    country = ""
    longitude = 0.0
    latitude = 0.0
    population = 0

def load_csv_population_data(fileName):
    dataSet = []
    ### load csv
    curDir = os.path.dirname(bpy.data.filepath)
    dataFilePath = os.path.join(curDir, fileName)
    if os.path.exists(dataFilePath) :
        f = open(dataFilePath, "r")
        for line in f.readlines():
            parts = line.strip().split(",")
            if len(parts) == 5:
                data = population_data()
                isValid = True
                try:
                    data.city = parts[0]
                    data.latitude = float(parts[1])
                    data.longitude = float(parts[2])
                    data.country = parts[3]
                    data.population = float(parts[4]) / 10000.0
                except:
                    isValid = False
                if isValid:
                    dataSet.append(data)
    return dataSet
 
vertices = []
edges = []
faces = []
populations = []
for data in load_csv_population_data("data.csv"):
    vertices.append((data.longitude, data.latitude, 0.0))
    populations.append(data.population)
 
# make mesh
plotDataMesh = bpy.data.meshes.new('plot_data')
plotDataMesh.from_pydata(vertices, edges, faces)

attr = plotDataMesh.attributes.new("population", 'FLOAT', 'POINT')
attr.data.foreach_set("value", populations)

plotDataMesh.update()

# make object from mesh
plotDataObject = bpy.data.objects.new('plot_data_object', plotDataMesh)

curScene = bpy.context.scene
curScene.collection.objects.link(plotDataObject)

これを実行すると以下のような点群が生成されます。 populationフィールドを使用して、縦棒グラフを作成するGeometryNodeは以下のようになります。

アトリビュートを使用してShaderで色付けします。

完成したものがこちらになります。

まとめ

BlenderPythonは点群を作るだけであれば、非常に高速で、この組み合わせはデータビジュアライゼーションにおいて、他のソフト(Maya、Houdiniなど)に対しても十分なアドバンテージがあるように思います。 このブログがなんらかの参考になれば幸いです。

Blender GeometryNodeでUV座標を投影する方法

Blender Geometry Nodeにおいて、他のオブジェクトのUV座標を取得する方法がわからなかったため、調査しました。

やりたいことは以下になります。

  • オブジェクトAとオブジェクトBが存在し、オブジェクトAのそれぞれの頂点からもっとも近くにあるオブジェクトBの平面上の点のUV座標を取得する

  • そのUVを使用してShaderでテクスチャを適用する

(最終イメージ例)

基本的なノード構成

Aの各頂点からもっとも近いBの表面上の点を探し、その点のUV座標を頂点のUV座標とすることを考えます。 基本的なノード構成は以下のようになります。

SampleNearestSurfaceで頂点からもっとも近い点のUV座標を取得し、Aのアトリビュートとして保持(Store Named Attribute)します。

分かりにくいのがPositionのノードです。Positionのノードには特に入力が入っておらず、それがAの頂点のポジションなのか、Bの頂点のPositionなのか、それを示す必要があるように思われますが、特にそうした設定はありません。 ただ、この場合、Aの頂点のポジションが正しく使用されます。なぜそうなるかは詳細は省きますが、以下のドキュメントに記載されていました。

フィールド — Blender Manual

また、「謎技研」さんのブログの解説もわかりやすいです。

【Geometry Nodes】フィールドって何?PositionやIndexの使い方【Blender】 | 謎の技術研究部

AのShaderは以下のようになります。

Attributeノードを使い、GeometryNode内で作成したUV座標を使用しています。 UVMapというAttribute名で保存しているのでなくても問題ないのではないかと考えたのですが、これがないと正しく動作しませんでした。

テクスチャ画像はBと同じものを使用しています(sample_picture.png)

ここまでの結果はこのようになります。

AやBを動かしても投影が変更されません。

これは、AやBのローカル座標でしか内部の計算を行っていないためです。

画面上での位置座標を反映した構成

Aの動きに対応させる部分、Bの動きに対応させる部分は以下のようになります。

Aの部分は、各ポイントに関して、自ノードのTransform情報を取得し適用しています。 簡単なノードがないかと探したのですが、見つけれず、Scale,Rotate,Translateそれぞれノードで構成しています。

BはTransformGeometryがあったため、それを適用しています。

この結果がこちらになります。

サンプルのダウンロード

サンプルのダウンロードは以下から行えます

20230315 - Google ドライブ

次回

次回は、さらにこれを発展させていく予定です。