【UEFN】Verseのシャローコピーとディープコピー(Verseの学習⑭)

ディープコピーとシャロウコピー

Verseのコピー演算子で気を付けたいこと

3月末からVerseを触ってきましたが、8月になって初めてあることに気づきました。

それは、

Verseのmapやarrayはディープコピーされる!

ということです。 辞書型やリスト型のコピー演算子は、シャロウコピーとなっているプログラミング言語の方が一般的だと思われ、この5か月間、Verseもシャロウコピーだろうと完全に勘違いしてコーディングを行っていました。😢

どういった場合がシャロウコピーになり、どういった場合がディープコピーになるのか、EpicのTim御大が直々フォーラムで返答していました。

Question on Copying Variables - Verse - Epic Developer Community Forums

投稿者の方が

Do variables create shallow, or deep copies when assigning one to another?

変数はシャロウコピーですか?ディープコピーですか?

と聞いたのに対して

Tim御大自身が

Deep, stopping only at instances of classes, which are referenced.

ディープです。クラスのインスタンス時にそれは終了し、参照になります。

と答えています。

え、じゃあ、mapやarrayは?となったのですが、検証してみると、ディープでした。

な、な、なんと・・・・!

JavaPythonC#などでプログラムを書いていた人はむしろびっくりな結果です。

そもそもディープコピー、シャロウコピーとはなんなのか、という説明もしながら、これを今回検証していきたいと思います。

シャローコピーとディープコピーの検証

Verseに限らず、多くのプログラミング言語では変数を準備して、それに対して作業を行っていきます。

この変数の情報はメモリに記憶されていきますが、この変数の値を別の変数にコピーしたい場合が多々発生します。

多くのプログラムでは、代入演算子"="でこれを実行しているかと思います。

# ValueAの値をValueBにコピーする
ValueA := 10
ValueB := ValueA

このプログラムの場合、ValueAの10という値が、ValueBにもコピーされます。 メモリ上には以下のように別々に記憶されます。

AとB

次にコピーした後に、ValueAを変更してみます。

# ValueAの値をValueBにコピーする
var ValueA : int = 10
ValueB := ValueA
ValueA = 1

当然ですが、メモリ上は以下のようになります。

Aを変更後

これがディープコピーの動作になります。

mapのコピーを検証

では、これをmap型で実施してみます。 MapAを準備してキーを2つセットし、その後、MapBにMapAをコピー、MapAとMapBをプリントしています。

var MapA : [int]string = map{}
if:
    set MapA[1] = "One"
    set MapA[2] = "Two"

MapB := MapA
if:
    set MapA[1] = "Ichi"
    set MapA[2] = "One"

Print("This is MapA")
for(Key->Value : MapA):
    Print("{Key}->{Value}")

Print("This is MapB")
for(Key->Value : MapB):
    Print("{Key}->{Value}")
        return

Fortnite上で実行してみると以下のようにプリントされます。

実行結果

これは以下のようになったことを意味しています。

まず、最初にMapAの値が、すべてMapBにコピーされます。

MapAからMapBへのコピー

次にMapAの値を変えています。

MapAの値を変更

MapAとMapBでは完全に別々のメモリを使用していることがわかる結果となりました。

え?普通の結果では?と思われる方もいるかもしれません。ディープコピーは要素全体を丸っとコピーする、いわば普通のコピーとなるため、むしろプログラムを初めて書かれる方にはこの動作の方が理解しやすいかとは思います。

ただ、前述したように多くの言語では、辞書型や配列型をディープコピーせず、シャロウコピーする方が一般的なため、プログラムをある程度されてきた方はかなり注意が必要なポイントです。

では、次にシャロウコピーとなる場合に関して検証します。

インスタンスのコピー

辞書を管理するクラス、map_managerを作成し、辞書はそのなかで管理する形とします。

その上で、その辞書型のインスタンスを準備し(InstA)、別のインスタンスへコピーを実行してやります。

コピー後にもとのインスタンスの値を変え、コピー元とコピー先、双方がどう変わったかチェックします。

テストコードはこのような形です。

まず、map_managerクラスです。

map_manager := class:
    var IntToStringMap : [int]string = map{}

    AddValue<public>(Key : int, Value: string):void=
        if:
            set IntToStringMap[Key] = Value
    PrintMap<public>() : void=
        for(Key->Value : IntToStringMap):
            Print("{Key}->{Value}")
        return

これを使用して以下のようなコードでテストします。

InstA := map_manager{}
InstA.SetValue(1, "One")
InstA.SetValue(2, "Two")

InstB := InstA

InstA.SetValue(1, "Ichi")
InstA.SetValue(2, "Ni")

### print b
Print("This is InstA")
InstA.PrintMap()
Print("This is InstB")
InstB.PrintMap()

結果は次のようになりました。

インスタンスのコピー

InstAからInstBにコピーしてから、InstAの値を変更しているのに、InstBの値も変わっています。

これはなぜかというと、インスタンスの変数が情報として保持しているのは「私はここのメモリを参照します」というメモリの参照情報だけだからです。

そのため、その参照先の情報はInstAとInstBでまったく同じものとなっており、InstAの参照先を変えると、InstBも変更されることになります。

インスタンス変数が持っているのは参照情報だけ

こうした参照情報だけのコピーをシャロウコピーと呼んでいます。

Verseでもインスタンスのコピーはシャロウコピーになっていることが確認できました。

まとめ

今回、2つのことを紹介しています。

プログラマーの方向けにお伝えしているのは、

  • マップやリストもディープコピーされる

初心者プログラマーの方向けにお伝えしているのは、

ということになります。

オブジェクト指向で組んでいくと、どういった場合がディープで、どういった場合なのがシャロウなのか、把握して組んでいく必要があり、代入演算子の動作は理解しておいた方がいいかと思います。(自分はmapはシャロウだと5か月間違えていた😢)

今回の記事が皆様のお役に立てば幸いです。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作っています。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com

【UEFN】UEFNのVerseで疎結合のコンポジションを行う(Verseの学習⑬)

オブジェクト指向プログラミングで疎結合コンポジションを行う

クラス構成をシンプルにする

オブジェクト指向プログラミング(以下OOP)では、プログラムの機能をクラスに分けることで、一つ一つのプログラムを小さくしていきます。

