【UEFN】Verseでシンプルなタイマーを作ってみた

## 背景
実はタイマー仕掛けというものが元々用意されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/f43cf383-607d-ba58-c237-6a562fe140e5.png)
しかし便利がゆえにごちゃごちゃしすぎる怖さがありました。
(設定できる項目がこんなにたくさん。。。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/bc751b98-d971-b126-9af8-48c947f859e5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/c70930ad-0b09-adee-5011-d97bf7d3b08e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/36f3aa77-5d01-abe1-aa58-32baf8eda473.png)
そこで今回はシンプルなタイマーを作ろうということになりました。

## 実装
### 全体コード
#### simple_timer_device
```
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }


simple_timer_device := class(creative_device):

    @editable
    edit_count:int=0

    @editable
    var is_count_up:logic=true

    var nowCount:int=0

    var isPlay:logic=false

    StartEvent<public>:event() = event(){}

    StopEvent<public>:event() = event(){}

    CountEvent<public>:event(int) = event(int){}

    FinishEvent<public>:event() = event(){}

    Start()<suspends>:void=
        set isPlay=true
        StartEvent.Signal()
        if (is_count_up=true):
            set nowCount = edit_count
            spawn { CountUp() }
        else:
            set nowCount = 0
            spawn { CountDown() }
        Print("Count: {nowCount}")

    Stop()<suspends>:void=
        set isPlay=false
        StopEvent.Signal()
        Print("Count: {nowCount}")

    Play()<suspends>:void=
        set isPlay=true
        if (is_count_up=true):
            spawn { CountUp() }
        else:
            spawn { CountDown() }
        Print("Count: {nowCount}")

    # 一度タイマーを止めないと切り替わりません
    Switch(isCountUp:logic)<suspends>:void=
        set is_count_up=isCountUp
        Print("Count: {nowCount}")

    CountDown()<suspends>:void=
        set nowCount = edit_count
        loop:
            Sleep(1.0)
            set nowCount-=1
            CountEvent.Signal(nowCount)
            if (nowCount <= 0):
                set isPlay=false
                FinishEvent.Signal()
                break
            else if (isPlay=false):
                Print("Stop")
                break
                
            Print("Count: {nowCount}")

    CountUp()<suspends>:void=
        set nowCount = 0
        loop:
            Sleep(1.0)
            set nowCount+=1
            CountEvent.Signal(nowCount)
            if (nowCount >= edit_count):
                set isPlay=false
                FinishEvent.Signal()
                break
            else if (isPlay=false):
                Print("Stop")
                break
            Print("Count: {nowCount}")
```
### 構成について
今回は**シンプルなタイマー**ということでスタート時とカウント時、ストップ時、終了時のイベントのみが見れるようにしました。またオプションとしてカウントアップなのかカウントダウンなのかも指定できるようにしてみました。さらに本家では60時間までしかはかれなかったのでそれ以上にの時間がはかれるようにもしてみました。（99時間まで行けます。）

### 利用する方法
#### やること
billboardに現在の時間を映してみます。

#### 動画
<iframe width="560" height="315" src="https://www.youtube.com/embed/TsWv1ESjw8o?si=fNj1YoRU2qAwrnzT" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

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


sample_timer_presenter_device := class(creative_device):

    @editable
    billboard:billboard_device= billboard_device{}

    @editable
    timer:simple_timer_device=simple_timer_device{}

    converter:seconds_to_time_converter=seconds_to_time_converter{}

    timeMessage<localizes>(msg:string):message="{msg}"

    OnBegin<override>()<suspends>:void=
        timer.Start()
        spawn { StartAwaitHandler() }
        spawn { CountAwaitHandler() }
        spawn { StopAwaitHandler() }
        spawn { FinishAwaitHandler() }

    StartAwaitHandler()<suspends>:void=
        loop:
            timer.StartEvent.Await()
            Print("Timer Start")

    CountAwaitHandler()<suspends>:void=
        loop:
            timeValue := timer.CountEvent.Await()
            if (timeString := converter.ConvertToTime[timeValue]):
                billboard.SetText(timeMessage(timeString))
            else:
                billboard.SetText(timeMessage("00:00:00"))

    StopAwaitHandler()<suspends>:void=
        loop:
            timer.StopEvent.Await()
            Print("Timer Stop")

    FinishAwaitHandler()<suspends>:void=
        loop:
            timer.FinishEvent.Await()
            billboard.SetText(timeMessage("FINISH"))
            Print("Timer Finish")
