古いchatGPTを使って、2Dゲームエンジンpyxelをつかって、段階的に、ブロック崩しゲームを作成していきます。日経BP「13歳からのPython超入門」を参考にしました。
以下のプロンプトを順番に入力して、検証していきましょう。
- はじめに画面640x480を作成してください。
- 画面に半径10のボールを表示してください。
- ボールが動くようにして、画面上端、両端で跳ね返るようにしてください。
- 左右矢印キーを使ってうごく、パッドを作り、ボールを打ち返せるようにしてください。
- ボールが画面下に落ちたら、Game Overとテキスト表示してください。
- ボールがパドルに当たったかどうかを判定する箇所の解説をお願いします。
- ブロックを表示してください。
- ボールでブロックを消してください。
- ボールがブロックに衝突したかを判定し、衝突した場合にボールの速度を反転させて跳ね返し、衝突したブロックを消す処理の箇所の解説をお願いします。
- パッドの当たり判定を調整してください。
- パッドのどの位置に当たったかで、角度を変えて下さい。パッドの真ん中で打つと、ボールは真上、パッドの左で打つと、ボールは左方向、パッドの右で打つと、ボールは右方向に跳ね返るようにしてください。
- 消したブロックの数をSCORE:で表示してください。
すべてのブロックをクリアしたら、再びすべてのブロックを表示してください。
GAME OVERになったら、また最初からはじめるか、終了か選べるようにしてください。 - ボールの速さ、パッドの動きは、どこで調整しますか。
- ブロックを大きくして、数を減らしてください。
1.はじめに画面640x480を作成してください。
以下のようなコードになります。Pyxelのinit関数を使って画面を初期化し、run関数でゲームループを開始しています。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
pass # ここにゲームの更新処理を書きます
def draw(self):
pyxel.cls(0) # 画面を黒でクリアします
if __name__ == "__main__":
BreakoutGame()
2.画面に半径10のボールを表示してください。
画面に半径10のボールを表示するために、drawメソッドで円を描いてみましょう。
Pyxelでは円を描くにはcirc(x, y, r, col)メソッドを使います。x,yが中心座標、rが半径、colが色(0-15の16色)です。
まずはボールの座標をクラスの変数にして、drawで描画します。
例えば、画面の中央に半径10の白いボールを描くコードはこうなります。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
pass
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7) # 7は白に近い色です
if __name__ == "__main__":
BreakoutGame()
3.ボールが動くようにして、画面上端、両端で跳ね返るようにしてください。
ボールが動くように速度(vx, vy)を追加し、updateメソッドで座標を更新します。
また、画面の左端、右端、上端にぶつかったら速度の方向を反転させて跳ね返るようにします。
(画面下端はまだ処理しません)
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 2 # ボールの横速度
self.vy = -2 # ボールの縦速度(上方向に動く)
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
# ボールの座標を速度分だけ移動
self.ball_x += self.vx
self.ball_y += self.vy
# 左右の壁に当たったら速度のx成分を反転
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
# 上の壁に当たったら速度のy成分を反転
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
if __name__ == "__main__":
BreakoutGame()
4.ボールの速度を(4, -4)に変更し、左右矢印キーを使ってうごく、パッドを作り、ボールを打ち返せるようにしてください。
ボールの速度を(4, -4)に変更し、左右矢印キーで動くパドル(横長の板)を作ってみましょう。
パドルは画面下に表示し、updateでキー入力を検知して動かします。
ボールがパドルに当たったら、ボールの速度のy成分を反転して跳ね返すようにします。
以下にコード例を示します。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
# ボールの初期位置と速度
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 4 # 速度を4に変更
self.vy = -4
# パドルの初期位置とサイズ
self.pad_width = 80
self.pad_height = 16
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10 # 画面下から少し上
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
# ボールの移動
self.ball_x += self.vx
self.ball_y += self.vy
# 壁で跳ね返る(左右と上)
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
# パドルの移動(左右矢印キー)
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
# ボールとパドルの当たり判定
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
self.vy = -self.vy # ボールのy速度を反転して跳ね返す
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9) # パドルを描画
if __name__ == "__main__":
BreakoutGame()
5.ボールが画面下に落ちたら、Game Overとテキスト表示してください。
updateでボールのy座標が画面の高さを超えたらゲームオーバー状態にして、
drawでその状態ならテキストを表示します。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 4
self.vy = -4
self.pad_width = 80
self.pad_height = 16
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10
self.game_over = False # ゲームオーバーフラグ
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
if not self.game_over:
self.ball_x += self.vx
self.ball_y += self.vy
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
# ボールとパドルの当たり判定
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
self.vy = -self.vy
# 画面下に落ちたらゲームオーバー
if self.ball_y - self.ball_r > self.height:
self.game_over = True
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9)
if self.game_over:
pyxel.text(self.width // 2 - 30, self.height // 2, "GAME OVER", 8)
if __name__ == "__main__":
BreakoutGame()
6.ブロックを大きめにして、3段くらいにして、表示してください。
ブロック同士の隙間を開けるために、ブロックの幅や配置座標を調整しましょう。
例えば、ブロック幅を少し狭くして(例:70)、ブロック間に10ピクセルの隙間を入れてみます。
以下のように修正します。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 4
self.vy = -4
self.pad_width = 80
self.pad_height = 16
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10
# ブロック設定(幅70、高さ24、隙間10)
self.block_width = 70
self.block_height = 24
self.block_gap = 10 # ブロック間の隙間
self.blocks = []
rows = 3
cols = self.width // (self.block_width + self.block_gap)
for row in range(rows):
for col in range(cols):
x = col * (self.block_width + self.block_gap)
y = 30 + row * (self.block_height + self.block_gap)
self.blocks.append((x, y))
self.game_over = False
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
if not self.game_over:
self.ball_x += self.vx
self.ball_y += self.vy
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
self.vy = -self.vy
if self.ball_y - self.ball_r > self.height:
self.game_over = True
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9)
for (x, y) in self.blocks:
pyxel.rect(x, y, self.block_width, self.block_height, 11)
if self.game_over:
pyxel.text(self.width // 2 - 30, self.height // 2, "GAME OVER", 8)
if __name__ == "__main__":
BreakoutGame()
7.ボールでブロックを消してください。
ボールがブロックに当たったら、そのブロックを消すようにしましょう。
やり方は、updateでボールの位置がブロックの矩形内に入っているかを判定し、当たったブロックをリストから削除します。
さらに、ボールの速度のy成分を反転して跳ね返す処理も入れます。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 4
self.vy = -4
self.pad_width = 80
self.pad_height = 16
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10
self.block_width = 70
self.block_height = 24
self.block_gap = 10
self.blocks = []
rows = 3
cols = self.width // (self.block_width + self.block_gap)
for row in range(rows):
for col in range(cols):
x = col * (self.block_width + self.block_gap)
y = 30 + row * (self.block_height + self.block_gap)
self.blocks.append((x, y))
self.game_over = False
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
if not self.game_over:
self.ball_x += self.vx
self.ball_y += self.vy
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
self.vy = -self.vy
# ブロックとの当たり判定と消去
for i, (x, y) in enumerate(self.blocks):
if (self.ball_x + self.ball_r > x and
self.ball_x - self.ball_r < x + self.block_width and
self.ball_y + self.ball_r > y and
self.ball_y - self.ball_r < y + self.block_height):
# ブロックに当たったので消す
self.blocks.pop(i)
self.vy = -self.vy
break
if self.ball_y - self.ball_r > self.height:
self.game_over = True
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9)
for (x, y) in self.blocks:
pyxel.rect(x, y, self.block_width, self.block_height, 11)
if self.game_over:
pyxel.text(self.width // 2 - 30, self.height // 2, "GAME OVER", 8)
if __name__ == "__main__":
BreakoutGame()
8.ボールがブロックに当たるとブロックが消え、ボールは跳ね返り、衝突判定の仕組みを詳しく説明してください。
衝突判定の仕組み
衝突判定は、ボール(円)とブロック(矩形)が重なっているかを判定しています。Pyxelでは衝突判定を自分で実装する必要があります。
今回の方法は、ボールの周囲の矩形領域(ボールの中心座標から半径分を引いたり足したりした範囲)と、ブロックの矩形領域が重なっているかを判定しています。
具体的には、以下の条件で判定しています。
if (self.ball_x + self.ball_r > block_x and # ボールの右端がブロックの左端より右にある
self.ball_x - self.ball_r < block_x + block_width and # ボールの左端がブロックの右端より左にある
self.ball_y + self.ball_r > block_y and # ボールの下端がブロックの上端より下にある
self.ball_y - self.ball_r < block_y + block_height): # ボールの上端がブロックの下端より上にある
# 衝突していると判断
これらの条件がすべて満たされると、ボールの円の範囲とブロックの矩形の範囲が重なっているので衝突と判断します。
衝突後の処理
衝突したブロックは self.blocks のリストから削除します。
ボールの速度のy成分を反転して跳ね返ります(速度のx成分は変えず、y成分だけ反転)。
9.パッドの当たり判定を調整してください。パッドのどの位置に当たったかで、角度を変えて下さい。パッドの真ん中で打つと、ボールは真上、パッドの左で打つと、ボールは左方向、パッドの右で打つと、ボールは右方向に跳ね返るようにしてください。
パドルのどの位置でボールが当たったかによって、ボールの跳ね返る角度を変えるように調整しましょう。
具体的には、パドルの中心からボールの当たった位置までの距離を計算し、
その値を使ってボールの速度のx成分を変えます。
y成分は必ず上向き(負の値)にし、x成分はパドルの左側で当たれば負の値、右側で当たれば正の値になるようにします。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.ball_r = 10
self.vx = 4
self.vy = -4
self.pad_width = 80
self.pad_height = 16
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10
self.block_width = 70
self.block_height = 24
self.block_gap = 10
self.blocks = []
rows = 3
cols = self.width // (self.block_width + self.block_gap)
for row in range(rows):
for col in range(cols):
x = col * (self.block_width + self.block_gap)
y = 30 + row * (self.block_height + self.block_gap)
self.blocks.append((x, y))
self.game_over = False
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def update(self):
if not self.game_over:
self.ball_x += self.vx
self.ball_y += self.vy
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
# パドルとの当たり判定(角度調整)
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
# パドルの中心からボールの当たった位置の差(-0.5~0.5の範囲)
hit_pos = (self.ball_x - self.pad_x) / self.pad_width - 0.5
# 水平方向の速度を調整(最大速度4の範囲)
self.vx = hit_pos * 8 # -4から4の範囲に調整
# 常に上向きに跳ね返る
self.vy = -abs(self.vy)
# ブロックとの当たり判定と消去
for i, (x, y) in enumerate(self.blocks):
if (self.ball_x + self.ball_r > x and
self.ball_x - self.ball_r < x + self.block_width and
self.ball_y + self.ball_r > y and
self.ball_y - self.ball_r < y + self.block_height):
self.blocks.pop(i)
self.vy = -self.vy
break
if self.ball_y - self.ball_r > self.height:
self.game_over = True
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9)
for (x, y) in self.blocks:
pyxel.rect(x, y, self.block_width, self.block_height, 11)
if self.game_over:
pyxel.text(self.width // 2 - 30, self.height // 2, "GAME OVER", 8)
if __name__ == "__main__":
BreakoutGame()
パドルの真ん中でボールが当たるとvxが0になり、ボールは真上に跳ね返ります。
パドルの左側で当たるとvxが負になり左方向に、右側で当たると正になり右方向に跳ね返ります。
10.消したブロックの数をSCORE:で表示してください。すべてのブロックをクリアしたら、再びすべてのブロックを表示してください。GAME OVERになったら、また最初からはじめるか、終了か選べるようにしてください。
消したブロックの数を「SCORE:」として画面に表示し、すべてのブロックを消したら再びブロックを全て表示するようにします。
また、ゲームオーバー時に「Rキーで再スタート、Qキーで終了」を選べるようにします。
以下の修正点があります。
消したブロック数をカウントするために score を用意します。
ブロックがゼロになったら再生成してゲーム続行。
ゲームオーバー時にキー入力を受けて再スタートか終了を選べる。
import pyxel
class BreakoutGame:
def __init__(self):
self.width = 640
self.height = 480
self.ball_r = 10
self.pad_width = 80
self.pad_height = 16
self.block_width = 70
self.block_height = 24
self.block_gap = 10
self.reset_game()
pyxel.init(self.width, self.height, title="Breakout Game")
pyxel.run(self.update, self.draw)
def reset_game(self):
self.ball_x = self.width // 2
self.ball_y = self.height // 2
self.vx = 4
self.vy = -4
self.pad_x = (self.width - self.pad_width) // 2
self.pad_y = self.height - self.pad_height - 10
self.blocks = []
rows = 3
cols = self.width // (self.block_width + self.block_gap)
for row in range(rows):
for col in range(cols):
x = col * (self.block_width + self.block_gap)
y = 30 + row * (self.block_height + self.block_gap)
self.blocks.append((x, y))
self.score = 0
self.game_over = False
def update(self):
if not self.game_over:
self.ball_x += self.vx
self.ball_y += self.vy
if self.ball_x - self.ball_r < 0 or self.ball_x + self.ball_r > self.width:
self.vx = -self.vx
if self.ball_y - self.ball_r < 0:
self.vy = -self.vy
if pyxel.btn(pyxel.KEY_LEFT):
self.pad_x = max(0, self.pad_x - 5)
if pyxel.btn(pyxel.KEY_RIGHT):
self.pad_x = min(self.width - self.pad_width, self.pad_x + 5)
# パドルとの当たり判定(角度調整)
if (self.ball_y + self.ball_r >= self.pad_y and
self.ball_y + self.ball_r <= self.pad_y + self.pad_height and
self.ball_x >= self.pad_x and self.ball_x <= self.pad_x + self.pad_width):
hit_pos = (self.ball_x - self.pad_x) / self.pad_width - 0.5
self.vx = hit_pos * 8
self.vy = -abs(self.vy)
# ブロックとの当たり判定と消去
for i, (x, y) in enumerate(self.blocks):
if (self.ball_x + self.ball_r > x and
self.ball_x - self.ball_r < x + self.block_width and
self.ball_y + self.ball_r > y and
self.ball_y - self.ball_r < y + self.block_height):
self.blocks.pop(i)
self.vy = -self.vy
self.score += 1
break
# すべてのブロックを消したら再生成
if len(self.blocks) == 0:
self.reset_blocks()
if self.ball_y - self.ball_r > self.height:
self.game_over = True
else:
# ゲームオーバー時の操作
if pyxel.btnp(pyxel.KEY_R):
self.reset_game()
elif pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
def reset_blocks(self):
rows = 3
cols = self.width // (self.block_width + self.block_gap)
for row in range(rows):
for col in range(cols):
x = col * (self.block_width + self.block_gap)
y = 30 + row * (self.block_height + self.block_gap)
self.blocks.append((x, y))
def draw(self):
pyxel.cls(0)
pyxel.circ(self.ball_x, self.ball_y, self.ball_r, 7)
pyxel.rect(self.pad_x, self.pad_y, self.pad_width, self.pad_height, 9)
for (x, y) in self.blocks:
pyxel.rect(x, y, self.block_width, self.block_height, 11)
# スコア表示
pyxel.text(5, 5, f"SCORE: {self.score}", 7)
if self.game_over:
pyxel.text(self.width // 2 - 50, self.height // 2 - 10, "GAME OVER", 8)
pyxel.text(self.width // 2 - 80, self.height // 2 + 10, "Press R to Restart, Q to Quit", 8)
if __name__ == "__main__":
BreakoutGame()
- 「SCORE:」は画面左上に表示されます。
- 全ブロックを消すと再びブロックが生成されます。
- ゲームオーバー時は「R」で再スタート、「Q」で終了できます。
11.ボールの速さ、パッドの動きは、どこで調整しますか。
ボールの速さやパッドの動きの速さは、コード内のupdateメソッドで変数の値として調整しています。
具体的には:
ボールの速さは、self.vx(横速度)とself.vy(縦速度)の値で決まります。これらの数値を大きくするとボールの移動速度が速くなり、小さくすると遅くなります。
パッドの動きの速さは、updateメソッド内で左右キーが押された時にパッドのx座標を増減させる量で調整しています。例えば、self.pad_x = max(0, self.pad_x - 5)の5の部分がパッドの移動量(速さ)です。この値を大きくするとパッドが速く動き、小さくすると遅くなります。
■「AIに書かせる」のではなく、「AIに下書きさせて、人間が仕上げる」というスタンスが大切です。
■「わかったつもり」になる(The Illusion of Competence)「自分はすごいものを作れる」と錯覚します。「なぜそのコードで動くのか」を必ず検証しながら、探求していきましょう。
一歩一歩です。ありがとうございます。
