「同じものがいくつもある」をリストとクラスで扱う
お菓子を 5 個取るごとにお化けが 1 体増え、最大 4 体になるようにします。 step1 最後の山場です。
むずかしいのは処理そのものではありません。 「同じものがいくつもある」状態をどう持つか——ここが今回のテーマです。 そしてこれは、step2 の倉庫番でも step3 のシューティングでも、 そのまま使い回せる考え方になります。
いま、お化けは 2 つの変数で持っています。
self.ghost_x = 0
self.ghost_y = 0
素直に考えると「4 体なら 8 個の変数」です。書けなくはありません。
# こうはしたくない
self.ghost_x1 = 0
self.ghost_y1 = 0
self.ghost_x2 = 0
self.ghost_y2 = 0
...
しかし、これでは「すべてのお化けを動かす」が書けません。 1 体ずつ、同じ処理を 4 回コピーすることになります。
# 動かす処理が 4 つ、当たり判定も 4 つ、描画も 4 つ…
if self.ghost_x1 < self.boy_x:
self.ghost_x1 += 1
...
if self.ghost_x2 < self.boy_x:
self.ghost_x2 += 1
...
リストの本当のありがたみは、「数を気にしなくてよくなる」ことです。
for ghost in self.ghosts:
ghost.move(self.boy_x, self.boy_y)
これは1 体でも 4 体でも、そのまま動きます。 「4 体になったら書き足す」必要がありません。 数が変わっても書き方が変わらない——これがリストを使う理由です。
では、リストの中身は何にすればよいでしょうか。 「x のリスト」と「y のリスト」の 2 本にする手もありますが、これはおすすめしません。
# 2 本のリストで持つ(おすすめしない)
self.ghost_xs = [0, 152, 0]
self.ghost_ys = [0, 0, 112]
2 本の対応がずれたら壊れるからです。1 体増やすときは必ず両方に足す、 消すときは必ず両方から消す——という約束を、人間が守り続けることになります。
そこで「お化け 1 体」を 1 つのものとして持ちます。
そのための道具が class です。
class は「同じ形のものを、いくつも作る」ための型
class App はすでに書いています。ただ App は 1 つしか作らなかったので、
class の本当のありがたみが見えていませんでした。
class Ghost:
"""お化け 1 体ぶんの居場所と動き"""
def __init__(self, x, y, move_every):
"""1 体作るときに、置く場所と動く間隔を決める"""
self.x = x
self.y = y
self.move_every = move_every
これは設計図です。この設計図から Ghost(0, 0, 1) と書けば、
実物が 1 体でき上がります。何体でも作れて、それぞれが自分の
x・y・move_every を持ちます。
self の意味が、ここではっきりします
self.x は「この 1 体の x」という意味です。
4 体いれば self.x は 4 つあり、それぞれ別の値です。
App が 1 つしかなかったときは
「self = このアプリ」でぼんやりしていましたが、
同じ設計図から複数の実物ができると、self が
「いま扱っている 1 個」を指していることがはっきり分かります。
App ではなく Ghost に move() として持たせると、
App 側は ghost.move(...) と頼むだけになります。
どう動くかは Ghost が知っていればよい、という分け方です。
お化けは全員同じ相手(男の子)を、同じやり方で追いかけます。 ということは、速さも同じなら、いずれ位置まで同じになります。 横のずれも縦のずれも、毎フレーム同じだけ縮まっていくからです。
実際に 600 フレーム(20 秒)動かして数えました。
| 速さの設定 | 20 秒後、4 体が何か所に分かれているか |
|---|---|
| 全部おなじ | 1 か所(完全に重なって 1 体に見える) |
| 2 体だけ同じ | 3 か所 |
| 4 体とも違う | 4 か所 |
つまり速さを散らすのは、見た目のためだけではありません。 散らさないと、4 体に増やした意味がなくなります。
速さを散らすとき、素直に思いつくのは 0.5 や 0.75 のような小数です。
ですがこれには落とし穴があります。
男の子が x = 76 で止まっているとき(実測)
速さ 0.5 → 76.0, 76.0, 76.0 … ぴったり止まる
速さ 0.75 → 75.75, 76.5, 75.75, 76.5 … ★ 永久に往復する
0.75 は 0.5 + 0.25 なので2 進数では正確に表せます。
それでも往復するのは、お化けが立てる座標に 76 が含まれていないからです
(0.75 の倍数で整数になるのは 3 の倍数だけ)。
くわしくは備忘録 #chase-speed にまとめました。
そこで小数を使わない方法を採ります。進む量は 1 ドットのまま、 動くフレームを間引くのです。
def move(self, target_x, target_y):
if pyxel.frame_count % self.move_every != 0:
return # 今回は自分の番ではない
...ここから下は 1 ドットずつ動かすだけ...
move_every を変えると、動く量ではなく動く回数が変わる1/3 のような、小数では危ない速さが安全に使えます。
増えたお化けが男の子のとなりに出てきたら理不尽です。 4 つの角のうち、そのとき男の子からいちばん遠い角を選びます。
def far_corner(x, y):
"""(x, y) からいちばん遠い角を返す"""
best_x, best_y = CORNERS[0]
best_distance = -1
for corner_x, corner_y in CORNERS:
distance = abs(corner_x - x) + abs(corner_y - y)
if distance > best_distance:
best_x, best_y = corner_x, corner_y
best_distance = distance
return best_x, best_y
「いちばん◯◯なものを 1 つ選ぶ」の型です。
「これまでで最高の記録」を best_distance に持ち、
それを超えたら更新する。この形はこの先も何度も出てきます。
abs() で「距離」を測っています
abs(a - b) は差の絶対値——マイナスを取り払った、へだたりです。
横のへだたりと縦のへだたりを足したものを距離として使っています。
ほんとうの直線距離ではありませんが、どの角がいちばん遠いかを比べるだけなら
これで十分で、ルートの計算もいりません。
| 機能 | 役割 |
|---|---|
class Ghost: | お化け 1 体ぶんの設計図。2 つめのクラス |
リスト [] と .append() | お化けを何体でも持つ/1 体足す |
for ghost in self.ghosts: | いる体数ぶんだけ、同じ処理をくり返す |
len() | いま何体いるかを数える |
%(あまり) | 「5 個ごと」「n フレームに 1 回」を作る |
abs() | 2 点のへだたりを測る |
| タプルのリスト | 4 隅の座標をまとめて持つ |
書きかえる場所が多いので、上から順に進めます。
GHOST_SPEED は使わなくなります。2 行に入れかえます。
GHOST_SIZE = 8 # お化けの絵の大きさ
GHOST_MOVE_EVERY = [1, 2, 3, 4] # 出てくる順に「何フレームに 1 回動くか」
GHOST_SPAWN_SCORE = 5 # お菓子を何個取るごとに 1 体増やすか
GHOST_MOVE_EVERY には 4 つ入っているので、お化けは最大 4 体です。
GHOST_MAX = 4 のような定数はいりません。
len(GHOST_MOVE_EVERY) で数えられるからです。
同じことを 2 か所に書くと、片方だけ直して食い違います。
BLINK_ON の下に足します。
CORNERS = [ # お化けが出てくる 4 隅
(0, 0),
(SCREEN_WIDTH - GHOST_SIZE, 0),
(0, SCREEN_HEIGHT - GHOST_SIZE),
(SCREEN_WIDTH - GHOST_SIZE, SCREEN_HEIGHT - GHOST_SIZE),
]
(0, 0) のような丸カッコの組を「タプル」と言います
リストとほとんど同じですが、あとから中身を変えられません。
「セットで 1 つ」という意味あいのものに向いています。座標はまさにそれです。x, y = (10, 20) のように2 つの変数へ一度に取り出せるのも、
座標を扱うときに便利なところです(フェーズ 5 の「もう一歩」でも使いました)。
far_corner() を足す
is_blink_on() の下、class の外に書きます。
self を使わないので外に置けます(フェーズ 6 と同じ基準)。
def far_corner(x, y):
"""(x, y) からいちばん遠い角を返す"""
best_x, best_y = CORNERS[0]
best_distance = -1
for corner_x, corner_y in CORNERS:
distance = abs(corner_x - x) + abs(corner_y - y)
if distance > best_distance:
best_x, best_y = corner_x, corner_y
best_distance = distance
return best_x, best_y
Ghost クラスを書く
class App: のすぐ上に書きます。
class Ghost:
"""お化け 1 体ぶんの居場所と動き"""
def __init__(self, x, y, move_every):
"""1 体作るときに、置く場所と動く間隔を決める"""
self.x = x
self.y = y
self.move_every = move_every
def move(self, target_x, target_y):
"""自分の番のフレームだけ、目標へ 1 ドット近づく"""
if pyxel.frame_count % self.move_every != 0:
return
if self.x < target_x:
self.x += 1
elif self.x > target_x:
self.x -= 1
if self.y < target_y:
self.y += 1
elif self.y > target_y:
self.y -= 1
中身はいまの move_ghost() とほとんど同じです。
self.ghost_x が self.x に、
GHOST_SPEED が 1 に変わり、
先頭に「自分の番か」の判定が付いただけです。
reset_game() を書きかえ、add_ghost() を足す def reset_game(self):
"""ゲームの中身を、最初の状態に戻す"""
self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2
self.score = 0
self.ghosts = [] # ← 空のリストから始める
self.add_ghost() # ← 1 体目を置く
self.place_candy()
def add_ghost(self):
"""男の子からいちばん遠い角に、お化けを 1 体足す"""
move_every = GHOST_MOVE_EVERY[len(self.ghosts)]
x, y = far_corner(self.boy_x, self.boy_y)
self.ghosts.append(Ghost(x, y, move_every))
self.ghosts = [] を先に書きます
add_ghost() は self.ghosts の長さを数えてから足すので、
空のリストが先にできていないと動きません。[] で作り直すのも大事です。
.clear() でも同じですが、[] のほうが
「まっさらから始める」という意味が読み取りやすくなります。
for にする
update_play() の呼び出し名を、複数形に変えます。
def update_play(self):
"""あそんでいる間の更新"""
self.move_boy()
self.move_ghosts() # ← s が付く
self.check_candy()
self.check_ghosts() # ← s が付く
move_ghost() はまるごとこの 4 行に置きかえます。
中身は Ghost へ引っ越したので、ここは頼むだけです。
def move_ghosts(self):
"""すべてのお化けを、男の子に近づける"""
for ghost in self.ghosts:
ghost.move(self.boy_x, self.boy_y)
check_ghost() も置きかえます。
def check_ghosts(self):
"""どれか 1 体にでも捕まったら、ゲームオーバーにする"""
for ghost in self.ghosts:
if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
ghost.x + HIT_OFFSET, ghost.y + HIT_OFFSET, HIT_SIZE):
self.scene = SCENE_GAMEOVER
self.gameover_frame = pyxel.frame_count
return
check_candy() の中、お菓子を置きなおしたあとに足します。
def check_candy(self):
"""お菓子にしっかり重なったら、スコアを増やして次の場所に置きなおす"""
if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
self.candy_x + HIT_OFFSET, self.candy_y + HIT_OFFSET, HIT_SIZE):
self.score += 1
self.place_candy()
if (self.score % GHOST_SPAWN_SCORE == 0
and len(self.ghosts) < len(GHOST_MOVE_EVERY)):
self.add_ghost()
for ghost in self.ghosts: # お化けは何体でも
pyxel.blt(ghost.x, ghost.y, 0, 8, 0,
GHOST_SIZE, GHOST_SIZE, pyxel.COLOR_BLACK)
# candy_hunt.py
# step1 フェーズ8:お菓子を集めるほどお化けを増やす
import random
import pyxel
SCREEN_WIDTH = 160
SCREEN_HEIGHT = 120
BOY_SIZE = 8 # 男の子の絵の大きさ(縦横とも 8 ドット)
BOY_SPEED = 2 # 1 フレームで進むドット数
CANDY_SIZE = 8 # お菓子の絵の大きさ
GHOST_SIZE = 8 # お化けの絵の大きさ
GHOST_MOVE_EVERY = [1, 2, 3, 4] # 出てくる順に「何フレームに 1 回動くか」
GHOST_SPAWN_SCORE = 5 # お菓子を何個取るごとに 1 体増やすか
HIT_SIZE = 4 # 当たり判定に使う四角の大きさ(絵の中央だけを見る)
HIT_OFFSET = (BOY_SIZE - HIT_SIZE) // 2 # 絵の左上から、判定の四角までの距離(= 2)
SCENE_PLAY = 0 # あそんでいる画面
SCENE_GAMEOVER = 1 # ゲームオーバーの画面
SCENE_TITLE = 2 # タイトル画面
GAMEOVER_WAIT = 15 # 捕まってから文字を出すまでのフレーム数(0.5秒)
BLINK_CYCLE = 30 # 点滅 1 周期のフレーム数(1秒)
BLINK_ON = 20 # そのうち文字が見えているフレーム数
CORNERS = [ # お化けが出てくる 4 隅
(0, 0),
(SCREEN_WIDTH - GHOST_SIZE, 0),
(0, SCREEN_HEIGHT - GHOST_SIZE),
(SCREEN_WIDTH - GHOST_SIZE, SCREEN_HEIGHT - GHOST_SIZE),
]
def is_hit(x1, y1, size1, x2, y2, size2):
"""2 つの四角が重なっていれば True を返す"""
return (x1 < x2 + size2 and x2 < x1 + size1 and
y1 < y2 + size2 and y2 < y1 + size1)
def draw_center(s, y, col):
"""文字列を画面の横中央に描く"""
x = (SCREEN_WIDTH - len(s) * 4) // 2 # 4 は1文字分のフォント幅
pyxel.text(x, y, s, col)
def is_blink_on():
"""点滅の「見えている」タイミングなら True を返す"""
return pyxel.frame_count % BLINK_CYCLE < BLINK_ON
def far_corner(x, y):
"""(x, y) からいちばん遠い角を返す"""
best_x, best_y = CORNERS[0]
best_distance = -1
for corner_x, corner_y in CORNERS:
distance = abs(corner_x - x) + abs(corner_y - y)
if distance > best_distance:
best_x, best_y = corner_x, corner_y
best_distance = distance
return best_x, best_y
class Ghost:
"""お化け 1 体ぶんの居場所と動き"""
def __init__(self, x, y, move_every):
"""1 体作るときに、置く場所と動く間隔を決める"""
self.x = x
self.y = y
self.move_every = move_every
def move(self, target_x, target_y):
"""自分の番のフレームだけ、目標へ 1 ドット近づく"""
if pyxel.frame_count % self.move_every != 0:
return
if self.x < target_x:
self.x += 1
elif self.x > target_x:
self.x -= 1
if self.y < target_y:
self.y += 1
elif self.y > target_y:
self.y -= 1
class App:
def __init__(self):
"""起動時の設定"""
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, title="Candy Hunt")
pyxel.load("candy_hunt.pyxres")
self.scene = SCENE_TITLE
self.reset_game()
pyxel.run(self.update, self.draw)
def reset_game(self):
"""ゲームの中身を、最初の状態に戻す"""
self.boy_x = SCREEN_WIDTH // 2 - BOY_SIZE // 2
self.boy_y = SCREEN_HEIGHT // 2 - BOY_SIZE // 2
self.score = 0
self.ghosts = []
self.add_ghost()
self.place_candy()
def add_ghost(self):
"""男の子からいちばん遠い角に、お化けを 1 体足す"""
move_every = GHOST_MOVE_EVERY[len(self.ghosts)]
x, y = far_corner(self.boy_x, self.boy_y)
self.ghosts.append(Ghost(x, y, move_every))
def update(self):
"""フレーム毎の更新処理"""
if self.scene == SCENE_TITLE:
self.update_title()
elif self.scene == SCENE_PLAY:
self.update_play()
else:
self.update_gameover()
def update_title(self):
"""タイトル画面。Enter でゲームを始める"""
if pyxel.btnp(pyxel.KEY_RETURN):
self.reset_game()
self.scene = SCENE_PLAY
def update_gameover(self):
"""ゲームオーバー画面。少し待ってから、Enter でゲームを再び始める"""
if pyxel.frame_count - self.gameover_frame < GAMEOVER_WAIT:
return
if pyxel.btnp(pyxel.KEY_RETURN):
self.reset_game()
self.scene = SCENE_PLAY
def update_play(self):
"""あそんでいる間の更新"""
self.move_boy()
self.move_ghosts()
self.check_candy()
self.check_ghosts()
def move_boy(self):
"""矢印キーで男の子を動かす"""
if pyxel.btn(pyxel.KEY_LEFT):
self.boy_x -= BOY_SPEED
if pyxel.btn(pyxel.KEY_RIGHT):
self.boy_x += BOY_SPEED
if pyxel.btn(pyxel.KEY_UP):
self.boy_y -= BOY_SPEED
if pyxel.btn(pyxel.KEY_DOWN):
self.boy_y += BOY_SPEED
# 画面の外へ出さない
self.boy_x = max(0, min(self.boy_x, SCREEN_WIDTH - BOY_SIZE))
self.boy_y = max(0, min(self.boy_y, SCREEN_HEIGHT - BOY_SIZE))
def move_ghosts(self):
"""すべてのお化けを、男の子に近づける"""
for ghost in self.ghosts:
ghost.move(self.boy_x, self.boy_y)
def place_candy(self):
"""男の子と重ならない場所へ、お菓子を置きなおす"""
while True:
self.candy_x = random.randint(0, SCREEN_WIDTH - CANDY_SIZE)
self.candy_y = random.randint(0, SCREEN_HEIGHT - CANDY_SIZE)
if not is_hit(self.boy_x, self.boy_y, BOY_SIZE,
self.candy_x, self.candy_y, CANDY_SIZE):
break
def check_candy(self):
"""お菓子がしっかり重なったら、スコアを増やして次の場所に置きなおす"""
if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
self.candy_x + HIT_OFFSET, self.candy_y + HIT_OFFSET, HIT_SIZE):
self.score += 1
self.place_candy()
if (self.score % GHOST_SPAWN_SCORE == 0
and len(self.ghosts) < len(GHOST_MOVE_EVERY)):
self.add_ghost()
def check_ghosts(self):
"""どれか 1 体にでも捕まったら、ゲームオーバーにする"""
for ghost in self.ghosts:
if is_hit(self.boy_x + HIT_OFFSET, self.boy_y + HIT_OFFSET, HIT_SIZE,
ghost.x + HIT_OFFSET, ghost.y + HIT_OFFSET, HIT_SIZE):
self.scene = SCENE_GAMEOVER
self.gameover_frame = pyxel.frame_count # 捕まった時刻を覚えておく
return
def draw(self):
"""フレーム毎の描画処理"""
pyxel.cls(pyxel.COLOR_BLACK)
if self.scene == SCENE_TITLE:
self.draw_title()
return
self.draw_play()
if self.scene == SCENE_GAMEOVER:
self.draw_gameover()
def draw_title(self):
"""タイトル画面を描く"""
draw_center("CANDY HUNT", 44, pyxel.COLOR_YELLOW)
if is_blink_on():
draw_center("PRESS ENTER", 68, pyxel.COLOR_WHITE)
def draw_play(self):
"""あそんでいる画面を描く"""
pyxel.blt(self.candy_x, self.candy_y, 0, 16, 0,
CANDY_SIZE, CANDY_SIZE, pyxel.COLOR_BLACK) # お菓子
for ghost in self.ghosts: # お化けは何体でも
pyxel.blt(ghost.x, ghost.y, 0, 8, 0,
GHOST_SIZE, GHOST_SIZE, pyxel.COLOR_BLACK)
pyxel.blt(self.boy_x, self.boy_y, 0, 0, 0,
BOY_SIZE, BOY_SIZE, pyxel.COLOR_BLACK) # 男の子
pyxel.text(4, 4, f"SCORE {self.score}", pyxel.COLOR_WHITE) # スコア
def draw_gameover(self):
"""ゲームオーバーの文字を重ねて描く"""
elapsed = pyxel.frame_count - self.gameover_frame # 捕まってからの経過
if elapsed < GAMEOVER_WAIT:
return
if elapsed % BLINK_CYCLE < BLINK_ON:
draw_center("GAME OVER", 52, pyxel.COLOR_RED)
draw_center("PRESS ENTER TO RETRY", 80, pyxel.COLOR_GRAY)
draw_center(f"SCORE {self.score}", 66, pyxel.COLOR_WHITE)
App()
GHOST_MOVE_EVERY[len(self.ghosts)] の読み方
add_ghost() の 1 行目です。リストの長さが、そのまま次の番号になります。
| いまいる体数 | len(self.ghosts) | 取り出す値 |
|---|---|---|
| 0 体(これから 1 体目) | 0 | GHOST_MOVE_EVERY[0] = 1 |
| 1 体(これから 2 体目) | 1 | GHOST_MOVE_EVERY[1] = 2 |
| 2 体(これから 3 体目) | 2 | GHOST_MOVE_EVERY[2] = 3 |
| 3 体(これから 4 体目) | 3 | GHOST_MOVE_EVERY[3] = 4 |
リストの番号が 0 から始まることが、ここでちょうど都合よくはまっています。 「何体目か」を数える変数を別に持つ必要がありません。
check_ghosts() の return for ghost in self.ghosts:
if is_hit(...):
self.scene = SCENE_GAMEOVER
self.gameover_frame = pyxel.frame_count
return # ← 1 体見つかれば、もう調べなくてよい
捕まったかどうかが知りたいだけなので、1 体見つかった時点で終わりです。 残りを調べても答えは変わりません。
return がないと、gameover_frame が上書きされます
4 体が重なっている状態だと、for が 4 回とも成立します。
return がないと gameover_frame が4 回書きかえられ、
結果として最後の 1 体の時刻になります。 if (self.score % GHOST_SPAWN_SCORE == 0
and len(self.ghosts) < len(GHOST_MOVE_EVERY)):
self.add_ghost()
条件が 1 行に収まらないときは、全体を丸カッコで囲むと途中で改行できます。
2 行目を深めに下げているのは、中身の self.add_ghost() と
見分けがつくようにするためです。同じ深さだと、どこまでが条件か分かりません。
% で書く
self.score % GHOST_SPAWN_SCORE == 0 は
「スコアを 5 で割ったあまりが 0」= 5 の倍数、という意味です。
点滅で使った % と同じ道具です。
update() で毎フレーム「スコアは 5 の倍数か」を調べると、
5 個目を取ったあとの数十フレーム、ずっと成立し続けます。
お化けが一瞬で 4 体に増えてしまいます。self.score += 1 のすぐそばに置いています。
これは備忘録の「1 回だけ処理する書き方」と同じ考え方です。
フェーズ 5 と同じ理由です。お化けは男の子の座標へ近づくだけで、
男の子は画面内に押しとどめられています。
だからお化けにも max・min はいりません。
角から出るときも同じです。CORNERS は
SCREEN_WIDTH - GHOST_SIZE のように絵の大きさを引いてあるので、
右下の角に置いても絵が画面からはみ出しません。
遊んでいるといつの間にか増えているので、気づいたときには囲まれています。 出てきた直後だけ点滅させると、「増えた」が伝わります。
class Ghost:
def __init__(self, x, y, move_every):
"""1 体作るときに、置く場所と動く間隔を決める"""
self.x = x
self.y = y
self.move_every = move_every
self.born_frame = pyxel.frame_count # 生まれた時刻を覚えておく
def draw_play(self):
...
for ghost in self.ghosts:
if pyxel.frame_count - ghost.born_frame < 30 and not is_blink_on():
continue # 出てきて 1 秒は、点滅の消える側で描かない
pyxel.blt(ghost.x, ghost.y, 0, 8, 0,
GHOST_SIZE, GHOST_SIZE, pyxel.COLOR_BLACK)
continue は「このくり返しだけ飛ばす」
return が関数ごと抜けるのに対し、
continue はいま扱っている 1 個をとばして、次の 1 個へ進みます。
for の中でだけ使える書き方です。
draw() の先頭で cls() が毎フレーム画面を消しているので、
blt() を呼ばなかったフレームは、そこに何もないことになります。
つまり消す命令はいりません。描くフレームと描かないフレームが交互になると、
点滅して見えます。and not を読みやすく書き直す案を
備忘録 #blink-by-skipping にまとめました。
pyxel.text(4, 4, f"SCORE {self.score}", pyxel.COLOR_WHITE)
pyxel.text(4, 12, f"GHOST {len(self.ghosts)}", pyxel.COLOR_GRAY)
len() で数えるだけです。体数を覚えておく変数はいりません。
リストが自分で知っています。
遊んでみて手ごたえが合わなければ、定数を変えるだけで調整できます。 どれもコードを書きかえずに済むように作ってあります。
| 変えるもの | 効果 |
|---|---|
GHOST_SPAWN_SCORE を 8 に | 増えるペースがゆるやかになる。いちばん効きます |
GHOST_MOVE_EVERY を [2, 3, 4, 5] に |
全体に遅くなる。1 体目でも逃げ切る時間が 5.0 秒 → 9.9 秒 |
GHOST_MOVE_EVERY に 5 つめを足す | 最大 5 体になる。ほかは何も直さなくてよい |
HIT_SIZE を 3 に | すりぬけやすくなる(#hitbox-size) |
[1, 2, 3, 4] のとき)。
3 体・4 体になってもこの数字自体はあまり変わりません——
すでに追いつめられているからです。| 症状 | 見るところ |
|---|---|
AttributeError: ... 'ghost_x' |
どこかに self.ghost_x が残っています。
move_ghosts / check_ghosts / draw_play の 3 か所を確認します |
IndexError: list index out of range |
5 体目を作ろうとしています。check_candy() の
len(self.ghosts) < len(GHOST_MOVE_EVERY) が抜けています |
| お化けが 1 体も出ない | reset_game() の self.add_ghost() を書き忘れています |
| お化けが増えない | check_candy() の中、if is_hit(...) の内側に
書けているか確認します。1 段浅いと、毎フレーム調べることになります |
| お菓子を 5 個取った瞬間に一気に 4 体出る | 増やす判定を update() 側に書いています。
取った瞬間の 1 回だけにするため、self.score += 1 のそばに置きます |
| 4 体いるはずが 1 体にしか見えない | GHOST_MOVE_EVERY が全部同じ数になっていませんか。
同じ速さだと本当に重なります(1-5 を参照) |
| 2 体目が男の子のすぐ近くに出る | add_ghost() で far_corner() を使わず、
(0, 0) を直接書いていませんか |
| お化けがぷるぷる震える | move() の縦・横を elif でつないでいます。
横のブロックと縦のブロックは別々の if です(フェーズ 5 と同じ) |
| やり直しても 4 体のまま | reset_game() の self.ghosts = [] がありません |
TypeError: 'Ghost' object is not subscriptable |
ghost[0] のように書いています。
Ghost はリストではないので ghost.x と書きます |
これでゲームの中身は完成です。残りは仕上げの 2 つになります。
フェーズ 9 では効果音と BGM をつけます。 お菓子を取った音、捕まった音、それに BGM。 「取った感じが薄い」で 「効果音がいちばん効く」と書いたとおり、 手ざわりがいちばん変わるフェーズです。 音は step0 で BGM の作り方 をやったので、その続きになります。
フェーズ 10 はパッケージ化して Web で公開です。 step0 の Star Voyager と同じ手順なので、こちらは復習になります。