一つ一つのクラスが正しい形で独立していれば、小さな単位でプログラムを作成、保守することが出来るため、バグの出にくいプログラミングを行うことができます。

OOPでもっとも使うプログラミング手法の一つがコンポジションです。

今回はUEFNのVerseで自分が採用しているコンポジションを行う手法について簡単に紹介します。

そもそもコンポジションとは?

例として、作成するゲームに必要な機能を次のようなものと考えます。

  • プレイヤー管理
  • 敵キャラクター管理
  • イベント管理

コンポジションを用いたプログラム方法としては、これらをそれぞれ部品のクラスとして作成し、その部品クラスを全体管理の親クラスに管理させることになります。

Verseの書き方としてはこんな形です。

# 部品クラス
ringo_event_manager  := class{(クラスの記述)}
ringo_player_manager := class{(クラスの記述)}
ringo_enemy_manager  := class{(クラスの記述)}

# 全体管理クラス
ringo_game_manager := class:
    EventManager : ringo_event_manager = ringo_event_manager{}
    PlayerManager : ringo_player_manager = ringo_player_manager{}
    PlayerManager : ringo_player_manager = ringo_player_manager{}
  (クラスの記述)

クラス構成

このような形で、全体管理の親クラスの中に、部品クラスを実体(インスタンス)として保有する形が、コンポジションの基本形となります。

親クラスと部品クラスは、相互にやり取りをしながらプログラムを実行していきます。

親クラスから部品クラスを参照する際は、親クラスが部品クラスのインスタンスを持っているため、簡単に実現することができます。

親から部品への参照

ringo_game_manager := class:
    EventManager : ringo_event_manager = ringo_event_manager{}
    DoSomething() : void=
        EventManager.DoSomethingInEvent()

では、部品が親を参照する場合は、どうすればいいでしょうか?まず、考えられるのは、部品に親の参照を渡しておくことです。

部品から親への参照

ringo_event_manager := class:
    GameManager : ringo_game_manager
    DoSomethingInEvent() : void=
        GameManager.DoSomethingInGameManager()

ただ、この相互に参照しあう形だと、以下のような問題が発生してきます。

  • 部品クラス内で親クラスの関数が直接実行できてしまうとどこでどういった処理が行われているか把握しずらくなる。オブジェクト指向の分割統治が壊れやすい)

  • 部品クラスの中で、親クラスの関数を実行しているということは、部品クラスの中に親の情報も入ってしまうため、この部品だけ他のもので使用するということができなくなってしまい再利用が行いづらくなる

コンポジションを行うときのコツは、部品が親を直接参照するような密な関係ではなく、疎な関係が組んでいくことです。 互いに疎な関係を構築できると保守のしやすいバグの生まれにくいコードとなります。

この疎な関係を実現するために、多くの開発環境では、様々な仕組みが導入されています。

  • UnrealEngineにおけるEventDispatcher
  • QtのSignal/Slot機構
  • UnityのUnityEvent

などは、こうした疎な関係を実現するために存在しているものです。

ただ、UEFNでは、そうした機能が非常に乏しいのが現状です。そこで今回はTriggerデバイスを使用して、コンポジション疎結合を行います。

コンポジションサンプル

結合が密な場合と疎な場合を以下の例で示していきます。

サンプル完成映像

  • シネマティックシーケンスを再生するイベント管理クラスを作成し、それをゲームマネージャーで管理する
  • ゲームマネージャーでは、シネマティックシーケンスイベント管理クラスのインスタンスをリストで保有し、前のイベントが終了したら、次のシネマティックシーケンスイベントを開始するようにする
  • アルファベット一つ一つの動きをそれぞれ1つづつのシネマティックシーケンスで作成しており、4つのイベントを管理する(もちろん、この表現ならこんなことする必要はありませんので、あくまでも例です)

クラスが密につながった例

まず、最初にクラスが密につながった例を紹介します。

ゲームマネージャークラスは以下のようにしました。

ringo_game_manager := class(creative_device):
    @editable
    CinematicEvents : []ringo_cinematic_event = array{}

    @editable
    StartButton : button_device = button_device{}

    var _CurrentEventIndex<private> : int = 0

    OnStartButtonInteracted(Agent : agent) : void=
        set _CurrentEventIndex = 0
        if:
            CinematicEvent := CinematicEvents[0]
        then:
            CinematicEvent.Start()
        return

    OnBegin<override>()<suspends>:void=
        StartButton.InteractedWithEvent.Subscribe(OnStartButtonInteracted)
        for(CinematicEvent : CinematicEvents):
            CinematicEvent.Initialize(Self)
        return

    DoNextEvent() : void=
        set _CurrentEventIndex += 1
        if:
            _CurrentEventIndex = CinematicEvents.Length
        then:
            set _CurrentEventIndex = 0

        if:
            CinematicEvent := CinematicEvents[_CurrentEventIndex]
        then:
            CinematicEvent.Start()
        return

次に、部品となるシネマティックシーケンスイベント管理クラスです。

ringo_cinematic_event<public> := class<concrete>:
    @editable
    CinematicSequenceDevice : cinematic_sequence_device = cinematic_sequence_device{}
    
    var _MaybeGameManager : ?ringo_game_manager = false

    Initialize<public>(GameManager : ringo_game_manager) : void=
        set _MaybeGameManager = option{GameManager}
        CinematicSequenceDevice.StoppedEvent.Subscribe(OnStoppedSequence)
        return

    Start<public>() : void=
        CinematicSequenceDevice.Play()
        return

    OnStoppedSequence() : void=
        if:
            GameManager := _MaybeGameManager?
        then:
            GameManager.DoNextEvent()
        return

この例で問題になりそうなのは、以下の部分です。

var _MaybeGameManager : ?ringo_game_manager = false
OnStoppedSequence() : void=
    if:
        GameManager := _MaybeGameManager?
    then:
        GameManager.DoNextEvent()
    return

ここで、部品クラスから親クラスへの参照が行われています。 そのため、この部品を他の場所で活用するためには親も同伴しないといけません。

本来的には、部品が親から独立したコードとなっていれば、別の場所にも使用可能なのですが、この書き方の場合、部品クラスの実装は、このゲームのマネージャーに深く依存した密な関係となっています。

