UEFN Verse言語 メタバース

【UEFN】カスタム仕様のカウントダウンタイマー(2)【Verse】

カスタム仕様のカウントダウンタイマー

こんにちは、あすか(@aars_inc)です。

今回は、UEFNカスタム仕様のカウントダウンタイマーについて解説します。

さっそくやっていきましょう。

なお、以下のコードの解説文は、プログラミング初心者の主がなんとか調べて記述したものです。

参考程度にどうぞ。

あすか

誤りがありましたら恐縮ですm(_ _)m

カスタム仕様のカウントダウンタイマー(2)

1がまだの方は先に1を進めましょう。

>> 備忘録UEFN:カスタム仕様のカウントダウンタイマー(1)

【UEFN】カスタム仕様のカウントダウンタイマー(1)【Verse】
備忘録UEFN:カスタム仕様のカウントダウンタイマー(1)

続きを見る

カウントダウンタイマーの右上に時間を表示

▼ 新しい AddedTimeWidget を作成し、RemainingTimeWidget と同じ配置値を使用しますが、カウントダウンタイマーの右上に時間を表示します。

AddedTimeWidget の [Offset (オフセット)] の [Left (左)] の余白を 50.0 に設定し、

RemainingTimeWidget の [Offset (オフセット)] の [Top (上)] の余白を 25.0 に設定します。

 countdown_timer := class:
  <# このブロックは、countdown_timer class のインスタンスごとに実行されます。
  ここに到達した際にキャンバスを設定できます。#>
  block:
      set Canvas = canvas:
          Slots := array:
              canvas_slot:
                  Anchors := anchors:
                      Minimum := vector2{X := 0.5, Y := 0.05}
                      Maximum := vector2{X := 0.5, Y := 0.05}
                  Alignment := vector2{X := 0.5, Y := 0.0}
                  Offsets := margin{Top := 0.0, Left := 50.0, Bottom := 0.0, Right := 0.0}
                  SizeToContent := true
                  Widget := AddedTimeWidget
              canvas_slot:
                  Anchors := anchors:
                      Minimum := vector2{X := 0.5, Y := 0.05}
                      Maximum := vector2{X := 0.5, Y := 0.05}
                  Alignment := vector2{X := 0.5, Y := 0.0}
                  Offsets := margin{Top := 25.0, Left := 0.0, Bottom := 0.0, Right := 0.0}
                  SizeToContent := true
                  Widget := RemainingTimeWidget

blockの詳細

canvas_slotについての詳細

Slots := array:の詳細

Anchors := anchors:の詳細

どうしてcanvas_slot:の定義が2つもあるの?

canvas_slot:~AddedTimeWidget版

アンカーとアラインメントの違い

アラインメントとオフセットの違い

canvas_slot:~RemainingTimeWidget版

2秒間表示の新たな関数を作成

▼ AddedTimeWidget の値を更新し、ウィジェットを再び非表示にする前にコールアウトを 2 秒間表示する「AddedTimeCallout()」という名前の新しい関数を作成します。

AddRemainingTime() で AddedTimeCallout() を呼び出します。

 AddRemainingTime(Time : float) : void =
     set RemainingTime += Time

     # 時間が追加された際に UI を即座に更新して、より優れたプレイヤー フィードバックを実現します。
     UpdateUI()

     # シンプルなコールアウトを起動して、追加される時間を表示します。
     spawn:
         AddedTimeCallout(Time)

 AddedTimeCallout(Time : float)<suspends> : void =
     if:
         PlayerUI := MaybePlayerUI?
         IntTime := Int[Time]
     then:
         AddedTimeWidget.SetVisibility(widget_visibility.Visible)
         AddedTimeWidget.SetText(AddedTimeText(IntTime))
         Sleep(2.0)
         AddedTimeWidget.SetVisibility(widget_visibility.Hidden)
