Archive for 'Python'

Animate Mario using Pygame

August 14th, 2009 by Maarten Hus under Programming, Pygame, Python. 1 Comment.

My first console was the SNES and my first game ever was Super Mario World. So naturally when I encountered Pygame it sounded interesting to create my own animated Mario. Just as I dreamed of when I was a kid.

Getting started

First things first I needed a game loop.

From the file game.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pygame
import mario
import world
 
screen = pygame.display.set_mode((336, 432))
pygame.display.set_caption("Mario Mayhem")
clock = pygame.time.Clock()
 
world = world.World()
mario = mario.Mario((250, 355), world)
 
running = True
 
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
	    running = False
 
    mario.handle_event(event)
    mario.handle_animation()
 
    screen.fill(pygame.Color('black'))
    screen.blit(world.image, world.rect)
    screen.blit(mario.image, mario.rect, mario.area)
 
    pygame.display.flip()
    clock.tick(15)

Nothing special here I import Pygame and start a loop that ends when there is a pygame.QUIT event. The loop paints a new screen at 15 frames per second. And lets the Mario instance handle events on line 19, and handles animation on line 20.

Line 22, 23 and line 24 are used to draw Mario and the World. But before that happens the screen is painted black. I do this because if you don’t everything gets drawn over each other. You can comment this line out to see it, its a pretty weird effect.

World

Mario needs something to walk on so I looked for an image online and found this one:

background

The image has various ridges and cliffs so I needed a way to make these solid so Mario could walk on them. I decided to put all the solid locations in a list. The list stores the X positions and the Y position of the ridges. Later I’ll use these locations to calculate if Mario is standing on something.

The file world.py looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pygame
 
class World(object):
    def __init__(self):
 
    self.image = pygame.image.load("images/background.png")
    self.rect = self.image.get_rect()
 
    # [[x1, x2, y], [[x1, x2, y], etc]
    self.solids = [
		    [-10, 290, 355],
		    [70, 165, 291],
		    [38, 120, 228],
		    [70, 149, 164],
		    [119, 216, 116],
		    [259, 336, 132]
		  ]

Mario

Now that the stage is set, I’ll show you how I made Mario.

Its a bit long but here is mario.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import pygame
from states import walking, running, jumping, falling, standing
 
class Mario(pygame.sprite.Sprite):
    """This class represents Mario"""
    def __init__(self, position, world):
        pygame.sprite.Sprite.__init__(self)
	self.world = world
 
	self.image = pygame.image.load('images/smw_mario_sheet.png')
	self.rect = self.image.get_rect()
 
	self.actions = {"front": (245, 75, 20, 30),
	  	        "back": (285, 75, 20, 30),
			"left": (165, 75, 20, 30),
		        "left-walking1": (45, 75, 20, 30),
		        "left-walking2": (5, 75, 20, 30),
		        "left-jump": (165, 115, 20, 30),
		        "left-fall": (125, 115, 20, 30),
		        "left-run1": (165, 155, 20, 30),
		        "left-run2": (125, 155, 20, 30),
		        "left-run3": (85, 155, 20, 30),
		        "left-turn": (5, 115, 20, 30),
 
		        "right": (205, 75, 20, 30),
		        "right-walking1": (325, 75, 20, 30),
		        "right-walking2": (365, 75, 20, 30),
		        "right-jump": (205, 115, 20, 30),
		        "right-fall": (245, 115, 20, 30),
		        "right-run1": (205, 155, 20, 30),
		        "right-run2": (245, 155, 20, 30),
		        "right-run3": (285, 155, 20, 30),
		        "right-turn": (365, 115, 20, 30)
                        }
 
	self.action = "left"
	self.area = pygame.rect.Rect(self.actions[self.action])
	self.rect.topleft = position
 
	self.walking_state = walking.Walking(self)
	self.running_state = running.Running(self)
	self.jumping_state = jumping.Jumping(self)
	self.falling_state = falling.Falling(self)
	self.standing_state = standing.Standing(self)
	self.state = self.standing_state
 
	self.direction = "left"
 
	self.jump_frame = 0
	self.jump_stop_frame = 13
	self.jump_forward = 0
	self.jump_air_power = 3 #sideways power in jump
	self.jump_speed = 5
 
	self.fall_speed = 5
	self.fall_tolerance = 3
 
	self.walking_speed = 5
	self.running_speed = 8
 
    def handle_event(self, event):
	if self.state != self.jumping_state:
	    self.check_if_falling()
 
	if event.type == pygame.KEYDOWN:
            self.action = self.state.handle_event(event)
	else:
	    self.action = self.state.no_event()		
 
    def handle_animation(self):
        self.check_bounds()
	self.area = pygame.rect.Rect(self.actions[self.action])
 
    def check_if_falling(self):
        for solid in self.world.solids:
	    if (self.rect.x >= solid[0] and self.rect.x <= solid[1] and solid[2] > (self.rect.y - self.fall_tolerance) and solid[2] < (self.rect.y + self.fall_tolerance)):
 
                self.rect.y = solid[2]
		return False
 
	  self.state = self.falling_state
	  return True
 
    def check_bounds(self):
        if self.rect.x < -10:
            self.rect.x = 335 			 	
 
        if self.rect.x > 335:
	    self.rect.x = -5
 
	if self.rect.y > 432:
	    self.rect.y = 0