この例でも正しく動作しますが、親クラスが部品クラスからも使えてしまうことで、どこでどういった動作が行われているのか管理のしにくい構成となります。

クラスのつながりを疎にした例

つながりを疎にするために、今回はTriggerDeviceを使用します。

TriggerDeviceをイベント管理クラスに追加し、シネマティックシーケンス再生後にトリガー発信するように変更します。

シネマティックシーケンスイベント管理クラスを以下の部分を追加します。

    @editable
    EndedTrigger<public> : trigger_device = trigger_device{}

    OnStoppedSequence() : void=
        EndedTrigger.Trigger()
        return

全体管理クラスは次のようになります。

OnBegin<override>()<suspends>:void=
    StartButton.InteractedWithEvent.Subscribe(OnStartButtonInteracted)
    for(CinematicEvent : CinematicEvents):
        #変更
        CinematicEvent.Initialize() 
        #追加
        CinematicEvent.EndedTrigger.TriggeredEvent.Subscribe(OnCinematicEventEnded) 
        return
# 追加
OnCinematicEventEnded(MaybeAgent : ?agent) : void=
    DoNextEvent()
    return

TriggerDeviceクラスの信号を受け取るための記述を親側のゲームマネージャークラスに記述することで、 全体管理クラスの方では、このイベント送信を受信するようにコード変更しています。

バイス設定はこのようにしています。

マネージャーの設定

このように変更することで、全体管理クラスからは、イベント管理クラスを参照していますが、イベント管理クラスからは、ゲームマネージャークラスを参照することがなくなりました。 部品として独立することができたため、別のゲームのゲームマネージャーでもこのクラスを使用することができるようになります。

まとめ

今回はUEFNでのコンポジションの作り方について説明しました。

基本方針として、部品クラスからは親クラスを直接参照しないというコード設計をすると、見通しがよく、保守性の高いプログラムになります。

VerseにはListenableというインターフェースがあり、将来的には、このインターフェースを実装することで、こうしたイベント処理ができるようになるのではないかと思われますが、現状はまだ未完成なようです。

将来的には、もう少しスマートなやり方が登場しそうですが、現状としてはTriggerDeviceなどを使うしかないのですが、自由な変数をトリガーに与えることができないため、ベストなやり方ではありません。

他の方法では、関数を変数として使用し、処理が終わった場合のコールバックにする方法などもあり、自分はその手法を多用しています。

このあたりは、また機会があれば紹介できればと思います。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com

【UEFN】条件付きボタンを使用して所持している武器を判別するサンプル(Verseの学習⑫)

条件付きボタンを使用して所持している武器を判別するサンプル

今回、かなり簡易な紹介にさせていただいています。

v25.10で関数追加されていたIsHoldingItem関数を使用して、所持している武器を条件付きボタンで判別するサンプルを紹介します。

条件付きボタンによる武器チェック

MutatorZoneにプレイヤーが入った際に、所持している武器をチェックします。

シーン上に条件付きボタンを配置し、プレイヤーが保持する可能性のある武器を登録しておきます。 (3つまでしか登録できないので、それ以上の場合は条件付きボタンを増やしていく必要があります)

条件付きボタンの設定

今回作成した簡単なコードです。

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

StringToMessage<public><localizes>(value:string)<computes> : message = "{value}"

HoldingItemCheckDevice := class(creative_device):

    @editable
    ConditionalButtonDevice : conditional_button_device = conditional_button_device{}

    @editable
    MutatorZone : mutator_zone_device = mutator_zone_device{}

    @editable
    HudMessageDevice : hud_message_device = hud_message_device{}


    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void=
        MutatorZone.AgentEntersEvent.Subscribe(OnAgentEnterMutatorZone)
        return

    OnAgentEnterMutatorZone(Agent :agent): void=
        var MessageString : string = ""
        if:
            ConditionalButtonDevice.IsHoldingItem[Agent, 0]
        then:
            set MessageString = "Assault" 
        if:
            ConditionalButtonDevice.IsHoldingItem[Agent, 1]
        then:
            set MessageString = "Machinegun" 
        if:
            ConditionalButtonDevice.IsHoldingItem[Agent, 2]
        then:
            set MessageString = "Shotgun" 

        HudMessageDevice.SetText(StringToMessage(MessageString))

        return

プロジェクト

今回のサンプルは以下にアップしておきます。 https://1drv.ms/u/s!AuxSGeVCl2Z8kGFLLRtRBEIGFCLK?e=LOgGBD

まとめ

このサンプルでは、MutatorZoneに入った際に判定していますが、敵がダメージを受けた際などに使用して武器ごとの差別化をするなど、かなり重宝する関数になるかと思います。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作っています。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com

【UEFN】UEFNのVerseでオブジェクト指向開発を行う(Verseの学習⑪)

[:contents]

VerseでOOP

Verseとオブジェクト指向

UEFNが登場して数か月が経過して、自分として検証を進めてきましたが、Verseはオブジェクト指向でバリバリとコーディングが行えるプログラミング言語という印象を持っています。

自分としてのコーディングのやり方がなんとなく固まってきたため、今回は、Verseで行うオブジェクト指向プログラミング(以下OOP)の入門的な内容をご紹介します。

対象としては、あくまで「OOPで開発を行っていない入門者向け」の内容になりますので、中上級者の方は、優しい目で、「あー、リンゴはこんな感じですかぁ」程度に読んでいただければと思います😇

簡単なサンプルをOOPを行わない場合とOOPを行った場合でコードを比較して解説していきます。

解説サンプル

次のようなシーンの作成を考えます

  • 3つボタンが存在する
  • それぞれのボタンを押した際に、押したボタンに応じたプロップが押したボタンの上に出現する

これをそれぞれ、OOP未適用な場合と、OOP適用した場合で紹介していきます。

サンプルの実行結果

OOP未適用なVerseコード

まず、最初に、OOP的ではなく、愚直にコーディングした場合です。(さすがにここまで愚直にコーディングする人はいないと思いますが・・・)

コードとしては、このようになります。

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

