📜 ⬆️ ⬇️

Writing a platformer in Python using pygame

image
Immediately make a reservation that it is written for the smallest beginners.

I have long wanted to try myself as an igrodel, and recently I had a chance to learn Python and fulfill an old dream.

What is a platformer?


Platformer (platformer) - a genre of computer games in which the main feature of the gameplay is jumping on platforms, climbing stairs, picking up items that are usually necessary to complete a level.
Wiki
')
One of my favorite games of this genre are Super Mario Brothers and Super Meat Boy. Let's try to create something in between.


The most - the very beginning.


Attention! Using python branches 2.x, from 3.x problems with running the scripts described below were found!

Probably not only games, but all applications that use pygame start like this:

#!/usr/bin/env python # -*- coding: utf-8 -*- #   pygame import pygame from pygame import * #  WIN_WIDTH = 800 #   WIN_HEIGHT = 640 #  DISPLAY = (WIN_WIDTH, WIN_HEIGHT) #        BACKGROUND_COLOR = "#004400" def main(): pygame.init() #  PyGame,   screen = pygame.display.set_mode(DISPLAY) #   pygame.display.set_caption("Super Mario Boy") #    bg = Surface((WIN_WIDTH,WIN_HEIGHT)) #    #     bg.fill(Color(BACKGROUND_COLOR)) #     while 1: #    for e in pygame.event.get(): #   if e.type == QUIT: raise SystemExit, "QUIT" screen.blit(bg, (0,0)) #      pygame.display.update() #        if __name__ == "__main__": main() 


The game will “spin” in a loop (while 1), each iteration needs to redraw everything (background, platforms, monsters, digital messages, etc.). It is important to note that the drawing goes sequentially, i.e. if you first draw a hero, and then fill the background, then the hero will not be visible, consider this for the future.

Running this code, we will see a window filled with green color.


(The picture is clickable)

Well, a start, we go further.

Level.



And how without him? By the word “level” we will mean a limited area of ​​virtual two-dimensional space, filled with all sorts of things, and along which our character will move.

To build a level, create a two-dimensional array of m by n. Each cell (m, n) will be a rectangle. A rectangle can contain something, or it can be empty. We are in the rectangles will draw the platform.

Add more constants

 PLATFORM_WIDTH = 32 PLATFORM_HEIGHT = 32 PLATFORM_COLOR = "#FF6262" 


Then add a level declaration to the main function.

 level = [ "-------------------------", "- -", "- -", "- -", "- -- -", "- -", "-- -", "- -", "- --- -", "- -", "- -", "- --- -", "- -", "- ----------- -", "- -", "- - -", "- -- -", "- -", "- -", "-------------------------"] 

And add the following to the main loop:
 x=y=0 #  for row in level: #   for col in row: #   if col == "-": # ,       pf = Surface((PLATFORM_WIDTH,PLATFORM_HEIGHT)) pf.fill(Color(PLATFORM_COLOR)) screen.blit(pf,(x,y)) x += PLATFORM_WIDTH #      y += PLATFORM_HEIGHT #      x = 0 #       


Those. We iterate over the two-dimensional level array, and if we find the “-” character, then by coordinates (x * PLATFORM_WIDTH, y * PLATFORM_HEIGHT), where x, y is the index in the level array

Running, we will see the following:



Character



Just cubes on the background - it is very boring. We need our character who will run and jump on the platforms.

Create a class of our hero.

For convenience, we will keep our character in a separate file player.py

 #!/usr/bin/env python # -*- coding: utf-8 -*- from pygame import * MOVE_SPEED = 7 WIDTH = 22 HEIGHT = 32 COLOR = "#888888" class Player(sprite.Sprite): def __init__(self, x, y): sprite.Sprite.__init__(self) self.xvel = 0 # . 0 -    self.startX = x #   ,      self.startY = y self.image = Surface((WIDTH,HEIGHT)) self.image.fill(Color(COLOR)) self.rect = Rect(x, y, WIDTH, HEIGHT) #   def update(self, left, right): if left: self.xvel = -MOVE_SPEED #  = x- n if right: self.xvel = MOVE_SPEED #  = x + n if not(left or right): # ,     self.xvel = 0 self.rect.x += self.xvel #     xvel def draw(self, screen): #     screen.blit(self.image, (self.rect.x,self.rect.y)) 