Mario gets animated by using a sprite sheet. A sprite sheet contains all the different states (walking, running, flying) of a character, in this case Mario. Because a walking image of every Mario in existence is no fun, I needed to find someway to cut all these Mario’s apart. Luckily there is a way to do that, without actually splitting the sprite sheet into separate images.

Every Sprite has three things an Image a Rect and an Area. The Rect is the position of the sprite on the screen (x, y, width, height). The Area of a Sprite determines what part of the Image gets drawn. Basically an Area allows you to crop the Image of a Sprite. All I needed to do them was to calculate the Area’s of every Mario I wanted to use. But I had to save these Area’s somehow where I could easily access them. So I used a dict the key’s of the would be the name of the Area. This dict is called actions and is located on line 13.

Here’s the sprite sheet:

smw_mario_sheet

Starting from line 40 you can see that I’m assigning states. These states represent what Mario can do such as jumping or running. I use states to determine how Mario reacts to user input, and which Mario to use for animation. But more on that later.

The function handle_event on line 61 handles all user input or lack thereof by passing them over to the current state of Mario. The states will decide what animation Mario should be using by returning a string which responds to a key in the actions dict. This action is then used by the function handle_animation on line 70 which picks the right Area from the actions dictionary.

As you can see in the constructor Mario holds a reference to the World he’s in. In the function check_if_falling on line 74 you can see that Mario uses this reference to check if he is standing. This is where the “solid list” from world.py comes in. If Mario is between X1 and X2, and Y is almost the same height Mario will stop falling.

Mario also checks his bounds in the function check_bounds. If he exceeds the limits of the screen he is teleported to the opposite side like in Super Mario Bros 2.

States

Like I said before Mario’s animation and user input is handled by a version of the state pattern. Before I’ll explain how let’s do the why first.

Handling Mario events may seem easy at first, when you press the spacebar he jumps, and when you press “d” he goes right. But Its not that simple! What happens when you press the spacebar twice, surly Mario doesn’t double jump. The solution is to create a massive amount of if-statements to accommodate for every situation. This, and I’ve tried, leads to an unreadable wall of code, and most importantly you just can’t see where to add new functionality.

I needed a way to make the code more intuitive. Enter the state pattern, a pattern according to the GoF to be applicable when: “Operations have large, multipart conditional statements that depend on the object’s state”. My interpretation: use this when you have a massive switch-statement.

Let’s look at my implementation: state.py in package “states”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class State(object):
    """This is the template for all the other states"""
 
    def __init__(self, mario):
        self.mario = mario
 
    def jump(self):
        self.mario.state = self.mario.jumping_state
	return self.mario.state.jump()	
 
    def fall(self):
        self.mario.state = self.mario.falling_state
	return self.mario.state.fall()
 
    def walk(self):
        self.mario.state = self.mario.walking_state
	return self.mario.state.walk()
 
    def run(self):
        self.mario.state = self.mario.running_state
        return self.mario.state.run()
 
    def stand(self):
        self.mario.state = self.mario.standing_state
	return self.mario.state.stand()
 
    def get_frame(self, frame_set):
        self.frame += 1
 
	if self.frame > (len(frame_set) - 1):
	    self.frame = 0
 
	return frame_set[self.frame]			
 
    def __str__(self):
        return "Default state"

This is the template state, It is supposed to be extended by the other states. It provides defaults actions such as walk, run and jump that the child should override when needed.

