Building Flappy Bean
Play it first → Bean is always falling, and every flap pushes it back up. Fly through the gap between each pair of pipes for a point. The more points you have, the faster the pipes come and the smaller the gaps get. Touch a pipe or the grass and it's game over.
This is how it was built, a step at a time. Every numbered step below is a whole program: copy it into the playground, press Run, and it works. Each step is the one before it with something added, so you can build the game the way it was built. Then break something on purpose and see what changes. That teaches you more than reading does.
There is nothing clever in it. No list comprehensions, no tricks, and every kaypy function it uses is on the API page. The pipes are plain rectangles, so the only picture is Bean.
It is one of the shortest games on the site, which makes it a good early one to build. If you have never built a game before, start with Bomb Dodge, which is shorter still. If you want something longer afterwards, try Coin Dash.
The one big idea: Bean never moves sideways
It looks as if Bean flies to the right. It doesn't. Bean only ever goes up
and down, and the pipes slide to the left. Your eye can't tell the
difference, and the game is much simpler this way: there is no camera to move,
and Bean's x is always 180.
Step 1 — Bean falls and flaps
from kaypy import *
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
kaypy(width=800, height=600, background=[121, 189, 220])
setGravity(1600)
loadSprite("bean", "images/bean.png")
bean = add([sprite("bean"), pos(180, 240), anchor("center"), area(), body()])
# The grass. It is solid, so Bean lands on it.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
def flap():
bean.jump(FLAP)
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
Bean drops as soon as the game starts, so be quick. Press space, up or click to flap.
setGravity(1600)makes anything with abody()fall. Bean has one, so Bean falls.- The grass is a rectangle with
body(isStatic=True), so it is solid and never moves. Bean lands on it. bean.jump(FLAP)throws Bean upwards. In most platform games you checkisGrounded()first so the player can't jump in mid-air. Here jumping in mid-air is the whole game, so there's no check.
onKeyPress("space", flap) means "when space is pressed, run
flap." It's the same as writing @onKeyPress("space")
above a function, but because three things all do the same job, it's
shorter to write flap once and hand it to all three.
No brackets after flap.
onKeyPress("space", flap) hands over the function, to be run
later. onKeyPress("space", flap()) runs it right now, once, and
hands over what it gave back, which is nothing. There's no error message. Bean
flaps once by itself at the start, and after that the space bar does
nothing.
Step 2 — pipes
Every 1.55 seconds a pair of pipes appears just off the right-hand edge and slides left.
from kaypy import *
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
PIPE_WIDTH = 70
MARGIN = 90 # how close to the top or the grass a gap may be
GAP = 210 # the hole between the pipes
SPEED = 190 # how fast the pipes slide, in pixels per second
WAIT = 1.55 # seconds between pipes
kaypy(width=800, height=600, background=[121, 189, 220])
setGravity(1600)
loadSprite("bean", "images/bean.png")
bean = add([sprite("bean"), pos(180, 240), anchor("center"), area(), body(),
z(5)])
# The grass. It is solid, so Bean lands on it.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
next_pipe = WAIT # seconds until the next pair of pipes
def flap():
bean.jump(FLAP)
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
@onUpdate
def each_frame():
global next_pipe
next_pipe = next_pipe - dt()
if next_pipe <= 0:
add_pipes()
next_pipe = WAIT
def add_pipes():
# Where the gap starts, picked at random.
top = rand(MARGIN, height() - GROUND - MARGIN - GAP)
bottom = top + GAP
x = width() # just off the right-hand edge
# The pipe above the gap, from the top of the window down.
add([rect(PIPE_WIDTH, top), pos(x, 0), color(76, 145, 65),
outline(4, (46, 100, 40)), area(), "pipe", "slides"])
# The pipe below the gap, from the gap down to the grass.
add([rect(PIPE_WIDTH, height() - GROUND - bottom), pos(x, bottom),
color(76, 145, 65), outline(4, (46, 100, 40)), area(),
"pipe", "slides"])
@onUpdate("slides")
def slide(thing):
thing.move(-SPEED, 0)
# Gone off the left-hand edge? Throw it away.
if thing.pos.x < -PIPE_WIDTH - 20:
thing.destroy()
The countdown. next_pipe is how many seconds
until the next pair. dt() is how long the last frame took, so
taking it away every frame counts down in real seconds. When it reaches zero,
add_pipes() makes a pair and the countdown starts again.
Two pipes and a gap. The gap is GAP pixels
tall, and top is where it starts, picked at random. The top pipe
runs from the top of the window down to top. The bottom pipe
starts at bottom, the bottom of the gap, and runs down to the
grass. MARGIN keeps the gap from being so near the top or the
grass that you couldn't get through it.
Sliding. Both pipes are tagged "slides", and
@onUpdate("slides") runs slide once a frame for
every object with that tag, handing each one in as thing. Once a
pipe is off the left-hand edge it is destroyed. Without that, the game would
keep every pipe it ever made, and after a few minutes there would be hundreds
of them out of sight, all still being moved every frame.
Bean now has z(5) in its list. Things are drawn in the order
they were added, so the pipes, added after Bean, would be drawn over it.
A higher z is drawn in front, so 5 keeps Bean in front of the
pipes, which have none.
The pipes have an area() but no body(). Bean flies
straight through them for now. With a body() they would shove
Bean out of the way instead, which looks like the game is broken rather than
like you lost. Step 4 makes touching one the end of the game.
Step 3 — points
A point is for getting past a pair of pipes. The simplest way to notice that is to put something invisible in the gap and score a point when Bean touches it.
from kaypy import *
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
PIPE_WIDTH = 70
MARGIN = 90 # how close to the top or the grass a gap may be
GAP = 210 # the hole between the pipes
SPEED = 190 # how fast the pipes slide, in pixels per second
WAIT = 1.55 # seconds between pipes
kaypy(width=800, height=600, background=[121, 189, 220])
setGravity(1600)
loadSprite("bean", "images/bean.png")
bean = add([sprite("bean"), pos(180, 240), anchor("center"), area(), body(),
z(5)])
# The grass. It is solid, so Bean lands on it.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
next_pipe = WAIT # seconds until the next pair of pipes
score = 0
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
def flap():
bean.jump(FLAP)
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
@onUpdate
def each_frame():
global next_pipe
next_pipe = next_pipe - dt()
if next_pipe <= 0:
add_pipes()
next_pipe = WAIT
def add_pipes():
# Where the gap starts, picked at random.
top = rand(MARGIN, height() - GROUND - MARGIN - GAP)
bottom = top + GAP
x = width() # just off the right-hand edge
# The pipe above the gap, from the top of the window down.
add([rect(PIPE_WIDTH, top), pos(x, 0), color(76, 145, 65),
outline(4, (46, 100, 40)), area(), "pipe", "slides"])
# The pipe below the gap, from the gap down to the grass.
add([rect(PIPE_WIDTH, height() - GROUND - bottom), pos(x, bottom),
color(76, 145, 65), outline(4, (46, 100, 40)), area(),
"pipe", "slides"])
# An invisible strip across the gap, at the BACK of the pipes.
# Touching it scores the point.
add([rect(6, GAP), pos(x + PIPE_WIDTH - 6, top), opacity(0),
area(), "point", "slides"])
@onUpdate("slides")
def slide(thing):
thing.move(-SPEED, 0)
# Gone off the left-hand edge? Throw it away.
if thing.pos.x < -PIPE_WIDTH - 20:
thing.destroy()
@bean.onCollide("point")
def scored(strip):
global score
strip.destroy()
score = score + 1
score_label.text = str(score)
The strip is a rectangle 6 pixels wide and as tall as the gap, with
opacity(0) so you can't see it. It's tagged "slides"
too, so it moves along with its pipes without any code of its own.
It sits at the back of the pipes, x + PIPE_WIDTH - 6,
not the front. Put it at the front and you'd get the point the moment you
flew into the gap, and could then hit the pipe on the way through. That feels
like the game cheating you. A point should mean "got past that one".
@bean.onCollide("point") runs once, at the moment Bean first
touches a strip, not on every frame they overlap, so one strip is one point.
Once it has scored, the strip has done its job, and
strip.destroy() gets rid of it.
Don't forget global.
Take out global score and the first point crashes the game with
UnboundLocalError: cannot access local variable 'score' where it is not
associated with a value. Without global, Python thinks
score = score + 1 is about a new variable of
scored's own, and that one has no value yet to add 1 to.
Step 4 — game over, and again
Touching a pipe or the grass, or flying off the top of the window, now ends the game. Flap again and a new one starts.
Starting again means throwing away every pipe and building everything
fresh. That's what a scene is for. Everything from Bean down
moves inside @scene("game"), one step further in, and
go("game") builds it. Calling go("game") again
throws the whole scene away and builds it from the start.
from kaypy import *
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
PIPE_WIDTH = 70
MARGIN = 90 # how close to the top or the grass a gap may be
GAP = 210 # the hole between the pipes
SPEED = 190 # how fast the pipes slide, in pixels per second
WAIT = 1.55 # seconds between pipes
kaypy(width=800, height=600, background=[121, 189, 220])
setGravity(1600)
loadSprite("bean", "images/bean.png")
score = 0
alive = True
next_pipe = 0
@scene("game")
def game():
global score, alive, next_pipe
score = 0
alive = True
next_pipe = WAIT
bean = add([sprite("bean"), pos(180, 240), anchor("center"), area(),
body(), z(5)])
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
# width=520 wraps a long message onto more than one line.
message = add([text("Press SPACE to flap", size=26, width=520),
pos(width() / 2, height() / 2 + 40), anchor("center"),
z(10)])
def flap():
if alive:
bean.jump(FLAP)
else:
go("game")
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
@onUpdate
def each_frame():
global next_pipe
if not alive:
return
# Flying off the top is losing too.
if bean.pos.y < -40:
die()
next_pipe = next_pipe - dt()
if next_pipe <= 0:
add_pipes()
next_pipe = WAIT
def add_pipes():
top = rand(MARGIN, height() - GROUND - MARGIN - GAP)
bottom = top + GAP
x = width()
add([rect(PIPE_WIDTH, top), pos(x, 0), color(76, 145, 65),
outline(4, (46, 100, 40)), area(), "pipe", "slides"])
add([rect(PIPE_WIDTH, height() - GROUND - bottom), pos(x, bottom),
color(76, 145, 65), outline(4, (46, 100, 40)), area(),
"pipe", "slides"])
add([rect(6, GAP), pos(x + PIPE_WIDTH - 6, top), opacity(0),
area(), "point", "slides"])
@onUpdate("slides")
def slide(thing):
if not alive:
return
thing.move(-SPEED, 0)
if thing.pos.x < -PIPE_WIDTH - 20:
thing.destroy()
@bean.onCollide("point")
def scored(strip):
global score
if not alive:
return
strip.destroy()
score = score + 1
score_label.text = str(score)
if score == 1:
message.text = ""
@bean.onCollide("pipe")
def hit_pipe(pipe):
die()
@bean.onCollide("ground")
def hit_ground(ground):
die()
def die():
global alive
if not alive:
return
alive = False
shake(10)
if score == 1:
message.text = "1 point -- press SPACE to try again"
else:
message.text = (str(score)
+ " points -- press SPACE to try again")
go("game")
alive is True while you're
flying. die() sets it to False, and everything else
checks it:
flapstarts a new game instead of flapping.slidestops moving the pipes, so they freeze where they are.each_framestops counting down to more pipes.dieitself does nothing the second time. Hitting a pipe is nearly always followed by falling onto the grass, and that callsdie()again. Without the check you would get a second shake, and the message written all over again.
Bean still has its body(), so after dying it keeps falling
until it lands on the grass. That reads as losing. If Bean froze in mid-air
too, it would look as if the game had crashed.
The variables are set back to their starting values at the top of the
scene, because the scene is built again every game. Leave out
score = 0 and your next game would start with the last game's
score.
text(..., width=520) wraps the message onto two lines when
it is longer than 520 pixels, which the game-over message is.
Step 5 — harder as you go
The gap, the speed and the time between pipes were fixed numbers. Now each one is worked out from the score by a small function.
from kaypy import *
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
PIPE_WIDTH = 70
MARGIN = 90 # how close to the top or the grass a gap may be
GAP_START = 210 # the hole between the pipes, to begin with
GAP_SMALLEST = 130 # ...the smallest it ever gets
GAP_SHRINK = 6 # ...and how much smaller each point makes it
SPEED_START = 190 # how fast the pipes slide, in pixels per second
SPEED_FASTEST = 420 # ...the fastest they ever go
SPEED_GAIN = 9 # ...and how much faster each point makes them
WAIT_START = 1.55 # seconds between pipes, to begin with
WAIT_SHORTEST = 0.95 # ...the shortest wait there ever is
WAIT_DROP = 0.02 # ...and how much shorter each point makes it
kaypy(width=800, height=600, background=[121, 189, 220])
setGravity(1600)
loadSprite("bean", "images/bean.png")
score = 0
alive = True
next_pipe = 0
def gap_size():
gap = GAP_START - score * GAP_SHRINK
if gap < GAP_SMALLEST:
gap = GAP_SMALLEST
return gap
def pipe_speed():
speed = SPEED_START + score * SPEED_GAIN
if speed > SPEED_FASTEST:
speed = SPEED_FASTEST
return speed
def pipe_wait():
seconds = WAIT_START - score * WAIT_DROP
if seconds < WAIT_SHORTEST:
seconds = WAIT_SHORTEST
return seconds
@scene("game")
def game():
global score, alive, next_pipe
score = 0
alive = True
next_pipe = pipe_wait()
bean = add([sprite("bean"), pos(180, 240), anchor("center"), area(),
body(), z(5)])
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
# width=520 wraps a long message onto more than one line.
message = add([text("Press SPACE to flap", size=26, width=520),
pos(width() / 2, height() / 2 + 40), anchor("center"),
z(10)])
def flap():
if alive:
bean.jump(FLAP)
else:
go("game")
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
@onUpdate
def each_frame():
global next_pipe
if not alive:
return
# Flying off the top is losing too.
if bean.pos.y < -40:
die()
next_pipe = next_pipe - dt()
if next_pipe <= 0:
add_pipes()
next_pipe = pipe_wait()
def add_pipes():
gap = gap_size()
top = rand(MARGIN, height() - GROUND - MARGIN - gap)
bottom = top + gap
x = width()
add([rect(PIPE_WIDTH, top), pos(x, 0), color(76, 145, 65),
outline(4, (46, 100, 40)), area(), "pipe", "slides"])
add([rect(PIPE_WIDTH, height() - GROUND - bottom), pos(x, bottom),
color(76, 145, 65), outline(4, (46, 100, 40)), area(),
"pipe", "slides"])
add([rect(6, gap), pos(x + PIPE_WIDTH - 6, top), opacity(0),
area(), "point", "slides"])
@onUpdate("slides")
def slide(thing):
if not alive:
return
thing.move(-pipe_speed(), 0)
if thing.pos.x < -PIPE_WIDTH - 20:
thing.destroy()
@bean.onCollide("point")
def scored(strip):
global score
if not alive:
return
strip.destroy()
score = score + 1
score_label.text = str(score)
if score == 1:
message.text = ""
if score == 5 or score == 10 or score == 20:
message.text = "faster!"
@wait(0.8)
def clear_message():
# Only if you are still flying -- otherwise this would wipe
# out the game-over message.
if alive:
message.text = ""
@bean.onCollide("pipe")
def hit_pipe(pipe):
die()
@bean.onCollide("ground")
def hit_ground(ground):
die()
def die():
global alive
if not alive:
return
alive = False
shake(10)
if score == 1:
message.text = "1 point -- press SPACE to try again"
else:
message.text = (str(score)
+ " points -- press SPACE to try again")
go("game")
Take gap_size(). It starts at GAP_START, 210
pixels, and takes off 6 for every point. At 10 points that's
210 - 10 * 6, which is 150. At 20 points it would be 90, but the
if stops it at GAP_SMALLEST, 130. The other two work
the same way.
Those stops matter. Without them, the gap keeps shrinking until Bean can't fit through it, and nobody could get past about thirty points. A game that gets harder and then levels off is one people keep playing.
At 5, 10 and 20 points the message says "faster!" for 0.8 seconds.
clear_message checks alive before clearing it. If
you die within 0.8 seconds of reaching 5 points, the "faster!" timer is still
counting, and without that check it would wipe out your game-over message.
Step 6 — the whole game
This is the game exactly as it is on the games page. It adds two things to step 5:
- Bean tilts. Nose up while rising, nose down while falling. It's not just for looks: it's the quickest way to see whether you are still going up or already coming down.
- A best score that is remembered.
getData(BEST_KEY, 0)reads the best score saved last time, or 0 if there isn't one, andsetDatasaves a new one when you beat it.
The tilt needs to know how fast Bean is falling. Nothing tells the game
that, so it works it out: last_y remembers where Bean was last
frame, and how far Bean moved divided by how long the frame took is its
speed, in pixels per second. Multiply by 0.06 and you get an
angle, and clamp() keeps that angle between −28 (nose up) and 75
(nose down). rotate(0) in Bean's list is what lets Bean turn at
all; without it, setting bean.angle does nothing.
It also has all the numbers worth changing in one settings block near the top, and comments that explain why each part is there.
"""Flappy Bean -- flap through the gaps, and don't touch anything.
space, up or click flap
Bean is always falling, and every flap pushes it back up. Fly through the
gap between each pair of pipes for a point. The more points you have, the
faster the pipes come and the smaller the gaps get. Touch a pipe or the
grass, or fly off the top, and it is game over. Your best score is
remembered.
HOW THIS FILE IS PUT TOGETHER
1. the window and the picture
2. the settings
3. how hard the game is
4. the game scene: Bean, the grass, the pipes, points, dying
5. start the game
The pipes are plain rectangles, so there is no art to find, and every number
that shapes the game is in the settings, ready to be argued about.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
kaypy(width=800, height=600, background=[121, 189, 220])
# How hard Bean falls. Lower it and Bean floats; raise it and Bean drops
# like a stone.
setGravity(1600)
loadSprite("bean", "images/bean.png")
# -------------------------------------------------------------- 2. settings
#
# Everything you would want to change is here. Change one at a time and
# play it -- that is the whole point of having them in one place.
FLAP = 520 # how hard a flap pushes Bean up
GROUND = 60 # how tall the strip of grass is
PIPE_WIDTH = 70
MARGIN = 90 # how close to the top or the grass a gap may be
GAP_START = 210 # the hole between the pipes, to begin with
GAP_SMALLEST = 130 # ...the smallest it ever gets
GAP_SHRINK = 6 # ...and how much smaller each point makes it
SPEED_START = 190 # how fast the pipes slide, in pixels per second
SPEED_FASTEST = 420 # ...the fastest they ever go
SPEED_GAIN = 9 # ...and how much faster each point makes them
WAIT_START = 1.55 # seconds between pipes, to begin with
WAIT_SHORTEST = 0.95 # ...the shortest wait there ever is
WAIT_DROP = 0.02 # ...and how much shorter each point makes it
# The name the best score is saved under. Every game on a website shares
# the same storage, so the game's own name goes in it -- a plain "best"
# would be overwritten by the next game that saved one.
BEST_KEY = "flappy_best"
# -------------------------------------------------- 3. how hard the game is
#
# Each of these works out a number from the score. They all get harder as
# the score goes up, and they all stop somewhere, so the game ends up hard
# but never impossible. Without the stops, nobody could get past about
# thirty points.
score = 0
def gap_size():
gap = GAP_START - score * GAP_SHRINK
if gap < GAP_SMALLEST:
gap = GAP_SMALLEST
return gap
def pipe_speed():
speed = SPEED_START + score * SPEED_GAIN
if speed > SPEED_FASTEST:
speed = SPEED_FASTEST
return speed
def pipe_wait():
seconds = WAIT_START - score * WAIT_DROP
if seconds < WAIT_SHORTEST:
seconds = WAIT_SHORTEST
return seconds
# ------------------------------------------------------- 4. the game scene
#
# These change while you play. A function that CHANGES one has to say
# `global` first, which means "the one up here, not a new one of my own".
alive = True
next_pipe = 0 # seconds until the next pair of pipes
last_y = 0 # where Bean was last frame, for the tilt
@scene("game")
def game():
global score, alive, next_pipe, last_y
# A new game, so everything starts again.
score = 0
alive = True
next_pipe = pipe_wait()
best = getData(BEST_KEY, 0)
# rotate(0) lets Bean tilt. It starts level. z(5) draws Bean in front
# of the pipes, which are added later and would otherwise cover it.
bean = add([sprite("bean"), pos(180, 240), anchor("center"), rotate(0),
area(), body(), z(5)])
last_y = bean.pos.y
# The grass. It is solid, so Bean lands on it -- and landing on it is
# losing.
add([rect(width(), GROUND), pos(0, height() - GROUND),
color(96, 156, 76), area(), body(isStatic=True), "ground"])
score_label = add([text("0", size=48), pos(width() / 2, 40),
anchor("center"), z(10)])
best_label = add([text("best " + str(best), size=18), pos(12, 12),
opacity(0.75), z(10)])
# width=520 wraps a long message onto more than one line.
message = add([text("Press SPACE to flap", size=26, width=520),
pos(width() / 2, height() / 2 + 40), anchor("center"),
z(10)])
# ------------------------------------------------------------ flapping
#
# Three ways to flap, all doing the same thing. Once the game is over,
# a flap starts a new one.
def flap():
if alive:
bean.jump(FLAP)
else:
go("game")
onKeyPress("space", flap)
onKeyPress("up", flap)
onClick(flap)
# --------------------------------------------------------- every frame
@onUpdate
def each_frame():
global next_pipe, last_y
if not alive:
return
# Tilt Bean nose-up while it rises and nose-down while it falls. How
# far it moved this frame, divided by how long the frame took, is
# how fast it is going. (dt() is never 0 while the game runs, but
# dividing by 0 would crash it, so it is checked anyway.)
if dt() > 0:
falling_speed = (bean.pos.y - last_y) / dt()
bean.angle = clamp(falling_speed * 0.06, -28, 75)
last_y = bean.pos.y
# Flying off the top is losing too. Without this you could fly
# above the pipes and score for ever.
if bean.pos.y < -40:
die()
# Time for more pipes? dt() is how long this frame took, so taking
# it away every frame counts down in real seconds.
next_pipe = next_pipe - dt()
if next_pipe <= 0:
add_pipes()
next_pipe = pipe_wait()
# --------------------------------------------------------------- pipes
def add_pipes():
gap = gap_size()
# Where the gap starts, picked at random -- but never so near the
# top, or the grass, that you could not fly through it.
top = rand(MARGIN, height() - GROUND - MARGIN - gap)
bottom = top + gap
x = width() # just off the right-hand edge
# The pipes have area() but NOT body(). Bean flies into them rather
# than bouncing off, and touching one ends the game.
# The pipe above the gap, from the top of the window down.
add([rect(PIPE_WIDTH, top), pos(x, 0), color(76, 145, 65),
outline(4, (46, 100, 40)), area(), "pipe", "slides"])
# The pipe below the gap, from the gap down to the grass.
add([rect(PIPE_WIDTH, height() - GROUND - bottom), pos(x, bottom),
color(76, 145, 65), outline(4, (46, 100, 40)), area(),
"pipe", "slides"])
# An invisible strip across the gap. Touching it is what scores the
# point. It sits at the BACK of the pipes, so the point only comes
# once you are through -- not the moment you fly into the gap.
add([rect(6, gap), pos(x + PIPE_WIDTH - 6, top), opacity(0),
area(), "point", "slides"])
# Everything tagged "slides" -- both pipes and the strip -- moves left.
@onUpdate("slides")
def slide(thing):
if not alive:
return
thing.move(-pipe_speed(), 0)
# Gone off the left-hand edge? Throw it away. Otherwise the game
# keeps every pipe it ever made, and after a few minutes there are
# hundreds of them out of sight, all still being moved.
if thing.pos.x < -PIPE_WIDTH - 20:
thing.destroy()
# -------------------------------------------------------------- points
@bean.onCollide("point")
def scored(strip):
global score
if not alive:
return
strip.destroy()
score = score + 1
score_label.text = str(score)
if score == 1:
message.text = ""
if score == 5 or score == 10 or score == 20:
message.text = "faster!"
@wait(0.8)
def clear_message():
# Only if you are still flying -- otherwise this would wipe
# out the game-over message.
if alive:
message.text = ""
# ------------------------------------------------------------- dying
@bean.onCollide("pipe")
def hit_pipe(pipe):
die()
@bean.onCollide("ground")
def hit_ground(ground):
die()
def die():
global alive
# Hit a pipe and Bean falls onto the grass -- that is a second call
# to die(), and only the first one should count.
if not alive:
return
alive = False
shake(10)
if score > best:
setData(BEST_KEY, score)
best_label.text = "best " + str(score)
if score == 1:
message.text = "1 point -- press SPACE to try again"
else:
message.text = (str(score)
+ " points -- press SPACE to try again")
# Nothing else is needed to stop the pipes: slide() does nothing
# once alive is False. Bean has a body(), so it keeps falling until
# it lands on the grass.
# ------------------------------------------------------- 5. start the game
go("game")
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Change the numbers.
FLAPand the gravity together decide how Bean feels. TrysetGravity(800)withFLAP = 360for a floatier Bean. - Change the steps. Make
GAP_SMALLESTbigger and the game never gets as hard. MakeSPEED_GAIN0 and the pipes never speed up at all. - A sound for each point.
loadSound("point", "sounds/small_beep.wav")near the top, andplay("point")inscored. Then one indieas well. - Coloured pipes. Change
color(76, 145, 65)to pick a colour at random for each pair, withrandi(0, 255)for each of the three numbers. Work it out once inadd_pipesso both pipes of a pair match.