DEV Community

Kajiru
Kajiru

Posted on

Getting Started with 2D Games Using Pyxel (Part 10): Implement Collision Detection

Implement Collision Detection

In this chapter, we will implement collision detection between bullets and asteroids.

The complete code is provided at the end.

1. Implement Collision Detection Logic

First, add a new intersects() method to the BaseSprite class in sprite.py.

This method checks whether the sprite overlaps with another sprite passed as an argument.

It returns True if they are colliding, and False otherwise.

(This implementation uses the AABB concept, but you don’t need to dive deep into it for now.)

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

2. Bullet and Asteroid Collisions

Next, we check for collisions between bullets and asteroids during the bullet update phase.

As before, since we remove elements from the list while iterating,

we traverse the list in reverse order to avoid index issues.

To keep the logic simple, we immediately return from the update() method

once a collision is detected.

When a collision occurs, the score is incremented by 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 x 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
Enter fullscreen mode Exit fullscreen mode

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

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

# 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 x asteroid)
            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 """
        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):
        """ 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 sprite around 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 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()
Enter fullscreen mode Exit fullscreen mode

Result

Running the game produces the following result:

Next Chapter

Thank you for reading!

In the next chapter, we’ll implement game over detection.

Stay tuned!!

Top comments (0)