oop_test_device := class(creative_device):
    
    @editable
    Button1 :button_device = button_device{}
    @editable
    Button2 :button_device = button_device{}
    @editable
    Button3 :button_device = button_device{}

    @editable
    NumberPropAsset1 : creative_prop_asset  = DefaultCreativePropAsset
    @editable
    NumberPropAsset2 : creative_prop_asset  = DefaultCreativePropAsset
    @editable
    NumberPropAsset3 : creative_prop_asset  = DefaultCreativePropAsset

    OnBegin<override>()<suspends>:void=
        Button1.InteractedWithEvent.Subscribe(OnButton1Interacted)
        Button2.InteractedWithEvent.Subscribe(OnButton2Interacted)
        Button3.InteractedWithEvent.Subscribe(OnButton3Interacted)
        return

    OnButton1Interacted(Agent :agent) : void=
        Position := Button1.GetTransform().Translation + vector3{X:= -500.0, Z:= 100.0}
        SpawnProp(NumberPropAsset1, Position, rotation{})
        return

    OnButton2Interacted(Agent :agent) : void=
        Position := Button2.GetTransform().Translation + vector3{X:= -500.0, Z:= 100.0}
        SpawnProp(NumberPropAsset2, Position, rotation{})
        return

    OnButton3Interacted(Agent :agent) : void=
        Position := Button3.GetTransform().Translation + vector3{X:= -500.0, Z:= 100.0}
        SpawnProp(NumberPropAsset3, Position, rotation{})
        return

ボタンの数だけ、Subscribeを実施し、関数もそれぞれに準備しています。

コード自体は簡単ですが、似たコードが重複しており、保守性が非常に低いコードとなっています。 UEFNのエディター上ではこのように設定することになります。

エディター上の設定

では次に、ボタンを押したらPropを発生するという機能を一つのクラスとしてまとめ、OOP的な改善をしたコードを紹介します。

OOPを適用したVerseコード

まず、ボタンを押したらPropを発生するという部分をprop_buttonクラスとして定義します。いろいろなやり方が考えられますが、今回はこのクラスの中にEditor上から編集可能な、ButtonDevice用の変数と、CreativePropAssetを設定するための変数を用意することとしました。

prop_button<public> := class<concrete>:
    @editable
    Button :button_device = button_device{}

    @editable
    NumberPropAsset : creative_prop_asset  = DefaultCreativePropAsset

    Initialize<public>() :void=
        Button.InteractedWithEvent.Subscribe(OnButtonInteracted)
        return

    OnButtonInteracted(Agent :agent) : void=
        Position := Button.GetTransform().Translation + vector3{X:= -500.0, Z:= 100.0}
        SpawnProp(NumberPropAsset, Position, rotation{})
        return

Initializeという部分にクラスの初期化部分(コンストラクタに相当する部分)を準備しています。Subscribe関数はこのInitialize関数の中に入れてしまっています。

Verseにもコンストラクタと呼ばれるクラスのインスタンスを準備するための機構が準備されていますが、グローバル関数として準備しなくてはならいため、自分としては使い辛く感じており、こうした関数を準備しています。(Verse的には正しくないかなと思います)

ボタンを押した際の動作を定義するOnButtonInteracted関数もこのクラス内に入れています。

UEFNにアトリビュートとして出せるように、クラスはconcreteになっていなければなりません。

この機能を使用するデバイスのメイン部分は以下のようになります。

oop_test_device := class(creative_device):
    @editable
    PropButtons : []prop_button = array{}

    OnBegin<override>()<suspends>:void=
        for(PropButton : PropButtons):
            PropButton.Initialize()
        return

OnBegin時にfor分でボタン数だけ、Initializeを行っています。 ボタン機能をクラスに分けたことで、それぞれのボタンに対する処理を書く必要がなくなりました。 先ほどのコードに比較して、シンプルな見通し手の良いコードになったのではないかと思います。

UEFNでの設定はこのような形になります。

配列として要素を追加していく

UEFN上では、PropButtonsのボタンを配列として追加していくことができるため、より多くのボタンにも対応でき、汎用性も高まっています。

まとめ

今回はオブジェクト指向プログラミング入門として、機能をクラス分けする簡単な例を紹介しました。

クラス分けしたものは別ファイルに記述することができるようになるため、1ファイルとしてのコード量も少なくなり、OOP的な開発ルールに従うことで、保守性が高く、再利用性が高く、バグの発生しやすい開発が可能になるのではないかと思います。

VerseでOOP的な開発を行う方法に関しては、まだまだ語ることがあるため、今後も別の機会で紹介できればと思います。

(ただ、内容的にどんどん人を選ぶものになってしまうかも・・・😓)

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

https://twitter.com/RingoGames2022twitter.com

【UEFN】UMGビューモデルを使用してリッチな見た目のPopUpDialogを作成する(Verseの学習⑩)

*※2023/06/18 追記 以下の方法で作成した場合、PADでボタンが選択できない問題が発生しています。調査中ですが、バグの可能性が高いです しばらく使用を待っていただいた方がよさそうです

  • ※2023/09/11 26.00 でPADでボタン選択ができない問題は修正されました。

画像を使用したUI!

UMGビューモデルを使用してUIを作成する

先日のアップデート(v25.00)で、UMGビューモデルの機能が修正されたため、今回は、UMGビューモデルを使用したUIをVerseから活用する方法について紹介します。

こちらの機能を使用すると、今回の作例ではVerseを使用していますが、Verseでのコーディングを行わなくても独自のボタンをもった入力画面を作成することができるようになります。

Verseに不慣れな方は、前回のやり方よりもこちらのやり方の方が導入しやすいはずです。

UMGビューモデルってなんじゃ?

というところですが、UMGビューモデルはUnrealEngine 5.1から導入された比較的新しい機能です。機能自体はまだベータ版としての扱いのようです。

Epicの公式ドキュメントは以下になります。

docs.unrealengine.com

UMGモデルビューを使用すると

  • ①エンジニアがUI機能の基本機能(ビューモデル)を開発する
  • ②アーティストはその基本機能をテンプレートとして使用しデザイン作業のみを行う

という形で作業を進めることができるようになります。

エンジニアとアーティストが作業を分けるための仕組みですね。

UEFNでは、①のUI基本機能(ビューモデル)が一つだけ用意されており、②の作業のみでゲームに使用するUIを作成することができます。

今回は、前回の作例をUMGビューモデルを活用した方法に変更していきます。

作業の流れ

