At the end of last year, I participated in Winter MelonJam 2022 with Hannah Ji. As two unexperienced game developers attending our first “real” game jam, we fumbled for 48 hours and made Flipping Houses in the process. Imagine our surprise when we found out that we placed first, prompting me to reflect on the experience. The judges looked favourably on our product, and with our unorthodox approach to making a game — it’s got to teach something interesting, right?
Here, I detail the game development journey (so far) for Flipping Houses, but with a twist: I’ll be highlighting the important yet mundane; the often-overlooked frameworks that video games build upon, from where we set the origin in a coordinate system, to how we messed up our animation framework, to the mathematics behind how to transform objects in 3D space.
Table of Contents
Processing Information
Both Hannah and I are novices at game development — so much so that neither of us are comfortable with using a “proper” game engine. However, there are two graphics engines that we’ve both used before: Processing and PyGame. We settled on using Processing since it has basic 3D support, something we didn’t find in PyGame’s documenation.
Processing is marketed as a tool for creating interactive algorithmic art, and its feature set certainly reflects so. At its core, it gives you:
- a canvas that refreshes at 60 Hz;
- functions for drawing shapes and images; and
- functions for mouse and keyboard input detection.
It offers no graphical user interface — everything must be done through code — and its very lightweight compared to specialized game development tools, so features that are typically taken for granted (e.g. animations, shadows, etc.) must be built from the ground-up. However, its simplicity brings forth advantages over more featureful tools: an overall lack of bloat and the ability to control the minute details of the visual output. In a way, making Flipping Houses felt more like coding a game engine over developing a game.
Unfortunately, game engines are complicated.
Along the way, we made mistakes; architectural missteps that complicated our computations, spaghettified our code, and “jankified” the end result. But with mistakes come learning opportunities, and by recreating our game jam prototype from scratch (with better code architecture and in JavaScript for the web), I was able to create a scalable project that I’m proud of.
Origin Story
The beginning starts with… the origin? In the non-tautological sense, I mean. If high school physics taught us anything useful, it would be that where you set the origin is important; it can make some problems go from seemingly impossible to trival — a lesson we overlooked.
Intuitively, you might expect the shadows to lie on the $z = 0$ plane; they lie on the ground, and it makes sense for the ground to be at height 0. In reality, we first implemented the board, so we defined $z = 0$ to bisect it (a logical choice when only considering the board). Unfortunately, when looking at the whole picture, we suboptimally set $z = 0$, leaving most objects positioned at $z = 0.075$, a rather awkward height. Computation wise, this created extra steps for positioning the object (the reason will hopefully become apparent further in the article). With similar stories regarding all axes, I moved the origin from the center of the nearest tile to the closest corner of the nearest tile when rewriting the code.
We can generalize this lesson into the following idea: chose wisely when defining the parameters in a relationship. For example, in the prototype code, the flippers are defined with the following properties:
axis
(the axis to flip upon; either $x$ or $y$)-
position
(flip about the lineaxis = position
) -
end1
(only flip tiles whereaxis >= end1
) -
end2
(only flip tiles whereaxis <= end2
) -
range
(only flip tiles whereabs(perpendicular(axis) - position) <= range
)
Confusing, right? Both Hannah and I found this system hard to work with. Although the math for handling flippers was as simple as can be, it was easy to make mistakes when creating new levels. In the rewritten code, I changed the flippers to be defined with the following properties instead:
axis
(the axis to flip upon; either $x$ or $y$)x
(the flipper’s $x$-position)y
(the flipper’s $y$-position)width
(the flipper’s $x$ span)height
(the flipper’s $y$ span)
To me, this is much easier to conceptualize, and by creating functions
to calculate position
and range
, the flipper
math mostly remains the same. Our end result is a parameter set that
is easy for both humans and computers to interpret — the best of
both worlds.
Animated Chaos
Animations are important to flipping houses — they contextualize the laws of the in-game world, and, in my opinion, they’re satisfying to watch too. They weren’t easy to make, however, since we had to start from scratch, with our prototype attempt being… subpar to say the least. Below is an excerpt from the prototype’s animation-handling code:
void handleAnim() {
if (animStep == 0) {
return;
}
switch (animStep) {
case 1:
animPercent += 0.06;
break;
case 2:
animPercent += 0.02;
break;
case 3:
animPercent += 0.045;
case 4:
animPercent += 0.008;
}
if (animPercent < 1) {
return;
}
animStep += 1;
animPercent = 0;
switch (animStep) {
case 2:
break;
case 3:
for (Token token : tokens) {
...
}
break;
case 4:
if (isWinningPosition()) {
...
}
else {
animStep = 0;
}
break;
default:
handleWin();
animStep = 0;
}
}
It’s a hardcoded mess that leads to inter-animation dependencies. In the rewritten version, I redesigned the animation system from scratch, this time using an object-oriented approach. The animation system code is a bit complicated for this post, but the UX for creating new animations compared to the old system is night and day. Here’s the new code for handling the “change colour” animation for tiles:
// anims/cell.js
const changeColourAnimation = (e) => {
let targetColour;
const onStart = ([colour]) => {
targetColour = colour;
};
const onEnd = () => {
e.colour = targetColour;
};
const effect = (progress) => {
fill(lerpColor(e.colour, targetColour, smoothenAnimation(progress)));
};
return new Animation(onStart, onEnd, effect);
};
// cell.js
require("./anims/cell.js");
class Cell {
...
constructor(...) {
...
this.changeColourAnimation = changeColourAnimation(this);
}
draw = () => {
...
this.changeColourAnimation.applyEffect();
...
}
...
}
And here’s the code to play that animation, which can be called at just about any time:
// in the Cell class
// fade to red over 0.25s and print "Finished animation!" upon completion
this.changeColourAnimation.play(0.25, { startArgs: [color(255, 0, 0)] }, () => {
console.log("Finished animation!");
});
That’s all we need.
By implementing a better code architecture and leveraging JavaScript’s concise lambda syntax (compared to Java), I remade the animation system such that animations are independent from each other and easy and quick to make. This system works well with all sorts of other transitions, including, but not limited to:
- fading from visible to invisible (and vice versa);
- animating the tile flips; and
- smoothly transitioning between camera angles (for the upcoming “overview” mode).
Had I been this careful when coding the original prototype, the rest of the prototype development would’ve gone much smoother.
Shady Activity
As an excercise, lets rotate an image to face the camera (i.e. $3\pi/4$ radians), which is how we implemented the Paper Mario-like art style. Using a linear algebra technique, we can apply the following transformation to each pixel, with $(\alpha_x, \alpha_y, \alpha_z)^T$ as the image’s anchor point and $(\beta_x, \beta_y, \beta_z)^T$ as the position of the pixel.
$$ T = \underbrace{ \begin{bmatrix} 1 & 0 & 0 & \alpha_x \\ 0 & 1 & 0 & \alpha_y \\ 0 & 0 & 1 & \alpha_z \\ 0 & 0 & 0 & 1 \end{bmatrix} }_\text{translate to the anchor} \times \underbrace{ \begin{bmatrix} \frac{-\sqrt{2}}{2} & \frac{-\sqrt{2}}{2} & 0 & 0 \\ \frac{\sqrt{2}}{2} & \frac{-\sqrt{2}}{2} & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} }_\text{rotate about the origin} \times \underbrace{ \begin{bmatrix} 1 & 0 & 0 & -\alpha_x \\ 0 & 1 & 0 & -\alpha_y \\ 0 & 0 & 1 & -\alpha_z \\ 0 & 0 & 0 & 1 \end{bmatrix} }_\text{translate to the origin} \times \begin{bmatrix} \beta_x \\ \beta_y \\ \beta_z \\ 1 \end{bmatrix} $$
Luckily for us, Processing has built-in functions to handle these operations, with the final code being:
pushMatrix();
transform(a_x, a_y, a_z); // translate to the anchor
rotateZ((3 * PI) / 4); // rotate about the origin
transform(-a_x, -a_y, -a_z); // translate to the origin
image(a_x, a_y, a_z);
popMatrix();
Much better.
As you can see, we need to be explicit in how we approach positioning objects in space, promoting a mindset that can lead to pleasing solutions to engine limitations; case in point: Flipping Houses’s shadow system for items, which can be summarized as follows:
-
Preprocessing
- Generate a translucent black silhouette of the target image based on the alpha values of its pixels.
- Skew the image to imitate a projection.
-
Rendering
- Draw the shadow at the position of the item.
- Rotate the shadow (with the anchor point at the bottom) about the $x$- and/or $y$-axis to lie flat on the board.
This system offers a lot of flexibility for the shadows. For example, we can modify Step 1.1 to generate shadows of any colour. This opens up the possibility of using shadow colours to indicate complimentary items, an idea that I think is worth looking into for the rewritten version of the game.
Conclusion
The judges gave us scores of 45/50 and 46.5/50, along with the following feedback:
-
Strengths
- Interesting concept
- Technically challenging to make
- Satisfying gameplay
-
Weaknesses
-
Could benefit from a fleshed out tutorial
- Unintuitive controls (at first)
-
Okay UI; could use improvements
- Add a reset button
- Make complementary items more obvious
- Allow players to view all flippers at once
-
Could benefit from a fleshed out tutorial
By my interpretation, the core elements of Flipping Houses are solid, but it could use some touches around the edges. In other words, it should be trivial to convert the prototype into a great experience. I plan on addressing (or have already addressed) all of the weaknesses in my rewritten version.
I left Winter MelonJam 2022 with a better understanding of the hidden mechanisms behind video games. I’d bet that for someone with more knowledge and experience than me, it might be worth it in some cases to ignore a game engine’s default systems in favour of creating a more flexible/performant alternative. For those closer to my skill level, working with a “lower”-level tool like Processing can foster an appreciation for feature-rich game development tools and offer some insight into how video games work. I’d definitely recommend the experience for all video game programmers to try it at least once!
No comments yet.