前回の記事では、円同士の当たり判定を実装しました。
aethergenesis.hatenablog.com
この知識があれば、円を使ってゲームが作れますね。今回は、2Dシューティングゲームを作ってみようと思います。
ゲームを作る前に、前回作ったサンプルをリファクタリングしていきます。
前回作ったサンプルだと、以下のようにいろんな変数をすべてAppクラスのメンバ変数に設定していました。
self.x = 0
self.y = 0
self.circ1_r = 8
self.circ1_col = 8
self.circ2_x = 80
self.circ2_y = 60
self.circ2_r = 16
self.circ2_col = 9
しかし、よく見てみるとcirc1とcirc2という名前違いの同じような変数が並んでいます。いまは円が2つなのでいいですが、
シューティングゲームなどになってくると、プレイヤー、敵機、弾など画面上に大量の円が必要です。それらすべてをメンバ変数に保持すると
同じような変数が大量に増えて、管理が非常に煩雑になってきます。
そこで、円という概念に必要な変数を一つのクラスにまとめて、オブジェクトとして汎用的に使いまわせるようにします。
まず、Circleクラスをつくります
class Circle:
def __init__(self, x, y, r, col):
self.x = x
self.y = y
self.r = r
self.col = col
def draw(self):
pyxel.circ(self.x, self.y, self.r, self.col)
pyxel.pset(self.x, self.y, 0)
次に、Appのメンバ変数を削除し、代わりにCircleクラスのインスタンスを生成します。
各処理では、各インスタンスの変数を参照・更新するように変更します。
class App:
def __init__(self):
pyxel.init(160, 120)
self.player_circle = Circle(0, 0, 8, 8)
self.static_circle = Circle(80, 60, 16, 9)
pyxel.run(self.update, self.draw)
def update(self):
if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT):
self.player_circle.x -= 1
elif pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT):
self.player_circle.x += 1
elif pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_UP):
self.player_circle.y -= 1
elif pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN):
self.player_circle.y += 1
dx = self.static_circle.x - self.player_circle.x
dy = self.static_circle.y - self.player_circle.y
dist_square = dx * dx + dy * dy
if dist_square <= (self.player_circle.r + self.static_circle.r) ** 2 :
self.collision = True
else:
self.collision = False
def draw(self):
pyxel.cls(6)
self.static_circle.draw()
self.player_circle.draw()
if hasattr(self, 'collision') and self.collision:
pyxel.text(0, 0, "Collision!", 8)
さらに、衝突判定も汎用的なメソッドに変えます。
class Circle:
def intersects(self, other):
if isinstance(other, Circle):
dx = self.x - other.x
dy = self.y - other.y
dist_square = dx * dx + dy * dy
return dist_square <= (self.r + other.r) ** 2
return False
def update(self):
if self.player_circle.intersects(self.static_circle):
self.player_circle.col = 11
else:
self.player_circle.col = 8
文字表示ではなく、接触時にプレイヤー側の円の色を変えるようにしました。
これでだいぶ記述が簡潔になり、尚且つ円の数も簡単に増やせるようになりました。
これをベースに、簡単なシューティングゲームを作っていきます。
完成したゲームはこんな感じ。

ソースコード全文はGithubで。
github.com
長くなったので、要点だけ解説します。
class Circle:
def __init__(self, x, y, r, col):
self.x = x
self.y = y
self.r = r
self.col = col
def intersects(self, other):
if isinstance(other, Circle):
dx = self.x - other.x
dy = self.y - other.y
dist_square = dx * dx + dy * dy
return dist_square <= (self.r + other.r) ** 2
return False
def draw(self):
pyxel.circ(self.x, self.y, self.r, self.col)
class Player(Circle):
def __init__(self, x, y, r, col,speed):
super().__init__(x, y, r, col)
self.speed = speed
class Enemy(Circle):
def __init__(self, x, y, r, col,speed,score=10):
super().__init__(x, y, r, col)
self.speed = speed
self.score = score
self.initial_y = y
class Bullet(Circle):
def __init__(self, x, y, r, col,speed):
super().__init__(x, y, r, col)
self.speed = speed
基底クラスCircleに、円に必要な変数(x/y座標、半径、色)と初期設定できるようにします。また、
intersectsメソッドで、他の円との接触判定を行えるようにします。
Player, Enemy, BulletはCircleを継承した子クラスです。拡張性を考えてこのように分けましたが、あまり差がなくなってしまいました。
class App:
def __init__(self):
pyxel.init(120, 160)
self.player = Player(x=10, y=pyxel.height - 10, r=5, col=2,speed=2)
self.enemy_list = [
Enemy(x=0, y=10, r=5, col=3,speed=1),
Enemy(x=0, y=40, r=5, col=4,speed=1.5),
Enemy(x=0, y=70, r=5, col=5,speed=2),
]
self.bullet_list = []
Appクラスのコンストラクタでは、クラスのインスタンスを生成します。
Playerは一つしか生成しませんが、Enemyは初期段階で3体分生成します。Bulletは発射時に生成されるので、格納するための空のリストを設定します。
def update(self):
self.player_update()
self.bullet_update()
self.enemy_update()
def player_update(self):
if (pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_B)) and len(self.bullet_list) < 3:
self.bullet_list.append(Bullet(self.player.x, self.player.y - self.player.r, 2, 9,3))
for enemy in self.enemy_list:
if self.player.intersects(enemy):
self.game_over = True
break
updateでは、それぞれの毎フレーム処理を呼び出します。
プレイヤーは毎フレーム移動判定、弾発射判定、敵との接触によるゲームオーバー判定を行います。
弾は画面内に3発までしか存在できないよう制御します。
def enemy_update(self):
for enemy in self.enemy_list:
enemy.x = (enemy.x + enemy.speed) % (pyxel.width + enemy.r * 2)
enemy.y = ( (math.sin(pyxel.frame_count * 0.1)) * 20 ) + enemy.initial_y
敵は、毎フレームmath.sin関数により上下に揺れながら左から右へ動きます。画面右端に到達したら、また左端から出てきます。
def bullet_update(self):
for bullet in self.bullet_list:
bullet.y -= bullet.speed
for enemy in self.enemy_list:
if bullet.intersects(enemy):
self.enemy_list.remove(enemy)
self.score += enemy.score
self.enemy_list.append(Enemy(x=0, y=pyxel.rndi(30, pyxel.height - 50), r=5, col=pyxel.rndi(1,15) ,speed=pyxel.rndf(1,3)))
self.bullet_list.remove(bullet)
break
if bullet in self.bullet_list and bullet.y + bullet.r < 0:
self.bullet_list.remove(bullet)
弾は、毎フレーム上方向に動きます。弾と敵が衝突すると両者が消え、スコアが加算されます。
また、弾が画面外に出ると、弾が消えます。
def draw(self):
pyxel.cls(0)
self.player.draw()
for enemy in self.enemy_list:
enemy.draw()
for bullet in self.bullet_list:
bullet.draw()
毎フレームごとにプレイヤー、敵、弾を順番にすべて描画します。
今回作ったゲームも、Github Pagesから遊べるようにしてみました。
ゲームというにはまだまだ足りていない要素が多いですが、円の基礎的な当たり判定ができるようになりました。
https://naritsan.github.io/pyxel_firstgame/pages/circle_shooting.html
次回は、円と同じくらいよく使われる、矩形の当たり判定について学習していきたいと思います。