Level Up!
Platformer Part 2
The finale upgrades your Episode 2 platformer into a real game: patrolling enemies you can stomp, a scrolling camera, moving platforms, spikes, checkpoints and a proper multi-level structure.
The finale upgrades your Episode 2 platformer into a real game: patrolling enemies you can stomp, a scrolling camera, moving platforms, spikes, checkpoints and a proper multi-level structure.
Add enemies that patrol platforms and hurt the player on contact.
enemies = [
{"rect": pygame.Rect(390, 236, 30, 24), "vx": 2, "min_x": 380, "max_x": 500},
{"rect": pygame.Rect(60, 416, 30, 24), "vx": 2, "min_x": 40, "max_x": 700},
]
for en in enemies:
en["rect"].x += en["vx"]
if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
en["vx"] = -en["vx"]enemies = [
{"rect": pygame.Rect(390, 236, 30, 24), "vx": 2, "min_x": 380, "max_x": 500},
{"rect": pygame.Rect(60, 416, 30, 24), "vx": 2, "min_x": 40, "max_x": 700},
]
for en in enemies:
en["rect"].x += en["vx"]
if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
en["vx"] = -en["vx"]
Implement the most famous mechanic in games: the head-stomp.
for en in enemies[:]:
if player.colliderect(en["rect"]):
if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
enemies.remove(en) # SQUASH!
vy = -10 # bounce off the kill
score += 5
else:
player.x, player.y = 100, 380 # ouch
vy = 0for en in enemies[:]:
if player.colliderect(en["rect"]):
if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
enemies.remove(en) # SQUASH!
vy = -10 # bounce off the kill
score += 5
else:
player.x, player.y = 100, 380 # ouch
vy = 0
if vy > if vy > and player.bottom - vy <= en["rect"].top + 8:
enemies. and player.bottom - vy <= en["rect"].top + 8:
enemies.(en)
vy = (en)
vy =
Grow the level to 2400 px wide and scroll the view to follow the player.
LEVEL_W = 2400
cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))
# every draw call subtracts cam_x:
pygame.draw.rect(screen, (255, 119, 0),
(player.x - cam_x, player.y, player.width, player.height))
for plat in platforms:
pygame.draw.rect(screen, (6, 214, 160),
(plat.x - cam_x, plat.y, plat.width, plat.height))LEVEL_W = 2400
cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))
# every draw call subtracts cam_x:
pygame.draw.rect(screen, (255, 119, 0),
(player.x - cam_x, player.y, player.width, player.height))
for plat in platforms:
pygame.draw.rect(screen, (6, 214, 160),
(plat.x - cam_x, plat.y, plat.width, plat.height))
Add platforms that patrol, and carry the player with them.
movers = [{"rect": pygame.Rect(900, 320, 120, 16),
"vx": 2, "min_x": 900, "max_x": 1200}]
for m in movers:
m["rect"].x += m["vx"]
if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
m["vx"] = -m["vx"]
# in the landing loop, also check movers; if landed on one:
riding = m
# after collisions:
if riding:
player.x += riding["vx"] # carried along!movers = [{"rect": pygame.Rect(900, 320, 120, 16),
"vx": 2, "min_x": 900, "max_x": 1200}]
for m in movers:
m["rect"].x += m["vx"]
if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
m["vx"] = -m["vx"]
# in the landing loop, also check movers; if landed on one:
riding = m
# after collisions:
if riding:
player.x += riding["vx"] # carried along!
if riding:
player.x += ridingif riding:
player.x += riding]]
Add hazards that kill and checkpoints that make death fair.
spikes = [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)]
checkpoints = [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)]
respawn = (100, 380)
for cp in checkpoints:
if player.colliderect(cp):
respawn = (cp.x, cp.y - 44)
def die():
global vy
player.x, player.y = respawn
vy = 0
for sp in spikes:
if player.colliderect(sp):
die()spikes = [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)]
checkpoints = [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)]
respawn = (100, 380)
for cp in checkpoints:
if player.colliderect(cp):
respawn = (cp.x, cp.y - 44)
def die():
global vy
player.x, player.y = respawn
vy = 0
for sp in spikes:
if player.colliderect(sp):
die()
Chain levels with a load function and finish the series like a boss.
def load_level(n):
if n == 0:
return {
"platforms": [pygame.Rect(0, 440, 2400, 40), ...],
"enemies": [...],
"movers": [...],
"spikes": [...],
"checkpoints": [...],
"door": pygame.Rect(2320, 380, 30, 60),
}
if n == 1:
return { ... }
level = load_level(0)
current = 0
if player.colliderect(level["door"]):
current += 1
if current < 2:
level = load_level(current)
die() # reuse: places player at respawn start
else:
state = "CHAMPION"def load_level(n):
if n == 0:
return {
"platforms": [pygame.Rect(0, 440, 2400, 40), ...],
"enemies": [...],
"movers": [...],
"spikes": [...],
"checkpoints": [...],
"door": pygame.Rect(2320, 380, 30, 60),
}
if n == 1:
return { ... }
level = load_level(0)
current = 0
if player.colliderect(level["door"]):
current += 1
if current < 2:
level = load_level(current)
die() # reuse: places player at respawn start
else:
state = "CHAMPION"
current += 1
if current < current += 1
if current < :
level = :
level = (current)
else:
state = "CHAMPION"(current)
else:
state = "CHAMPION"
Patrolling enemies with head-stomps, a 2400px scrolling camera, carry-you moving platforms, deadly spikes, fair checkpoints, and two chained levels ending in a CHAMPION screen , one file. Both levels are completable. Save it as game.pygame.py, run python game.pypython game.py, and play. Stuck on a step? Compare your file against this.
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper: The Finale")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)
LEVEL_W = 2400
def load_level(n):
if n == 0:
return {
"platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
pygame.Rect(300, 340, 140, 16),
pygame.Rect(620, 260, 150, 16),
pygame.Rect(1500, 340, 160, 16)],
"enemies": [{"rect": pygame.Rect(500, 416, 30, 24), "vx": 2, "min_x": 460, "max_x": 720},
{"rect": pygame.Rect(1700, 416, 30, 24), "vx": 2, "min_x": 1650, "max_x": 1860}],
"movers": [{"rect": pygame.Rect(900, 320, 120, 16), "vx": 2, "min_x": 900, "max_x": 1240}],
"spikes": [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)],
"checkpoints": [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)],
"coins": [pygame.Rect(350, 300, 18, 18), pygame.Rect(670, 220, 18, 18), pygame.Rect(1560, 300, 18, 18)],
"door": pygame.Rect(2320, 380, 30, 60),
}
return {
"platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
pygame.Rect(450, 330, 140, 16),
pygame.Rect(1100, 300, 160, 16),
pygame.Rect(1800, 340, 160, 16)],
"enemies": [{"rect": pygame.Rect(700, 416, 30, 24), "vx": 3, "min_x": 640, "max_x": 980},
{"rect": pygame.Rect(1500, 416, 30, 24), "vx": 3, "min_x": 1450, "max_x": 1700}],
"movers": [{"rect": pygame.Rect(1150, 300, 120, 16), "vx": 3, "min_x": 1100, "max_x": 1500}],
"spikes": [pygame.Rect(600, 424, 80, 16), pygame.Rect(1300, 424, 80, 16), pygame.Rect(2000, 424, 80, 16)],
"checkpoints": [pygame.Rect(1050, 380, 12, 60), pygame.Rect(1950, 380, 12, 60)],
"coins": [pygame.Rect(500, 290, 18, 18), pygame.Rect(1160, 260, 18, 18), pygame.Rect(1850, 300, 18, 18)],
"door": pygame.Rect(2320, 380, 30, 60),
}
current = 0
level = load_level(0)
player = pygame.Rect(100, 380, 34, 44)
vy = 0
on_ground = False
respawn = (100, 380)
score = 0
state = "PLAY"
def die():
global vy
player.x, player.y = respawn
vy = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and state == "CHAMPION":
current = 0
level = load_level(0)
player.x, player.y = 100, 380
vy = 0
respawn = (100, 380)
score = 0
state = "PLAY"
elif event.key == pygame.K_SPACE and on_ground and state == "PLAY":
vy = -16
on_ground = False
if state == "PLAY":
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= 5
if keys[pygame.K_RIGHT]:
player.x += 5
if player.left < 0:
player.left = 0
if player.right > LEVEL_W:
player.right = LEVEL_W
for en in level["enemies"]:
en["rect"].x += en["vx"]
if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
en["vx"] = -en["vx"]
for m in level["movers"]:
m["rect"].x += m["vx"]
if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
m["vx"] = -m["vx"]
vy += 1
player.y += vy
on_ground = False
riding = None
for plat in level["platforms"]:
if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
player.bottom = plat.top
vy = 0
on_ground = True
for m in level["movers"]:
if player.colliderect(m["rect"]) and vy > 0 and player.bottom - vy <= m["rect"].top:
player.bottom = m["rect"].top
vy = 0
on_ground = True
riding = m
if riding:
player.x += riding["vx"] # carried by the platform
for en in level["enemies"][:]:
if player.colliderect(en["rect"]):
if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
level["enemies"].remove(en) # STOMP
vy = -10
score += 5
else:
die()
for sp in level["spikes"]:
if player.colliderect(sp):
die()
for cp in level["checkpoints"]:
if player.colliderect(cp):
respawn = (cp.x, cp.y - 44)
for c in level["coins"][:]:
if player.colliderect(c):
level["coins"].remove(c)
score += 1
if player.top > 480:
die()
if player.colliderect(level["door"]):
current += 1
if current < 2:
level = load_level(current)
player.x, player.y = 100, 380
vy = 0
respawn = (100, 380)
else:
state = "CHAMPION"
# ---- draw ----
cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))
screen.fill((25, 30, 60))
for plat in level["platforms"]:
pygame.draw.rect(screen, (6, 214, 160), (plat.x - cam_x, plat.y, plat.width, plat.height))
for m in level["movers"]:
pygame.draw.rect(screen, (129, 140, 248), (m["rect"].x - cam_x, m["rect"].y, m["rect"].width, m["rect"].height))
for sp in level["spikes"]:
pygame.draw.polygon(screen, (248, 113, 113),
[(sp.x - cam_x, sp.bottom), (sp.centerx - cam_x, sp.y), (sp.right - cam_x, sp.bottom)])
for cp in level["checkpoints"]:
pygame.draw.rect(screen, (250, 204, 21), (cp.x - cam_x, cp.y, cp.width, cp.height))
for c in level["coins"]:
pygame.draw.ellipse(screen, (255, 209, 102), (c.x - cam_x, c.y, c.width, c.height))
for en in level["enemies"]:
pygame.draw.rect(screen, (239, 68, 68), (en["rect"].x - cam_x, en["rect"].y, en["rect"].width, en["rect"].height))
d = level["door"]
pygame.draw.rect(screen, (167, 139, 250), (d.x - cam_x, d.y, d.width, d.height))
pygame.draw.rect(screen, (255, 119, 0), (player.x - cam_x, player.y, player.width, player.height))
screen.blit(font.render("Score: " + str(score) + " Level " + str(current + 1), True, (255, 255, 255)), (12, 12))
if state == "CHAMPION":
msg = big_font.render("CHAMPION!", True, (255, 209, 102))
screen.blit(msg, msg.get_rect(center=(400, 210)))
sub = font.render("Final score " + str(score) + " - press R", True, (255, 255, 255))
screen.blit(sub, sub.get_rect(center=(400, 260)))
pygame.display.flip()
clock.tick(60)
pygame.quit()import pygame
pygame.init()
screen = pygame.display.set_mode((800, 480))
pygame.display.set_caption("Sky Hopper: The Finale")
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)
big_font = pygame.font.SysFont(None, 64)
LEVEL_W = 2400
def load_level(n):
if n == 0:
return {
"platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
pygame.Rect(300, 340, 140, 16),
pygame.Rect(620, 260, 150, 16),
pygame.Rect(1500, 340, 160, 16)],
"enemies": [{"rect": pygame.Rect(500, 416, 30, 24), "vx": 2, "min_x": 460, "max_x": 720},
{"rect": pygame.Rect(1700, 416, 30, 24), "vx": 2, "min_x": 1650, "max_x": 1860}],
"movers": [{"rect": pygame.Rect(900, 320, 120, 16), "vx": 2, "min_x": 900, "max_x": 1240}],
"spikes": [pygame.Rect(760, 424, 40, 16), pygame.Rect(1400, 424, 80, 16)],
"checkpoints": [pygame.Rect(1000, 380, 12, 60), pygame.Rect(1900, 380, 12, 60)],
"coins": [pygame.Rect(350, 300, 18, 18), pygame.Rect(670, 220, 18, 18), pygame.Rect(1560, 300, 18, 18)],
"door": pygame.Rect(2320, 380, 30, 60),
}
return {
"platforms": [pygame.Rect(0, 440, LEVEL_W, 40),
pygame.Rect(450, 330, 140, 16),
pygame.Rect(1100, 300, 160, 16),
pygame.Rect(1800, 340, 160, 16)],
"enemies": [{"rect": pygame.Rect(700, 416, 30, 24), "vx": 3, "min_x": 640, "max_x": 980},
{"rect": pygame.Rect(1500, 416, 30, 24), "vx": 3, "min_x": 1450, "max_x": 1700}],
"movers": [{"rect": pygame.Rect(1150, 300, 120, 16), "vx": 3, "min_x": 1100, "max_x": 1500}],
"spikes": [pygame.Rect(600, 424, 80, 16), pygame.Rect(1300, 424, 80, 16), pygame.Rect(2000, 424, 80, 16)],
"checkpoints": [pygame.Rect(1050, 380, 12, 60), pygame.Rect(1950, 380, 12, 60)],
"coins": [pygame.Rect(500, 290, 18, 18), pygame.Rect(1160, 260, 18, 18), pygame.Rect(1850, 300, 18, 18)],
"door": pygame.Rect(2320, 380, 30, 60),
}
current = 0
level = load_level(0)
player = pygame.Rect(100, 380, 34, 44)
vy = 0
on_ground = False
respawn = (100, 380)
score = 0
state = "PLAY"
def die():
global vy
player.x, player.y = respawn
vy = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and state == "CHAMPION":
current = 0
level = load_level(0)
player.x, player.y = 100, 380
vy = 0
respawn = (100, 380)
score = 0
state = "PLAY"
elif event.key == pygame.K_SPACE and on_ground and state == "PLAY":
vy = -16
on_ground = False
if state == "PLAY":
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player.x -= 5
if keys[pygame.K_RIGHT]:
player.x += 5
if player.left < 0:
player.left = 0
if player.right > LEVEL_W:
player.right = LEVEL_W
for en in level["enemies"]:
en["rect"].x += en["vx"]
if en["rect"].x <= en["min_x"] or en["rect"].x >= en["max_x"]:
en["vx"] = -en["vx"]
for m in level["movers"]:
m["rect"].x += m["vx"]
if m["rect"].x <= m["min_x"] or m["rect"].x >= m["max_x"]:
m["vx"] = -m["vx"]
vy += 1
player.y += vy
on_ground = False
riding = None
for plat in level["platforms"]:
if player.colliderect(plat) and vy > 0 and player.bottom - vy <= plat.top:
player.bottom = plat.top
vy = 0
on_ground = True
for m in level["movers"]:
if player.colliderect(m["rect"]) and vy > 0 and player.bottom - vy <= m["rect"].top:
player.bottom = m["rect"].top
vy = 0
on_ground = True
riding = m
if riding:
player.x += riding["vx"] # carried by the platform
for en in level["enemies"][:]:
if player.colliderect(en["rect"]):
if vy > 0 and player.bottom - vy <= en["rect"].top + 8:
level["enemies"].remove(en) # STOMP
vy = -10
score += 5
else:
die()
for sp in level["spikes"]:
if player.colliderect(sp):
die()
for cp in level["checkpoints"]:
if player.colliderect(cp):
respawn = (cp.x, cp.y - 44)
for c in level["coins"][:]:
if player.colliderect(c):
level["coins"].remove(c)
score += 1
if player.top > 480:
die()
if player.colliderect(level["door"]):
current += 1
if current < 2:
level = load_level(current)
player.x, player.y = 100, 380
vy = 0
respawn = (100, 380)
else:
state = "CHAMPION"
# ---- draw ----
cam_x = player.centerx - 400
cam_x = max(0, min(LEVEL_W - 800, cam_x))
screen.fill((25, 30, 60))
for plat in level["platforms"]:
pygame.draw.rect(screen, (6, 214, 160), (plat.x - cam_x, plat.y, plat.width, plat.height))
for m in level["movers"]:
pygame.draw.rect(screen, (129, 140, 248), (m["rect"].x - cam_x, m["rect"].y, m["rect"].width, m["rect"].height))
for sp in level["spikes"]:
pygame.draw.polygon(screen, (248, 113, 113),
[(sp.x - cam_x, sp.bottom), (sp.centerx - cam_x, sp.y), (sp.right - cam_x, sp.bottom)])
for cp in level["checkpoints"]:
pygame.draw.rect(screen, (250, 204, 21), (cp.x - cam_x, cp.y, cp.width, cp.height))
for c in level["coins"]:
pygame.draw.ellipse(screen, (255, 209, 102), (c.x - cam_x, c.y, c.width, c.height))
for en in level["enemies"]:
pygame.draw.rect(screen, (239, 68, 68), (en["rect"].x - cam_x, en["rect"].y, en["rect"].width, en["rect"].height))
d = level["door"]
pygame.draw.rect(screen, (167, 139, 250), (d.x - cam_x, d.y, d.width, d.height))
pygame.draw.rect(screen, (255, 119, 0), (player.x - cam_x, player.y, player.width, player.height))
screen.blit(font.render("Score: " + str(score) + " Level " + str(current + 1), True, (255, 255, 255)), (12, 12))
if state == "CHAMPION":
msg = big_font.render("CHAMPION!", True, (255, 209, 102))
screen.blit(msg, msg.get_rect(center=(400, 210)))
sub = font.render("Final score " + str(score) + " - press R", True, (255, 255, 255))
screen.blit(sub, sub.get_rect(center=(400, 260)))
pygame.display.flip()
clock.tick(60)
pygame.quit()