Lets look at the function run on line 19. Run changes Mario’s internal state to running_state, and then asks Mario’s state to call the run method. The run method will then return the correct animation.

It’s pretty simple when you call jump, fall, walk, run or stand it changes Mario’s state and returns the right animation.

State also provides a way to handle frames of animation in the function get_frame. This function takes a frame_set: a list of strings each representing a Mario, and returns its current frame. When the last frame is returned it resets self.frame to 0. This creates a loop of animations. Some states later such as Running and Walking use get_frame to make Mario run/walk.

Standing

Lets look at the simplest state: Standing in standing.py in package “states”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pygame
import state
 
class Standing(state.State):
    def __init__(self, mario):
        super(Standing, self).__init__(mario)
	self.left_stand = "left"
	self.right_stand = "right"
 
    def handle_event(self, event):
	if event.key == pygame.K_a:
	    self.mario.direction = "left"
	    return self.walk()
	elif event.key == pygame.K_d:
	    self.mario.direction = "right"
	    return self.walk()
	elif event.key == pygame.K_SPACE:
	    return self.jump()
	elif event.key == pygame.K_j:
	    return self.run()
        else:
            return self.no_event()	
 
    def no_event(self):
	return self.stand()
 
    def stand(self):
	if self.mario.direction == "left":
	    return self.left_stand
	else:
	    return self.right_stand
 
    def __str__(self):
        return "standing"

The __init__ calls the parent __init__ with mario as an argument. This way the state has a reference to the Mario instance. The constructor also assigns some animation variables, left_stand and right_stand. The values are key’s in the actions dictionary in mario.py.

The next function is handle_event this function handles all the keyboard events for the Standing state. Because standing is a neutral state, every action changes the state of Mario. Spacebar makes him jump, “a” makes him walk and so does “d”, pressing and holding “j” makes Mario run.

When there are no events Standing just returns its own stand function. The stand function handles the actual animating part, it just returns left_stand or right_stand based on the direction of Mario.

There is also a __str__ function this function is used for debugging. I used this to determine if Mario reacted properly to events.

Walking

Here’s is walking.py in the package “states”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pygame
import state
 
class Walking(state.State):
    def __init__(self, mario):
        super(Walking, self).__init__(mario)
	self.left_walking_frames = ["left", "left-walking1", "left-walking2"]
	self.right_walking_frames = ["right", "right-walking1", "right-walking2"]
	self.frame = 0
 
    def handle_event(self, event):
        if event.key == pygame.K_a:
	    self.mario.direction = "left"
	    return self.walk()
	elif event.key == pygame.K_d:
	    self.mario.direction = "right"
	    return self.walk()
	elif event.key == pygame.K_SPACE:
	    if self.mario.direction == "left":
	        self.mario.jump_forward = -5
	    else:
		self.mario.jump_forward = 5
	    return self.jump()
	elif event.key == pygame.K_j:
	    return self.run()
        else:
            return self.no_event()	
 
    def no_event(self):
        return self.stand()
 
    def walk(self):
        if self.mario.direction == "left":
	    self.mario.rect.move_ip(-self.mario.walking_speed, 0)
	    return self.get_frame(self.left_walking_frames)
	else:
	    self.mario.rect.move_ip(self.mario.walking_speed, 0)
	    return self.get_frame(self.right_walking_frames)
 
    def __str__(self):
        return "walking"

Walking has the exact same functions as Standing, the difference is the implementation. handle_event its just as you expect it to be, when Mario is already walking and you press “a” you continue walking. Jumping is a little different this time around. When you’re walking and you jump you also jump forwards, instead of just upwards. Thats what the variable mario.jump_forward is for.

When no events are fired Mario will stop Walking and change state to Stand.

Now the actual walking happens in the function walk on line 30. To move Mario forward I move Mario’s Rect to the left or right depending on Mario’s direction. After that its a matter of returning the right animation frame by calling State’s get_frame.

Running

Lets make Mario run: this is running.py in the package “states”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pygame
import state
 
