DEV Community

Kajiru
Kajiru

Posted on

Getting Started with 2D Games Using Pyxel (Part 16): Vampire Shooting Game (Sample)

Vampire Shooting Game (Sample)

In this article, I’ll introduce a simple game where you try to survive while escaping from approaching monsters.

(This article only provides sample code.)

1. Prepare the Assets

Use Pyxel Editor to draw the background assets.

Download the resource file from the link below

(Click the Download button in the top-right corner):

Sample Code

Below is the complete sample code used in this game.

# sprite.py
import pyxel
import math
import random

class BaseSprite:

    def __init__(self, x, y, u, v, spd, w=8, h=8):
        """ Constructor """
        self.x = x
        self.y = y
        self.u = u
        self.v = v
        self.spd = spd
        self.w = w
        self.h = h
        self.vx = 0
        self.vy = 0
        self.right_flg = True  # Facing right flag

    def update(self):
        """ Update """
        self.x += self.vx
        self.y += self.vy

    def draw(self):
        """ Draw """
        u = 0 if self.right_flg else 8
        pyxel.blt(
            self.x, self.y, 0,
            self.u + u, self.v, self.w, self.h, 0
        )

    def get_center(self):
        """ Get center position """
        return (self.x + self.w / 2, self.y + self.h / 2)

    def set_center(self, x, y):
        """ Set center position """
        self.x = x - self.w / 2
        self.y = y - self.h / 2

    def move(self, deg):
        """ Move by angle """
        rad = (deg * math.pi) / 180
        self.vx = self.spd * math.cos(rad)
        self.vy = self.spd * math.sin(rad)
        if deg == 90 or deg == 270:
            return
        self.right_flg = not (90 < deg < 270)

    def stop(self):
        """ Stop movement """
        self.vx = 0
        self.vy = 0

    def intersects(self, other):
        """ Rectangle collision (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

    def contains_center(self, other):
        """ Check if center point is inside another rectangle """
        x, y = self.get_center()
        if other.x + other.w < x:
            return False
        if x < other.x:
            return False
        if other.y + other.h < y:
            return False
        if y < other.y:
            return False
        return True

    def get_distance(self, other):
        """ Calculate distance """
        dx = self.x - other.x
        dy = self.y - other.y
        return math.sqrt(dx * dx + dy * dy)

    def get_direction(self, other):
        """ Calculate direction angle """
        dx = other.x - self.x
        dy = other.y - self.y
        rad = math.atan2(dy, dx)
        return (rad * (180 / math.pi) + 360) % 360


class PlayerSprite(BaseSprite):

    def __init__(self, x, y, u, v, spd, game):
        """ Constructor """
        super().__init__(x, y, u, v, spd)
        self.shot_interval = 12
        self.shot_counter = 0
        self.game = game

    def update(self):
        """ Update """
        super().update()
        self.shot_counter += 1
        if self.shot_interval < self.shot_counter:
            self.shot_counter = 0
            self.game.on_shot_event(self)


class Monster(BaseSprite):

    def __init__(self, x, y, u, v, spd, think_interval, target):
        """ Constructor """
        super().__init__(x, y, u, v, spd)
        self.think_interval = think_interval
        self.think_counter = 0
        self.target = target

    def update(self):
        """ Update """
        super().update()
        self.think_counter += 1
        if self.think_interval < self.think_counter:
            self.think_counter = 0
            direction = self.get_direction(self.target)
            self.move(direction)


class Bullet(BaseSprite):

    def __init__(self, x, y, spd, life=10):
        """ Constructor """
        super().__init__(x, y, 0, 0, spd)
        self.life = life

    def update(self):
        """ Update """
        super().update()
        self.life -= 1
        if self.life < 0:
            self.stop()

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

    def is_dead(self):
        """ Dead flag """
        return self.life < 0


class Particle(BaseSprite):

    def __init__(self, x, y, life=10):
        """ Constructor """
        super().__init__(x, y, 0, 0, 0)
        self.life = life
        self.area_r = 8
        self.circ_r = 0
        self.off_x = 0
        self.off_y = 0

    def update(self):
        """ Update """
        super().update()
        self.life -= 1
        if self.life < 0:
            self.stop()
        self.circ_r = random.randint(2, 4)
        self.off_x = random.randint(0, self.area_r) - self.area_r / 2
        self.off_y = random.randint(0, self.area_r) - self.area_r / 2

    def draw(self):
        """ Draw """
        pyxel.circ(
            self.x + self.off_x,
            self.y + self.off_y,
            self.circ_r,
            7
        )

    def is_dead(self):
        """ Dead flag """
        return self.life < 0
Enter fullscreen mode Exit fullscreen mode
# main.py
import pyxel
import random
import sprite

W, H = 160, 120

START_X = W / 2
START_Y = H / 2 - 10

MODE_TITLE = "title"
MODE_PLAY = "play"
MODE_GAME_OVER = "game_over"

PLAYER_SPD = 1.2
BULLET_SPD = 2.4

MONSTERS = [
    {"u": 32, "v": 72, "spd": 0.1,  "think_interval": 60},
    {"u": 48, "v": 72, "spd": 0.12, "think_interval": 90},
    {"u":  0, "v": 80, "spd": 0.14, "think_interval": 120},
    {"u": 16, "v": 80, "spd": 0.16, "think_interval": 150},
    {"u": 32, "v": 80, "spd": 0.25, "think_interval": 180},
]


class Game:
    def __init__(self):
        """ Constructor """
        self.score = 0
        self.game_mode = MODE_TITLE
        self.player = sprite.PlayerSprite(
            START_X, START_Y, 0, 72, PLAYER_SPD, self
        )
        self.reset()
        pyxel.init(W, H, title="Hello, Pyxel!!")
        pyxel.load("vampire.pyxres")
        pyxel.run(self.update, self.draw)

    def update(self):
        """ Update """
        self.control()
        if self.game_mode != MODE_PLAY:
            return

        self.player.update()
        self.overlap_area(self.player)

        for monster in self.monsters[:]:
            monster.update()
            self.overlap_area(monster)
            if self.player.contains_center(monster):
                self.player.stop()
                self.game_mode = MODE_GAME_OVER
                pyxel.play(0, 16, loop=False)

        for bullet in self.bullets[:]:
            bullet.update()
            if bullet.is_dead():
                self.bullets.remove(bullet)
            else:
                for monster in self.monsters[:]:
                    if monster.intersects(bullet):
                        self.bullets.remove(bullet)
                        self.append_particle(monster)
                        self.kick_area(monster)
                        self.score += 10
                        break

        for particle in self.particles[:]:
            particle.update()
            if particle.is_dead():
                self.particles.remove(particle)

    def draw(self):
        """ Draw """
        pyxel.cls(0)
        pyxel.bltm(0, 0, 0, 0, 128, 192, 128, 0)
        self.player.draw()

        for monster in self.monsters:
            monster.draw()

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

        for particle in self.particles:
            particle.draw()

        if self.game_mode == MODE_TITLE:
            msg = "SPACE TO PLAY"
            pyxel.text(W / 2 - len(msg) * 2, H / 2, msg, 7)
            msg = "CONTROL: WASD"
            pyxel.text(W / 2 - len(msg) * 2, H / 2 + 10, msg, 7)
        elif self.game_mode == MODE_GAME_OVER:
            msg = "GAME OVER"
            pyxel.text(W / 2 - len(msg) * 2, H / 2, msg, 7)

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

    def reset(self):
        """ Reset stage """
        self.player.x = START_X
        self.player.y = START_Y

        self.monsters = []
        for _ in range(64):
            x = random.randint(0, W)
            y = random.randint(0, H)
            item = random.choice(MONSTERS)
            monster = sprite.Monster(
                x, y,
                item["u"],
                item["v"],
                item["spd"],
                item["think_interval"],
                self.player
            )
            if monster.get_distance(self.player) < 24:
                continue
            monster.move(monster.get_direction(self.player))
            self.monsters.append(monster)

        self.bullets = []
        self.particles = []

    def control(self):
        """ Control """
        if pyxel.btnp(pyxel.KEY_SPACE):
            if self.game_mode == MODE_TITLE:
                self.game_mode = MODE_PLAY
            elif self.game_mode == MODE_GAME_OVER:
                self.game_mode = MODE_TITLE
                self.reset()
            return

        if self.game_mode == MODE_PLAY:
            if pyxel.btnp(pyxel.KEY_W):
                self.player.move(270)
            elif pyxel.btnr(pyxel.KEY_W):
                self.player.stop()
            if pyxel.btnp(pyxel.KEY_A):
                self.player.move(180)
            elif pyxel.btnr(pyxel.KEY_A):
                self.player.stop()
            if pyxel.btnp(pyxel.KEY_S):
                self.player.move(90)
            elif pyxel.btnr(pyxel.KEY_S):
                self.player.stop()
            if pyxel.btnp(pyxel.KEY_D):
                self.player.move(0)
            elif pyxel.btnr(pyxel.KEY_D):
                self.player.stop()

    def overlap_area(self, spr):
        """ Screen wrap """
        if W < spr.x:
            spr.x = 0
        if spr.x < 0:
            spr.x = W
        if H < spr.y:
            spr.y = 0
        if spr.y < 0:
            spr.y = H

    def kick_area(self, spr):
        """ Force move to screen edge """
        if random.randint(0, 1) == 0:
            spr.set_center(0, random.randint(0, H))
        else:
            spr.set_center(random.randint(0, W), 0)
        pyxel.play(1, 1, loop=False)

    def get_nearest_monster(self):
        """ Get nearest monster """
        nearest = None
        dist_min = 999
        for monster in self.monsters:
            dist = monster.get_distance(self.player)
            if dist < dist_min:
                dist_min = dist
                nearest = monster
        return nearest

    def on_shot_event(self, spr):
        """ Fire bullet """
        if not self.monsters:
            return
        x, y = spr.get_center()
        bullet = sprite.Bullet(x, y, BULLET_SPD)
        monster = self.get_nearest_monster()
        bullet.move(self.player.get_direction(monster))
        self.bullets.append(bullet)

    def append_particle(self, spr):
        """ Spawn particle """
        x, y = spr.get_center()
        self.particles.append(sprite.Particle(x, y))


def main():
    """ Main entry """
    Game()


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

Result

The game will run like this:

Final Words

Thank you very much for reading.

I hope this series helps you get started with game development. ޱ(ఠ皿ఠ)ว

(If you enjoyed this article, a 👍 would be greatly appreciated!)

Top comments (0)