さくさく理解する 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++ライブラリ 連載目次