統計データクリエイター

仕掛けで数値を保存して、その値を基準にランキングする方法についてです。map型を用いた実装は下の方に書いておきます。

Verseよくわかんないやーって方は、完成形のコードがあるのでそれをコピペで使うのもアリです。

message変換

AgentToMessage<localizes>(InAgent:agent):message="{InAgent}"
ValueToMessage<localizes>(InValue:int):message="{InValue}"
EmptyMessage<localizes>:message=" "

まずはビルボードの「名前」と「値」を表示させるために、変換用の関数を作成します。これらはクラス外に配置しても良いです。

また、ビルボードの文字をクリアする用の空のmessageを用意しておきます。よく見ると「” “」の部分は空白が一つだけ入っています。というのも、「””」と何も文字を入れないとテキストが更新されない仕様なので、「透明文字」という意味を込めて半角スペースを入れておきます。

billboard管理

ranking_board_billboard := class<concrete>:
    @editable
    NameBillboard:billboard_device = billboard_device{}

    @editable
    ValueBillboard:billboard_device = billboard_device{}

    SetValue(InAgent:agent, InValue:int):void=
        NameBillboard.SetText(AgentToMessage(InAgent))
        ValueBillboard.SetText(ValueToMessage(InValue))

    SetEmpty():void=
        NameBillboard.SetText(EmptyMessage)
        ValueBillboard.SetText(EmptyMessage)

次に、ビルボードを格納するranking_boardクラスを作成します。今回は「名前」と「現在の値」を表示するため、「NameBillboard」と「ValueBillboard」の二つにしましたが、より情報を増やしたい場合などはeditableの変数を増やしたりしてみましょう。

デバイス作成

ranking_board_device := class(creative_device):
    @editable
    RankingBoards:[]ranking_board_billboard = array{}

    @editable
    ValueStat:stat_creator_device = stat_creator_device{}

    OnBegin<override>()<suspends>:void=
        loop:
            Sleep(1.0)
            UpdateBillboards()

    UpdateBillboards():void=
        AgentValues := GetAgentValues()
        SortedAgentValues := SortBy(AgentValues, SortByValue)
        
        for:
            Index -> Board : RankingBoards
        do:
            if(AgentValue := SortedAgentValues[Index]):
                Board.SetValue(AgentValue(0), AgentValue(1))
            else:
                Board.SetEmpty()

    GetAgentValues():[]tuple(agent, int)=
        for:
            PlayerElement : GetPlayspace().GetPlayers()
            PlayerValue := ValueStat.GetValue[PlayerElement]
        do:
            (PlayerElement, PlayerValue)

    SortByValue(X:tuple(agent, int), Y:tuple(agent, int))<computes><decides>:void=
        X(1) > Y(1)

実際に値の確認とビルボードの更新をする用のデバイスを作成します。

RankingBoards

先ほど作ったビルボードを格納するクラスを配列にし、editableとして公開します。

このようにレベル上にビルボードを表示し

Verseデバイス上では、配列に対応する形で入れていきます。今回は統計データの値を大きいもの順で並び替えているため、インデックスの値が0に近いランキングボードほど値が大きいプレイヤーということになります。

また、今回の仕組みではプレイヤーの人数とビルボードの人数が一致しなくても大丈夫なようになっています。そのため、プレイヤーに対してビルボードが少ない場合は、ビルボードの上限以上のプレイヤーは切り捨てられて表示されます。逆に、プレイヤーの方が少ない場合は、ビルボードは空のテキストが入るため表示されなくなります。

ValueStat

ValueStatには統計データクリエイターを入れるのですが、一つ注意点があります。

「最大値」にチェックを入れてあげないと、プレイヤーのデータを正常にとることができない場合があります。そのため、仮でもよいので最大値を「10000000」などに設定して、Verseデバイスで参照しましょう。

GetAgentValues関数

統計データの値を、tuple(agent, int)の配列にして取得する関数を用意します。

一応、ちょっと特殊な書き方をしていますが、

    GetAgentValues():[]tuple(agent, int)=
        var Result:[]tuple(agent, int) = array{}
        for:
            PlayerElement : GetPlayspace().GetPlayers()
            PlayerValue := ValueStat.GetValue[PlayerElement]
        do:
            set Result += array{(PlayerElement, PlayerValue)}
        return Result

と同じ意味であるとみて大丈夫です(書き方が気持ち悪ければこっちに置き換えるでもヨシ)

仕組み

気になる人向けに原理を説明します。Verse言語はfor, ifやその他機能は「式」として扱われます。そのため、呼び出すと呼び出し元で何らかの結果を返すようになっています。
forであれば、doの中に書かれた最後の値をもとに、配列として返します。そのため

Result := 
    for:
        Value := 1..10
    do:
        Value

という処理があれば、Resultの中には「1, 2, 3, 4, 5, 6, 7, 8, 9, 10」という配列が入ります。

また、関数はreturnを省略できるという特性があります。その場合、関数の戻り値は関数内で最後に実行されたモノの値が渡されます。今回の関数ではforで配列を作るような処理しか入っていないため、それが最後の処理として見られ、戻り値には(PlayerElement, PlayerValue)が入った配列が渡されます。

SortByValue

SortBy関数でソートの基準を指定する用の関数です。

