Pyxel을 이용한 2D 게임 시작하기 (Part 10): 충돌 감지 구현

발행: (2026년 1월 14일 오전 12:00 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

이번 장에서는 총알과 소행성 간의 충돌 감지를 구현합니다.
전체 코드는 마지막에 제공됩니다.

1. 충돌 감지 로직 구현

새로운 intersects() 메서드를 sprite.pyBaseSprite 클래스에 추가합니다.
이 메서드는 인자로 전달된 다른 스프라이트와 겹치는지 확인하고, 충돌하면 True, 그렇지 않으면 False를 반환합니다. (구현은 AABB 개념을 사용하지만, 지금은 자세히 알 필요 없습니다.)

# sprite.py (added to BaseSprite)
def intersects(self, other):
    """Rectangle‑based collision detection (AABB)"""
    if other.x + other.w < self.x: return False
    if self.x + self.w < other.x: return False
    if other.y + other.h < self.y: return False
    if self.y + self.h < other.y: return False
    return True

2. 총알과 소행성 충돌

총알 업데이트 단계에서 총알과 소행성 간의 충돌을 검사합니다.
리스트를 순회하면서 요소를 제거하기 때문에 인덱스 문제를 피하기 위해 역순으로 리스트를 순회합니다.

로직을 단순하게 유지하기 위해 충돌이 감지되면 즉시 update() 메서드에서 return 합니다. 충돌이 발생하면 점수가 1점 증가합니다.

# main.py (added to the Game.update() method)

# Update bullets (reverse order)
for bullet in self.bullets[::-1]:
    bullet.update()
    # Remove bullets that leave the screen
    if bullet.y < 0:
        self.bullets.remove(bullet)
        continue
    # Collision detection (bullet × asteroid)
    for asteroid in self.asteroids[::-1]:
        if asteroid.intersects(bullet):
            self.score += 1          # Increase score
            self.bullets.remove(bullet)
            self.asteroids.remove(asteroid)
            return                    # Simplified handling

전체 코드

아래는 지금까지 구현된 모든 기능이 포함된 전체 코드입니다.

sprite.py

import pyxel
import math
import random

class BaseSprite:
    def __init__(self, x, y, w=8, h=8):
        """Constructor"""
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.vx = 0
        self.vy = 0

    def update(self):
        """Update position"""
        self.x += self.vx
        self.y += self.vy

    def draw(self):
        """Draw (implemented in subclasses)"""
        pass

    def move(self, spd, deg):
        """Move in a direction"""
        rad = deg * math.pi / 180
        self.vx = spd * math.cos(rad)  # X velocity
        self.vy = spd * math.sin(rad)  # Y velocity

    def flip_x(self):
        """Flip X direction"""
        self.vx *= -1

    def intersects(self, other):
        """Rectangle‑based collision detection (AABB)"""
        if other.x + other.w < self.x: return False
        if self.x + self.w < other.x: return False
        if other.y + other.h < self.y: return False
        if self.y + self.h < other.y: return False
        return True

class ShipSprite(BaseSprite):
    def __init__(self, x, y):
        """Constructor"""
        super().__init__(x, y)

    def draw(self):
        """Draw ship"""
        pyxel.blt(self.x, self.y, 0, 0, 0, self.w, self.h, 0)

class AsteroidSprite(BaseSprite):
    def __init__(self, x, y):
        """Constructor"""
        super().__init__(x, y)
        self.index = random.randint(2, 7)  # Asteroid image index

    def draw(self):
        """Draw asteroid"""
        pyxel.blt(self.x, self.y, 0,
                  self.w * self.index, 0,
                  self.w, self.h, 0)

class BulletSprite(BaseSprite):
    def __init__(self, x, y):
        """Constructor"""
        super().__init__(x, y)
        self.x += self.w / 2 - 1  # Center alignment

    def draw(self):
        """Draw bullet"""
        pyxel.rect(self.x, self.y, 2, 2, 12)

main.py

import pyxel
import math
import random
import sprite

W, H = 160, 120
SHIP_SPD = 1.4

ASTEROID_INTERVAL = 20
ASTEROID_LIMIT = 30

ASTEROID_SPD_MIN = 1.0
ASTEROID_SPD_MAX = 2.0
ASTEROID_DEG_MIN = 30
ASTEROID_DEG_MAX = 150

BULLET_SPD = 3

# Game
class Game:
    def __init__(self):
        """Constructor"""

        # Initialize score
        self.score = 0

        # Initialize player
        self.ship = sprite.ShipSprite(W / 2, H - 40)
        deg = 0 if random.random() < 0.5 else 180
        self.ship.move(SHIP_SPD, deg)

        # Asteroids
        self.asteroid_time = 0
        self.asteroids = []

        # Bullets
        self.bullets = []

        # Start Pyxel
        pyxel.init(W, H, title="Hello, Pyxel!!")
        pyxel.load("shooter.pyxres")
        pyxel.run(self.update, self.draw)

    def update(self):
        """Update"""

        # Update player
        self.ship.update()
        self.control_ship()
        self.overlap_spr(self.ship)

        self.check_interval()  # Spawn asteroids

        # Update asteroids
        for asteroid in self.asteroids:
            asteroid.update()
            self.overlap_spr(asteroid)

        # Update bullets (reverse order)
        for bullet in self.bullets[::-1]:
            bullet.update()
            # Remove bullets that leave the screen
            if bullet.y < 0:
                self.bullets.remove(bullet)
                continue
            # Collision detection (bullet × asteroid)
            for asteroid in self.asteroids[::-1]:
                if asteroid.intersects(bullet):
                    self.score += 1
                    self.bullets.remove(bullet)
                    self.asteroids.remove(asteroid)
                    return  # Simplified handling

    # ... (other methods such as draw, control_ship, overlap_spr, check_interval, etc.)

다른 모든 헬퍼 메서드(draw, control_ship, overlap_spr, check_interval 등)는 변경되지 않았습니다.

게임 루프 – 충돌, 그리기 및 컨트롤

for asteroid in self.asteroids[::-1]:
    if asteroid.intersects(bullet):
        self.score += 1
        self.bullets.remove(bullet)
        self.asteroids.remove(asteroid)
        return

def draw(self):
    """Draw everything on the screen."""
    pyxel.cls(0)

    # Draw score
    pyxel.text(10, 10, "SCORE:{:04}".format(self.score), 12)

    # Draw player
    self.ship.draw()

    # Draw asteroids
    for asteroid in self.asteroids:
        asteroid.draw()

    # Draw bullets
    for bullet in self.bullets:
        bullet.draw()

def control_ship(self):
    """Handle player actions."""
    if pyxel.btnp(pyxel.KEY_SPACE):
        self.ship.flip_x()          # Reverse movement
        # Fire bullet
        bullet = sprite.BulletSprite(self.ship.x, self.ship.y)
        bullet.move(BULLET_SPD, 270)
        self.bullets.append(bullet)

def overlap_spr(self, spr):
    """Wrap a sprite around the screen edges."""
    if spr.x < -spr.w:
        spr.x = W
        return
    if W < spr.x:
        spr.x = -spr.w
        return
    if spr.y < -spr.h:
        spr.y = H
        return
    if H < spr.y:
        spr.y = -spr.h
        return

def check_interval(self):
    """Spawn asteroids at regular intervals."""
    # Asteroid spawn interval
    self.asteroid_time += 1
    if self.asteroid_time < ASTEROID_INTERVAL:
        return
    self.asteroid_time = 0

    # Limit asteroid count
    if ASTEROID_LIMIT < len(self.asteroids):
        return

    # Spawn asteroid
    x = random.random() * W
    y = 0
    spd = random.uniform(ASTEROID_SPD_MIN, ASTEROID_SPD_MAX)
    deg = random.uniform(ASTEROID_DEG_MIN, ASTEROID_DEG_MAX)
    asteroid = sprite.AsteroidSprite(x, y)
    asteroid.move(spd, deg)
    self.asteroids.append(asteroid)

def main():
    """Main entry point."""
    Game()

if __name__ == "__main__":
    main()

결과

게임을 실행하면 다음과 같은 결과가 나타납니다:

게임 스크린샷

다음 장

읽어 주셔서 감사합니다!

다음 장에서는 게임 오버 감지를 구현할 것입니다.

계속 기대해 주세요!!

Back to Blog

관련 글

더 보기 »