📜 ⬆️ ⬇️

We write a platformer in Python. Part 2. Part 1, preparation for creating a level editor


Hello friends!

We continue to deal with our MarioBoy. Start here , continued here . In this part of the second part we will prepare to create a level editor, namely: add a turbo run mode to the hero, deadly platforms, moving monsters, teleporters, a princess and a level parser so as not to be distracted by all this in the second part.

Hero upgrade


Add our hero the opportunity to accelerate. To do this, change the code of the update method.

First, add constants
MOVE_EXTRA_SPEED = 2.5 #  JUMP_EXTRA_POWER = 1 #    ANIMATION_SUPER_SPEED_DELAY = 0.05 #      

')
Next, add a motion animation left - right in accelerated mode. We will insert the same pictures, but with a different frame rate.
 #    boltAnim = [] boltAnimSuperSpeed = [] for anim in ANIMATION_RIGHT: boltAnim.append((anim, ANIMATION_DELAY)) boltAnimSuperSpeed.append((anim, ANIMATION_SUPER_SPEED_DELAY)) self.boltAnimRight = pyganim.PygAnimation(boltAnim) self.boltAnimRight.play() self.boltAnimRightSuperSpeed = pyganim.PygAnimation(boltAnimSuperSpeed) self.boltAnimRightSuperSpeed.play() #    boltAnim = [] boltAnimSuperSpeed = [] for anim in ANIMATION_LEFT: boltAnim.append((anim, ANIMATION_DELAY)) boltAnimSuperSpeed.append((anim, ANIMATION_SUPER_SPEED_DELAY)) self.boltAnimLeft = pyganim.PygAnimation(boltAnim) self.boltAnimLeft.play() self.boltAnimLeftSuperSpeed = pyganim.PygAnimation(boltAnimSuperSpeed) self.boltAnimLeftSuperSpeed.play() 

Added 2 sets of animations when accelerating self.boltAnimRightSuperSpeed , self.boltAnimLeftSuperSpeed , we will display them just below

Now let's do the update method itself.

Add an input parameter running
 def update(self, left, right, up, running, platforms): 


Modify the handling of character movements by adding behavior during acceleration.
 if up: if self.onGround: # ,       self.yvel = -JUMP_POWER if running and (left or right): #       self.yvel -= JUMP_EXTRA_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 running: #   self.xvel-=MOVE_EXTRA_SPEED #    if not up: #     self.boltAnimLeftSuperSpeed.blit(self.image, (0, 0)) #     else: #    if not up: #    self.boltAnimLeft.blit(self.image, (0, 0)) #    if up: #    self.boltAnimJumpLeft.blit(self.image, (0, 0)) #    if right: self.xvel = MOVE_SPEED #  = x + n self.image.fill(Color(COLOR)) if running: self.xvel+=MOVE_EXTRA_SPEED if not up: self.boltAnimRightSuperSpeed.blit(self.image, (0, 0)) else: if not up: self.boltAnimRight.blit(self.image, (0, 0)) if up: self.boltAnimJumpRight.blit(self.image, (0, 0)) 


And in the main file, add handling of the left shift event.
 running = False *** if e.type == KEYDOWN and e.key == K_LSHIFT: running = True *** if e.type == KEYUP and e.key == K_LSHIFT: running = False 

All key codes here

And don't forget to add arguments when calling the hero.update () method
 hero.update(left, right, up, running, platforms) 


See the results (I changed the background color to black , brutal color for brutal MarioBoy )
No acceleration

Accelerated jump


Deadly Spikes


A real hero is a real danger. Let's create a new kind of blocks, in contact with which instant death will occur.

Create a class that inherits from Platform.
 class BlockDie(Platform): def __init__(self, x, y): Platform.__init__(self, x, y) self.image = image.load("%s/blocks/dieBlock.png" % ICON_DIR) 


Next, add the behavior of the hero in contact with him. To do this, add 2 methods to the character class. The first method is death behavior, the second is moving to the specified coordinates (which will be useful to us once again just below)
 def die(self): time.wait(500) self.teleporting(self.startX, self.startY) #     def teleporting(self, goX, goY): self.rect.x = goX self.rect.y = goY 

Those. when we die, the game stops for a while, then we move to the beginning of the level and play on.

Well, we describe the behavior itself when crossing with the death block in the collide () method
 *** if isinstance(p, blocks.BlockDie): #    - blocks.BlockDie self.die()#  *** 


Now, basically the class will change the level
 level = [ "----------------------------------", "- -", "- -- -", "- * -", "- -", "- -- -", "-- -", "- -", "- ---- --- -", "- -", "-- -", "- * -", "- --- -", "- -", "- -", "- * --- * -", "- -", "- ------- ---- -", "- -", "- - -", "- -- -", "- *** -", "- -", "----------------------------------"] 