AddRemainingTime(Time : float) : void =
     set RemainingTime += Time
     # RemainingTime(残り時間)にTime(追加される時間)を足す

     # 時間が追加された際に UI を即座に更新して、より優れたプレイヤー フィードバックを実現します。
     UpdateUI()

     # シンプルなコールアウトを起動して、追加される時間を表示します。
     spawn:
         AddedTimeCallout(Time)
         # AddedTimeCallout関数を生成し、追加される時間を表示します。

AddedTimeCallout(Time : float)<suspends> : void =
     if:
         PlayerUI := MaybePlayerUI?
         IntTime := Int[Time]
     then:
         AddedTimeWidget.SetVisibility(widget_visibility.Visible)
         # AddedTimeWidgetの表示を可視化します。

         AddedTimeWidget.SetText(AddedTimeText(IntTime))
         # AddedTimeWidgetにIntTime(整数化されたTime)のテキストを設定します。

         Sleep(2.0)
         # 2秒間待機します。

         AddedTimeWidget.SetVisibility(widget_visibility.Hidden)
         # AddedTimeWidgetの表示を非表示にします。

AddRemainingTimeの関数の定義の詳細

UpdateUI()とspawnの詳細

AddedTimeCallout(Time :関数の定義の詳細

<suspends>の詳細

AddedTimeWidget.SetVisibility(widget_visibility.Visible)の詳細

SetVisibilityメソッドを使う理由

(widget_visibility.Visible)の詳細

SetVisibilityメソッドを使う理由

AddedTimeWidget.SetVisibilityの間の.の意味

AddedTimeWidget.SetText(AddedTimeText(IntTime))の詳細

SetTextメソッドの詳細

AddedTimeText:IntTimeが整数化したテキストを返す関数である理由

Sleep(2.0)~t_visibility.Hidden)までの詳細

SetVisibility機能の詳細

Sleep関数の詳細

プレイテスト

UEFN Verse カウントダウンタイマー プレイテスト
出典:公式ドキュメント

プレイテストを行うと、カウントダウンが 30 から開始し、タイマーが 0 になるまで毎秒更新されて、その後カウントダウンが UI から消えます。

プレイヤーがボタンとやり取りすると、カウントダウンに 20 秒が追加され、追加された時間を示すコールアウトが 2 秒間表示されます。

公式ドキュメント

カウントダウンタイマーの終了を知らせる

このチュートリアルでは、ボタンの仕掛けの InteractedWithEvent を使用して、プレイヤーがいつボタンを押したかを知り、カウントダウン タイマーにさらに時間を追加しました。

しかし、コードに何かが起こったときに、他のユーザーがそれを把握するために使用できるカスタム仕様のイベントを独自に作成することもできます。

この例では、カスタム仕様のイベントの次の動作を使用する方法を示しています。

Signal():この関数は、イベントを待っているものすべてに、イベントが発生したことを知らせます。

Await():この非同期関数は、イベントが通知されるまで、この関数を含むコンテキストの実行をブロックします。

この例では、エンド ゲームの仕掛けを起動できるようにカウントダウンの終了したことを通知するイベントをカウントダウン タイマーに追加します。

公式ドキュメント

このチュートリアルでは、カウントダウンタイマーにさらに時間を追加するためにボタンを使用し、ボタンが押された時刻を把握します。

また、コードに何か特定のイベントが発生したときに、他のユーザーにそれを通知するためのカスタム仕様のイベントを作成する方法も紹介されています。

イベントの作成方法は以下のとおり。

  • Signal() 関数:
    • この関数は、イベントが発生したことを待っているすべての要素に通知します。
  • Await() 関数:
    • この非同期関数は、イベントが通知されるまで、それを含むコンテキストでの実行を一時停止します。

上記の要点をまとめると、カウントダウンタイマーが終了したことを通知するためのイベントをカウントダウンタイマーに追加する方法が示されています。

このカスタムイベントを使うと、エンドゲームのトリガーポイントとして利用できるわけですね。

