The approach of using if
s inside of draw()
to figure out which scene to render might be fine for small sketches, but isn't especially scalable, involving booleans, hard-to-reason-about conditions, shared state and/or the program having to know about magic/hardcoded variable names.
Whenever you find yourself dealing with thing1
, thing2
, thing3
, ..., thingN
, the path forward is almost always an object or an array. For long if
-else
or switch
chains, the common refactor is to use an array or object of functions.
Scenes are essentially state machines, which are a perfect use-case for keyable/indexable objects or arrays of functions. If you're proceeding through your scenes in a stepwise manner, arrays are probably best since they're ordered and can be stepped through sequentially with an index or shift()
. See this answer for an approach to this.
If your scenes aren't quite so linear, giving them names rather than numbers might be a better way to navigate through them. Here's an example:
const scenes = {
loading: () => {
let ticks = 0;
frameRate(3);
mousePressed = () => {};
draw = () => {
clear();
text(`loading sceen, please wait${".".repeat(ticks % 4)}`, 50, 50);
if (++ticks > 10) {
frameRate(60);
scenes.menu();
}
};
},
menu: () => {
mousePressed = () => {
scenes.gamePlay();
};
draw = () => {
clear();
text("menu scene. click to play", 50, 50);
};
},
gamePlay: () => {
mousePressed = () => {
fill(0);
scenes.gameOver();
};
let x = 0;
fill(50, 50, 160);
draw = () => {
clear();
text("gameplay scene. click to go to game over", cos(x) * 20 + 50, 50);
x += 0.1;
};
},
gameOver: () => {
mousePressed = () => {
scenes.menu();
};
draw = () => {
clear();
text("game over scene. click to go to menu", 50, 50);
};
},
// ... more scenes ...
};
function setup() {
createCanvas(500, 100);
textSize(20);
}
function draw() {
scenes.loading();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
Naturally, you'll have more complex conditions for switching scenes and you probably won't be using the mouse to make the switch in many cases (game state or buttons are just as likely), but this example illustrates the idea that each scene will replace p5's window library functions with their own implementation in an event-driven manner, avoiding any if
s and keeping the code readable and modular. You can break the scenes into separate files quite easily.
The "setup" logic for each scene is baked in the top-level scenes.yourScene
function, which provides a nice closure for state that should persist between draw
calls without polluting other scenes. You can move variables out to a global or shared scope as necessary to persist data between scenes, such as an overall top score.
Although the relationships between scenes in the above example are simple, scenes can test different conditions to transition to any scene, such as separate win/loss scenes. The condition block that triggers the transition represents the cleanup/teardown logic for the exiting state. For very complex games and animations, nesting scenes and using combinations of arrays and objects for stepwise and state machine transitions respectively should be pretty workable.
It's often useful to draw a diagram showing the scenes in your app and which conditions trigger transitions. Optionally, write down which actions should be taken to set up and tear down each scene:
For example, the above app's scenes could be visualized like this:
.---------.
| loading |
`---------`
|
loaded
|
v
.------. .-----------.
| menu |--click-->| game play |
`------` `-----------`
^ |
| click
| |
click v
| .-----------.
+--------------| game over |
`-----------`
Lastly, if it bothers you to overwrite the draw
function that p5 knows about, you can always add a layer of indirection, flipping a local function between a few different options, then calling it from draw
in a way that's less invasive of p5:
const menuScene = () => {
if (someCondition) {
renderScene = gameScene;
}
// update and draw menu stuff
};
const gameScene = () => {
if (someCondition) {
renderScene = menuScene;
}
// update and draw game stuff
};
let renderScene = menuScene;
function draw() {
renderScene();
}
Without the closures, we lose the scene-specific setup code and local variables, but that's easy enough to reintroduce using a pattern similar to the one shown in the first example.
The same strategy works for setting handlers such as mouse and keyboard per scene.
See also Trying to use p5 to change states after pressing button, but nothing happens after clicking the button for a DOM-oriented example.