ゲーム開発入門③ タイトル・プレイ・ゲームオーバーを切り替えよう
タイトル・プレイ・ゲームオーバーを、実行できる小さなPythonプログラムで切り替えます。状態遷移、初期化、ifとelifの違い、一時停止の練習と解答を解説します。
タイトル画面を出せた。プレイヤーも動く。ところが、ゲームオーバーの文字を出しても敵が動き続ける……。画面を描くだけでは、裏の処理は止まらないんですね。
今回は「今は何をしている時間なのか」を1つの値で決め、必要な処理だけを動かします。画像は使わず、Pythonの文字入力で状態の切り替えを確かめるので、画面制作の前に仕組みへ集中できます。
状態を名前で表す
まず、ゲームの場面を3つに分けます。
| 状態 | 受け付けること | 動かさないこと |
|---|---|---|
TITLE |
ゲーム開始 | 得点やダメージの更新 |
PLAYING |
コイン取得、ダメージ | 開始前の案内 |
GAME_OVER |
再挑戦 | プレイ中の得点更新 |
状態は、その瞬間の状況を表すデータです。ここでは場面の名前を文字列で持ちます。文字列とは "TITLE" のように引用符で囲んだ文字のデータです。
scene = "TITLE"
scene という変数に、現在の場面を入れています。変数は値を名前で扱う仕組みです。書き方がまだ難しければ、Pythonの変数から確認してください。
ゲームエンジンのシーンは、オブジェクトや配置も含むまとまりを指すことがあります。この記事の scene は、読みやすさのために付けた変数名で、エンジンのシーン機能を直接操作するものではありません。
操作をきっかけに切り替える
「どこから、何をきっかけに、どこへ移るか」を先に表へまとめます。
| 今の状態 | きっかけ | 次の状態 |
|---|---|---|
TITLE |
開始を選ぶ | PLAYING |
PLAYING |
体力が0になる | GAME_OVER |
GAME_OVER |
再挑戦を選ぶ | PLAYING |
状態が移ることを状態遷移(じょうたいせんい)と呼びます。状態と切り替えの規則をまとめた仕組みは、ステートマシンと呼ばれます。
名前は難しそうですが、今回は「3つの場面と、行き先のルール」です。全部を同時に動かすのではなく、今の状態に合う処理だけ選びます。
if文で処理を分ける
if文は、条件に合う場合だけ処理を行う書き方です。elif は「最初が違ったら、次の条件を調べる」という意味です。
次は構造を読むための例です。update_title などは、別途作る関数の名前なので、この断片だけでは実行しません。
if scene == "TITLE":
update_title()
elif scene == "PLAYING":
update_game()
elif scene == "GAME_OVER":
update_game_over()
== は「等しいか」を比較する記号です。= は代入なので、役割が違います。今回は scene が "TITLE" なら最初の処理だけが動き、後ろの2つは動きません。
ここで「すべて別々の if にしてもいい?」という疑問が出ます。処理の途中で scene を変える場合は、結果が変わることがあります。完成例を見た後で、この違いを確かめます。
小さな完成形を動かしてみよう
次を scene_game.py として保存し、Python 3で実行します。実行方法はPythonの最初のプログラムを参考にしてください。
これは画像付きのリアルタイムゲームではなく、文字で操作する状態管理の練習です。s で開始、c で得点、h でダメージ、r で再挑戦、q で終了します。
def start_game():
return "PLAYING", 0, 3
scene = "TITLE"
score = 0
hp = 3
while True:
print(f"状態: {scene} / 得点: {score} / 体力: {hp}")
command = input("s:開始 c:コイン h:ダメージ r:再挑戦 q:終了 > ").strip().lower()
if command == "q":
break
if scene == "TITLE":
if command == "s":
scene, score, hp = start_game()
else:
print("まずはsで始めてみましょう。")
elif scene == "PLAYING":
if command == "c":
score += 10
elif command == "h":
hp -= 1
if hp <= 0:
scene = "GAME_OVER"
else:
print("プレイ中はcかhを選べます。")
elif scene == "GAME_OVER":
if command == "r":
scene, score, hp = start_game()
else:
print("ゲーム終了です。rで再挑戦できます。")
print("終了しました。")
1. 入力を1つだけ受け取る
input は入力してEnterを押すまで待ち、文字列を返します。strip() は前後の空白を取り除き、lower() は英字を小文字へそろえます。S と入力しても s として読めるようにしています。
while True は繰り返しです。q が入力されたときに break で抜けます。今回は毎回入力を待つため、待たずに高速で回り続けるループではありません。
2. 現在の状態から処理を選ぶ
TITLE では s だけで開始できます。c を押しても得点は増えません。PLAYING にいるときだけ、コイン取得やダメージの処理へ進みます。
h で体力を1減らし、減らした後の値が0以下なら GAME_OVER に移ります。判定を減らす前に書くと、体力0になった回で終了せず、次の入力まで待つようなずれが起きます。
3. 変化を順番に追う
次の操作を順に入力した場合の、各操作後の状態です。
| 入力 | 状態 | 得点 | 体力 |
|---|---|---|---|
| 初期状態 | TITLE | 0 | 3 |
s |
PLAYING | 0 | 3 |
c |
PLAYING | 10 | 3 |
h |
PLAYING | 10 | 2 |
h |
PLAYING | 10 | 1 |
h |
GAME_OVER | 10 | 0 |
c |
GAME_OVER | 10 | 0 |
r |
PLAYING | 0 | 3 |
ゲームオーバー後の c では得点が増えません。状態によって同じ入力の扱いが変わっていることを確かめてください。
切り替え時に初期化する
初期化は、開始時の値へ整えることです。今回の start_game は、"PLAYING"、得点0、体力3をまとめて返しています。
scene, score, hp = start_game()
この行は3つの戻り値を順番に3変数へ受け取ります。global を使って関数の外を直接変更せず、返した値を受け取る形にしています。
もし scene = "PLAYING" だけで再挑戦すると、体力が0のままです。「リトライした瞬間にまたゲームオーバー」という不具合は、こうした初期化漏れが原因になりえます。
作品が大きくなったら、位置・敵・残り時間・取得済みコインなども一覧にします。一方、最高得点や設定音量は残したいかもしれません。毎回戻すものと、続けて残すものを分けるのがコツです。
別々のifとelifの違い
次の短い例は、予想してから読むと分かりやすいです。
scene = "TITLE"
if scene == "TITLE":
scene = "PLAYING"
if scene == "PLAYING":
print("同じ回でプレイ処理も動いた")
1つ目で scene が変わり、2つ目の if は変更後の値を調べます。そのため表示されます。
一方、2つ目を elif にすると、最初の条件が成立した回では後ろを試しません。この違いは、一度のボタン操作で2段階進んでしまう不具合を調べるときに役立ちます。
入力にも注意が必要です。キーの「押しっぱなし」を毎回「押した」と扱うと、一時停止と再開を交互に繰り返すことがあります。リアルタイムのゲームでは、押された瞬間だけを受け取る入力機能を使います。
練習:一時停止を追加する
プレイ中に p を入力すると PAUSE、もう一度 p で PLAYING へ戻るようにしてください。一時停止中は c や h で得点・体力を変えないものとします。
模範解答
完成例の elif scene == "PLAYING": の中にある入力の分岐を、次へ置き換えます。
elif scene == "PLAYING":
if command == "p":
scene = "PAUSE"
elif command == "c":
score += 10
elif command == "h":
hp -= 1
if hp <= 0:
scene = "GAME_OVER"
else:
print("c:コイン h:ダメージ p:一時停止")
elif scene == "PAUSE":
if command == "p":
scene = "PLAYING"
else:
print("一時停止中です。pで再開できます。")
GAME_OVER の分岐は、この後ろへそのまま残します。入力の案内文にも p:一時停止/再開 を追加すると、操作が分かりやすくなります。
解説
PAUSE は PLAYING と同じ階層の elif です。その中では得点も体力も変えないため、c や h を押しても止まった状態を保てます。
p で再開するときに start_game() を呼んでいない点も重要です。再開は途中から続けたいので、得点や体力はそのまま。再挑戦は最初からなので初期化します。
小さな完成形を目指そう
最初はタイトル・プレイ・ゲームオーバーの3つで十分です。開始でき、終わりがあり、もう一度遊べる。それだけで作品のまとまりが生まれます。
文字入力で確認した切り替えをゲームループへ組み込み、当たり判定をコインやダメージのきっかけにすれば、画像のあるゲームにも考え方を広げられます。