具体的には、ボタンが押された時刻を把握しながら、カウントダウンタイマーの終了を待ち、終了したことを通知するためのカスタムイベントを作成します。

countdown_timerにイベントフィールドを追加

▼ 下記のコードは、カウントダウン終了のためのイベントを追加するための手順を示しています。

 CountdownEndedEvent : event() = event(){}

イベントフィールドとは

event(t) クラスの詳細

非同期のカウントダウンを実行する関数

RunCountdown() を更新して CountdownEndedEvent に通知し、ループから出る前に他のコードにカウントダウンが終了したことを知らせます。

 RunCountdown()<suspends> : void =
     # TimerTickPeriod を使ってループします。
     # UI も毎回更新されます。
     loop:
         Sleep(TimerTickPeriod)
         set RemainingTime -= TimerTickPeriod
         UpdateUI()

         # タイマーの終了
         if (RemainingTime <= 0.0):
             if (UI := MaybePlayerUI?):
                 UI.RemoveWidget(Canvas)
             CountdownEndedEvent.Signal()
             break
RunCountdown()<suspends>: void =
# カウントダウンを実行する非同期関数です。実行中に一時停止することがあります。

    # TimerTickPeriod を使ってループします。
    # カウントダウンを進めるために指定の間隔で処理が実行されます。
    loop:
        # TimerTickPeriod の間一時停止します。
        Sleep(TimerTickPeriod)

        # 残り時間を更新します。
        set RemainingTime -= TimerTickPeriod

        # UI を更新します。
        UpdateUI()

        # タイマーの終了を確認します。
        # 残り時間が0以下になった場合に処理が実行されます。
        if (RemainingTime <= 0.0):

            # もしプレイヤーのUIが存在する場合、UIからカウントダウンウィジェットを削除します。
            if (UI := MaybePlayerUI?):
                UI.RemoveWidget(Canvas)

            # CountdownEndedEvent にイベントを通知します。
            CountdownEndedEvent.Signal()

            # ループから抜けて関数の実行を終了します。
            break

UI.RemoveWidget(Canvas)のCanvasの記述理由

CountdownEndedEvent.Signal()の()をつける意味

UI.RemoveWidget(Canvas)のUIの要素

【重要】Sleepがない場合に想定される状況

TimerTickPeriodの詳細

TimerTickPeriodの間一時停止する理由

Loop and Breakの詳細

CountdownEndedEvent.Signal()コードがない場合

Signalの詳細

CountdownEndedEventの詳細

SignalとAwait関数の詳細

RemoveWidget関数の詳細

キャンバス・スロット・ウィジェットの関係性

エンドゲームの仕掛けを起動

▼ 「countdown_timer_example.verse」で CountdownEndedEvent を待ち、エンド ゲームの仕掛けを起動します。

 OnBegin<override>()<suspends> : void =
     AddMoreTimeButton.InteractedWithEvent.Subscribe(OnButtonInteractedWith)
     if:
         FirstPlayer := Self.GetPlayspace().GetPlayers()[0]
         PlayerUI := GetPlayerUI[player[FirstPlayer]]
     then:
         set CountdownTimer = countdown_timer{MaybePlayerUI := option{PlayerUI}, RemainingTime := InitialCountdownTime}
        
         CountdownTimer.StartCountdown()
         CountdownTimer.CountdownEndedEvent.Await()
            
         EndGame.Activate(FirstPlayer)
     else:
         Print("Can't find player")
