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.
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.
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
):
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.
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.
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:
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.
Code that made the above level
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.
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))
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!):
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()
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))
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
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)