Step 4 - Creating a Level#

In this step, we will be creating a small level for our player to run in.

We will build our level out of basic rectangle hitboxes. To get them to draw, we'll specify a fill color in their constructor.

First let's set a variable for the level size. This will be the width of the level; 120% the resolution of the screen in this case. Note that it needs to be an integer, because it represents the width of the level in pixels.

shared.py#
1import rubato as rb
2from player_controller import PlayerController
3
4##### MISC #####
5
6level1_size = int(rb.Display.res.x * 1.2)

Along with that lets add some nice colors to our shared.py file.

shared.py#
 1import rubato as rb
 2from player_controller import PlayerController
 3
 4##### MISC #####
 5
 6level1_size = int(rb.Display.res.x * 1.2)
 7
 8##### COLORS #####
 9
10platform_color = rb.Color.from_hex("#b8e994")
11background_color = rb.Color.from_hex("#82ccdd")
12win_color = rb.Color.green.darker(75)

The darker() function allows us to darken a color by an amount. It simply subtracts that amount from each of the red, green, and blue color channels.

Next, we'll create a new file level1.py to house the elements unique to our level.

level1.py holds a scene with our level in it. All the scene work we did up until now should have really been put in level1.py. So lets make a scene in level1.py and move our scene code there (deleting it from main.py):

level1.py#
1import shared
2import rubato as rb
3
4scene = rb.Scene("level1", background_color=shared.background_color)
5
6# Add the player to the scene
7scene.add(shared.player)

Since we just added a new file, we'll need to import it. Since main.py doesn't need shared.py anymore, simply replace the import shared line in main.py with import level1

Now onto the floor. We create the ground by initializing a GameObject and adding a Rectangle hitbox to it.

level1.py#
 4scene = rb.Scene("level1", background_color=shared.background_color)
 5
 6ground = rb.GameObject().add(
 7    ground_rect := rb.Rectangle(
 8        width=1270,
 9        height=50,
10        color=shared.platform_color,
11        tag="ground",
12    )
13)
14ground_rect.bottom_left = rb.Display.bottom_left

Notice how we used the Rectangle.bottom_left property to place the floor correctly. We also give a tag to our floor, to help us identify it later when the player collides with it.

Also update the scene.add line to add the floor to the scene.

level1.py#
scene.add(shared.player, ground)

You can also change the player gravity to rb.Vector(y=rb.Display.res.y * -1.5), which will make the game more realistic. It should look like this now:

../../../_images/13.png

The process for adding the remaining platforms is the same as what we've just done. Easy! This is a great place to unleash your creativity and make a better level than we did.

Below is a very basic example for the rest of the tutorial.

../../../_images/21.png


Code that made the above level
level1.py#
14ground_rect.bottom_left = rb.Display.bottom_left
15
16end_location = rb.Vector(rb.Display.left + shared.level1_size - 128, 450)
17
18# create platforms
19platforms = [
20    rb.Rectangle(
21        150,
22        40,
23        offset=rb.Vector(-650, -200),
24    ),
25    rb.Rectangle(
26        150,
27        40,
28        offset=rb.Vector(500, 40),
29    ),
30    rb.Rectangle(
31        150,
32        40,
33        offset=rb.Vector(800, 200),
34    ),
35    rb.Rectangle(256, 40, offset=end_location - (0, 64 + 20))
36]
37
38for p in platforms:
39    p.tag = "ground"
40    p.color = shared.platform_color
41
42# create pillars, learn to do it with Game Objects too
43pillars = [
44    rb.GameObject(pos=rb.Vector(-260)).add(rb.Rectangle(
45        width=100,
46        height=650,
47    )),
48    rb.GameObject(pos=rb.Vector(260)).add(rb.Rectangle(
49        width=100,
50        height=400,
51    )),
52]
53
54for pillar in pillars:
55    r = pillar.get(rb.Rectangle)
56    r.bottom = rb.Display.bottom + 50
57    r.tag = "ground"
58    r.color = shared.platform_color

And remember to add everything to the scene.

Tip

wrap() is a rubato helper function that lets us make GameObjects and automatically add components to them in fewer lines of code.

59scene.add(shared.player, ground, wrap(platforms), *pillars)

Now that you have a level built, you may notice that you are currently able to walk or jump out of the frame of the window. Let's fix this by adding an invisible hitbox on either side of the play area.

shared.py#
46player.add(player_comp := PlayerController())
47
48##### SIDE BOUDARIES #####
49left = rb.GameObject(pos=rb.Display.center_left - rb.Vector(25, 0)).add(rb.Rectangle(width=50, height=rb.Display.res.y))
50right = rb.GameObject().add(rb.Rectangle(width=50, height=rb.Display.res.y))
level1.py#
54for pillar in pillars:
55    r = pillar.get(rb.Rectangle)
56    r.bottom = rb.Display.bottom + 50
57    r.tag = "ground"
58    r.color = shared.platform_color
59
60# program the right boundary
61shared.right.pos = rb.Display.center_left + (shared.level1_size + 25, 0)
62
63scene.add(
64    shared.player,
65    ground,
66    rb.wrap(platforms),
67    *pillars,
68    shared.left,
69    shared.right,
70)

Remember!

To not have the hitbox render, don't pass a color to the hitbox! All other functionality will remain untouched.

You'll now notice that the player is unable to fall off the world. This is because the hitbox is blocking its path.

