当たり判定を追加

弾が撃てるようになりましたし、敵も何か動いていますし、当たり判定を追加してみます。

「当たり判定」の実装方法はいろいろありますけれど、まずシンプルに矩形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()
タイトルとURLをコピーしました