DEV Community

Kajiru
Kajiru

Posted on

Getting Started with 2D Games Using Pyxel (Part 9): Shooting Bullets

Shooting Bullets

In this chapter, we will implement bullet firing from the player sprite.

Here, we will build the core mechanics of a shooting game, including:

  • Adding a new sprite (bullets)
  • Managing multiple bullets using a list
  • Removing bullets that are no longer needed

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

1. Creating a Bullet Sprite

As in the previous chapter, add a new BulletSprite class to sprite.py.

This time, instead of using an image, we will draw a simple 2×2 square using pyxel.rect().
This is more than enough to represent small bullets.

# sprite.py (add this class)

class BulletSprite(BaseSprite):

    def __init__(self, x, y):
        """ Constructor """
        super().__init__(x, y)
        self.x += self.w / 2 - 1  # Adjust to center

    def draw(self):
        """ Draw processing """
        pyxel.rect(self.x, self.y, 2, 2, 12)
Enter fullscreen mode Exit fullscreen mode

2. Defining Constants

Next, add a constant for bullet speed in main.py.

By defining it as a constant, you can easily tweak the bullet speed later.

# main.py (add a constant)

BULLET_SPD = 3  # Bullet speed
Enter fullscreen mode Exit fullscreen mode

3. Firing Bullets

Now, let’s add the logic for firing bullets.

In this sample, bullets are fired when the player presses the space key—the same timing used to flip the player’s direction left and right.

Bullets will move straight upward (270 degrees).

# main.py (add to the Game class method `control_ship()`)

def control_ship(self):
    """ Action """
    if pyxel.btnp(pyxel.KEY_SPACE):
        self.ship.flip_x()  # Reverse movement
        # Fire a bullet
        bullet = sprite.BulletSprite(self.ship.x, self.ship.y)
        bullet.move(BULLET_SPD, 270)
        self.bullets.append(bullet)
Enter fullscreen mode Exit fullscreen mode

4. Updating and Drawing Bullets

Next, add the update logic for bullets.

In the update() method of the Game class, update all bullets and remove those that go off-screen.

Because elements are removed during iteration, the list is processed in reverse order.
This prevents bugs caused by index shifting.

# main.py (add to the Game class `update()` method)

# Update bullets (reverse order)
for bullet in self.bullets[::-1]:
    bullet.update()
    # Remove bullets that go off-screen
    if bullet.y < 0:
        self.bullets.remove(bullet)
        continue
Enter fullscreen mode Exit fullscreen mode

Then, draw all bullets together in the draw() method.

# main.py (add to the Game class `draw()` method)

# Draw bullets
for bullet in self.bullets:
    bullet.draw()
Enter fullscreen mode Exit fullscreen mode

With this, the player can now shoot bullets.

Complete Code

Below is the complete code with all features implemented so far.

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

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

    def move(self, spd, deg):
        """ Move """
        rad = deg * math.pi / 180
        self.vx = spd * math.cos(rad)  # X-axis speed
        self.vy = spd * math.sin(rad)  # Y-axis speed

    def flip_x(self):
        """ Flip movement on the X-axis """
        self.vx *= -1

class ShipSprite(BaseSprite):

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

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

class BulletSprite(BaseSprite):

    def __init__(self, x, y):
        """ Constructor """
        super().__init__(x, y)
        self.x += self.w / 2 - 1  # Adjust to center

    def draw(self):
        """ Draw processing """
        pyxel.rect(self.x, self.y, 2, 2, 12)
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
ASTEROID_LIMIT = 30

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

BULLET_SPD = 3  # Bullet speed

# 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 processing """

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

        self.check_interval()  # Add 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()
            if bullet.y < 0:
                self.bullets.remove(bullet)
                continue

    def draw(self):
        """ Draw processing """
        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):
        """ Action """
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.ship.flip_x()  # Reverse movement
            # Fire a 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 around 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
        # Limit the number of asteroids
        if ASTEROID_LIMIT < len(self.asteroids):
            return
        # Add an 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()
Enter fullscreen mode Exit fullscreen mode

When you run this program, the result will look like this:

Coming Up Next...

Thank you for reading!
The next chapter is titled “Implementing Collision Detection.”

Stay tuned!!

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.