UEProg by milkc0de

    Search

    Home

    UEFN Verse: ガード撃破位置にVFXを出す

    Guardヘッドショットで「headshot」を1秒表示する(3デバイスのみ)

    UEFN / Verseでプレイヤー名を表示する方法

    UEFN Verseサンプル - Trigger起動でBearのPropがプレイヤーについてくる

    ガードの行動範囲を「プレイヤー中心」に縛る(Leash)ガードAI

    通常編集とシンプル編集で 窓開けリセットの最短時間を測定

    UEFN: 建物内が暗くなる場合の対処(Cast Shadow一括OFF) by milkc0de

    UEFN / VerseのみでPINコード・パスコード入力パッドを実装する

    UEFN / Verseのみで作ったPINコード入力パッドを初心者向けに分解して解説する

    UEProg by milkc0de

    Home

    UEFN / Verseでプレイヤー名を表示する方法

    UEFN Verseサンプル - Trigger起動でBearのPropがプレイヤーについてくる

    UEFN Verse: ガード撃破位置にVFXを出す

    Guardヘッドショットで「headshot」を1秒表示する(3デバイスのみ)

    ガードの行動範囲を「プレイヤー中心」に縛る(Leash)ガードAI

    通常編集とシンプル編集で 窓開けリセットの最短時間を測定

    UEFN: 建物内が暗くなる場合の対処(Cast Shadow一括OFF) by milkc0de

    UEFN / VerseのみでPINコード・パスコード入力パッドを実装する

    UEFN / Verseのみで作ったPINコード入力パッドを初心者向けに分解して解説する

    milkc0de

    XTwitch
    UEProg by milkc0de
    /
    UEFN / Verseのみで作ったPINコード入力パッドを初心者向けに分解して解説する
    UEFN / Verseのみで作ったPINコード入力パッドを初心者向けに分解して解説する

    UEFN / Verseのみで作ったPINコード入力パッドを初心者向けに分解して解説する

    前回の記事では、VerseのみでPINコード / パスコード入力パッドを実装する方法を紹介しました。

    前回の記事はこちらです。

    UEFN / VerseのみでPINコード・パスコード入力パッドを実装する

    今回はその続編として、前回掲載した約400行のVerseコードを、UEFNを始めたばかりの方でも読みやすいように分解して解説していきます。

    完成コードをそのまま貼るだけだと、どうしても

    • どこがUI生成なのか
    • どこが入力処理なのか
    • どこがマルチプレイヤー対応なのか
    • なぜ player ごとにMapを持っているのか

    が見えにくくなります。

    そこで今回は、コード全体の役割を順番にほどいていく記事にしました。

    記事の後半には、前回と同じソースコード全文もそのまま載せています。

    この記事は前回記事の続編です

    前回の記事では、以下をVerseのみで実装しました。

    • PIN入力UIの生成
    • 数字ボタン
    • delete
    • enter
    • close
    • プレイヤーごとのUI分離
    • 同時操作対応
    • 複数PIN対応
    • 成功トリガー分岐

    今回の記事では、そのコードを題材にして、なぜこの構造になっているのかを初心者向けに解説します。

    まず、このPINパッドは何をしているのか

    このVerseデバイスを一文で言うと、

    「トリガーで開いて、プレイヤー専用のPIN入力UIをVerseだけで表示し、入力内容に応じて別々のTriggerを発火させる仕組み」

    です。

    たとえば以下のような用途に使えます。

    • 金庫のロック
    • 秘密の扉
    • 謎解き
    • セキュリティ端末
    • 隠しコマンド入力

    コード全体の見取り図

    約400行ありますが、実際には大きく6ブロックです。

    1. @editable の設定項目
    2. プレイヤーごとの状態管理
    3. UIを開く処理
    4. 入力文字列を更新する処理
    5. PINの正誤判定
    6. UIレイアウトの構築

    つまり、アルゴリズムが異常に複雑というより、UIをVerseで全部組み立てているので長いというのが実態です。

    1. @editable は何を設定しているのか

    最初に出てくるこの部分です。

    @editable
    OpenTrigger : trigger_device = trigger_device{}
    
    @editable
    FailedTrigger : trigger_device = trigger_device{}
    
    @editable
    PinCodes : []string = array{}
    
    @editable
    SuccessTriggers : []trigger_device = array{}
    
    @editable
    TitleText : string = "PIN PAD"
    
    @editable
    MaxDigits : int = 8

    OpenTrigger

    UIを開くためのトリガーです。

    ボタン、Interaction Device、Triggerなどから接続します。

    FailedTrigger

    PINが間違っていたときに発火するトリガーです。

    PinCodes

    正解として扱うPINコードの一覧です。複数設定できます。

    SuccessTriggers

    PIN成功時に発火するトリガーの一覧です。

    PinCodes と同じインデックスで対応します。

    例:

    • PinCodes[0] に一致したら SuccessTriggers[0]
    • PinCodes[1] に一致したら SuccessTriggers[1]

    TitleText

    UI上部のタイトルです。

    PIN PAD や SECURITY LOCK など自由に変更できます。

    MaxDigits

    最大入力桁数です。

    2. このコードで一番大事なのはプレイヤーごとの状態分離

    次の部分が、このデバイスの核です。

    var PlayerRoots : [player]widget = map{}
    var PlayerInputTexts : [player]text_block = map{}
    var PlayerInputs : [player]string = map{}
    var PlayerUIs : [player]player_ui = map{}

    これは全部、playerをキーにしたMapです。

    なぜ必要なのか

    マルチプレイヤー対応のUIで1つの変数しか持たないと、状態が混ざります。

    たとえばこういう事故が起きます。

    • Aさんの入力がBさんの画面に出る
    • Aさんが閉じたらBさんのUIまで消える
    • PINの入力状態が共有される

    それを防ぐために、プレイヤーごとに別々の状態を保存しています。

    各Mapの役割

    PlayerRoots

    そのプレイヤーに表示したルートWidgetです。

    閉じるときに RemoveWidget するために使います。

    PlayerInputTexts

    入力中の文字を表示する text_block です。

    UpdateDisplay で更新する相手になります。

    PlayerInputs

    現在の入力文字列です。

    例:

    • Aさん → "1234"
    • Bさん → "9999"

    のように完全に分かれます。

    PlayerUIs

    そのプレイヤーの player_ui を保存しています。

    3. UIはどうやって開いているのか

    まず OnBegin で OpenTrigger にイベントを登録しています。

    OnBegin<override>()<suspends>:void =
        OpenTrigger.TriggeredEvent.Subscribe(OnOpenTriggered)

    つまり、

    OpenTriggerが発火したら OnOpenTriggered を呼ぶ

    という意味です。

    次にこちらです。

    OnOpenTriggered(MaybeAgent:?agent):void =
        if (A := MaybeAgent?):
            if (P := player[A]):
                OpenForPlayer(P)

    Triggerイベントから渡ってくるのは ?agent なので、

    1. agentが存在するか確認
    2. player に変換
    3. 変換できたら OpenForPlayer(P)

    という流れになっています。

    4. OpenForPlayer が実際にUIを表示する

    ここで大事なのは最初に CloseForPlayer(P) を呼んでいることです。

    なぜ最初に閉じるのか

    同じプレイヤーが何度も開いたときに

    • UIが二重に重なる
    • 前回のWidgetが残る
    • 入力状態が中途半端に残る

    といった事故を防ぐためです。

    そのあとで

    • GetPlayerUI[P] を取る
    • BuildRootWidget(P) でUIを作る
    • AddWidget で表示する
    • 入力文字列を空に初期化する
    • 表示を更新する

    という順番で進みます。

    5. UIを閉じる処理はかなりシンプル

    CloseForPlayer(P:player):void =
        if (UI := PlayerUIs[P]):
            if (Root := PlayerRoots[P]):
                UI.RemoveWidget(Root)

    やっていることは、そのプレイヤーに紐づいているUIだけを消すことです。

    全員分を消しているわけではありません。

    6. 表示更新は UpdateDisplay

    UpdateDisplay(P:player):void =
        if (TB := PlayerInputTexts[P]):
            if (Current := PlayerInputs[P]):
                if (Current.Length > 0):
                    TB.SetText(ToMessage(Current))
                else:
                    TB.SetText(ToMessage(""))

    これは、入力中の文字列を画面の表示欄へ反映する関数です。

    数字を押すたびに

    • PlayerInputs[P] を更新
    • UpdateDisplay(P) を呼ぶ

    という流れになります。

    7. ToMessage はなぜ必要なのか

    ToMessage<localizes>(S:string) : message = "{S}"

    VerseのUIは string ではなく message を要求する場面があります。

    そのため、小さな変換関数を1つ置いています。

    UIをVerseで組むと、こういう補助関数はかなり便利です。

    8. 数字追加処理は AppendDigit

    AppendDigit(P:player, Digit:string):void =
        if (Current := PlayerInputs[P]):
            if (Current.Length >= MaxDigits):
                return
    
            NewValue : string = "{Current}{Digit}"
            if (set PlayerInputs[P] = NewValue):
            UpdateDisplay(P)

    ここでは

    1. 現在の文字列を取得
    2. 最大桁数チェック
    3. 新しい数字を後ろに追加
    4. 保存
    5. 表示更新

    をしています。

    たとえば "12" の状態で "3" を押せば "123" になります。

    9. DELETEは末尾1文字を切るだけ

    DeleteLastDigit(P:player):void =
        if (Current := PlayerInputs[P]):
            if (Current.Length <= 0):
                return
    
            NewValue : string = Substring(Current, 0, Current.Length - 1)
            if (set PlayerInputs[P] = NewValue):
            UpdateDisplay(P)

    これは末尾1文字を削除しています。

    例:

    • "1234" → "123"
    • "9" → ""

    10. Substring を自前で書いている理由

    DELETE処理のために、文字列の一部を切り出す関数を用意しています。

    この関数は

    • 開始位置と長さを受け取る
    • 範囲チェックする
    • 1文字ずつ連結する
    • 結果を返す

    という流れです。

    Verseでは、他言語の感覚で便利な文字列操作を期待しすぎない方が安全なので、

    確実に動く処理を自前で持っていると考えると分かりやすいです。

    11. PIN判定の中心は SubmitPin

    これがPIN判定の本体です。

    流れ

    1. 現在の入力文字列を取得
    2. PinCodes を順番に見る
    3. 一致したら成功
    4. 対応する SuccessTriggers[Index] を発火
    5. 一致しなければ FailedTrigger
    6. 最後にUIを閉じる

    12. なぜ Index を使っているのか

    この実装では

    • PinCodes
    • SuccessTriggers

    を同じ番号同士で対応させています。

    例:

    • PinCodes[0] = "1234" → SuccessTriggers[0]
    • PinCodes[1] = "4321" → SuccessTriggers[1]
    • PinCodes[2] = "0000" → SuccessTriggers[2]

    これにより、Verseコードを大きく書き換えなくても、Detailsから分岐を増やせます。

    13. 400行の大半は BuildRootWidget です

    コードが長く見える最大の理由はここです。

    BuildRootWidget(P:player):canvas =

    この関数で

    • 背景
    • パネル
    • タイトル
    • 表示欄
    • 数字ボタン
    • DELETE
    • ENTER
    • CLOSE
    • 配置座標

    を全部Verseだけで組み立てています。

    つまり、行数が多いのはVerseだけでUIを全部書いているからです。

    14. BuildRootWidget の中身を分けて見る

    背景の暗幕

    Dimmer : color_block = color_block{
        DefaultColor := color{R := 0.0, G := 0.0, B := 0.0},
        DefaultOpacity := 0.60,
        DefaultDesiredSize := vector2{X := 1920.0, Y := 1080.0}
    }

    モーダルっぽく見せるための全画面の半透明背景です。

    メインパネル

    Panel : color_block = color_block{
        DefaultOpacity := 0.98,
        DefaultDesiredSize := vector2{X := 560.0, Y := 800.0},
        DefaultColor := color{R := 0.0, G := 0.0, B := 1.0}
    }

    中央の青いパネルです。

    タイトル

    Title : text_block = text_block{
        DefaultText := ToMessage(TitleText),
        DefaultTextColor := color{R := 1.0, G := 1.0, B := 1.0},
        DefaultTextSize := 36.0,
        DefaultJustification := text_justification.Center
    }

    入力表示欄

    DisplayBG : color_block = color_block{
        DefaultColor := color{R := 0.0, G := 0.0, B := 0.0},
        DefaultOpacity := 1.0,
        DefaultDesiredSize := vector2{X := 420.0, Y := 74.0}
    }
    
    DisplayText : text_block = text_block{
        DefaultText := ToMessage(""),
        DefaultTextColor := color{R := 1.0, G := 1.0, B := 1.0},
        DefaultTextSize := 34.0,
        DefaultJustification := text_justification.Center
    }

    表示欄は

    • 背景
    • 文字

    の2つでできています。

    15. DisplayText をMapに保存している意味

    if (set PlayerInputTexts[P] = DisplayText):

    これをしておかないと、あとで UpdateDisplay(P) を呼んだときに、

    どの text_block を更新すればいいか分かりません。

    つまり、表示先のWidgetをプレイヤーごとに記録しているわけです。

    16. ボタンは1個ずつ生成している

    数字ボタン、DELETE、ENTER、CLOSEを全部Verseで生成しています。

    ボタン種類を分けている理由

    • 数字とENTERは目立たせたいので button_loud
    • DELETEは普通の button_regular
    • CLOSEは控えめな button_quiet

    という見た目上の使い分けです。

    17. ボタン押下イベントは Subscribe でつなぐ

    これは

    「このボタンが押されたらこの関数を呼ぶ」

    という接続です。

    18. 数字ごとに関数を分けているのは初心者向けにはむしろ分かりやすい

    後半にこういう関数が並んでいます。

    OnDigit1(Msg:widget_message):void =
        if (P := player[Msg.Player]):
            AppendDigit(P, "1")

    これは見た目上は少し冗長ですが、初心者向けにはかなり分かりやすいです。

    理由は、

    • どのボタンが何をしているかすぐ読める
    • widget_message から player を取る流れが見える
    • クリックイベントの構造が理解しやすい

    からです。

    19. canvas_slot が大量にあるのは配置情報だから

    BuildRootWidget の後半では canvas_slot が大量に出てきますが、これはロジックではなく座標指定です。

    たとえば

    canvas_slot:
        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
        Offsets := margin{Left := -130.0, Top := -110.0, Right := 0.0, Bottom := 0.0}
        Alignment := vector2{X := 0.5, Y := 0.5}
        SizeToContent := true
        Widget := Btn1

    これは

    「画面中央基準で、少し左上にBtn1を置く」

    という意味です。

    最低限の見方

    • Anchors は基準位置
    • Offsets はそこからのズレ
    • Alignment はWidget自身の基準点

    この程度で読めれば十分です。

    20. このコードを読むおすすめ順序

    400行を最初から最後まで追うと重いので、以下の順序がおすすめです。

    1. @editable
    2. PlayerRoots などのMap
    3. OnBegin
    4. OnOpenTriggered
    5. OpenForPlayer
    6. AppendDigit
    7. DeleteLastDigit
    8. UpdateDisplay
    9. SubmitPin
    10. BuildRootWidget

    この順番だと、意味の流れがつかみやすいです。

    21. ソースコード全文

    以下が前回の記事で掲載したPIN入力パッドのVerseコード全文です。

    22. このコードのポイントを一言でまとめると

    この実装で一番重要なのは、

    UIそのものよりも、プレイヤーごとの状態をMapで分離していること

    です。

    ここができているからこそ、

    • 複数プレイヤーが同時に開いても壊れない
    • 入力が混ざらない
    • 表示更新先がズレない
    • UIの開閉が独立する

    という、実戦投入できるPINパッドになります。

    まとめ

    前回の記事では、VerseのみでPIN入力パッドを完成形として紹介しました。

    今回はその続編として、コード全体を初心者向けに分解して見ていきました。

    約400行あると長く見えますが、実際には

    • 設定
    • 状態管理
    • UI表示
    • 入力更新
    • 正誤判定
    • レイアウト

    に分かれているだけです。

    特に BuildRootWidget が長いのは、VerseだけでUIを全部組み立てているからで、

    複雑な演算が大量に入っているわけではありません。

    Verseだけでもここまで作れるようになると、

    • 金庫
    • 秘密扉
    • 謎解き
    • セキュリティ端末
    • 特殊な分岐ギミック

    など、かなりゲームらしい仕掛けが作れるようになります。

    前回の記事と合わせて読むと、完成コードと構造理解の両方が見えると思います。

    ぜひ自分の島に合わせて拡張してみてください。

    UEProg by milkc0de

    OpenForPlayer(P:player):void =
        CloseForPlayer(P)
    
        if (UI := GetPlayerUI[P]):
            if (set PlayerUIs[P] = UI):
                Root := BuildRootWidget(P)
    
                UI.AddWidget(
                    Root,
                    player_ui_slot{
                        ZOrder := 100,
                        InputMode := ui_input_mode.All
                    }
                )
    
                if (set PlayerRoots[P] = Root):
                if (set PlayerInputs[P] = ""):
                UpdateDisplay(P)
    Substring(S:string, Start:int, L:int):string =
        if ((Start >= 0) and (L >= 0) and (Start < S.Length)):
            var End : int = Start + L
            if (End > S.Length):
                set End = S.Length
    
            var Result : string = ""
            var I : int = Start
            loop:
                if (I >= End):
                    break
                if (C := S[I]):
                    set Result = "{Result}{C}"
                set I = I + 1
            return Result
        return ""
    SubmitPin(P:player):void =
        if (Current := PlayerInputs[P]):
            for (Index -> ExpectedPin : PinCodes):
                if (Current = ExpectedPin):
                    if (Agent := agent[P]):
                        if (SuccessTrigger := SuccessTriggers[Index]):
                            SuccessTrigger.Trigger(Agent)
                        else:
                            FailedTrigger.Trigger(Agent)
                    CloseForPlayer(P)
                    return
    
            if (Agent := agent[P]):
                FailedTrigger.Trigger(Agent)
            CloseForPlayer(P)
    Btn1 : button_loud = button_loud{ DefaultText := ToMessage(" 1 ") }
    Btn2 : button_loud = button_loud{ DefaultText := ToMessage(" 2 ") }
    Btn3 : button_loud = button_loud{ DefaultText := ToMessage(" 3 ") }
    Btn4 : button_loud = button_loud{ DefaultText := ToMessage(" 4 ") }
    Btn5 : button_loud = button_loud{ DefaultText := ToMessage(" 5 ") }
    Btn6 : button_loud = button_loud{ DefaultText := ToMessage(" 6 ") }
    Btn7 : button_loud = button_loud{ DefaultText := ToMessage(" 7 ") }
    Btn8 : button_loud = button_loud{ DefaultText := ToMessage(" 8 ") }
    Btn9 : button_loud = button_loud{ DefaultText := ToMessage(" 9 ") }
    Btn0 : button_loud = button_loud{ DefaultText := ToMessage(" 0 ") }
    
    BtnDelete : button_regular = button_regular{ DefaultText := ToMessage("DELETE") }
    BtnEnter : button_loud = button_loud{ DefaultText := ToMessage("ENTER") }
    BtnClose : button_quiet = button_quiet{ DefaultText := ToMessage(" X ") }
    Btn1.OnClick().Subscribe(OnDigit1)
    Btn2.OnClick().Subscribe(OnDigit2)
    Btn3.OnClick().Subscribe(OnDigit3)
    Btn4.OnClick().Subscribe(OnDigit4)
    Btn5.OnClick().Subscribe(OnDigit5)
    Btn6.OnClick().Subscribe(OnDigit6)
    Btn7.OnClick().Subscribe(OnDigit7)
    Btn8.OnClick().Subscribe(OnDigit8)
    Btn9.OnClick().Subscribe(OnDigit9)
    Btn0.OnClick().Subscribe(OnDigit0)
    
    BtnDelete.OnClick().Subscribe(OnDeletePressed)
    BtnEnter.OnClick().Subscribe(OnEnterPressed)
    BtnClose.OnClick().Subscribe(OnClosePressed)
    using { /Fortnite.com/Devices }
    using { /Fortnite.com/UI }
    using { /UnrealEngine.com/Temporary/UI }
    using { /UnrealEngine.com/Temporary/SpatialMath }
    using { /Verse.org/Colors }
    using { /Verse.org/Simulation }
    
    pinpad_device := class(creative_device):
    
        @editable
        OpenTrigger : trigger_device = trigger_device{}
    
        @editable
        FailedTrigger : trigger_device = trigger_device{}
    
        @editable
        PinCodes : []string = array{}
    
        @editable
        SuccessTriggers : []trigger_device = array{}
    
        @editable
        TitleText : string = "PIN PAD"
    
        @editable
        MaxDigits : int = 8
    
        var PlayerRoots : [player]widget = map{}
        var PlayerInputTexts : [player]text_block = map{}
        var PlayerInputs : [player]string = map{}
        var PlayerUIs : [player]player_ui = map{}
    
        ToMessage<localizes>(S:string) : message = "{S}"
    
        OnBegin<override>()<suspends>:void =
            OpenTrigger.TriggeredEvent.Subscribe(OnOpenTriggered)
    
        OnOpenTriggered(MaybeAgent:?agent):void =
            if (A := MaybeAgent?):
                if (P := player[A]):
                    OpenForPlayer(P)
    
        OpenForPlayer(P:player):void =
            CloseForPlayer(P)
    
            if (UI := GetPlayerUI[P]):
                if (set PlayerUIs[P] = UI):
                    Root := BuildRootWidget(P)
    
                    UI.AddWidget(
                        Root,
                        player_ui_slot{
                            ZOrder := 100,
                            InputMode := ui_input_mode.All
                        }
                    )
    
                    if (set PlayerRoots[P] = Root):
                    if (set PlayerInputs[P] = ""):
                    UpdateDisplay(P)
    
        CloseForPlayer(P:player):void =
            if (UI := PlayerUIs[P]):
                if (Root := PlayerRoots[P]):
                    UI.RemoveWidget(Root)
    
        UpdateDisplay(P:player):void =
            if (TB := PlayerInputTexts[P]):
                if (Current := PlayerInputs[P]):
                    if (Current.Length > 0):
                        TB.SetText(ToMessage(Current))
                    else:
                        TB.SetText(ToMessage(""))
    
        AppendDigit(P:player, Digit:string):void =
            if (Current := PlayerInputs[P]):
                if (Current.Length >= MaxDigits):
                    return
    
                NewValue : string = "{Current}{Digit}"
                if (set PlayerInputs[P] = NewValue):
                UpdateDisplay(P)
    
        DeleteLastDigit(P:player):void =
            if (Current := PlayerInputs[P]):
                if (Current.Length <= 0):
                    return
    
                NewValue : string = Substring(Current, 0, Current.Length - 1)
                if (set PlayerInputs[P] = NewValue):
                UpdateDisplay(P)
    
        Substring(S:string, Start:int, L:int):string =
            if ((Start >= 0) and (L >= 0) and (Start < S.Length)):
                var End : int = Start + L
                if (End > S.Length):
                    set End = S.Length
    
                var Result : string = ""
                var I : int = Start
                loop:
                    if (I >= End):
                        break
                    if (C := S[I]):
                        set Result = "{Result}{C}"
                    set I = I + 1
                return Result
            return ""
    
        SubmitPin(P:player):void =
            if (Current := PlayerInputs[P]):
                for (Index -> ExpectedPin : PinCodes):
                    if (Current = ExpectedPin):
                        if (Agent := agent[P]):
                            if (SuccessTrigger := SuccessTriggers[Index]):
                                SuccessTrigger.Trigger(Agent)
                            else:
                                FailedTrigger.Trigger(Agent)
                        CloseForPlayer(P)
                        return
    
                if (Agent := agent[P]):
                    FailedTrigger.Trigger(Agent)
                CloseForPlayer(P)
    
        BuildRootWidget(P:player):canvas =
            Dimmer : color_block = color_block{
                DefaultColor := color{R := 0.0, G := 0.0, B := 0.0},
                DefaultOpacity := 0.60,
                DefaultDesiredSize := vector2{X := 1920.0, Y := 1080.0}
            }
    
            Panel : color_block = color_block{
                DefaultOpacity := 0.98,
                DefaultDesiredSize := vector2{X := 560.0, Y := 800.0},
                DefaultColor := color{R := 0.0, G := 0.0, B := 1.0}
            }
    
            Title : text_block = text_block{
                DefaultText := ToMessage(TitleText),
                DefaultTextColor := color{R := 1.0, G := 1.0, B := 1.0},
                DefaultTextSize := 36.0,
                DefaultJustification := text_justification.Center
            }
    
            DisplayBG : color_block = color_block{
                DefaultColor := color{R := 0.0, G := 0.0, B := 0.0},
                DefaultOpacity := 1.0,
                DefaultDesiredSize := vector2{X := 420.0, Y := 74.0}
            }
    
            DisplayText : text_block = text_block{
                DefaultText := ToMessage(""),
                DefaultTextColor := color{R := 1.0, G := 1.0, B := 1.0},
                DefaultTextSize := 34.0,
                DefaultJustification := text_justification.Center
            }
            if (set PlayerInputTexts[P] = DisplayText):
    
            ButtonWidth : float = 120.0
            ButtonHeight : float = 90.0
            SmallButtonWidth : float = 140.0
            SmallButtonHeight : float = 90.0
    
            Btn1 : button_loud = button_loud{ DefaultText := ToMessage(" 1 ") }
            Btn2 : button_loud = button_loud{ DefaultText := ToMessage(" 2 ") }
            Btn3 : button_loud = button_loud{ DefaultText := ToMessage(" 3 ") }
            Btn4 : button_loud = button_loud{ DefaultText := ToMessage(" 4 ") }
            Btn5 : button_loud = button_loud{ DefaultText := ToMessage(" 5 ") }
            Btn6 : button_loud = button_loud{ DefaultText := ToMessage(" 6 ") }
            Btn7 : button_loud = button_loud{ DefaultText := ToMessage(" 7 ") }
            Btn8 : button_loud = button_loud{ DefaultText := ToMessage(" 8 ") }
            Btn9 : button_loud = button_loud{ DefaultText := ToMessage(" 9 ") }
            Btn0 : button_loud = button_loud{ DefaultText := ToMessage(" 0 ") }
    
            BtnDelete : button_regular = button_regular{ DefaultText := ToMessage("DELETE") }
            BtnEnter : button_loud = button_loud{ DefaultText := ToMessage("ENTER") }
            BtnClose : button_quiet = button_quiet{ DefaultText := ToMessage(" X ") }
    
            Btn1.OnClick().Subscribe(OnDigit1)
            Btn2.OnClick().Subscribe(OnDigit2)
            Btn3.OnClick().Subscribe(OnDigit3)
            Btn4.OnClick().Subscribe(OnDigit4)
            Btn5.OnClick().Subscribe(OnDigit5)
            Btn6.OnClick().Subscribe(OnDigit6)
            Btn7.OnClick().Subscribe(OnDigit7)
            Btn8.OnClick().Subscribe(OnDigit8)
            Btn9.OnClick().Subscribe(OnDigit9)
            Btn0.OnClick().Subscribe(OnDigit0)
    
            BtnDelete.OnClick().Subscribe(OnDeletePressed)
            BtnEnter.OnClick().Subscribe(OnEnterPressed)
            BtnClose.OnClick().Subscribe(OnClosePressed)
    
            Root : canvas = canvas:
                Slots := array:
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.0, Y := 0.0}, Maximum := vector2{X := 0.0, Y := 0.0}}
                        Offsets := margin{Left := 0.0, Top := 0.0, Right := 1920.0, Bottom := 1080.0}
                        Alignment := vector2{X := 0.0, Y := 0.0}
                        SizeToContent := false
                        Widget := Dimmer
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := 0.0, Right := 560.0, Bottom := 800.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := false
                        Widget := Panel
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := -320.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Title
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 212.0, Top := -325.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := BtnClose
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := -230.0, Right := 420.0, Bottom := 74.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := false
                        Widget := DisplayBG
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := -230.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := DisplayText
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := -130.0, Top := -110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn1
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := -110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn2
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 130.0, Top := -110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn3
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := -130.0, Top := 0.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn4
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := 0.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn5
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 130.0, Top := 0.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn6
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := -130.0, Top := 110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn7
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := 110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn8
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 130.0, Top := 110.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn9
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 0.0, Top := 220.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := Btn0
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := -130.0, Top := 330.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := BtnDelete
    
                    canvas_slot:
                        Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                        Offsets := margin{Left := 130.0, Top := 330.0, Right := 0.0, Bottom := 0.0}
                        Alignment := vector2{X := 0.5, Y := 0.5}
                        SizeToContent := true
                        Widget := BtnEnter
    
            Root
    
        OnDigit1(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "1")
    
        OnDigit2(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "2")
    
        OnDigit3(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "3")
    
        OnDigit4(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "4")
    
        OnDigit5(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "5")
    
        OnDigit6(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "6")
    
        OnDigit7(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "7")
    
        OnDigit8(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "8")
    
        OnDigit9(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "9")
    
        OnDigit0(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                AppendDigit(P, "0")
    
        OnDeletePressed(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                DeleteLastDigit(P)
    
        OnEnterPressed(Msg:widget_message):void =
            if (P := player[Msg.Player]):
                SubmitPin(P)
    
        OnClosePressed(Msg:widget_message):void =
            if (P := player[Msg.Player]):
    	            CloseForPlayer(P)