OnBegin<override>()<suspends> : void =
# OnBeginイベントをオーバーライドし、このコードブロックの実行を一時停止します。

    AddMoreTimeButton.InteractedWithEvent.Subscribe(OnButtonInteractedWith)
    # AddMoreTimeButtonのInteractedWithEventに対して購読(Subscribe)し、そのイベントが発生した時にOnButtonInteractedWith関数を呼び出します。

    if:
    # 条件をチェックするためのif文:

        FirstPlayer := Self.GetPlayspace().GetPlayers()[0]
        # Playspaceオブジェクトに所属するプレイヤーのリストを取得し、その最初のプレイヤーをFirstPlayer変数に代入します。

        PlayerUI := GetPlayerUI[player[FirstPlayer]]
        # GetPlayerUI関数を使用して最初のプレイヤーに関連するUIを取得し、変数PlayerUIに代入します。

    then:
    # もしif文の条件が真(true)の場合、以下のブロックを実行します:

        set CountdownTimer = countdown_timer{MaybePlayerUI := option{PlayerUI}, RemainingTime := InitialCountdownTime}
        # countdown_timerクラスのインスタンスを作成し、MaybePlayerUIにPlayerUIをオプションの引数として渡し、RemainingTimeにInitialCountdownTimeを設定します。

        CountdownTimer.StartCountdown()
        # CountdownTimerインスタンスのカウントダウンを開始します。

        CountdownTimer.CountdownEndedEvent.Await()
        # CountdownTimerのCountdownEndedEventが発生するまで待機します。

        EndGame.Activate(FirstPlayer)
        # EndGameを最初のプレイヤーをパラメータとしてアクティブ化します。

    else:
    # もしif文の条件が偽(false)の場合、以下のブロックを実行します:

        Print("Can't find player")
        # "Can't find player"というメッセージを出力し、最初のプレイヤーが見つからなかったことを示します。

GetPlayerUIの詳細

コンポーネントとは

コンポーネントとメソッドの違い

PlayerUI := GetPlayerUI[player[FirstPlayer]]の記述理由

PlayerUI := GetPlayerUI[player[FirstPlayer]]の記述理由

FirstPlayerキーが取得する値

条件式の記述理由

Selfの詳細

if FirstPlayer := S~の詳細

AddMoreTimeButton.InteractedWithEvent.Subscribe(OnButtonInteractedWith)の詳細

オーバーライドとは

計算式の論理

GetPlayerUI[player[FirstPlayer]]のplayerはなくてもいい?

FirstPlayerとPlayerUIの格納内容の違い

then:~EndGame.Activateまでの詳細

引数は関数以外でも使用する

コンストラクタの由来

コンストラクタの例

コンストラクタがない場合に想定される問題

MaybePlayerUI := option{PlayerUI}, RemainingTime := InitialCountdownTimeの詳細

option{PlayerUI}とInitialCountdownTimeである理由

CountdownTimer.StartCountdown()~の詳細

countdown_timerクラスのインスタンスを作成するコードでMaybePlayerUIにPlayerUIをオプションの引数として渡す記述をする理由

EndGame.Activate(FirstPlayer)の詳細

プレイテスト

プレイテストを行うと、カウントダウンが 30 から開始し、タイマーが 0 になるまで毎秒更新されます。

カウントダウンが終了するとすぐにカウントダウンが UI から消え、ゲームは終了します。

プレイヤーがボタンとやり取りすると、カウントダウンに 20 秒が追加され、追加された時間を示すコールアウトが 2 秒間表示されます。

出典:公式ドキュメント

他のコードで使用できるようにクラスを準備する

これで独自のカスタム仕様のカウントダウン タイマー クラスを作成し、タイマーのインスタンス化と制御に Verse で作成した仕掛けを使用しました。

独自のカスタムクラスを作成する場合 (実際にはどのようなコードでもそうですが)、作成したものにアクセスできるものを指定することが重要です。たとえば、カウントダウン タイマーだけがその UI の作成と変更を行えるようにするべきです。Verse では、アクセス指定子 を使用してコードのアクセス レベルを設定できます。

パブリック とは識別子がユニバーサルにアクセス可能であることを意味するため、他のものがアクセスできるようにしたい識別子に public 指定子を追加します。この例では、countdown_timer_example の仕掛けに次のものがすべて使用されているため、これらにはパブリック アクセスが必要です。

出典:公式ドキュメント
  • CountdownEndedEvent<public> : event() = event(){}
  • StartCountdown<public>() : void =
  • AddRemainingTime<public>(Time : float) : void =