There's one big issue, however. Jumps don't come back, even once you hit the ground. Not to worry. We will implement this in Step 5 - Finishing Touches.

Your code should currently look like this (with your own level of course!):

main.py#
 1import rubato as rb
 2
 3# initialize a new game
 4rb.init(
 5    name="Platformer Demo",  # Set a name
 6    res=(1920, 1080),  # Set the window resolution (in pixels).
 7    fullscreen=True,  # Set the window to be fullscreen
 8)
 9
10import level1
11
12# begin the game
13rb.begin()
shared.py#
 1import rubato as rb
 2from player_controller import PlayerController
 3
 4##### MISC #####
 5
 6level1_size = int(rb.Display.res.x * 1.2)
 7
 8##### COLORS #####
 9
10platform_color = rb.Color.from_hex("#b8e994")
11background_color = rb.Color.from_hex("#82ccdd")
12win_color = rb.Color.green.darker(75)
13
14##### PLAYER PREFAB #####
15
16# Create the player and set its starting position
17player = rb.GameObject(
18    pos=rb.Display.center_left + rb.Vector(50, 0),
19    z_index=1,
20)
21
22# Create animation and initialize states
23p_animation = rb.Spritesheet.from_folder(
24    path="files/dino",
25    sprite_size=rb.Vector(24, 24),
26    default_state="idle",
27)
28p_animation.scale = rb.Vector(4, 4)
29p_animation.fps = 10  # The frames will change 10 times a second
30player.add(p_animation)  # Add the animation component to the player
31
32# define the player rigidbody
33player_body = rb.RigidBody(
34    gravity=rb.Vector(y=rb.Display.res.y * -1.5),  # changed to be stronger
35    pos_correction=1,
36    friction=0.8,
37)
38player.add(player_body)
39
40# add a hitbox to the player with the collider
41player.add(rb.Rectangle(
42    width=64,
43    height=64,
44    tag="player",
45))
46player.add(player_comp := PlayerController())
47
48##### SIDE BOUDARIES #####
49left = rb.GameObject(pos=rb.Display.center_left - rb.Vector(25, 0)).add(rb.Rectangle(width=50, height=rb.Display.res.y))
50right = rb.GameObject().add(rb.Rectangle(width=50, height=rb.Display.res.y))
player_controller.py#
 1import rubato as rb
 2
 3class PlayerController(rb.Component):
 4
 5    def setup(self):
 6        # Called when added to Game Object.
 7        # Specifics can be found in the Custom Components tutorial.
 8        self.initial_pos = self.gameobj.pos.clone()
 9
10        self.animation: rb.Animation = self.gameobj.get(rb.Animation)
11        self.rigid: rb.RigidBody = self.gameobj.get(rb.RigidBody)
12
13        # Tracks the number of jumps the player has left
14        self.jumps = 2
15
16        rb.Radio.listen(rb.Events.KEYDOWN, self.handle_key_down)
17
18    def update(self):
19        # Runs once every frame.
20        # Movement
21        if rb.Input.key_pressed("a"):
22            self.rigid.velocity.x = -300
23            self.animation.flipx = True
24        elif rb.Input.key_pressed("d"):
25            self.rigid.velocity.x = 300
26            self.animation.flipx = False
27
28    def handle_key_down(self, event: rb.KeyResponse):
29        if event.key == "w" and self.jumps > 0:
30            if self.jumps == 2:
31                self.rigid.velocity.y = 800
32                self.animation.set_state("jump", freeze=2)
33            elif self.jumps == 1:
34                self.rigid.velocity.y = 800
35                self.animation.set_state("somer", True)
36            self.jumps -= 1
level1.py#
 1import shared
 2import rubato as rb
 3
 4scene = rb.Scene("level1", background_color=shared.background_color)
 5
 6ground = rb.GameObject().add(
 7    ground_rect := rb.Rectangle(
 8        width=1270,
 9        height=50,
10        color=shared.platform_color,
11        tag="ground",
12    )
13)
14ground_rect.bottom_left = rb.Display.bottom_left
15
16end_location = rb.Vector(rb.Display.left + shared.level1_size - 128, 450)
17
18# create platforms
19platforms = [
20    rb.Rectangle(
21        150,
22        40,
23        offset=rb.Vector(-650, -200),
24    ),
25    rb.Rectangle(
26        150,
27        40,
28        offset=rb.Vector(500, 40),
29    ),
30    rb.Rectangle(
31        150,
32        40,
33        offset=rb.Vector(800, 200),
34    ),
35    rb.Rectangle(256, 40, offset=end_location - (0, 64 + 20))
36]
37
38for p in platforms:
39    p.tag = "ground"
40    p.color = shared.platform_color
41
42# create pillars, learn to do it with Game Objects too
43pillars = [
44    rb.GameObject(pos=rb.Vector(-260)).add(rb.Rectangle(
45        width=100,
46        height=650,
47    )),
48    rb.GameObject(pos=rb.Vector(260)).add(rb.Rectangle(
49        width=100,
50        height=400,
51    )),
52]
53
54for pillar in pillars:
55    r = pillar.get(rb.Rectangle)
56    r.bottom = rb.Display.bottom + 50
57    r.tag = "ground"
58    r.color = shared.platform_color
59
60# program the right boundary
61shared.right.pos = rb.Display.center_left + (shared.level1_size + 25, 0)
62
63scene.add(
64    shared.player,
65    ground,
66    rb.wrap(platforms),
67    *pillars,
68    shared.left,
69    shared.right,
70)