What's so interesting?
To begin with, we create a new class, inheriting from the pygame.sprite.Sprite class, thereby inheriting all the characteristics of the sprite.
A sprite is a moving bitmap. It has a number of useful methods and properties.

self.rect = Rect (x, y, WIDTH, HEIGHT) , in this line we create the actual borders of our character, a rectangle along which we will not only move the hero, but also check him for collisions. But more about that below.

The update method (self, left, right)) is used to describe the behavior of an object. Overrides the parent update (* args) → None . May be called in groups of sprites.

The draw (self, screen) method is used to display a character on the screen. Next, we remove this method and use a more interesting way to display the hero.

Let's add our hero to the main part of the program.

Before determining the level, we will add a definition of the hero and its movement variables.

 hero = Player(55,55) #    (x,y)  left = right = False #   —  


In the event check we add the following:

 if e.type == KEYDOWN and e.key == K_LEFT: left = True if e.type == KEYDOWN and e.key == K_RIGHT: right = True if e.type == KEYUP and e.key == K_RIGHT: right = False if e.type == KEYUP and e.key == K_LEFT: left = False 


Those. If you press the "left" key, then go left. If you let go - stop. Also with the "right" button

The movement itself is invoked as follows: (add after redrawing the background and platforms)

 hero.update(left, right) #  hero.draw(screen) #  


image

But, as we see, our gray block moves too quickly, we add a limit in the number of frames per second. To do this, after determining the level, add a timer.

 timer = pygame.time.Clock() 


And at the beginning of the main loop, add the following:

 timer.tick(60) 


Hung in the air



Yes, our hero is in a hopeless situation, he is frozen in the air.
Add gravity and the ability to jump.

And so, we work in the file player.py

Add more constants

 JUMP_POWER = 10 GRAVITY = 0.35 # ,      


Add the following lines to the _init_ method:

  self.yvel = 0 #    self.onGround = False #    ? 


Add an input argument to the update method
def update (self, left, right, up):
And at the beginning of the method add:
 if up: if self.onGround: # ,       self.yvel = -JUMP_POWER 


And before the line self.rect.x + = self.xvel
Add

 if not self.onGround: self.yvel += GRAVITY self.onGround = False; #   ,    (( self.rect.y += self.yvel 


And add to the main part of the program:
After the line left = right = False
Add a variable up
 up = false 


In the event check we add

 if e.type == KEYDOWN and e.key == K_UP: up = True if e.type == KEYUP and e.key == K_UP: up = False 


And change the call to the update method by adding a new argument up :
hero.update (left, right )
on
 hero.update(left, right, up) 


Here we have created the force of gravity, which will pull us down, constantly increasing speed, if we do not stand on the ground, and we do not know how to jump in flight. And we still can not firmly stand on something, so the next animation, our hero falls far beyond the limits of visibility.
image

Stand with both feet on your land.



How to know that we are on the ground or another solid surface? The answer is obvious - use the check for intersection, but for this we will change the creation of platforms.

Let's create another file.py , and transfer the platform description to it.

 PLATFORM_WIDTH = 32 PLATFORM_HEIGHT = 32 PLATFORM_COLOR = "#FF6262" 


Next, create a class, inheriting from pygame.sprite.Sprite

 class Platform(sprite.Sprite): def __init__(self, x, y): sprite.Sprite.__init__(self) self.image = Surface((PLATFORM_WIDTH, PLATFORM_HEIGHT)) self.image.fill(Color(PLATFORM_COLOR)) self.rect = Rect(x, y, PLATFORM_WIDTH, PLATFORM_HEIGHT) 


There is not anything we do not already know, go ahead.

We make changes in the main file, before adding the level array we add

 entities = pygame.sprite.Group() #   platforms = [] # ,        entities.add(hero) 


Group sprites will use entities to display all elements of this group.
We will use the platforms array to check for intersection with the platform.