作業の流れは次のようになります。

  1. UI用のWidgetBlueprintを作成
  2. WidgetBlueprintにUEFNで準備されているPopUpDialog用のビューモデルを設定する
  3. PopUpDialogデバイスをレベルに配置
  4. PopUpDialogデバイスに1で作成したWidgetBlueprintを設定
  5. 従来通りのやり方でPopUpDialogを設定

UEFNで現状用意されているのは、PopUpDialogの基本機能を使用するビューモデルになります。 注意点として、このやり方では、PopUpDialogで使用できる6つより多くのボタンは使うことができません

では、この順番で作業を行っていきます。

1. UI用のWidgetBlueprintを作成

UI用のWidgetBlueprintを作成します。 コンテンツブラウザ上で右クリックし、UserInterface -> Widget Blueprint で選択します

ParentClassは Modal Dialog Variant を選択します。

WidgetBlueprintの作成

出来上がったWidgetBlueprintを開き、Canvas、Image、Buttonを使用して以下のように作成します。

WidgetBlueprintのデザイン

2. UIに対してビューモデルを設定

1で作成したUIにビューモデルを設定していきます。

ビューモデルWindowが出てない場合は、まず、上記のWindowからView Bindingの画面を有効にし、CreateViewModelのボタンを押下します。

View Bindingの作成

ビューモデル選択画面で、Creative Modal Dialog Viewmodel を選択し、Selectを押下します。

ViewModelの選択

これでViewModelが作成されました。

このCreative Modal Dialog ViewmodelがUEFNで準備されているPopupDialog用の基本機能(ビューモデル)になります。

次に、現在WidgetBlueprintで作った2つのボタン(RINGO ボタンとBANANAボタン)とこのビューモデルで準備されたボタン機能をつないでいきます。

まず、リンゴボタン用にBindingを作成します。

RINGOボタンのBinding

同様に、バナナボタン用にもBindingを作成します。 ①と④の設定部分のみ異なります。

BANANAボタンのBinding

最終的にBindingはこのようになります。

最終的なBinding

3,4,5. レベルの設定

作成したWidgetBlueprintを使用するために、レベルの作成を行っていきます。

PopupDialogDeviceを画面に一つ配置し、TemplateOverrideClassに作成したWidgetBlueprintを設定します。

PopupDialogにWidgetBlueprintを設定

自分の作例では、さらにボタンを一つ追加し、ボタンを押した際にPopupDialogが動作するようにします。

Verseコード

モデルビューを使用したUIは、PopupDialogとして動作するため、Verseコードなしで動作させることができますが、Verseを使用する場合のコードを紹介しておきます。 (※Verseなしでも、通常のPopupDialogDeviceのボタン設定をすれば動作させることが可能)

コードは以下のようになります。

fruit_select_device := class(creative_device):
    @editable
    _Button : button_device = button_device{}

    @editable
    _PopopDialogDevice : popup_dialog_device = popup_dialog_device{}

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

        _PopupDialogDevice.RespondingButtonEvent.Subscribe(OnPopupRespondingButtonEvent)
        _Button.InteractedWithEvent.Subscribe(OnFruitSelectButtonInteracted)

    OnFruitSelectButtonInteracted(Agent :agent) : void=
        _PopupDialogDevice.Show(Agent)
        return

    OnPopupRespondingButtonEvent(Agent : agent, Index : int) : void=
        case(Index):
            0 =>
                ## リンゴ発生処理
            1 =>
                ## バナナ発生処理
            _=>
                ## 何もしない
        return

注意点として、モデルビューとしては Get Response Button 1 がリンゴ、Get Response Button 2 がバナナとして設定していましたが、RespondingButtonEventで送信されてくるときは、0がリンゴ、1がバナナとなり、インデックスが1減った状態で送られてきます。

終結

これを実行するとこのような形になります。

完成映像

まとめ

今回は、モデルビューを使用して、簡易にUIを作成する方法について紹介しました。

ボタン数が6つまでしか使えないという制限はありますが、Verseコードなしでも実装でき、細かなデザイン調整も可能なため、多くの場合では、こちらのやり方が有効なのではと思います。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com

【UEFN】ボタンに反応するUIをVerseで作成する(Verseの学習⑨)

ボタンをもったUIでの処理

VerseのUI機能

VerseのUI機能は現状で決して充実しているとは言えず、今後のアップデートが期待されている項目の一つです。 今後、機能拡張があるとはいえ、基本的には現状ある機能の流れを踏襲した形になると思われ、現状の機能を理解しておくことは、今後の開発の大きな手助けになるはずです。

(また、UMG ViewModelの機能が、24.20までは使用できていたのですが、記事作成時点の24.40バージョンでは、バグにより使用できず、Verseなしで、ボタンを持ったGUIを独自にカスタムすることが現状できなくなっているように思われます)

EPIC本家のページでもUIに関するVerseのドキュメントはありますが、ボタンとの連携といった部分はないようでしたので、今回は、ボタンを持ったPopupDialogとしての使用方法を中心に説明していきたいと思います。

今回開発するもの

  • ゲーム上のボタンデバイスを押した際に、PopUpDialogでUIを表示する
  • PopUpされたUIには、上部のメッセージと、2つのボタンがある
  • ボタンを押した際に、そのボタンに応じた処理を実行する

簡単なテキストをVerseから表示するプログラム

まず、最初に、テキストが表示されるまでの部分を組んでみます。

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

StringToMessage<localizes>(value:string)<computes> : message = "{value}"

fruit_select_ui_device := class(creative_device):
  @editable
  _FruitSelectButton : button_device = button_device{}

  OnBegin<override>()<suspends>:void=
    _FruitSelectButton.InteractedWithEvent.Subscribe(OnInteractedWithFruitsSelectButton)
  
  OnInteractedWithFruitsSelectButton(Agent : agent) : void=
    if:
      Player := player[Agent]
      PlayerUI := GetPlayerUI[Player]
    then:
      FruitWidget := canvas:
        Slots := array:
          canvas_slot:
            Widget := text_block{DefaultText := StringToMessage("Hello World!")}
      PlayerUI.AddWidget(FruitWidget)
    return

ポイントとしては、この部分です。

文字列をMessage型に変換するための簡単な関数を準備しています。

StringToMessage<localizes>(value:string)<computes> : message = "{value}"

文字列のままでは画面に表示することができないため、こうしてmessage型に変換することが必要となります。

