This article is about us improving the game we have created so far using Pygame.
This section is going to be combination of many small topics or “improvements” so things might seem a little random at times. Below is a list of these improvements we’re going to be adding.
- Fixing how the Player lands on a platform
- Improving the jump mechanic
- Improving platform generation (collisions between platforms)
Creating a game in Pygame is easy, it’s improving and perfecting it that’s actually a challenge. For instance, making a character jump is easy enough. But’s it’s hard to perfect the jump, the landing, the interaction with other sprites while jumping etc. This section is all about these little improvements.
Part 3 – Code
The code from the previous tutorial. We can use this as reference as we make new additions into our code.
import pygame
from pygame.locals import *
import sys
import random
pygame.init()
vec = pygame.math.Vector2
HEIGHT = 450
WIDTH = 400
ACC = 0.5
FRIC = -0.12
FPS = 60
FramePerSec = pygame.time.Clock()
displaysurface = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Game")
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.surf = pygame.Surface((30, 30))
self.surf.fill((255,255,0))
self.rect = self.surf.get_rect()
self.pos = vec((10, 360))
self.vel = vec(0,0)
self.acc = vec(0,0)
def move(self):
self.acc = vec(0,0.5)
pressed_keys = pygame.key.get_pressed()
if pressed_keys[K_LEFT]:
self.acc.x = -ACC
if pressed_keys[K_RIGHT]:
self.acc.x = ACC
self.acc.x += self.vel.x * FRIC
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
self.rect.midbottom = self.pos
def jump(self):
hits = pygame.sprite.spritecollide(self, platforms, False)
if hits:
self.vel.y = -15
def update(self):
hits = pygame.sprite.spritecollide(P1 ,platforms, False)
if P1.vel.y > 0:
if hits:
self.vel.y = 0
self.pos.y = hits[0].rect.top + 1
class platform(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.surf = pygame.Surface((random.randint(50,100), 12))
self.surf.fill((0,255,0))
self.rect = self.surf.get_rect(center = (random.randint(0,WIDTH-10),
random.randint(0, HEIGHT-30)))
def move(self):
pass
def plat_gen():
while len(platforms) < 7 :
width = random.randrange(50,100)
p = platform()
p.rect.center = (random.randrange(0, WIDTH - width),
random.randrange(-50, 0))
platforms.add(p)
all_sprites.add(p)
PT1 = platform()
P1 = Player()
PT1.surf = pygame.Surface((WIDTH, 20))
PT1.surf.fill((255,0,0))
PT1.rect = PT1.surf.get_rect(center = (WIDTH/2, HEIGHT - 10))
all_sprites = pygame.sprite.Group()
all_sprites.add(PT1)
all_sprites.add(P1)
platforms = pygame.sprite.Group()
platforms.add(PT1)
for x in range(random.randint(5, 6)):
pl = platform()
platforms.add(pl)
all_sprites.add(pl)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
P1.jump()
if P1.rect.top <= HEIGHT / 3:
P1.pos.y += abs(P1.vel.y)
for plat in platforms:
plat.rect.y += abs(P1.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()
displaysurface.fill((0,0,0))
P1.update()
plat_gen()
for entity in all_sprites:
displaysurface.blit(entity.surf, entity.rect)
entity.move()
pygame.display.update()
FramePerSec.tick(FPS)
The Jump Mechanic
Our current jump mechanic is rather bland and simplistic. Even for a simple game like Platformer the jump mechanic should be better.
Here we’ll be introducing the concept of long and short jumps. The current jump system allows us to make only a single type of jump. Even a slight tap on the space bar will result in full jump. But what if you just want to clear a small gap? An all-out jump just seems overkill right?
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
...
...
self.jumping = False
def jump(self):
hits = pygame.sprite.spritecollide(self, platforms, False)
if hits and not self.jumping:
self.jumping = True
self.vel.y = -15
def cancel_jump(self):
if self.jumping:
if self.vel.y < -3:
self.vel.y = -3
The first change is the addition of a new variable called self.jumping. We’ll be using this to determine whether the player is in a jump position or not.
As for the cancel_jump()
function, just remember that’s it’s purpose is to decrease the velocity of the player.
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
P1.jump()
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
P1.cancel_jump()
Basically, when you press down the on the SPACE key the jump()
function activates with a velocity of magnitude 15. As soon as you remove your hand from the SPACE key the cancel_jump()
is called and it begins rapidly decreasing the velocity of the player. If you want a “maximum” jump, do not let go of the SPACE bar until the jump is over or almost over.
The results of our hard work. By pressing on the space bar for different periods of time we were able to vary the duration and magnitude of the jump, allowing us to have both short and long jumps.
Don’t try using this code yet. The GIF above was taken with the code of Section1 and Section3 in this article. It won’t work unless you have both. You’ll run into a bug with collision detection with the platforms.
Platform Generation
Just look at the image below and you’ll see the worst case scenario with our current system of level generation. That’s three platforms all huddled close in one small of the screen. Not only does this look bad, but it may prevent us from moving upwards if the platforms don’t generate with enough space between them.
The purpose of this function is to return True
if the platforms are too close and return False
if the platform generation was successful. It’s actually simpler than it looks, just remember the previous line as we explain this.
def check(platform, groupies):
if pygame.sprite.spritecollideany(platform,groupies):
return True
else:
for entity in groupies:
if entity == platform:
continue
if (abs(platform.rect.top - entity.rect.bottom) < 50) and (abs(platform.rect.bottom - entity.rect.top) < 50):
return True
C = False
This function takes a newly generated platform as input, and the Sprite group called “groupies” where all the platforms are stored. Currently, this newly generated platform has not been drawn or added into any Sprite group. It will only be added if it passes this check.
The newly generated platform and the group are passed into the spritecollideany()
function to check if there are any collisions, if there are the Function will return True and end right here.
We could stop right here, but we decided to go a step further. We decided to also check the immediate surrounding areas around the newly generated platform and return True if there was any platform close by. This is to ensure a better spacing of platforms.
The code in the else statement is for this very purpose. We used abs()
to prevent negative values and return just the magnitude of the distance. We set the boundary at 50 pixels. This ensures a minimum of 50 pixels between each platform. You can tweak this value if you want.
Of course, now we’ll have to adjust the plat_gen() function to accommodate this new function as well. Compare this to the plat_gen() in the code given at the start of this article.
def plat_gen():
while len(platforms) < HARD :
width = random.randrange(50,100)
p = platform()
C = True
while C:
p = platform()
p.rect.center = (random.randrange(0, WIDTH - width),
random.randrange(-50, 0))
C = check(p, platforms)
platforms.add(p)
all_sprites.add(p)
The only change here is that the (second) While loop will continue to run as long as the check()
function keeps returning True. Once it returns False, the while loop will end and the platform will get added in the group.
Player Landing
Currently there is a little issue with the way our players on the platform. If you’ve actually played the game, you’ll understand this. We only have to make some changes to the update()
method to fix this problem.
def update(self):
hits = pygame.sprite.spritecollide(self ,platforms, False)
if self.vel.y > 0:
if hits:
if self.pos.y < hits[0].rect.bottom:
self.pos.y = hits[0].rect.top +1
self.vel.y = 0
self.jumping = False
The below line is actually the most important new change here.
if self.pos.y < lowest.rect.bottom:
It makes sure that the “landing” isn’t registered until the player’s y-position has not gone above the bottom of the platform. This prevents a weird bug where the player magically ends up on the platform even though he hasn’t fully jumped over it yet (basically if the player only made it half way through).
Not that we’ve fixed a few bugs and added some extra functionality, it’s time to actually finish the game. Our game still lacks an end point, it lacks a reason to actually play it. And finally, it also isn’t very challenging yet. We’ll be addressing all of these issues in the next and final section.
You’ll find the complete code for this section available in the next Part.
This article marks the end of the Pygame Series Improving the game article. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the article content can be asked in the comments section below.