Next, block
 if col == "-": # ,       pf = Surface((PLATFORM_WIDTH,PLATFORM_HEIGHT)) pf.fill(Color(PLATFORM_COLOR)) screen.blit(pf,(x,y)) 


Replace with
 if col == "-": pf = Platform(x,y) entities.add(pf) platforms.append(pf) 


Those. create an instance of the Platform class, add it to the group of sprites entities and the array of platforms . In entities , for each block not to write display logic. In platforms added to then check the array of blocks at the intersection with the player.

Further, we remove all level generation code from the loop.

And the same line
hero.draw (screen) # display
Replace with
 entities.draw(screen) #   


Running, we will see that nothing has changed. Right. After all, we do not check our hero for collisions. Let's start this fix.

We work in the file player.py

Remove the draw method, we no longer need it. And add a new collide method

 def collide(self, xvel, yvel, platforms): for p in platforms: if sprite.collide_rect(self, p): #       if xvel > 0: #    self.rect.right = p.rect.left #     if xvel < 0: #    self.rect.left = p.rect.right #     if yvel > 0: #    self.rect.bottom = p.rect.top #     self.onGround = True #    -  self.yvel = 0 #     if yvel < 0: #    self.rect.top = p.rect.bottom #     self.yvel = 0 #     

In this method there is a check for the intersection of the coordinates of the hero and the platforms, if there is one, then the above described logic takes place.

Well, and for this to happen, you need to call this method.
Change the number of arguments for the update method, now it looks like this:

 update(self, left, right, up, platforms) 


And do not forget to change his call in the main file.

And lines
 self.rect.y += self.yvel self.rect.x += self.xvel #     xvel 


Replace with:
 self.rect.y += self.yvel self.collide(0, self.yvel, platforms) self.rect.x += self.xvel #     xvel self.collide(self.xvel, 0, platforms) 


Those. they moved the hero vertically, checked the intersection vertically, moved horizontally, again checked the intersection horizontally.

That's what happens when we run.

image

Fu [y]! A moving rectangle is not beautiful!



Let's embellish our MarioBoy a bit .

Let's start with the platforms. To do this, in the blocks.py file we will make small changes.

Replace the fill color with a picture, for this line
self.image.fill (Color (PLATFORM_COLOR))
Replace with
 self.image = image.load("blocks/platform.png") 


We upload a picture instead of a solid color. Of course, the file “platform.png” should be located in the “blocks” folder, which should be located in the directory with source codes .

That's what happened



Now go to our hero. For a moving object, you need not a static picture, but an animation. In order to facilitate this task, let us use the wonderful library pyganim . Let's get started

First we add constants to the block.

 ANIMATION_DELAY = 0.1 #    ANIMATION_RIGHT = [('mario/r1.png'), ('mario/r2.png'), ('mario/r3.png'), ('mario/r4.png'), ('mario/r5.png')] ANIMATION_LEFT = [('mario/l1.png'), ('mario/l2.png'), ('mario/l3.png'), ('mario/l4.png'), ('mario/l5.png')] ANIMATION_JUMP_LEFT = [('mario/jl.png', 0.1)] ANIMATION_JUMP_RIGHT = [('mario/jr.png', 0.1)] ANIMATION_JUMP = [('mario/j.png', 0.1)] ANIMATION_STAY = [('mario/0.png', 0.1)] 


Here, I think, clearly, animation of different actions of the hero.

Now add the following to the __init__ method
 self.image.set_colorkey(Color(COLOR)) #    #    boltAnim = [] for anim in ANIMATION_RIGHT: boltAnim.append((anim, ANIMATION_DELAY)) self.boltAnimRight = pyganim.PygAnimation(boltAnim) self.boltAnimRight.play() #    boltAnim = [] for anim in ANIMATION_LEFT: boltAnim.append((anim, ANIMATION_DELAY)) self.boltAnimLeft = pyganim.PygAnimation(boltAnim) self.boltAnimLeft.play() self.boltAnimStay = pyganim.PygAnimation(ANIMATION_STAY) self.boltAnimStay.play() self.boltAnimStay.blit(self.image, (0, 0)) # -,  self.boltAnimJumpLeft= pyganim.PygAnimation(ANIMATION_JUMP_LEFT) self.boltAnimJumpLeft.play() self.boltAnimJumpRight= pyganim.PygAnimation(ANIMATION_JUMP_RIGHT) self.boltAnimJumpRight.play() self.boltAnimJump= pyganim.PygAnimation(ANIMATION_JUMP) self.boltAnimJump.play() 


