I'm learning some graphics programming, and am in the midst of my first such project of any substance. But, I am really struggling at the moment with how to architect it cleanly. Let me explain.
To display complicated graphics in my current language of choice (JavaScript -- have you heard of it?), you have to draw graphical content onto a <canvas> element. And to do animation, you must clear the <canvas> after every frame (unless you want previous graphics to remain).
Thus, most canvas-related JavaScript demos I've seen have a function like this:
function render() {
clearCanvas();
// draw stuff here
requestAnimationFrame(render);
}
render, as you may surmise, encapsulates the drawing of a single frame. What a single frame contains at a specific point in time, well... that is determined by the program state. So, in order for my program to do its thing, I just need to look at the state, and decide what to render. Right?
Right. But that is more complicated than it seems.
My program is called "Critter Clicker". In my program, you see several cute critters bouncing around the screen. Clicking on one of them agitates it, making it bounce around even more. There is also a start screen, which says "Click to start!" prior to the critters being displayed.
Here are a few of the objects I'm working with in my program:
StartScreenView // represents the start screen
CritterTubView // represents the area in which the critters live
CritterList // a collection of all the critters
Critter // a single critter model
CritterView // view of a single critter
Nothing too egregious with this, I think. Yet, when I set out to flesh out my render function, I get stuck, because everything I write seems utterly ugly and reminiscent of a certain popular Italian dish. Here are a couple of approaches I've attempted, with my internal thought process included, and unrelated bits excluded for clarity.
Approach 1: "It's conditions all the way down"
// "I'll just write the program as I think it, one frame at a time."
if (assetsLoaded) {
if (userClickedToStart) {
if (critterTubDisplayed) {
if (crittersDisplayed) {
forEach(crittersList, function(c) {
if (c.wasClickedRecently) {
c.getAgitated();
}
});
} else {
displayCritters();
}
} else {
displayCritterTub();
}
} else {
displayStartScreen();
}
}
That's a very much simplified example. Yet even with only a fraction of all the rendering conditions visible, render is already starting to get out of hand. So, I dispense with that and try another idea:
Approach 2: Under the Rug
// "Each view object shall be responsible for its own rendering.
// "I'll pass each object the program state, and each can render itself."
startScreen.render(state);
critterTub.render(state);
critterList.render(state);
In this setup, I've essentially just pushed those crazy nested conditions to a deeper level in the code, hiding them from view. In other words, startScreen.render would check state to see if it needed actually to be drawn or not, and take the correct action. But this seems more like it only solves a code-aesthetic problem.
The third and final approach I'm considering that I'll share is the idea that I could invent my own "wheel" to take care of this. I'm envisioning a function that takes a data structure that defines what should happen at any given point in the render call -- revealing the conditions and dependencies as a kind of tree.
Approach 3: Mad Scientist
renderTree({
phases: ['startScreen', 'critterTub', 'endCredits'],
dependencies: {
startScreen: ['assetsLoaded'],
critterTub: ['startScreenClicked'],
critterList ['critterTubDisplayed']
// etc.
},
exclusions: {
startScreen: ['startScreenClicked'],
// etc.
}
});
That seems kind of cool. I'm not exactly sure how it would actually work, but I can see it being a rather nifty way to express things, especially if I flex some of JavaScript's events.
In any case, I'm a little bit stumped because I don't see an obvious way to do this. If you couldn't tell, I'm coming to this from the web development world, and finding that doing animation is a bit more exotic than arranging an MVC application for handling simple requests - responses.
What is the clean, established solution to this common-I-would-think problem?