Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 テキサスホールデムポーカー【第2回】
2022.08.03
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 テキサスホールデムポーカー【第2回】

目次

  • 移動・フリップ可能カード

■移動・フリップ可能カード

カードは、プリフロップにて中央デッキから各プレイヤーに配布(移動)され、人間プレイヤーのカードのみオープンされる (これは人間が手札を認識するためで、他のAIプレイヤーはその情報を参照したりはしない ;^p)。 さらに、ショーダウンでは、降りていないプレイヤー全員の手札がオープンされる。
CardBF はそのためのシーンで、カードのランク(数字)・スート(マーク)が表示が可能で、上記の移動・オープン機能を持つ。
ノードツリーは下図のような構成になっている。ルートは Node2D で、表面と裏面用のノードを子ノードとして所有している。

A, 2, 3, … K のランクは Label で、アライアルボールドフォントの文字を表示している。

スートは Sprite で、すべてのスートを横に並べた画像(下図)をアニメーションで切り替える。

また、カード裏画像も保持し、カードをめくるアニメーションででは表・裏を切り替える。

CardBF はランク・スート情報を持ち、それらゲッター・セッター関数を持つ(下記コード参照)。

const NumTable = "234567890JQKA"
var sr = 0      # (suit << 4) | rank
var rank = 0
func get_sr():      # スート、ランク取得
    return sr
func set_suit(st):      # スート、ランク設定
    $Front/Suit.set_frame(st)
func get_rank(): return rank
func set_rank(r):       # ランク設定、表示更新
    rank = r
    if rank == RANK_10:
        $Front/Label.text = "10"
    else:
        $Front/Label.text = NumTable[rank]
func set_sr(st, rank):  # スート・ランク設定
    sr = (st << N_RANK_BITS) | rank
    set_suit(st)
    set_rank(rank)

CardBF は移動機能も持つ。実装は下記の通りで、Chip とほぼ同じだ。

signal moving_finished
var moving = false
var waiting_time = 0.0          # ウェイト時間(単位:秒)
func wait_move_to(wait : float, dst : Vector2, dur : float):
    waiting_time = wait
    move_to(dst, dur)
func move_to(dst : Vector2, dur : float):
    src_pos = get_position()
    dst_pos = dst
    move_dur = dur
    move_elapsed = 0.0
    moving = true
func _process(delta):
    if waiting_time > 0.0:
        waiting_time -= delta
        return
    if moving:      # 移動処理中
        move_elapsed += delta   # 経過時間
        move_elapsed = min(move_elapsed, move_dur)  # 行き過ぎ防止
        var r = move_elapsed / move_dur             # 位置割合
        set_position(src_pos * (1.0 - r) + dst_pos * r)     # 位置更新
        if move_elapsed == move_dur:        # 移動終了の場合
            moving = false
            emit_signal("moving_finished")  # 移動終了シグナル発行
    ....

Chip と異なるのは、移動開始までのウェイト時間を指定できる wait_move_to() 関数があることだ。 これはデッキから各プレイヤーにカードを配布するとき、全枚数を同時に移動させるのではなく、 ディーラの次から順番に配布しているように見せるためだ。

次に、カードめくり関連の実装を下記に示す。

signal opening_finished
enum {      # state
    STATE_NONE = 0,
    OPENING_FH,         # オープン中 前半
    OPENING_SH,         # オープン中 後半
    CLOSING_FH,         # オープン中 前半
    CLOSING_SH,         # オープン中 後半
}
var bFront = false              # 表示面
var state : int = 0
var theta = 0.0
func do_open():
    state = OPENING_FH
    theta = 0.0
    $Front.hide()
    $Back.show()
    $Back.set_scale(Vector2(1.0, 1.0))
func do_close():
    state = CLOSING_FH
    theta = 0.0
    $Back.hide()
    $Front.show()
    $Front.set_scale(Vector2(1.0, 1.0))
func _process(delta):
    .....
    if state == OPENING_FH:         # カードめくり前半
        theta += delta * TH_SCALE       # 角度更新
        if theta < PI/2:                # まだ裏面の場合
            $Back.set_scale(Vector2(cos(theta), 1.0))   # 角度を考慮した裏面表示
        else:                           # 半分を超えた場合
            state = OPENING_SH          # 状態遷移
            $Front.show()               # 表面表示
            $Back.hide()                # 裏面非表示
            theta -= PI                 # 角度補正
            $Front.set_scale(Vector2(cos(theta), 1.0))  # 角度を考慮した表面表示
    elif state == OPENING_SH:       # カードめくり後半
        theta += delta * TH_SCALE
        theta = min(theta, 0)
        if theta < 0:
            $Front.set_scale(Vector2(cos(theta), 1.0))  # 角度を考慮した表面表示
        else:
            state = STATE_NONE          # カードめくり終了
            $Front.set_scale(Vector2(1.0, 1.0))
            emit_signal("opening_finished")     # オープニング終了シグナル発行
    elif state == CLOSING_FH:
        .....
    elif state == CLOSING_SH:
        .....

オープン開始時は裏面を表示し、theta を 0.0 に設定する。経過時間に従い、横方向スケールを cos(theta) とする。 半分を経過したら、theta からπを引き、後半は表面を表示し、前半同様に横方向スケールを cos(theta) とする。
オープン処理が終了したら opening_finished シグナルを発行する。

下記に、次のラウンドに遷移する関数である next_round() で、初期状態からプリフロップに遷移する処理部分のコードを示す。

var deck_ix = 0         # デッキトップインデックス
var deck = []           # 要素:(suit << 4) | rank (※ rank:0~12 の数値、0 for 2,... 11 for King, 12 for Ace)
var CardBF = load("res://CardBF.tscn")      # カード裏表面
func next_round():          # 次のラウンドに遷移
    if state == INIT:       # 初期状態の場合
        state = PRE_FLOP    # プリフロップに遷移
        .....
        players_card1.resize(N_PLAYERS)
        for i in range(N_PLAYERS):
            var di = (dealer_ix + 1 + i) % N_PLAYERS        # カード配布先インデックス
            var cd = CardBF.instance()      # カード裏面
            players_card1[di] = cd
            # カードは deck 配列にあり、すでにシャフルされているものとする
            cd.set_sr(card_to_suit(deck[deck_ix]), card_to_rank(deck[deck_ix]))
            deck_ix += 1    # 次のカード
            cd.set_position(deck_pos)
            $Table.add_child(cd)        # テーブルの子ノードとして追加
            cd.connect("moving_finished", self, "on_moving_finished")       # シグナルを処理関数に接続
            cd.connect("opening_finished", self, "on_opening_finished")        # シグナルを処理関数に接続
            var dst = players[di].get_position() + Vector2(-CARD_WIDTH/2, -4)   # 移動先
            cd.wait_move_to(i * 0.1, dst, 0.3)      # 時間差でカードを移動

プリフロップでは、中央のデッキから各プレイヤーに手札を配布する。
CardBF.instance() でカードノードをインスタンス化し、deck 配列を参照し、スート・ランクを設定する。 カードの初期位置は中央デッキ位置とし、wait_move_to() で各プレイヤー背景位置まで移動する。
移動が終わると moving_finished シグナルが発行されるので、それを処理するために、self.on_opening_finished() に接続する。

これで、移動が終わると on_moving_finished() がコールされるので、次に人間の手札についてのみ do_open() をコールして、 手札をめくって表にする。

TechProjin Godot入門 関連連載リンク

Godotで学ぶゲーム制作
さくさく理解するGodot入門 連載目次

標準C++ライブラリの活用でコーディング力UP!
「競技プログラミング風」標準C++ライブラリ 連載目次