Tutorial: Red Baron#

In this chapter we will build a Side-Scrolling shoter together, step by step.

The techniques of creating a parallax backgrounds, managing speed and velocity and generating enemies is common in games and after seeing it here you should be able to incorporate it into your own projects.

  • Based on: https://github.com/kantel/pygamezero/tree/master/tappyplane

  • License: Attribution-NonCommercial-ShareAlike 4.0 International

##b Step 1: Create a base framework

Create a base framework: You need a world on which actors can be placed.

Your last line must be world.run().

from miniworlds import *
world = World(800, 480)

// your code here

world.run()

Prepare your folder#

You must place images for backgrounds, player and enemies in a directory images inside your code directory.

my_code
|
|--images
|----images/planered1.png
|----images/background.png
|----images/groundgrass.png
|----images/shipbeige.png
|----images/shipblue.png
|----images/shipgreen.png
|----images/shippink.png
|----images/shipyellow.png

(You can find the images in this repository: miniworlds-cookbook - red baron )

Create backgrounds#

With the following code, you can generate two backgrounds.

You need two backgrounds to generate a infinite scrolling-effect:

Generate this background next to each other so they are filling the complete screen:

back0 = Actor()
back0.add_costume("background")
back0.size = world.width, world.height
back1 = Actor(world.width, 0)
back1.size = world.width, world.height
back1.add_costume("background")
backs = [back0, back1]

Now we animate the backgrounds:

  • Both backgrounds move constantly from left to right.

  • if a background leaves the left side of screen, it will be be moved to the right side.

@world.register
def act(self):
    for back in backs:
        back.x -= 1
        if back.x <= - world.width:
            back.x = world.width
    for ground in grounds:
        ground.x -= 2
        if ground.x <= - world.width:
            ground.x = world.width

This will generate a *infinite scrolling background.

Step 2: Create a plane class#

Create a plane class#

Create a plane class as template for your player.

class Plane(Actor):
    def on_setup(self):
        self.add_costume("planered1")

Create a plane instance#

At the end of your code before world.run() create an instance of this template-class:

plane = Plane(100, world.height / 2)

Add physics#

Now we add physics to the plane-Actor.

Modify the on_setup()-Method of Plane:

    def on_setup(self):
        self.add_costume("planered1")
        self.gravity = 0.1
        self.velocity_y = 0
  • velocity describes the current velocity. The Plane will move velocity steps in y-direction.

  • gravity describes the current gravity: The y-velocity of player will be reduced by gravity in every frame.

Simulate physics#

Physics can be simulated in the act()-Method of Actor:

    def act(self):
        self.velocity_y += self.gravity
        self.velocity_y *= 0.9 # friction
        self.y += self.velocity_y

This will add the velocity to the y-coordinates of player. The gravity constantly reduces the velocity.

Line 3 will smoothen the simulation.

Add force on key press#

You must use the on_key_down event to add a force to the actor:

    def on_key_down_w(self):
        self.velocity_y -= 5

Step 3: Add enemies#

Import randint and choice for randomization at the beginning of your code:

from random import randint, choice

Add an enemy class#

Add an enemy class as template:

class Enemy(Actor):
    
    def on_setup(self):
        enemy.add_costume(choice(enemyships))

    def reset(self):
        self.x = randint(world.width + 50, world.width + 500)
        self.y = randint(25, world.height - 85)  

The on_setup-Method adds a random costume to the actor.

Add enemy instances from class-template#

Add mutliple enemies to the world with a for-loop at the end of your code before world.run().

enemies = []
for _ in range(10):
    enemy = Enemy()
    enemy.reset()
    enemies.append(enemy)

Move enemies#

Modify the on_setup-Method:

def on_setup(self):
    self.add_costume(choice(enemyships))
    self.speed = -1.5

self.speed defines how many steps the Enemy-Actor will move in x-direction each frame.

Add an act()-Method:

def act(self):
    self.x += self.speed
    if self.x <= -self.width:
        self.reset()

Step 3: Add shooting#

Add a Bullet-Class to your Actor:

class Bullet(Actor):
    def on_setup(self):
        self.add_costume("laserred")
        self.x = plane.x
        self.y = plane.y
        self.speed = 25
        self.fire = False
    
    def act(self):
        self.x += self.speed

    def on_detecting_enemy(self, enemy):
        enemy.reset()
        
    def on_detecting_not_on_world(self):
        self.remove()

The Bullet has sensors - With the method on_detecting_enemy it can detect all Actors with of class `Enemey’ and call their reset()-Methods.

With on_detecting_not_on_world it an detect, if Bullet is on the world.

Complete Code:#

Red Baron

from miniworlds import *
from random import randint, choice

# based on https://github.com/kantel/pygamezero/tree/master/tappyplane

world = World(800, 480)

left = world.width / 2
bottom = world.height / 2
bottomground = world.height - 35

no_enemies = 10
enemyships = ["shipbeige", "shipblue", "shipgreen", "shippink", "shipyellow"]

# Add backgrounds
back0 = Actor()
back0.add_costume("background")
back0.size = world.width, world.height
back1 = Actor(world.width, 0)
back1.size = world.width, world.height
back1.add_costume("background")
backs = [back0, back1]

ground0 = Actor((0, bottomground))
ground0.add_costume("groundgrass")
ground0.width = world.width 
ground0.costume.is_scaled = True
ground1 = Actor((world.width, bottomground))
ground1.add_costume("groundgrass")
ground1.width = world.width
ground1.costume.is_scaled = True
grounds = [ground0, ground1]


groundlevel = world.height - 85

@world.register
def act(self):
    for back in backs:
        back.x -= 1
        if back.x <= - world.width:
            back.x = world.width
    for ground in grounds:
        ground.x -= 2
        if ground.x <= - world.width:
            ground.x = world.width

class Plane(Actor):
    def on_setup(self):
        self.add_costume("planered1")
        self.gravity = 0.1
        self.velocity_y = 0
        self.fire = False

    def act(self):
        self.velocity_y += self.gravity
        self.velocity_y *= 0.9 # friction
        self.y += self.velocity_y
        if self.y >= groundlevel:    
            self.y = groundlevel
            self.velocity_y = 0
        if self.y <= 20:             
            self.y = 20
            self.velocity_y = 0

    def on_key_down_w(self):
        self.velocity_y -= 5
        
    def on_key_down_d(self):
        if not self.fire:
            self.fire = True
            bullet = Bullet()
            @timer(frames=30)
            def downtime():
                self.fire = False
      
class Bullet(Actor):
    def on_setup(self):
        self.add_costume("laserred")
        self.x = plane.x
        self.y = plane.y
        self.speed = 25
        self.fire = False
    
    def act(self):
        self.x += self.speed

    def on_detecting_enemy(self, enemy):
        enemy.reset()
        
    def on_detecting_not_on_world(self):
        self.remove()
        
class Enemy(Actor):
    
    def on_setup(self):
        self.add_costume(choice(enemyships))
        self.speed = -1.5

    def reset(self):
        self.x = randint(world.width + 50, world.width + 500)
        self.y = randint(25, groundlevel)    

    def act(self):
        self.x += self.speed
        if self.x <= -self.width:
            self.reset()

plane = Plane((100, world.height / 2))

enemies = []
for _ in range(no_enemies):
    enemy = Enemy()
    enemy.reset()
    enemies.append(enemy)

        
world.run()