DEV Community

Kajiru
Kajiru

Posted on

Getting Started with 2D Games Using Pyxel (Part 13): Creating a Scrolling Star Background

Creating a Scrolling Star Background

In this article, we will create a scrolling star background to make the game screen more lively.

Here, we focus on introducing sample code only.

The complete source code is provided at the end.

1. Creating Classes to Draw Stars

In sprite.py, we define a Star class for drawing individual stars, and a Background class that updates and draws all stars in the background.

# sprite.py (excerpt)
class Star:

    def __init__(self, x, y, w, h):
        """ Constructor """
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.c = random.randint(0, 15)  # Color
        self.spd = random.randint(1, 3) # Falling speed

    def update(self):
        """ Update process """
        self.y += self.spd
        if self.h < self.y:
            self.y = 0

    def draw(self):
        """ Draw process """
        pyxel.pset(self.x, self.y, self.c)

class Background:

    def __init__(self, w, h):
        """ Constructor """
        self.w = w  # Screen width
        self.h = h  # Screen height
        self.stars = []  # List to manage stars
        for _ in range(30):
            x = random.randint(0, w)
            y = random.randint(0, h)
            star = Star(x, y, self.w, self.h)
            self.stars.append(star)

    def update(self):
        """ Update process """
        for star in self.stars:
            star.update()

    def draw(self):
        """ Draw process """
        for star in self.stars:
            star.draw()
Enter fullscreen mode Exit fullscreen mode

2. Using the Background Class

First, create a Background instance in the constructor of the Game class in main.py.

# main.py (inside Game.__init__)
# Background
self.background = sprite.Background(W, H)
Enter fullscreen mode Exit fullscreen mode

Next, call the update process inside update().

# main.py (inside Game.update)
# Background
self.background.update()
Enter fullscreen mode Exit fullscreen mode

Finally, call the draw process inside draw().

# main.py (inside Game.draw)
# Background
self.background.draw()
Enter fullscreen mode Exit fullscreen mode

By grouping related processes into classes like this, you can keep your code clean and easy to read.

Complete Code

Below is the complete code implementing all the features introduced in this article.

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

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

    def move(self, spd, deg):
        """ Move sprite """
        rad = deg * math.pi / 180
        self.vx = spd * math.cos(rad)
        self.vy = spd * math.sin(rad)

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

    def intersects(self, other):
        """ AABB collision detection """
        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 process """
        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 process """
        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 adjustment

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

class Star:

    def __init__(self, x, y, w, h):
        """ Constructor """
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.c = random.randint(0, 15)
        self.spd = random.randint(1, 3)

    def update(self):
        """ Update process """
        self.y += self.spd
        if self.h < self.y:
            self.y = 0

    def draw(self):
        """ Draw process """
        pyxel.pset(self.x, self.y, self.c)

class Background:

    def __init__(self, w, h):
        """ Constructor """
        self.w = w  # Screen width
        self.h = h  # Screen height
        self.stars = []  # List of stars
        for _ in range(30):
            x = random.randint(0, w)
            y = random.randint(0, h)
            star = Star(x, y, self.w, self.h)
            self.stars.append(star)

    def update(self):
        """ Update process """
        for star in self.stars:
            star.update()

    def draw(self):
        """ Draw process """
        for star in self.stars:
            star.draw()
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

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

        # Game over flag
        self.game_over_flg = False

        # Initialize score
        self.score = 0

        # Background
        self.background = sprite.Background(W, H)

        # 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 = []

        # Initialize Pyxel
        pyxel.init(W, H, title="Hello, Pyxel!!")
        pyxel.load("shooter.pyxres")

        # Sound (shot)
        pyxel.sound(0).set(
            "c4g3",
            tones="p",
            volumes="76",
            effects="f",
            speed=20
        )

        # Sound (hit)
        pyxel.sound(1).set(
            "e4d4c4",
            tones="p",
            volumes="76",
            effects="n",
            speed=20
        )

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

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

        if self.game_over_flg:
            return

        # Background
        self.background.update()

        # Player
        self.ship.update()
        self.control_ship()
        self.overlap_spr(self.ship)

        self.check_interval()

        # Asteroids
        for asteroid in self.asteroids:
            asteroid.update()
            self.overlap_spr(asteroid)
            if asteroid.intersects(self.ship):
                self.game_over_flg = True

        # Bullets (reverse order)
        for bullet in self.bullets[::-1]:
            bullet.update()
            if bullet.y < 0:
                self.bullets.remove(bullet)
                continue
            for asteroid in self.asteroids[::-1]:
                if asteroid.intersects(bullet):
                    self.score += 1
                    self.bullets.remove(bullet)
                    self.asteroids.remove(asteroid)
                    pyxel.play(1, 1)
                    return

    def draw(self):
        """ Draw process """
        pyxel.cls(0)

        # Background
        self.background.draw()

        if self.game_over_flg:
            msg = "GAME OVER"
            pyxel.text(W / 2 - len(msg) * 2, H / 2, msg, 13)

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

        self.ship.draw()

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

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

    def control_ship(self):
        """ Player action """
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.ship.flip_x()
            bullet = sprite.BulletSprite(self.ship.x, self.ship.y)
            bullet.move(BULLET_SPD, 270)
            self.bullets.append(bullet)
            pyxel.play(0, 0)

    def overlap_spr(self, spr):
        """ Wrap 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):
        self.asteroid_time += 1
        if self.asteroid_time < ASTEROID_INTERVAL:
            return
        self.asteroid_time = 0
        if ASTEROID_LIMIT < len(self.asteroids):
            return
        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 process """
    Game()

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Result

The game will look like this when running.

Final Words

Thank you for reading until the end.

I hope this series becomes a trigger for starting game development.

If you liked it, a 👍 would be greatly appreciated!

Top comments (0)