Here for each action we create a set of animations, and turn them on (that is, turn on frame changes).
 for anim in ANIMATION_LEFT: boltAnim.append((anim, ANIMATION_DELAY 
))
Each frame has a picture and a show time.

It remains at the right time to show the desired animation.


Add a animation change to the update method .

 if up: if self.onGround: # ,       self.yvel = -JUMP_POWER self.image.fill(Color(COLOR)) self.boltAnimJump.blit(self.image, (0, 0)) if left: self.xvel = -MOVE_SPEED #  = x- n self.image.fill(Color(COLOR)) if up: #       self.boltAnimJumpLeft.blit(self.image, (0, 0)) else: self.boltAnimLeft.blit(self.image, (0, 0)) if right: self.xvel = MOVE_SPEED #  = x + n self.image.fill(Color(COLOR)) if up: self.boltAnimJumpRight.blit(self.image, (0, 0)) else: self.boltAnimRight.blit(self.image, (0, 0)) if not(left or right): # ,     self.xvel = 0 if not up: self.image.fill(Color(COLOR)) self.boltAnimStay.blit(self.image, (0, 0)) 

Voila!
image

More, need more space


We will overcome the size limit of the window by creating a dynamic camera .

To do this, create a class Camera

 class Camera(object): def __init__(self, camera_func, width, height): self.camera_func = camera_func self.state = Rect(0, 0, width, height) def apply(self, target): return target.rect.move(self.state.topleft) def update(self, target): self.state = self.camera_func(self.state, target.rect) 


Next, add the initial configuration of the camera.

 def camera_configure(camera, target_rect): l, t, _, _ = target_rect _, _, w, h = camera l, t = -l+WIN_WIDTH / 2, -t+WIN_HEIGHT / 2 l = min(0, l) #      l = max(-(camera.width-WIN_WIDTH), l) #      t = max(-(camera.height-WIN_HEIGHT), t) #      t = min(0, t) #      return Rect(l, t, w, h) 


Create an instance of the camera, add before the main loop:

 total_level_width = len(level[0])*PLATFORM_WIDTH #     total_level_height = len(level)*PLATFORM_HEIGHT #  camera = Camera(camera_configure, total_level_width, total_level_height) 


What have we done?

We created inside a large rectangle whose dimensions are calculated as follows:

 total_level_width = len(level[0])*PLATFORM_WIDTH #     total_level_height = len(level)*PLATFORM_HEIGHT #  


smaller rectangle, identical in size to the window size.

The smaller rectangle is centered relative to the main character ( update method), and all objects are drawn in a smaller rectangle ( apply method), thereby creating the impression of camera movement.

For the above, you need to change the drawing objects.

Replace the line
entities.draw (screen) # display
On
 for e in entities: screen.blit(e.image, camera.apply(e)) 


And before it we add
 camera.update(hero) #     


Now we can change the level.

 level = [ "----------------------------------", "- -", "- -- -", "- -", "- -- -", "- -", "-- -", "- -", "- ---- --- -", "- -", "-- -", "- -", "- --- -", "- -", "- -", "- --- -", "- -", "- ------- ---- -", "- -", "- - -", "- -- -", "- -", "- -", "----------------------------------"] 

This is the result.
image

The result can be downloaded, a link to GitHub

In the next part, if it is claimed by the community, we will create our own level generator with blackjack and whores with different types of platforms, monsters, teleports, and of course, a princess.

upd pygame can be downloaded from here , thank you, Chris_Griffin for the comment
upd1 second part

Source: https://habr.com/ru/post/193888/


All Articles