class Running(state.State):
    def __init__(self, mario):
        super(Running, self).__init__(mario)
	self.left_running_frames = ["left-run1", "left-run2", "left-run3"]
	self.right_running_frames = ["right-run1", "right-run2", "right-run3"]
	self.frame = 0
 
    def handle_event(self, event):
        if event.key == pygame.K_a:
	    self.mario.direction = "left"
	    return self.run()
	elif event.key == pygame.K_d:
	    self.mario.direction = "right"
	    return self.run()
	elif event.key == pygame.K_SPACE:
	    if self.mario.direction == "left":
	        self.mario.jump_forward = -8
	    else:
		self.mario.jump_forward = 8
	    return self.jump()
	elif event.key == pygame.K_j:
	    return self.run()
        else:
            return self.no_event()
 
    def no_event(self):
	return self.walk()	
 
    def run(self):
	if self.mario.direction == "left":
	    self.mario.rect.move_ip(-self.mario.running_speed, 0)
	    return self.get_frame(self.left_running_frames)
	else:
	    self.mario.rect.move_ip(self.mario.running_speed, 0)
	    return self.get_frame(self.right_running_frames)
 
    def __str__(self):
	return "running"

Running is not that much different from Walking. The frames are different and it has a slightly higher jump_forward power.

Jumping

This is Jumping in jumping.py in the package “states”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pygame
import state
 
class Jumping(state.State):
    def __init__(self, mario):
        super(Jumping, self).__init__(mario)
	self.left_jump = "left-jump"
	self.right_jump = "right-jump"
 
    def handle_event(self, event):
	if event.key == pygame.K_a:
	    self.mario.jump_forward = 0
	    self.mario.rect.move_ip(-self.mario.jump_air_power, 0)
	elif event.key == pygame.K_d:
	    self.mario.jump_forward = 0
	    self.mario.rect.move_ip(self.mario.jump_air_power, 0)
	return self.jump()	
 
    def no_event(self):
        return self.jump()
 
    def jump(self):
	self.mario.jump_frame += 1
	self.mario.rect.move_ip(self.mario.jump_forward, -self.mario.jump_speed)
 
	if self.mario.jump_frame > self.mario.jump_stop_frame:
	    if self.mario.jump_forward != 0:
	        if self.mario.direction == "left":
		    self.mario.jump_forward = -4
		else:
		    self.mario.jump_forward = 4
	    return self.fall()			
 
	if self.mario.direction == "left" :
	    return self.left_jump
	else:
	    return self.right_jump
 
    def __str__(self):
        return "jumping"

Lets start with handle_event, this time its smaller than usual and thats because most input doesn’t affect Mario during jumping. It is possible however to steer Mario while he is jumping. The power of this movement is called mario.jump_air_power.

The actual jumping happens in jump, this function moves Mario upwards and depending on events left or right. Mario only jumps a specific height, line 26 checks if that height is met. If that happens jumping is aborted and the state is changed to Fall. If the height is not met then Mario continues to jump.

Falling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pygame
import state
 
class Falling(state.State):
    def __init__(self, mario):
        super(Falling, self).__init__(mario)
	self.left_fall = "left-fall"
	self.right_fall = "right-fall"
 
    def handle_event(self, event):
	if event.key == pygame.K_a:
	    self.mario.jump_forward = 0
	    self.mario.rect.move_ip(-self.mario.jump_air_power, 0)
	elif event.key == pygame.K_d:
	    self.mario.jump_forward = 0
	    self.mario.rect.move_ip(self.mario.jump_air_power, 0)
        return self.fall()
 
    def no_event(self):
        return self.fall()
 
    def fall(self):
        self.mario.rect.move_ip(self.mario.jump_forward, self.mario.fall_speed)
 
	if not self.mario.check_if_falling():
	    self.mario.jump_frame = 0
	    self.mario.jump_forward = 0
	    return self.stand()
 
	if self.mario.direction == "left":
	    return self.left_fall
	else:
	    return self.right_fall	
 
    def __str__(self):
        return "falling"

Falling shares handle_event with Jumping, the only difference is that Falling returns fall instead of jump. Fall itself makes Mario move down. Each time fall is called, Fall will check if Mario has hit solid ground by calling mario.check_if_falling. If he is standing some variables are reset and the state is set to Standing. If Mario is not hitting solid ground he’ll continue falling.

Conclusion

Well thats it thats how I made a animated Mario. Its not perfect however, some variables could use a better name, and it could use more animations such as turning. I also regret that I didn’t save a version of the insane if-else-tree that I used for state previously. Its the first time that I’ve actually used a pattern, and it shows. I think the code is pretty clear, Its really easy to add a ducking Mario for instance. Maybe one day I’ll create the whole level. That means figuring out how they move the screen through the level, as Mario walks by. But thats something for another post.

Here is the complete package:

Mario Mayhem

Tags:, , , , .

spacer