Verse言語には値をソートする用の関数が用意されているのですが、そのソートの基準は「<decides><computes>」の関数をもとに行われます。まあ、詳しくは上記の記事にも書いてあるので、見てみましょう!

UpdateBillboards

ここでは実際にビルボードに反映させる処理が書かれています。

for:
    Index -> Board : RankingBoards
do:
    if(AgentValue := SortedAgentValues[Index]):
        Board.SetValue(AgentValue(0), AgentValue(1))
    else:
        Board.SetEmpty()

の部分ではRankingBoardsを分割し、そのインデックスを取得するような処理を書いています。

SortedAgentValuesには統計データの値を基準にソートされたプレイヤーの配列が入っています。そのため、ビルボードのインデックスに対応させることで、ビルボードの0番目から大きい順でプレイヤーの名前が表示されます。

elseと分岐している部分では、島内のプレイヤーがビルボードの数に満たしていなかった場合の処理となっています。RankingBoards.Length > SortedAgentValues.Lengthであれば、「SortedAgentValues[Index]」の部分で無いものを取り出そうとしてifが失敗するため、elseでキャッチしてSetEmptyで半角スペースをビルボードに反映させます。

OnBegin

最後に、今回は他の仕掛けを使わなかったため、毎秒ごとにビルボードを更新する処理を書きました。より負荷対策を考えたいなどであれば、「○○の条件を満たしたらUpdateBillboardsを呼び出す」などをしてみましょう。

完成形

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

AgentToMessage<localizes>(InAgent:agent):message="{InAgent}"
ValueToMessage<localizes>(InValue:int):message="{InValue}"
EmptyMessage<localizes>:message=" "

ranking_board_billboard := class<concrete>:
    @editable
    NameBillboard:billboard_device = billboard_device{}

    @editable
    ValueBillboard:billboard_device = billboard_device{}

    SetValue(InAgent:agent, InValue:int):void=
        NameBillboard.SetText(AgentToMessage(InAgent))
        ValueBillboard.SetText(ValueToMessage(InValue))

    SetEmpty():void=
        NameBillboard.SetText(EmptyMessage)
        ValueBillboard.SetText(EmptyMessage)

ranking_board_device := class(creative_device):
    @editable
    RankingBoards:[]ranking_board_billboard = array{}

    @editable
    ValueStat:stat_creator_device = stat_creator_device{}

    OnBegin<override>()<suspends>:void=
        loop:
            Sleep(1.0)
            UpdateBillboards()

    UpdateBillboards():void=
        AgentValues := GetAgentValues()
        SortedAgentValues := SortBy(AgentValues, SortByValue)
        
        for:
            Index -> Board : RankingBoards
        do:
            if(AgentValue := SortedAgentValues[Index]):
                Board.SetValue(AgentValue(0), AgentValue(1))
            else:
                Board.SetEmpty()

    GetAgentValues():[]tuple(agent, int)=
        for:
            PlayerElement : GetPlayspace().GetPlayers()
            PlayerValue := ValueStat.GetValue[PlayerElement]
        do:
            (PlayerElement, PlayerValue)

    SortByValue(X:tuple(agent, int), Y:tuple(agent, int))<computes><decides>:void=
        X(1) > Y(1)

コピペ用に全体図も貼っておきます。

map型

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

AgentToMessage<localizes>(InAgent:agent):message="{InAgent}"
ValueToMessage<localizes>(InValue:int):message="{InValue}"
EmptyMessage<localizes>:message=" "

ranking_board_billboard := class<concrete>:
    @editable
    NameBillboard:billboard_device = billboard_device{}

    @editable
    ValueBillboard:billboard_device = billboard_device{}

    SetValue(InAgent:agent, InValue:int):void=
        NameBillboard.SetText(AgentToMessage(InAgent))
        ValueBillboard.SetText(ValueToMessage(InValue))

    SetEmpty():void=
        NameBillboard.SetText(EmptyMessage)
        ValueBillboard.SetText(EmptyMessage)

ranking_board_device := class(creative_device):
    @editable
    RankingBoards:[]ranking_board_billboard = array{}

    var ValuePerAgent:[agent]int = map{}

    OnBegin<override>()<suspends>:void=
        loop:
            Sleep(1.0)
            UpdateBillboards()

    UpdateBillboards():void=
        AgentValues := GetAgentValues()
        SortedAgentValues := SortBy(AgentValues, SortByValue)
        
        for:
            Index -> Board : RankingBoards
        do:
            if(AgentValue := SortedAgentValues[Index]):
                Board.SetValue(AgentValue(0), AgentValue(1))
            else:
                Board.SetEmpty()

    GetAgentValues():[]tuple(agent, int)=
        for:
            Agent -> Value : ValuePerAgent
        do:
            (Agent, Value)

    SortByValue(X:tuple(agent, int), Y:tuple(agent, int))<computes><decides>:void=
        X(1) > Y(1)

大体の個所は同じです。

まず、ValueStatをValuePerAgentという[agent]intのマップに置き換えました。

次にGetAgentValuesの中身をガラッと変えています。mapに対するforでは、キーと値を同時に取り出すことができるため、forの呼び出し元に配列が返る特性を生かしてこのような処理にしました。

最後に

完成形の動画となります。ぜひ、皆様も作ってみてくださいね!