```
タイマーとbillboardを紐づけるクラスになります。
#### seconds_to_time_converter
```
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

seconds_to_time_converter := class():

    ConvertToTime(sec:int)<transacts><decides>:string=
        if ( sec >= 3600):
            hours := sec / 3600
            minutes := Mod[sec, 3600] / 60
            seconds := Mod[sec, 3600] 
            S := Mod[seconds, 60]
            H:int=Ceil(hours)
            M:int=Ceil(minutes)
            if (H < 10):
                if (M < 10):
                    if (seconds < 10):
                        timeString := "0{H}:0{M}:0{seconds}"
                        return timeString
                    else:
                        timeString := "0{H}:0{M}:{seconds}"
                        return timeString
                else:
                    if (seconds < 10):
                        timeString := "0{H}:{M}:0{seconds}"
                        return timeString
                    else:
                        timeString := "0{H}:{M}:{seconds}"
                        return timeString
            else:
                if (M < 10):
                    if (seconds < 10):
                        timeString := "{H}:0{M}:0{seconds}"
                        return timeString
                    else:
                        timeString := "{H}:0{M}:{seconds}"
                        return timeString
                else:
                    if (seconds < 10):
                        timeString := "{H}:{M}:0{seconds}"
                        return timeString
                    else:
                        timeString := "{H}:{M}:{seconds}"
                        return timeString
        else if (sec < 3600 and sec >= 60):
            minutes := sec / 60
            seconds := Mod[sec, 60]
            M:int=Ceil(minutes)
            if (M < 10):
                if (seconds < 10):
                    timeString := "00:0{M}:0{seconds}"
                    return timeString
                else:
                    timeString := "00:0{M}:{seconds}"
                    return timeString
            else:
                if (seconds < 10):
                    timeString := "00:{M}:0{seconds}"
                    return timeString
                else:
                    timeString := "00:{M}:{seconds}"
                    return timeString
        else if (sec <= 59):
            seconds := sec
            if (seconds < 10):
                timeString := "00:00:0{seconds}"
                return timeString
            else:
                timeString := "00:00:{seconds}"
                return timeString
        else:
            timeString := "00:00:00"
            return timeString
```
こちらもっと最適化できそうなコードではありますがただの`int型`の値から`00:00:00`というstring型に変換するクラスになります。変換するクラスをpresenterに書いてしまうと視認性が悪くなるので別のクラスにしました。

#### デバイス関連
配置
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/2ead48ee-63a2-ffbd-9332-3541392e4dfa.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/961f47ab-eccd-c3c6-a789-3bb8ed999cad.png)
インスペクタでの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/1e46cbf3-4716-9b8c-2677-9d8f31c97e9a.png)
- `edit_count`で制限時間設定できる
- `is_count_up`でカウントアップかカウントダウンか設定できる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692976/d1b14d4e-cbad-fc96-bd93-27f019fc43bd.png)
- simple_timerとbillboardを結び付ける

### コードについて
今回初めてブログで`event`を利用してみました。はじめは`listenable型`が用意されているかなと持ったのですが使えなかったので`event`を使う形になりました。 

tutiyaさんの記事が分かりやすくとても参考になりました。
https://zenn.dev/t_tutiya/articles/b413668767a612

loop文内でeventをawaitすることでSubscribeと似た動作ができる感じになっています。
1. Eventのsignalが呼ばれる
1. awaitが解除される
1. それ以下の処理が行われる
1. loopの初めに戻る
1. awaitで次のsignalを待つ
```
 loop:
        timer.StartEvent.Await()
        Print("Timer Start")
```

## まとめ
今回はガッツリコードを書いていきました。int型から時間表示に変換するコードを考えるのが少しつらかったです（笑）。今後は`event`型はよく使っていくことになりそうなので覚えていて損は無さそうです！

## 余談
この度UEFN/Verseに関するオープンコミュニティサーバーを建ち上げました。ちょっとでも興味があれば奮ってご参加くださいませ。

https://discord.gg/cK4daQyD