Getting Started with 2D Games Using Pyxel (Part 8): Spawning Asteroids




Spawn Asteroids

In this chapter, we implement logic to spawn asteroids at regular intervals.

Here, we’ll build more game-like systems such as:

  • Managing multiple sprites
  • Executing actions at fixed time intervals

The complete code is shown at the end of this chapter.



1. Create an Asteroid Sprite

First, let’s prepare a sprite class for asteroids.

Add a new AsteroidSprite class to sprite.py.

In its constructor, we use a random value to select which asteroid image to display.

# 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
Enter fullscreen mode

Exit fullscreen mode

With this setup, each asteroid will have a different appearance

every time it is generated.



2. Prepare Constants and Variables

Next, add asteroid-related constants to main.py.

By grouping these values as constants,

it becomes much easier to adjust the game difficulty later.

# main.py
# add constants

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
Enter fullscreen mode

Exit fullscreen mode

Next, add variables for managing asteroids

to the constructor of the Game class.

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

# Asteroids
self.asteroid_time = 0   # Spawn interval counter
self.asteroids = []      # Asteroid list
Enter fullscreen mode

Exit fullscreen mode



3. Asteroid Spawning Logic

Add a check_interval() method to the Game class.

This method spawns asteroids at fixed intervals.

Asteroids appear every ASTEROID_INTERVAL frames.

The total number of asteroids is limited by ASTEROID_LIMIT.

The spawn position is randomly chosen along the top edge of the screen.

Speed and angle are also randomly selected within predefined ranges.

# main.py
# add to Game class

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)
Enter fullscreen mode

Exit fullscreen mode



4. Updating and Drawing Asteroids

Next, add update and draw logic for asteroids.

In the update() method of the Game class,

handle asteroid spawning and updating.

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

self.check_interval()  # Spawn asteroids

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

Exit fullscreen mode

In the draw() method,

draw all asteroids at once.

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

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

Exit fullscreen mode

Now, multiple asteroids will flow across the screen.



Complete Code

Below is the complete code implementing all the features in this chapter.

# 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
Enter fullscreen mode

Exit fullscreen mode

# 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

# 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()
Enter fullscreen mode

Exit fullscreen mode

When you run the game, it looks like this.



Next Time…

Thank you for reading!

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

Stay tuned!



Source link