プライベート とは、識別子が現在のスコープ、つまりこの識別子を囲んでいるスコープ (この場合、countdown_timer クラス) のみでアクセス可能であることを意味するため、他のものがアクセスできないようにしたい識別子には private 指定子を追加します。

この例では、次のものにプライベート アクセスが必要です。

出典:公式ドキュメント
  • RemainingTimeWidget<private> : text_block = text_block{DefaultTextColor := NamedColors.White}
  • AddedTimeWidget<private> : text_block = text_block{DefaultTextColor := NamedColors.White}
  • AddedTimeText<localizes><private>(AddedTime : int) : message = " +{AddedTime}!"
  • RemainingTimeText<localizes><private>(CurrentRemainingTime : int) : message = "{CurrentRemainingTime}"
  • var Canvas<private> : canvas = canvas{}
  • TimerTickPeriod<private> : float = 1.0
  • RunCountdown<private>()<suspends> : void =
  • AddedTimeCallout<private>(Time : float)<suspends> : void =
  • UpdateUI<private>() : void =

コンストラクタ を使用すると、クラスの変数を 公開 することなく、新しいクラス インスタンスの初期値を設定することができます。コンストラクタは、関連付けられているクラスのインスタンスを作成する特別な関数です。

RemainingTime と MaybePlayerUI 変数を更新する countdown_timer クラスのためのコンストラクタを作成します。

出典:公式ドキュメント
MakeCountdownTimer<constructor><public>(MaxTime : float, InPlayer : agent) := countdown_timer:
    RemainingTime := MaxTime
    MaybePlayerUI := option{GetPlayerUI[player[InPlayer]]} 

コンストラクタで設定された変数 RemainingTime と MaybePlayerUI は、パブリック アクセスを無効にするべきですが、コンストラクタで設定されている場合はプライベート アクセスを有効にできません。これらの変数には internal 指定子を使用できます。これは、現在のモジュール、つまりこの識別子を囲んでいるモジュール内のみでこの識別子にアクセスできることを意味します。

出典:公式ドキュメント
  • MaybePlayerUI<internal> : ?player_ui = false
  • var RemainingTime<internal> : float = 0.0

まとめ:完全なスクリプト

今回は、カスタム仕様のカウントダウンタイマー(2)を解説しました。

countdown_timer.verse

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

MakeCountdownTimer<constructor><public>(MaxTime : float, InPlayer : agent) := countdown_timer:
    RemainingTime := MaxTime
    MaybePlayerUI := option{GetPlayerUI[player[InPlayer]]} 