Verseで、UIを作成しようとすると非常によく使うコードですので、この書き方は覚えておいた方がいいかと思います。

Widgetの形でUIパーツをまず作成し、ボタンに触ったプレイヤーのUIに作成したWidgetを追加するという流れになります。

canvas_slotで区切られた部分がtextのパーツ部分です。このようにslotと呼ばれる単位でパーツを追加していきます。

          canvas_slot:
            Widget := text_block{DefaultText := StringToMessage("Hello World!")}

VerseをビルドしてできるCreativeDeviceに、適当なボタンを割り付け、実行してみます。

ボタンを押した際に、画面左上に”Hello, Worldと表示されれば正解です。

画面左上にHello World

ボタン2個とテキスト1個を表示するプログラム

では、次にボタンを2個追加し、レイアウトも調整します。

OnInteractedWithFruitsSelectButton_step01(Agent : agent) : void=

  if:
    Player := player[Agent]
    PlayerUI := GetPlayerUI[Player]
  then:
    FruitWidget := canvas:
      Slots := array:
        canvas_slot:
          Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
          Offsets := margin{Left := -175.0, Top := -50.0, Right := 350.0, Bottom := 50.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := text_block:
            DefaultText := StringToMessage("Which Do You Like?")
            DefaultTextColor := NamedColors.Black
            DefaultJustification := text_justification.Center
        canvas_slot:
          Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
          Offsets := margin{Left := -300.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := button_loud{DefaultText := StringToMessage("Ringo")}
        canvas_slot:
          Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
          Offsets := margin{Left := 20.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
          Alignment := vector2{X := 0.0, Y := 0.0}
          SizeToContent := false
          Widget := button_loud{DefaultText := StringToMessage("Banana")}

    InputMode := player_ui_slot:
      InputMode := ui_input_mode.All
    PlayerUI.AddWidget(FruitWidget, InputMode)

  return

テキスト1つとボタン2つになったため、canvas_slotの部分が3つに増えています。

ボタンの部分は以下のようになっており、様々な調整項目がセットされています。

この部分を例にとり、解説していきます。

canvas_slot:
    Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
    Offsets := margin{Left := -300.0, Top := 35.0, Right := 280.0, Bottom := 130.0}
    Alignment := vector2{X := 0.0, Y := 0.0}
    SizeToContent := false
    Widget := button_loud{DefaultText := StringToMessage("Ringo")}

上記の部分のプロパティは、WidgetBlueprintで作成する場合の値と同一の値になるため、WidgetBlueprintを仮に作成して、レイアウトの調整はGUI上で実施すればデザインしやすいかと思います。

どういったことか説明するために、WidgetBlueprintを一つ作成し、そこにCanvasとボタンを一つ配置して説明していきます。

Widget Blueprintの作成

キャンバスの作成とボタンの配置

Verse上のコードと、WidgetBlueprintの対応関係はこのようになっています。

VerseとWidgetBlueprintの対応

このような形で、WidgetBlueprint上で、レイアウトを行って、その値をVerseコードに持っていけば、スムーズにレイアウトすることができます。

コードを実行し、ボタンを押した際に、以下のように表示されれば、正常に動作しています。

テキストと2つのボタンを持ったUI

ただ、このままでは、ボタンを押すことができません。ここで、ボタンを押せるようにさらにプログラムを改修していきます。

ボタンを押すとメッセージを表示するプログラム完成版

2つのパーツに分けて解説していきます。 まず、前半部分を以下のように変えます。

fruit_select_ui_device := class(creative_device):
  @editable
  _FruitSelectButton : button_device = button_device{}

  _RingoButton : button_loud = button_loud{}
  _BananaButton : button_loud = button_loud{}
  

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

    _RingoButton.SetText(StringToMessage("Ringo"))
    _RingoButton.OnClick().Subscribe(OnRingoButtonClicked)

    _BananaButton.SetText(StringToMessage("Banana"))
    _BananaButton.OnClick().Subscribe(OnBananaButtonClicked)

    _FruitSelectButton.InteractedWithEvent.Subscribe(OnInteractedWithFruitsSelectButton)

RingoButtonとBananaButtonをまず、事前に準備しています。

そのボタンを押したときの動作としてOnRingoButtonClicked関数と、OnBananaButtonClicked関数を登録しています。

次に後半部分です。

  ## map
  var _FruitWidgetMap : [player]widget = map{}

  OnInteractedWithFruitsSelectButton(Agent : agent) : void=
    if:
      Player := player[Agent]
      PlayerUI := GetPlayerUI[Player]
    then:
      FruitWidget := canvas:
        Slots := array:
          canvas_slot:
            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
            Offsets := margin{Left := -175.0, Top := -250.0, Right := 350.0, Bottom := 50.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := text_block:
              DefaultText := StringToMessage("Which Do You Like?")
              DefaultTextColor := NamedColors.Black
              DefaultJustification := text_justification.Center
          canvas_slot:
            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
            Offsets := margin{Left := -300.0, Top := -60.0, Right := 280.0, Bottom := 130.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := _RingoButton
          canvas_slot:
            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
            Offsets := margin{Left := 20.0, Top := -60.0, Right := 280.0, Bottom := 130.0}
            Alignment := vector2{X := 0.0, Y := 0.0}
            SizeToContent := false
            Widget := _BananaButton

      InputMode := player_ui_slot:
        InputMode := ui_input_mode.All
      PlayerUI.AddWidget(FruitWidget, InputMode)

      ### save widget info
      if:
        set _FruitWidgetMap[Player] = FruitWidget


  OnRingoButtonClicked(WidgetMessage : widget_message) : void=
    HideWidget(WidgetMessage.Player)
    ### Process for Ringo
    Print("Ringo")

     return

  OnBananaButtonClicked(WidgetMessage : widget_message) : void=
    HideWidget(WidgetMessage.Player)
    ### Process for Banana
    Print("Banana")
    return

  HideWidget(Player : player) : void = 
    if:
      PlayerUI := GetPlayerUI[Player]
      FruitWidget := _FruitWidgetMap[Player]
    then:
      PlayerUI.RemoveWidget(FruitWidget)
      
    return

Widgetはボタンを押したプレイヤーにのみ、表示する必要があります。

そのため、ボタンを押し、Widgetが表示されたプレイヤーがどのWidgetを保持しているのか、保存しておく必要があります。そのため、これを覚えておくための変数を準備しています。

  var _FruitWidgetMap : [player]widget = map{}

canvas_slot登録時は事前に作成しておいたボタンを登録するように変えています。

          canvas_slot:
            (中略)
            Widget := _RingoButton

ボタンが入力を受け取れるように、InputModeを変更して登録しています。

      InputMode := player_ui_slot:
        InputMode := ui_input_mode.All
      PlayerUI.AddWidget(FruitWidget, InputMode)

ボタンを押したプレイヤーと、Widgetの関係を記録しておきます。

      if:
        set _FruitWidgetMap[Player] = FruitWidget

ボタンを押した際には、Widgetをプレイヤーから削除しています。

  HideWidget(Player : player) : void = 
    if:
      PlayerUI := GetPlayerUI[Player]
      FruitWidget := _FruitWidgetMap[Player]
    then:
      PlayerUI.RemoveWidget(FruitWidget)
      
    return

あとは、適宜、ボタンを押した際の処理を、OnRingoButtonClicked関数とOnBananaButtonClicked関数の中に実装していけば完成となります。

自分の作例では、リンゴとバナナを出現させる処理を書いています。

ボタンを押したらリンゴが出現

まとめ

今回は、VerseからUIを作成する方法について解説しました。Verseからは画像が使用できず、まだまだ機能として十分とは言えませんが、今後、UI周りは大きく改善していくと思われ、一度、現状でできることを確認しておくのがいいのではないでしょうか。

やや、後半、コードの難易度があがってしまっており、あまりに記事が長くなってしまったため、そこを駆け足で説明してしまっています。

説明が足りていない部分もあるかと思いますので、わからない点があれば、ご質問ください。どういった点を説明すればいいのか、参考にさせていただければと思います。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com

【UEFN】Verseのアニメーションコントローラーで複雑なアニメーションを実現する (Verseの学習⑧)

AnimationControllerを使用すれば
Verseから複雑なアニメーションが実現可能!

UEFNのAnimationControllerによるアニメーション

UEFNでは、LevelSequenceでのキーフレームアニメーションをGUI上で行うかと思います。

LevelSequenceでは、ゲーム上のオブジェクトに対して、どういった時間でどういった状態になっているかをキーフレームとして事前登録しておき、これを再生することで、ゲーム上のオブジェクトのアニメーションを作成します。

LevelSequenceでアニメーション設定を行っている画面
事前にアニメーションを作成しておく必要がある

ただ、LevelSequenceは、すべて事前にEditor上で行わなければならず、ゲーム中にキーフレームを変更することはできません。

これを解決するのがAnimationControllerです。

VerseのAnimationController機能を使えば、ゲームの状態に応じて自由にキーフレームを与え、複雑なアニメーションをさせることができます

実は、今までこのブログでも何度か使用してきた、MoveTo関数(「指定した位置、回転、スケールに、指定した時間で移動するための関数)も、内部的にはAnimationControllerの機能を使用して実装されているようです。

MoveToだけでは、できることが限られていますが、AnimationController自体を使いこなせるようになれば、できることが大幅に広がるようになります。

そこで、今回は、このAnimationControllerの基本を簡単に紹介していきます。

AnimationControllerを使用する流れ

AnimationControllerは以下の流れで使用していきます。

  1. (位置情報、回転情報、時間情報など)といった情報をひとまとめにしたkeyframe_delta型の配列を作成
  2. creative_propについているAnimationControllerの情報を取得
  3. 1で作成したキーフレーム配列を、AnimationControllerにセットし、再生を開始。

Verse上から制御できるアイテムは、現状CreativePropだけかと思いますが、CreativePropは特に何も設定しなくても、内部的にAnimationControllerが設定されています

このAnimationControllerを取得し、そこにキーフレームを与えて動作させる、といった流れになります。

AnimationControllerのイベント(特定のキーフレームまでアニメーションが再生された、アニメーションが終了したなど)で何らかの処理をしたい場合は、それぞれのイベントに対して、実行する処理を登録しておくことも可能です。

AnimationController基本形

もっとも基本的なAnimationControllerを使用したサンプルは以下のようになります。

ボタンを押したら、RingoPropで設定したものを回転させながら、X軸方向に200cm動かすサンプルになります。

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

### Added
using { /Fortnite.com/Devices/CreativeAnimation } 
using { /UnrealEngine.com/Temporary/SpatialMath }

## Ringo Means Apple in Japanese
animation_controller_test_device := class(creative_device):
    @editable
    RingoProp : creative_prop = creative_prop{}

    @editable
    ActionButton : button_device = button_device{}

    OnBegin<override>()<suspends>:void=
        ActionButton.InteractedWithEvent.Subscribe(OnButtonInteracted)
        return

    OnButtonInteracted<public>(Agent : agent) : void = 
        if:
            AnimationController := RingoProp.GetAnimationController[] ### *01
        then:
            ## make keyframe ### *02
            KeyFrame := keyframe_delta :
                  DeltaLocation := vector3{X:=200.0}
                  DeltaRotation := MakeRotationFromYawPitchRollDegrees(180.0, 0.0, 0.0)
                  Time := 10.0

            ## make keyframe array ### *03
            KeyFrames : []keyframe_delta = array { KeyFrame }
            
            ## SetAnimation To Animation Controller ### *04
            AnimationController.SetAnimation(KeyFrames, ?Mode:=animation_mode.OneShot)
        
            ## Play Animation ### *05
            AnimationController.Play()

        return
  • まず、プロップについているAnimationControllerを取得します(*01)。 これは失敗する可能性があるため、失敗コンテクストの中で実施します。基本的にCreativePropには、特別な設定をしなくても、最初からAnimationControllerが与えられています。

  • 次にkeyframe_delta型でキーフレーム情報を一つ作っています(*02)。keyframe_delta型は以下の形になっています。

        keyframe_delta<native><public> := struct:
            # Target position of the `creative_prop`. 
            # This is a world-space coordinate in cm, 
            # with the initial position of the `creative_prop` acting as coordinate (0,0).
            DeltaLocation<native><public>:vector3

            # Target rotation for the `creative_prop`. 
            # Rotations are relative to the starting rotation ofthe `creative_prop`
            DeltaRotation<native><public>:rotation

            # Target scale for the `creative_prop`. 
            # Scale is multiplicative to the starting Scale of the `creative_prop`
            DeltaScale<native><public>:vector3 = external {}

            # Time in seconds the `creative_prop` should animate between
            #  its last frame and this frame.
            Time<native><public>:float

            # Interpolation mode for this `keyframe_delta`.
            # Default = `InterpolationTypes.Linear`
            Interpolation<native><public>:cubic_bezier_parameters = external {}
  • 移動、回転、スケールの情報を入力します。DeltaLocation,DeltaRotation,DeltaScaleとなっているように、現在値からの差分値(Delta)で指定する必要があります。

  • 今回の場合は、初期値が与えられているDeltaScaleとInterpolationに関しては省略可能なため、省略しています。

  • 回転に関しては、MakeRotationFromYawPitchRollDegreesを使用して回転情報の作成を行っています。

  • この場合はキーフレームを一つしか作成していませんが、実際には複数のキーフレームを設定してアニメーションすることになります。

  • SetAnimation関数の引数はkeyframe_deltaの配列にする必要があるため、配列にしています(*03)。

  • 作成した配列をアニメーションコントローラにセットします(*04)。

  • このキーフレームをどういった再生モードで再生するかanimation_mode型で指定することで選択することができます。animation_modeは列挙型になっており、以下の3つのパターンから選択することができます。

        # Animation play modes.
        animation_mode<native><public> := enum:
            # Stop after playing the animation once.
            OneShot
            # Reverse direction after reaching the final `keyframe_delta`,
            # then play the animation in reverse.
            PingPong
            # Play the animation in a loop. 
            # This requires the animation ends exactly where it began.
            Loop
  • これを指定している引数 Modeは先頭に?マークが付いています。

  • これは名前付き引数と呼ばれる文法で、名前付き引数に値を与えるときはこのような書き方をする必要があります。

AnimationController.SetAnimation(KeyFrames, ?Mode:=animation_mode.OneShot)
  • 通常、名前付き引数は変数としての初期値が与えられている場合が多いですが、SetAnimation関数では特に与えられていません。かつ、名前付き引数も一つしかないため、これを名前付き引数にする意味が現状では、あまりないように思われます。不思議な形になっていますが、将来的な拡張のために、あえて名前付き引数にしてあるのではないかと予想しています

最後にこれをプレイすることで動作します。

シーン上にボタンと適当なプロップを配置し、実行します。

ゲーム上のものを適宜設定

ボタンを押したタイミングで、プロップが横に動けば正常に動作しています。

ボタンを押すとRingoが動く

AnimationController発展形

では、次に、発展形として、ボタンを押した際に、Propが大きくなるアニメーションを組んでみたいと思います。

イメージとしては、マリオがキノコを食べたときに大きくなる時のように、瞬間的に膨らんでから少し縮むを繰り返しながら大きくなるイメージを目指します。

ボタンを押したときのコードを以下のように書き換えます。

        if:
            AnimationController := RingoProp.GetAnimationController[] ### *01
        then:
            ## make empty keyframes 
            var KeyFrames : []keyframe_delta = array {}
            
            ## append keyframes
            for(Index := 0..2):
                set KeyFrames += array{
                    keyframe_delta :
                                DeltaLocation := vector3{X:=0.0}
                                DeltaRotation := rotation{}
                                DeltaScale := vector3{X:=1.3, Y:=1.3, Z:=1.3}
                                Interpolation := InterpolationTypes.EaseOut
                                Time := 0.05
                    keyframe_delta :
                                DeltaLocation := vector3{X:=0.0}
                                DeltaRotation := rotation{}
                                DeltaScale := vector3{X:=0.95, Y:=0.95, Z:=0.95}
                                Interpolation := InterpolationTypes.EaseInOut
                                Time := 0.15
                }
            
            ## SetAnimation To Animation Controller ###
            AnimationController.SetAnimation(KeyFrames, ?Mode:=animation_mode.OneShot)
        
            ## Play Animation ###
            AnimationController.Play()
  • まず、keyframe_deltaを入れておく空の配列を準備します。for文を使用し、これに対してキーフレームを追加していきます。

  • 追加しているキーフレームは、0.07秒後に大きさを一度1.3倍にするキーフレームと、その0.15秒後に0.95倍に縮めるキーフレームです。この2つのキーフレームで大きさが1.235倍(1.3 x 0.95)になることになります。

  • これを3回繰り返しています。

  • キーフレーム補完モードもInterpolationで設定しています。

  • 手動で設定することもできますが、今回はUEFNで準備されているInterpolationTypesを使用して簡単に設定しています。

  • InterpolationTypesとしては以下が準備されており、この中からEaseOutとEaseInOutをそれぞれ選択しました。

        InterpolationTypes<public> := module:
            # `Linear` animations move at a constant speed.
            Linear<public>:cubic_bezier_parameters = external {}

            # `Ease` animations start slowly, speed up, then end slowly.
            Ease<public>:cubic_bezier_parameters = external {}

            # `EaseIn` animations start slow, then speed up towards the end.
            EaseIn<public>:cubic_bezier_parameters = external {}

            # `EaseOut` animations start fast, then slow down towards the end.
            EaseOut<public>:cubic_bezier_parameters = external {}

            # `EaseInOut` animations are similar to `Ease`
            # but the start and end animation speed is symmetric.
            EaseInOut<public>:cubic_bezier_parameters = external {}

さきほどと同じように実行してみます。

ボタンを押したタイミングで、リンゴがアニメーションしながら大きくなります。

グン、グン、グンといった感じで大きくなるアニメーション

(Gifではただのノイズに見えてしまっていますが・・・(涙))

まとめ

今回は簡単なサンプルで、AnimationControllerの使用方法を解説しました。

MoveTo関数のみではあまり複雑なことはできませんが(現状の仕様だとできてもあまり滑らかにならない)、AnimationControllerを使用すれば、Verse上からキーフレームアニメーションを自由に作ることができます。

UEFNのVerse制御では、このAnimationControllerが現状では、複雑なアニメーションを組むための唯一の選択になるため、ぜひ一度使用されることをお勧めします。

UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。

ringogames.hatenablog.com

ぜひほかの記事も見ていただけると嬉しいです。

RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。

Twitterはこちら

twitter.com