And add the creation of a block of death, if the level has a symbol "*"
 if col == "*": bd = BlockDie(x,y) entities.add(bd) platforms.append(bd) 

Result:


Portals


What modern plumber does without teleport? So let's not make our hero a black sheep.

Create a new block type. We work in the blocks.py file

First add constants.
 ANIMATION_BLOCKTELEPORT = [ ('%s/blocks/portal2.png' % ICON_DIR), ('%s/blocks/portal1.png' % ICON_DIR)] 

Then create a new class.
 class BlockTeleport(Platform): def __init__(self, x, y, goX,goY): Platform.__init__(self, x, y) self.goX = goX #    self.goY = goY #    boltAnim = [] for anim in ANIMATION_BLOCKTELEPORT: boltAnim.append((anim, 0.3)) self.boltAnim = pyganim.PygAnimation(boltAnim) self.boltAnim.play() def update(self): self.image.fill(Color(PLATFORM_COLOR)) self.boltAnim.blit(self.image, (0, 0)) 

There is nothing new here. When creating, not only the coordinates of the location of the portal are specified, but also the coordinates of the hero’s movement when it hits the teleporter.

Next, we add to our hero's behavior in contact with the portal
 *** elif isinstance(p, blocks.BlockTeleport): self.teleporting(p.goX, p.goY) *** 

And add one portal to the map. Only now we will describe the coordinates manually. When we make the level editor it will be easier.
Add another group of sprites, which will contain animated blocks
 animatedEntities = pygame.sprite.Group() #   ,    

And create a teleporter.
 tp = BlockTeleport(128,512,800,64) entities.add(tp) platforms.append(tp) animatedEntities.add(tp) 

Finally, add a call to the update () method on all animated sprites.
 animatedEntities.update() #   


Something like this


Monsters


Scary, moving, deadly lights.

The difference between monsters and deadly blocks is that monsters can move.

Let's start.

We will work in the new file, so as not to get confused. Let's call it very original - monsters.py

Create a new class Monster . There is nothing in it that we have not used before.
Whole file content
 #!/usr/bin/env python # -*- coding: utf-8 -*- from pygame import * import pyganim import os MONSTER_WIDTH = 32 MONSTER_HEIGHT = 32 MONSTER_COLOR = "#2110FF" ICON_DIR = os.path.dirname(__file__) #       ANIMATION_MONSTERHORYSONTAL = [('%s/monsters/fire1.png' % ICON_DIR), ('%s/monsters/fire2.png' % ICON_DIR )] class Monster(sprite.Sprite): def __init__(self, x, y, left, up, maxLengthLeft,maxLengthUp): sprite.Sprite.__init__(self) self.image = Surface((MONSTER_WIDTH, MONSTER_HEIGHT)) self.image.fill(Color(MONSTER_COLOR)) self.rect = Rect(x, y, MONSTER_WIDTH, MONSTER_HEIGHT) self.image.set_colorkey(Color(MONSTER_COLOR)) self.startX = x #   self.startY = y self.maxLengthLeft = maxLengthLeft #  ,       self.maxLengthUp= maxLengthUp #  ,      ,  self.xvel = left # c   , 0 -    self.yvel = up #    , 0 -   boltAnim = [] for anim in ANIMATION_MONSTERHORYSONTAL: boltAnim.append((anim, 0.3)) self.boltAnim = pyganim.PygAnimation(boltAnim) self.boltAnim.play() def update(self, platforms): #    self.image.fill(Color(MONSTER_COLOR)) self.boltAnim.blit(self.image, (0, 0)) self.rect.y += self.yvel self.rect.x += self.xvel self.collide(platforms) if (abs(self.startX - self.rect.x) > self.maxLengthLeft): self.xvel =-self.xvel #    ,      if (abs(self.startY - self.rect.y) > self.maxLengthUp): self.yvel = -self.yvel #    ,     ,  def collide(self, platforms): for p in platforms: if sprite.collide_rect(self, p) and self != p: #   -  -  self.xvel = - self.xvel #      self.yvel = - self.yvel 

When creating a monster, you need to specify 6 arguments: x , y - coordinates, left - horizontal movement speed, up - vertical movement speed, maxLengthLeft - maximum distance to one side that the monster can go, maxLengthUp - similar to the previous one, but vertical.

Now add the death of the hero from contact with fire.

Replace strings
 if isinstance(p, blocks.BlockDie): #    - blocks.BlockDie self.die()#  