countdown_timer := class:
    <# このブロックは、countdown_timer class のインスタンスごとに実行されます。
    ここに到達した際にキャンバスを設定できます。#>
    block:
        set Canvas = canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors:
                        Minimum := vector2{X := 0.4, Y := 0.3}
                        Maximum := vector2{X := 0.6, Y := 0.3}
                    Alignment := vector2{X := 0.5, Y := 0.5 }
                    Offsets := margin{Top := 0.0, Left := 0.0, Bottom := 0.0, Right := 0.0}
                    SizeToContent := true
                    Widget := RemainingTimeTextBlock 
                canvas_slot:
                    Anchors := anchors:
                        Minimum := vector2{X := 0.4, Y := 0.3}
                        Maximum := vector2{X := 0.6, Y := 0.3}
                    Alignment := vector2{X := 0.0, Y := 1.0}
                    Offsets := margin{Top := 0.0, Right := 0.0}
                    SizeToContent := true
                    Widget := AddedTimeTextBlock

    CountdownEndedEvent<public> : event(float) = event(float){}

    StartCountdown<public>() : void =
        if (PlayerUI := MaybePlayerUI?):
            PlayerUI.AddWidget(Canvas)

            # タイマーの開始時に UI を更新して、画面に RemainingTime の初期値を表示します
            UpdateUI()
            spawn:
                RunCountdown()

    AddRemainingTime<public>(Time : float) : void =
        set RemainingTime += Time
        # 時間が追加された際に UI を即座に更新して、より優れたプレイヤー フィードバックを実現します。
        UpdateUI()

        # シンプルなコールアウトを起動して、追加される時間を表示します。
        spawn:
            AddedTimeCallout(Time)

    MaybePlayerUI<internal> : ?player_ui = false
    var RemainingTime<internal> : float = 0.0
    RemainingTimeTextBlock<private> : text_block = text_block{} 
    AddedTimeTextBlock<private> : text_block = text_block{}
    RemainingTimeText<localizes><private>(CurrentRemainingTime : int) : message = "{CurrentRemainingTime}"
    AddedTimeText<localizes><private>(AddedTime : int) : message = " +{AddedTime}!"
    var Canvas<private> : canvas = canvas{}
    var TotalTime<private> : float = 0.0
    # タイマーの「精度」を表し、ティックの頻度を秒単位で表します。
    TimerTickPeriod<private> : float = 1.0

    RunCountdown<private>()<suspends> : void =
        # TimerTickPeriod を使ってループします。
        # UI も毎回更新されます。
        loop:
            Sleep(TimerTickPeriod)
            set TotalTime += TimerTickPeriod
            set RemainingTime -= TimerTickPeriod
            UpdateUI()

            # タイマーの終了
            if (RemainingTime <= 0.0):
                
                Canvas.RemoveWidget(RemainingTimeTextBlock)

                 if (UI := MaybePlayerUI?):
                     UI.RemoveWidget(Canvas)

                CountdownEndedEvent.Signal(TotalTime)
                break

    AddedTimeCallout<private>(Time : float)<suspends> : void =
        if:
            PlayerUI := MaybePlayerUI?
            IntTime := Int[Time]
        then:
            AddedTimeTextBlock.SetVisibility(widget_visibility.Visible)
            AddedTimeTextBlock.SetText(AddedTimeText(IntTime))
            Sleep(2.0)
            AddedTimeTextBlock.SetVisibility(widget_visibility.Hidden)
    
    UpdateUI<private>() : void =
        if (IntTime := Int[RemainingTime]):
            RemainingTimeTextBlock.SetText(RemainingTimeText(IntTime))

countdown_timer_example.verse

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

countdown_timer_example := class(creative_device):

    @editable
    AddMoreTimeButton : button_device = button_device{}

    @editable
    EndGame : end_game_device = end_game_device{}

    var CountdownTimer : countdown_timer = countdown_timer{}
    InitialCountdownTime : float = 30.0

    OnBegin<override>()<suspends> : void =
        AddMoreTimeButton.InteractedWithEvent.Subscribe(OnButtonInteractedWith)

        if:
            FirstPlayer := Self.GetPlayspace().GetPlayers()[0]
            PlayerUI := GetPlayerUI[player[FirstPlayer]]
        then:
            set CountdownTimer = countdown_timer{MaybePlayerUI := option{PlayerUI}, RemainingTime := InitialCountdownTime}
        
            CountdownTimer.StartCountdown()

            CountdownTimer.CountdownEndedEvent.Await()
            
            EndGame.Activate(FirstPlayer)
        else:
            Print("Can't find player")

    OnButtonInteractedWith(Agent : agent) : void =
        TimeToAdd : float = 20.0
        CountdownTimer.AddRemainingTime(TimeToAdd)

今回もお疲れさまでした🍵

  • この記事を書いた人
  • 最新記事

あすか(Asuka)

自己紹介:UEFNクリエイター
Verse言語独学中 | UE・プログラミング・3Dモデリング 6月開始 | 備忘録として学習記録をブログに残しています。
■好きな言葉
徳は弧ならず必ず隣あり.
■ひとこと
まだまだ未熟者ですが、
よろしくお願いします。

-UEFN, Verse言語, メタバース
-, ,