Pyxel을 사용한 2D 게임 시작하기 (Part 8): 소행성 생성

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

Source: Dev.to

1. 소행성 스프라이트 만들기

먼저, 소행성을 위한 스프라이트 클래스를 준비합니다. **sprite.py**에 새로운 AsteroidSprite 클래스를 추가합니다. 생성자에서는 무작위 값을 사용하여 표시할 소행성 이미지를 선택합니다.

# sprite.py
# add this class

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"""
        pyxel.blt(
            self.x, self.y, 0,
            self.w * self.index, 0,
            self.w, self.h, 0
        )  # Asteroid

이 설정으로, 소행성은 생성될 때마다 서로 다른 외형을 갖게 됩니다.

2. 상수와 변수 준비

다음으로, 소행성 관련 상수를 **main.py**에 추가합니다. 이러한 값을 상수로 묶어두면 나중에 게임 난이도를 조정하기가 훨씬 쉬워집니다.

# main.py
# add constants

ASTEROID_INTERVAL = 20   # Spawn interval (frames)
ASTEROID_LIMIT    = 30   # Maximum number of asteroids

ASTEROID_SPD_MIN = 1.0   # Minimum speed
ASTEROID_SPD_MAX = 2.0   # Maximum speed
ASTEROID_DEG_MIN = 30    # Minimum angle
ASTEROID_DEG_MAX = 150   # Maximum angle

Game 클래스의 생성자에 소행성을 관리하기 위한 변수를 추가합니다.

# main.py
# add to Game.__init__()

# Asteroids
self.asteroid_time = 0   # Spawn interval counter
self.asteroids = []      # List of asteroids

3. 소행성 생성 로직

Game 클래스에 check_interval() 메서드를 추가합니다. 이 메서드는 고정된 간격(매 ASTEROID_INTERVAL 프레임)마다 소행성을 생성합니다. 소행성의 총 개수는 ASTEROID_LIMIT으로 제한됩니다. 생성 위치는 화면 상단 가장자리에서 무작위로 선택되며, 속도와 각도도 미리 정의된 범위 내에서 무작위로 선택됩니다.

# main.py
# add to Game class

def check_interval(self):
    # Asteroid spawn interval
    self.asteroid_time += 1
    if self.asteroid_time = ASTEROID_LIMIT:
        return

    # Add a new 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)

4. 소행성 업데이트 및 그리기

업데이트

Game 클래스의 update() 메서드에서 소행성 생성 및 업데이트를 처리합니다.

# main.py
# add to Game.update()

self.check_interval()  # Spawn asteroids

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

그리기

draw() 메서드에서 모든 소행성을 그립니다.

# main.py
# add to Game.draw()

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

이제 여러 개의 소행성이 화면을 가로질러 흐르게 됩니다.


전체 코드

아래는 이 장에서 다루는 모든 기능을 구현한 전체 코드입니다.

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"""
        self.x += self.vx
        self.y += self.vy

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

    def move(self, spd, deg):
        """Move"""
        rad = deg * math.pi / 180
        self.vx = spd * math.cos(rad)  # x‑axis velocity
        self.vy = spd * math.sin(rad)  # y‑axis velocity

    def flip_x(self):
        """Flip movement in the x direction"""
        self.vx *= -1

class ShipSprite(BaseSprite):

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

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

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"""
        pyxel.blt(
            self.x, self.y, 0,
            self.w * self.index, 0,
            self.w, self.h, 0
        )  # Asteroid

main.py

import pyxel
import math
import random
import sprite

W, H = 160, 120
SHIP_SPD = 1.4

ASTEROID_INTERVAL = 20   # Spawn interval
ASTEROID_LIMIT    = 30   # Maximum number of asteroids

ASTEROID_SPD_MIN = 1.0   # Minimum speed
ASTEROID_SPD_MAX = 2.0   # Maximum speed
ASTEROID_DEG_MIN = 30    # Minimum angle
ASTEROID_DEG_MAX = 150   # Maximum angle

class Game:
    def __init__(self):
        pyxel.init(W, H, caption="Asteroid Shooter")
        self.ship = sprite.ShipSprite(W // 2, H - 20)

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

    def check_interval(self):
        # Asteroid spawn interval
        self.asteroid_time += 1
        if self.asteroid_time = ASTEROID_LIMIT:
            return

        # Add a new 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 overlap_spr(self, asteroid):
        # Placeholder for collision detection logic
        pass

    def update(self):
        # Ship movement (example)
        if pyxel.btn(pyxel.KEY_LEFT):
            self.ship.x -= SHIP_SPD
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.ship.x += SHIP_SPD

        self.check_interval()  # Spawn asteroids

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

    def draw(self):
        pyxel.cls(0)

        # Draw ship
        self.ship.draw()

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

Game()
pyxel.run(Game().update, Game().draw)

Source:

최대 각도

# 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   # Spawn interval counter
        self.asteroids = []      # Asteroid list

        # 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)

    def draw(self):
        """Draw"""
        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()

    def control_ship(self):
        """Action"""
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.ship.flip_x()  # Reverse movement

    def overlap_spr(self, spr):
        """Move to the opposite side when leaving the screen"""
        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):
        # Asteroid spawn interval
        self.asteroid_time += 1
        if self.asteroid_time < ASTEROID_INTERVAL:
            return
        self.asteroid_time = 0

        # Do not exceed the maximum number of asteroids
        if ASTEROID_LIMIT < len(self.asteroids):
            return

        # Add a new 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"""
    Game()

if __name__ == "__main__":
    main()

전체 화면 모드로 전환

전체 화면 모드 종료

게임을 실행하면 다음과 같이 보입니다:

Game screenshot

Next Time…

Thank you for reading!

In the next chapter, we’ll cover “Let’s Fire Bullets.”

Stay tuned!

Back to Blog

관련 글

더 보기 »