On
  if isinstance(p, blocks.BlockDie) or isinstance(p, monsters.Monster): #   - blocks.BlockDie  Monster self.die()#  

And do not forget to add import from the file monsters.py

And, of course, add the creation of a monster in the main file.

Create another group of sprites, which will put our monsters.
 monsters = pygame.sprite.Group() #    

Question: Why do we need another group? Why did not have the previous one? After all, in the group of animatedEntities sprites we call the update () method.
Answer: In the previous group, we call the update () method with no arguments, and in the monsters group, this method will be called with an argument.

Create the monster itself.
 mn = Monster(190,200,2,3,150,15) entities.add(mn) platforms.append(mn) monsters.add(mn) 

And move it
 monsters.update(platforms) #    

We look at the result.


Princess


It is a matter of honor for any plumber to save the princess.

The princess class does not contain anything interesting for us, so I will not show the code. Who will be interested - look in the blocks.py file

Our character will add the property winner , by which we will judge that it is time to complete the level.
 self.winner = False 

And make changes to the collide () method
 elif isinstance(p, blocks.Princess): #    self.winner = True # !!! 


And further, we will write the code for creating a princess
 if col == "P": pr = Princess(x,y) entities.add(pr) platforms.append(pr) animatedEntities.add(pr) 


Do not forget to insert the symbol "P" in the level.

We look


Level


Finally we got to the parsing level. We will keep them in the levels catalog. I give an example of the level from the file 1.txt
 [ ----------------------------------| - * -| - * *P -| ---- *--** --| - -- -| - -| - -| - -| -- ---- | - -| -- -| - ** -| - --- -| - -| - -| - --- -| - -| - -------- * ---- -| - -| - - -| - ** -- -| - * -| - ** -| --------------- *** -- -| - -| - -| ----------------------------------| ] player 55 44 portal 128 512 900 35 portal 170 512 700 64 monster 190 250 2 1 150 10 monster 190 400 2 3 150 150 monster 150 200 1 2 150 100 / 


What do we see here? Neither of which is not considered in this post (including the first part). First, generate static platforms using the characters "[", "-", "*", "]", "|"
Where "[" - shows the parser to the beginning of the level
"]" - respectively, end of level
"|" - end of line
"-" is a common platform
"*" - studded platform

Then, in the line "player 55 44" we indicate the initial coordinates of our hero
"Portal 128 512 900 35" - the first two numbers - the coordinates of the portal, the second - the coordinates of the movement
"Monster 150 200 1 2 150 100" - the first two numbers, similarly, the coordinates of the monster, then the second two - the horizontal and vertical speed, and the last - the maximum distance in one direction horizontally and vertically.
As you have already noticed, there can be as many portals and monsters as you like.
The symbol "/" means the end of the file. All data after it will not be read.

Now, let's write the parser itself.
We work in the main file.

To begin, transfer all arrays and groups from the main () function to the body of the main program.
 *** level = [] entities = pygame.sprite.Group() #   animatedEntities = pygame.sprite.Group() #   ,    monsters = pygame.sprite.Group() #    platforms = [] # ,        if __name__ == "__main__": main() 

Then, remove the level, the variable should be empty. Also, delete the creation of monsters and portals.

And add a new feature
 def loadLevel(): global playerX, playerY #   ,    levelFile = open('%s/levels/1.txt' % FILE_DIR) line = " " commands = [] while line[0] != "/": #       line = levelFile.readline() #  if line[0] == "[": #      while line[0] != "]": # ,       line = levelFile.readline() #    if line[0] != "]": #       endLine = line.find("|") #      level.append(line[0: endLine]) #          "|" if line[0] != "": #     commands = line.split() #      if len(commands) > 1: #    > 1,     if commands[0] == "player": #    - player playerX= int(commands[1]) #     playerY = int(commands[2]) if commands[0] == "portal": #    portal,    tp = BlockTeleport(int(commands[1]),int(commands[2]),int(commands[3]),int(commands[4])) entities.add(tp) platforms.append(tp) animatedEntities.add(tp) if commands[0] == "monster": #    monster,    mn = Monster(int(commands[1]),int(commands[2]),int(commands[3]),int(commands[4]),int(commands[5]),int(commands[6])) entities.add(mn) platforms.append(mn) monsters.add(mn) 

Do not forget to call this function and specify the variables startX and startY as starting coordinates for our hero.
 def main(): loadLevel() *** hero = Player(playerX,playerY) #    (x,y)  *** 


Download the result .

Now it’s not very interesting to edit the level file with your hands, therefore, in the next part we will write the level editor itself.
PS Level created above, quite passable, go for it.

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


All Articles