弾が撃てるようになりましたし、敵も何か動いていますし、当たり判定を追加してみます。
「当たり判定」の実装方法はいろいろありますけれど、まずシンプルに矩形vs矩形の当たり判定にします。矩形の位置を GameObject の中心に固定してしまえば、だいぶ話が簡単になりますね。
変わった形の敵を作るとき、当たり判定をもっと自由に設定したいな、となったら、あとから拡張してやれば良いでしょう。まずは簡単なバージョンで動かしてしまいます。
GameObject に、hit_rect_w, hit_rect_h の2つのパラメータを追加します。
- 中心位置 x, y
- 幅 hit_rect_w、高さ hit_rect_h の矩形
これらの情報があれば、GameObject ごとに当たり判定に使う矩形の位置を表現できますね。obj0, obj1 の当たり判定関数(当たっていたら True を返す)is_hit(obj0, obj1) を次のように書いてみました。
def is_hit(obj0, obj1):
# x方向の当たり判定
w0 = obj0.hit_rect_w * 0.5
w1 = obj1.hit_rect_w * 0.5
obj0_min_x = obj0.x - w0
obj0_max_x = obj0.x + w0
obj1_min_x = obj1.x - w1
obj1_max_x = obj1.x + w1
if obj0_min_x > obj1_max_x or obj0_max_x < obj1_min_x:
return False
# y方向の当たり判定
h0 = obj0.hit_rect_h * 0.5
h1 = obj1.hit_rect_h * 0.5
obj0_min_y = obj0.y - h0
obj0_max_y = obj0.y + h0
obj1_min_y = obj1.y - h1
obj1_max_y = obj1.y + h1
if obj0_min_y > obj1_max_y or obj0_max_y < obj1_min_y:
return False
return True
この書き方もいろいろありますけれど、「どうなっていたら当たらないか」を判定して False を返す、とした方が自分にはわかりやすいです。
- obj0 の x 座標の最小値が、obj1 の x 座標の最大値より大きければ当たっていない
- obj0 の x 座標の最大値が、obj1 の x 座標の最小値より小さければ当たっていない
- 同じことを y 座標の最小・最大値で判定する
さて、これが書けたら、GameObject の組み合わせで is_hit 判定すれば良いわけです。今のプログラムではまだ敵が弾を撃たないので、
- プレイヤー vs 敵
- プレイヤーの弾 vs 敵
を判定したいですね。
GameObject に type を追加して、「プレイヤーまたはプレイヤーの弾なら TYPE_PLAYER」「敵なら TYPE_ENEMY」を設定しておきます。
全ての GameObject は game_objects に入っているので、そこから取り出す感じで書けますね。「プレイヤー or プレイヤーの弾」と「敵」で総当たり。で、is_hit だったら on_hit 関数を呼んで、その中で具体的に何か処理を書く仕様にします。当たったときのエフェクトとかも出してみたくなりますし。
リストの処理は何だか無駄なので、そもそもプレイヤーとか敵は別のリストで管理するように変えたほうが良いかも。
enemies = [obj for obj in game_objects if obj.type == TYPE_ENEMY]
players = [obj for obj in game_objects if obj.type == TYPE_PLAYER]
for player in players:
for enemy in enemies:
if is_hit(player, enemy):
player.on_hit(enemy)
以上をまとめると、こんなリストになりました。
- プレイヤーから撃った弾が敵に当たったら、弾と敵が消える
- プレイヤーと敵が当たったら、プレイヤーが消える
処理が実装できました。
import pyglet
from pyglet import shapes
from pyglet.window import key
from collections import defaultdict
import random
import copy
title = "Pyglet Shooting Game"
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
window = pyglet.window.Window(WINDOW_WIDTH, WINDOW_HEIGHT, title)
batch = pyglet.graphics.Batch()
LINE_WIDTH = 2
PLAYER_SPEED = 120
TYPE_NONE = 0
TYPE_PLAYER = 1
TYPE_ENEMY = 2
@window.event
def on_draw():
window.clear()
batch.draw()
key_status = defaultdict(bool)
@window.event
def on_key_press(symbol, modifiers):
key_status[symbol] = True
@window.event
def on_key_release(symbol, modifiers):
key_status[symbol] = False
game = None
game_objects = []
new_game_objects = []
class GameObject():
def __init__(self):
self.is_dead = False
self.line_shapes = []
self.lines = None
self.x = 0
self.y = 0
self.type = TYPE_NONE;
self.hit_rect_w = 0
self.hit_rect_h = 0
def create_lines(self, lines, color):
self.lines = lines
for line in lines:
x1 = self.x + line[0][0]
y1 = self.y + line[0][1]
x2 = self.x + line[1][0]
y2 = self.y + line[1][1]
self.line_shapes.append(shapes.Line(x1, y1, x2, y2, width=LINE_WIDTH, color=color, batch=batch))
def on_update(self, dt):
for (line, line_shape) in zip(self.lines, self.line_shapes):
line_shape.x = self.x + line[0][0]
line_shape.y = self.y + line[0][1]
line_shape.x2 = self.x + line[1][0]
line_shape.y2 = self.y + line[1][1]
def on_hit(self, target):
pass
def is_hit(obj0, obj1):
# x方向の当たり判定
w0 = obj0.hit_rect_w * 0.5
w1 = obj1.hit_rect_w * 0.5
obj0_min_x = obj0.x - w0
obj0_max_x = obj0.x + w0
obj1_min_x = obj1.x - w1
obj1_max_x = obj1.x + w1
if obj0_min_x > obj1_max_x or obj0_max_x < obj1_min_x:
return False
# y方向の当たり判定
h0 = obj0.hit_rect_h * 0.5
h1 = obj1.hit_rect_h * 0.5
obj0_min_y = obj0.y - h0
obj0_max_y = obj0.y + h0
obj1_min_y = obj1.y - h1
obj1_max_y = obj1.y + h1
if obj0_min_y > obj1_max_y or obj0_max_y < obj1_min_y:
return False
return True
def on_update(dt):
game.on_update(dt)
for game_object in game_objects:
game_object.on_update(dt)
enemies = [obj for obj in game_objects if obj.type == TYPE_ENEMY]
players = [obj for obj in game_objects if obj.type == TYPE_PLAYER]
for player in players:
for enemy in enemies:
if is_hit(player, enemy):
player.on_hit(enemy)
for dead_object in [obj for obj in game_objects if obj.is_dead]:
game_objects.remove(dead_object)
game_objects.extend(new_game_objects)
new_game_objects.clear()
pyglet.clock.schedule_interval(on_update, 1 / 120.0)
class Shot(GameObject):
def __init__(self, x, y):
super().__init__()
self.x = x
self.y = y
lines = (
((0, -8), (0, 8)), # 縦に1本、16ピクセルで弾を表示
)
color = (0, 230, 0)
self.create_lines(lines, color)
self.speed = 160
self.type = TYPE_PLAYER
self.hit_rect_h = 16
self.hit_rect_w = 2
def on_update(self, dt):
d = dt * self.speed
self.y += d
# 画面外判定
if self.y >= WINDOW_HEIGHT + 16:
self.is_dead = True
super().on_update(dt)
def on_hit(self, target):
target.is_dead = True
self.is_dead = True
class Player(GameObject):
def __init__(self, x, y):
super().__init__()
self.x = x
self.y = y
# 菱形
lines = (
((0, 16), (8, 0)), ((8, 0), (0, -16)),
((0, -16), (-8, 0)), ((-8, 0), (0, 16))
)
color = (0, 230, 0)
self.create_lines(lines, color)
self.type = TYPE_PLAYER
self.prev_space_key_status = False
self.hit_rect_h = 4
self.hit_rect_w = 4
def on_update(self, dt):
d = dt * PLAYER_SPEED
dx = 0
dy = 0
if key_status[key.W]: # up
dy = d
elif key_status[key.S]: # down
dy = -d
if key_status[key.A]: # left
dx = -d
elif key_status[key.D]: # right
dx = d
space_key_status = key_status[key.SPACE]
if space_key_status and not self.prev_space_key_status:
new_game_objects.append(Shot(self.x, self.y + 24))
self.prev_space_key_status = space_key_status
self.x += dx
self.y += dy
self.x = max(0, min(self.x, WINDOW_WIDTH - 16))
self.y = max(0, min(self.y, WINDOW_HEIGHT - 16))
super().on_update(dt)
def on_hit(self, target):
self.is_dead = True
class Enemy0(GameObject):
def __init__(self, x, y):
super().__init__()
self.x = x
self.y = y
self.speed = 80
# 三角形
lines = (
((-16, 16), (16, 16)), ((16, 16), (0, -16)), ((0, -16), (-16, 16)),
)
color = (230, 230, 0)
self.create_lines(lines, color)
self.type = TYPE_ENEMY
self.hit_rect_h = 32
self.hit_rect_w = 32
def on_update(self, dt):
d = dt * self.speed
self.y -= d
if self.y < -32:
self.is_dead = True
super().on_update(dt)
class Game():
def __init__(self):
self.enemy_timer = 0
def on_update(self, dt):
self.enemy_timer += dt
if self.enemy_timer > 1.0:
self.enemy_timer = 0
enemy_size = 32
enemy_x = random.randint(enemy_size, WINDOW_WIDTH - enemy_size)
enemy_y = WINDOW_HEIGHT + enemy_size
enemy = Enemy0(enemy_x, enemy_y)
game_objects.append(enemy)
def create_player():
player = Player(WINDOW_WIDTH // 2, WINDOW_HEIGHT // 3)
game_objects.append(player)
def create_game():
global game
game = Game()
create_game()
create_player()
pyglet.app.run()