Scenes: A Program in Several Parts
A program can be made of several scenes: a title screen, the game and a game-over screen, or the parts of a show that play one after another. Each scene has its own setup, update, draw and on_ functions, and starts empty. This page shows two screens of a game, then scenes on a timer, then the transitions between them, then the scrubber.
A file with no scenes in it is one scene, as it always was. Nothing on this page changes it.
In Circuitry there is nothing to import: a Python file that has a scene in it is given Scene, go and the rest, and runs in the preview beside your code, with its transitions and its scrubber. The same file runs with pyctures run game.py on your own computer. JavaScript has the same scenes: Scenes in pictures.js.
In a file that begins from pymations import *, Scene is Pymations' own: these scenes are for a file that does not.
Scenes are set up as Pymations scenes are: a class for each, every one playing in the order written. If you know Pymations says what is the same and what is different.
Scenes cost nothing in a file that does not use them. A file with no scene in it is run exactly as before.
Two screens
A scene is a class with Scene in brackets. Inside it go the same functions a file has, each with self as its first name.
class Title(Scene):
def draw(self):
background("midnightblue")
text("Click to start", width / 2, height / 2, "white", 30, "centre")
def on_mouse_down(self, x, y):
go("Game")
class Game(Scene):
def setup(self):
self.x = 0
def update(self, dt):
self.x = (self.x + 200 * dt) % width
def draw(self):
background("black")
circle(self.x, height / 2, 20, "gold")
def on_mouse_down(self, x, y):
go("Title")
You do not create a scene or call anything. Pyctures finds every Scene class in the file. The first one written starts, and go("Game") changes to another by its name. go(Game), with the class itself, does the same.
Three things to know:
- A scene's own variables go on
self.self.xbelongs to the scene. Set them insetup(). setup()is called every time the scene begins, so each game starts afresh.- Only the scene on show is called. While
Titleis on show,Game'supdate,drawandon_mouse_downare not called.
What is shared and what is a scene's own
Anything made at the top level of the file, outside every class, is shared by all the scenes: the score, a synth, a picture that was loaded. A scene that changes one says global, as any function does.
score = 0 # shared: Game adds to it and GameOver shows it
Each scene starts empty:
| A scene's own | What happens when the scene ends |
|---|---|
Its variables on self | Forgotten. The next time it begins they are set again by setup() |
Dials and other controls it made in setup() (dial, slider, button…) | Taken away. Controls made at the top level of the file stay |
| The picture | The next scene starts with a clear canvas and the drawing settings (ctx.fill_style, ctx.font…) as they start |
Sound is not a scene's own. A song or a tune that is playing goes on playing across scenes until something stops it (stop_sound(), song.stop()). To stop it with a scene, do so in the scene's on_stop(self), which is called when the scene ends.
The file's own top-level functions still work. A def on_key_down(key): outside every class is called for every key, before the scene's own, whichever scene is on show: a good place for a key that mutes the sound. A top-level draw() is drawn before the scene's.
Going between scenes
| Call | What it does |
|---|---|
go("Game"), go(Game) | Change to that scene. Capital letters in the name do not matter |
next_scene() | Change to the next scene written. After the last one comes the first |
restart() | Begin this scene again, from empty |
go("Menu", keep=True) | Go back to a scene as it was left: see below |
The change happens at the start of the next frame, so the function that asked for it runs to its end.
Keep and resume
go("Menu", keep=True) goes back to a scene with its variables as they were when it was left. Its setup() is not called again. If the scene has a resume(self), that is called in its place.
Use it for a pause screen: the game goes to "Pause", and the pause screen goes back with go("Game", keep=True).
from pyctures import *
from pydials import *
class Game(Scene):
def setup(self):
self.x = 0
self.speed = dial("speed", 50, 400, 200)
def resume(self):
self.speed = dial("speed", 50, 400, 200) # controls are not kept: make them again here
def update(self, dt):
self.x = (self.x + self.speed.value * dt) % width
def draw(self):
background("black")
circle(self.x, height / 2, 20, "gold")
def on_key_down(self, key):
if key == "p":
go("Pause")
class Pause(Scene):
def draw(self):
background("midnightblue")
text("Paused. Click to carry on", width / 2, height / 2, "white", 24, "centre")
def on_mouse_down(self, x, y):
go("Game", keep=True)
Controls a scene made are not kept. They are taken away when the scene ends, kept or not, so a scene that is come back to makes them again in resume(). Its variables on self are as they were.
Without classes
If you are not ready for classes, scene groups plain functions under a name.
def draw_title():
background("midnightblue")
text("Click to start", width / 2, height / 2, "white", 30, "centre")
def draw_game():
background("black")
circle(mouse.x, mouse.y, 20, "gold")
def start_game(x, y):
go("game")
def back_to_title(x, y):
go("title")
scene("title", draw=draw_title, on_mouse_down=start_game)
scene("game", draw=draw_game, on_mouse_down=back_to_title)
The names are the ones a class would use: setup, update, draw, on_mouse_down, on_key_down and the rest, and the settings below (length=4, transition="wipe"). Scenes made this way keep their variables at the top level of the file, so they are not emptied when a scene begins: set them in the scene's setup.
Scenes on a timer
Give a scene a length in seconds and it ends by itself. The next scene written begins, and after the last one the first comes round again.
class Intro(Scene):
length = 4
def draw(self):
background("navy")
text("Part one", width / 2, height / 2, "white", 40, "centre")
class Main(Scene):
length = 8
def draw(self):
background("black")
circle(width * scene_progress, height / 2, 30, "tomato")
| Name | What it is |
|---|---|
scene_time | Seconds since this scene began |
scene_progress | How far through its length the scene is, from 0 to 1. It is 0 in a scene with no length |
scene_name | The name of the scene on show |
update(dt) works as it always did.
Lengths in beats
A scene that goes with music can be so many beats long, or so many bars. A bar is four beats.
from synths import *
set_tempo(120)
class Verse(Scene):
bars = 4 # 16 beats: 8 seconds at 120 beats a minute
class Break(Scene):
beats = 8
The beats are the sound library's (set_tempo, beat()), so a scene ends on the beat however the frames fall. In a program with no sound a beat is half a second. See Sound and music.
To end a scene when a song reaches a place of its own, ask the song in update and call next_scene():
from synths import *
song = play_song("tune.mod")
class Opening(Scene):
def update(self, dt):
if song.position["order"] >= 2:
next_scene()
Settings for every scene
config, at the top of the file, sets things for every scene.
config(loop=False, transition="crossfade", transition_time=0.8)
| Setting | Starts as | What it is |
|---|---|---|
loop | True | After the last scene, begin the first again. With False the last scene stays on show |
transition | "cut" | How every scene arrives, unless it says otherwise |
transition_time | 0.5 | How long a transition takes, in seconds |
transition_color | "black" | The colour a "fade" goes through |
beats_per_bar | 4 | How many beats bars = 1 is |
Transitions
A scene can arrive with a transition. Set it on the scene:
class Main(Scene):
transition = "crossfade"
transition_time = 0.8
or say it when you change scene:
go("Game", transition="wipe", transition_time=0.5)
| Transition | What you see |
|---|---|
"cut" | The new scene at once. This is what happens when nothing is said. "none" is the same |
"crossfade" | The old scene fades into the new one |
"fade" | The old scene fades to a colour, and the new one comes out of it. transition_color = "white" changes the colour |
"wipe" | An edge crosses from the left with the new scene behind it. "wipe_left", "wipe_up" and "wipe_down" cross the other ways |
"slide_left" | The old scene slides out to the left and the new one follows it in. Also "slide_right", "slide_up", "slide_down" |
During a transition the old scene is a still picture of its last frame. The new scene has already begun: its setup() has been called and its update and draw are running underneath. A transition starts and ends gently.
A transition is smooth however busy Python is. Python only tells the page which transition to make. The page keeps the old scene's last picture and fades, wipes or slides it away itself, at the screen's own rate, while your new scene's frames arrive underneath. A recording of the program has its transitions in it.
The first scene of a program arrives with no transition. A 3D program has the same transitions.
One limit. In a 3D program a slide is not captured in a recording. The page cannot read a 3D picture back to draw it sliding, so it moves the old picture itself, and a recording, which is made from what is drawn, does not have that movement in it. The other transitions are recorded in 3D, and every transition is recorded in 2D.
Sections and seeking: the scrubber
When every scene has a length, the program is a sequence with a length of its own, and it has a scrubber. Move the pointer over the picture to see it.
- The button pauses and plays.
- The track has a tick where each scene starts. Point at a tick for the scene's name.
- Press or drag on the track to go to that time. The scene there is set up, and
scene_timeis already at that point in it.
section("name") adds a tick of your own at the moment it is called:
class Main(Scene):
length = 20
def update(self, dt):
if scene_time > 12:
section("The chorus")
A program that plays a song can move the song when the scrubber moves, with a top-level on_seek. A song is moved by its orders, so the program says which order that time is:
def on_seek(seconds):
song.seek(int(seconds / 8)) # in this song an order lasts 8 seconds
A program with a scene that has no length has no scrubber: its length is not known. Pausing the scrubber pauses the sound too.
Mistakes
A mistake in a scene says which scene, on its line:
In the scene Menu: LookupError: go("nowhere"): there is no scene called "nowhere". The scenes are: Menu, Game
Things to know:
- Every class with
Scenein brackets plays, in the order written, including one made from another scene. - Name scenes at the top level.
scene("name", ...)inside a function that is called later is too late.
If you know Pymations
Pymations has scenes too, and the words here are the same wherever the thing is the same.
The same:
class Intro(Scene):is a scene. EverySceneclass in the file plays, in the order written, each starting empty.setup(self)is called when the scene begins.section("name")is a tick on the scrubber.transition = "crossfade"andtransition_time = 0.5on a scene, andconfig(transition="crossfade")for every scene."crossfade","slide_left","slide_right","wipe"and"none"are the same transitions, drawn the same way.- The scrubber: play and pause, a tick where each scene starts, the time.
Different:
setup,update(dt)anddraw()in place ofconstruct(). A Pymations scene describes a timeline once. A scene here runs every frame, for as long as it is on show.go(),next_scene()andrestart(). A Pymations scene ends when its animations do. Here the program decides, or the scene has alength.length,beatsandbars. A Pymations scene is as long as what it plays.- The last scene is followed by the first. A Pymations timeline stops at its end;
config(loop=False)does that here. "cut","fade"and the other wipes and slides are added here."none"still works.section()takes a name only. A transition here belongs to a change of scene.- In a file that begins
from pymations import *,Scene,sectionandconfigare Pymations' own. Scenes of this kind are for a file that does not.