Last week, I participated in the annual GMTK Game Jam to make an entire video game in just 48 hours. The result?
Rubber Juggle, a minigame where the goal is to keep balloons on screen for as long as possible! You do that by rubber-banding pegs on a pegboard that the balloons bounce off of, like below.
That bounce may look fairly innoculous, but it turns out to be slightly tricky to code (especially for me who hadn't touched any linear algebra in years)! The bounce mechanic was the core of Rubber Juggle, so I spent most of my time during the jam coding it to work well and feel good.
In this article, I want to detail my thought process in implementing the bounce of Rubber Juggle, focusing on key questions that can help implement virtually any game mechanic.
From Pictures to Numbers
Permalink to "From Pictures to Numbers"Humans and computers see the world very differently. If I say, "Imagine a bounce," most people will probably envision a ball hitting a wall or bouncing several times on the floor, tracing out an imaginary path; it is a very pictoral way of predicting an outcome. Computers, however, just see a bunch of numbers. To them, the ball is just a few numbers that change over time according to some rules.
The numbers a computer sees only have meaning when interpretted by a person. That interpretation is what we call a domain model in code. A domain model is just a way of representing real world objects and processes in a way a computer can understand. For example, one way to model a cup of tea on a wooden table is with two numbers: one for its x
location and one for its y
location. Modelling is all about identifying how to represent important objects for the processes we want to automate.
Programmers are interpretters. We focus on giving meaning to the cacaphony of numbers chugging inside the machine.
So to get a good bounce working for our balloon, we want to start thinking of a way to create a domain model. There is no one way to always create a good model; heck, the entire field of software engineering is devoted to making accurate models! That said, a good approach is to come up with key questions that can get us closer and closer to solving the problem.
Identifying Objects
Permalink to "Identifying Objects"For our first question, we need to identify what things are even important.
To answer this question, we can try to summarize the entire scenario in a sentence. Doing so, let's pay special attention to the nouns in the sentence and highlight them.
A balloon bounces on a rubber band tied between two pegs.
As it turns out, the nouns are probably very important objects to model! With that, we can start to actually draft out some code.
class Balloon { }
class RubberBand { }
class Peg { }
It is important to note that as we learn more about the scenario, we may come up with more things to model, and that's all right! The key questions are not meant to be answered all at once; modelling is an iterative process (which, by the way, is why developers spend a lot of time making code that is easy to change).
Identifying Properties
Permalink to "Identifying Properties"Now that we have objects, we want to start contemplating what about those objects is actually important for the bounce.
Let's go back to our one-summary sentence from before. This time, however, we're going to highlight the verbs.
A balloon bounces on a rubber band tied between two pegs.
The verbs are very important actions within our domain. Knowing what the actions are, we can start to think about what properties are necessary in order to model them.
- To know when the bounce happens, we must know the positions of the balloon and the rubber band
- The bounce will cause a change in the velocity of the balloon
- Since the rubber band is tied between two pegs, the positions of the pegs determines the position of the rubber band
- And finally, position and velocity are two-dimensional vectors since we working on a 2d game
Knowing these, we can start to add to our model!
// Adding Vector to our model to represent two-dimensional properties
class Vector {
x: number
y: number
}
class Balloon {
position: Vector
velocity: Vector
}
// "tied" between two pegs
class RubberBand {
from: Peg
to: Peg
}
class Peg {
position: Vector
}
As with the previous question, it's very possible (read: likely) that we may encounter more properties to add to our domain. Once we have a starting point, though, we can begin to think about how to code the processes, in our case the bounce itself.
Bumping into Walls
Permalink to "Bumping into Walls"Now that we have a way of representing the balloon and pegs, we must tackle the next critical question:
In other words, how do we know when a balloon is colliding with a band? This ultimately turns out to be collision detection, which is one of the most common themes in game development, enough so that most game engines provide in-built support for detecting collisions.
Now, in the game jam I was using raw PixiJS, which is a WebGL renderer and not a game engine, so it didn't have any utilities for collision detection. Because of that, I needed to find a way to determine when a bounce was starting using the domain represention created above.
Detecting Collision
Permalink to "Detecting Collision"Faced with a task like this, I personally like drawing a bunch of scenarios to see if a pattern can be picked out. For instance:
Pictorially, it can be easy to pick out in which scenarios a bounce should happen, but it doesn't really help actually solve how to code the bounce. For that, we have to start thinking in terms of our domain model, simplifying the problem a bit and bringing us a step closer to how the computer sees the interacting elements.
Redrawing the exact same three scenarios above but using only our domain representation, we get this:
(x, y)
represents the balloon's position, which in our code isballoon.position
(ax, ay)
and(bx, by)
represent the peg positions, which in or code isband.from.position
andband.to.position
r
represents the balloon's radius/size, which is actually not in our representation ofBalloon
yet! That's fine, if we need it, we can add it as a property calledballoon.radius
.
Meanwhile, d
does not actually have a represention in the domain model. Instead, it represents the shortest distance from the center of the balloon to the rubber band. Drawing d
into the diagram actually reveals a key insight into how we can determine a collision:
- In the first diagram,
d
is much longer thanr
, and there is no collision - In the second diagram,
d
is equal tor
, and there is a collision - In the third diagram,
d
is much shorter thanr
, and there is a collision
This means that if the shortest distance from the balloon to the band is less than or equal to the size of the balloon, then a collision must be happening! This allows us to begin writing a function in our model:
class Balloon {
position: Vector
radius: number // NEW!
isCollidingWith(band: RubberBand): boolean {
return band.distanceTo(this.position) <= this.radius
}
}
class RubberBand {
distanceTo(p: Vector): number {
// ???
}
}
Next, we figure out the distanceTo()
method.
If we look at our pictoral representation, what we have is a balloon represented by a point (x, y)
and a band represented by two points (ax, ay)
and (bx, by)
. I don't know about you, but I don't casually have memorized the formula for getting the distance between a line and a point. Thankfully, the Internet is here to save us, and we find that the mathy-looking formula is thus:
Even if the formula looks intimidating at first, translating it into code just requires puting the right variables into the right places. Doing so, we get:
class RubberBand {
from: Peg
to: Peg
distanceTo(p: Vector): number {
const a = this.from.position
const b = this.to.position
const dx = b.x - a.x
const dy = b.y - a.y
const area = Math.abs(dx * (a.y - p.y) - dy * (a.x - p.x))
const length = Math.sqrt(dx * dx + dy * dy)
return area / length
}
}
And this seems to work great! The interactive below uses this formula to determine a collision between the rubber band and the balloon.
Unfortunately, there is a very significant problem with this model...
The Software Bug
Permalink to "The Software Bug"It is very rare in programming to get things working the first time around. When I first coded the bouncing logic during the game jam, I kept running into this mysterious bug where the balloon would seem to bounce off of thin air. If we take our interactive from above, but this time shimmy the balloon to the right a bit, it reveals an interesting thing:
A collision is clearly being detected when it shouldn't be! In fact, it's almost as if there's an invisible line extending the rubber band.
As it turns out, that's because there is an infinite line stretching the entire span of the band, mathematically speaking. The formula used above helps find the distance from a point to a line defined by two other points; in math, lines are infinitely long, meaning our formula for collision detection treats the band as infinitely long too.
What we really want is the distance from a point to a line segment. Knowing this, we can add a couple more diagrams to our scenarios above:
Finding this version of d
will be far more accurate for collision detection.
If the original distance formula was wrong all along, why did I bring it up in this article? The point is to highlight a kind of software bug that does not result from writing the code wrong. Indeed, the code above implements the distance formula perfectly fine. The bug was not in the code, but in the domain understanding.
I wanted to simulate a balloon bouncing off a rubber band, and I modeled it using a formula which was too simple to accurately represent that. As a result, the code worked fine but the outcome was wrong.
It is perfectly fine, and honestly normal, to incrementally update our understanding of the problem and solution over time, and this is a core reason why writing maintainable code is of such importance.
Distance Between a Point and Line Segment
Permalink to "Distance Between a Point and Line Segment"Ok, so what's the formula for the distance between a point and a line segment?
As it turns out, this question is a bit more specialized, so it's more difficult finding a nice concise formula. In fact, some of the first search results show how to solve this problem in code. Having looked through them all, though, my favorite find was a post made on the mathematics stack exchange.
The idea can essentially be summarized in four steps. Given we represent the balloon's x and y position as a vector called
- Define
- Determine
- Determine
- Find the distance:
Let's modify the distanceTo()
function to match this new formula one step at a time.
- Define
For the first step, we need to create a function that performs a few basic math operations on vectors. Since we are adding vectors together, we can actually implement the rules in the Vector
class.
class Vector {
/* x, y */
// We add these functions to our vector model
add = (other: Vector) =>
new Vector(this.x + other.x, this.y + other.y)
minus = (other: Vector) =>
new Vector(this.x - other.x, this.y - other.y)
scaleBy = (scalar: number) =>
new Vector(this.x * scalar, this.y * scalar)
}
class RubberBand {
distanceTo(p: Vector): number {
const a = this.from.position
const b = this.to.position
const s = (t: number) => a.add(b.minus(a).scaleBy(t))
// ...
}
}
- Determine
Defining Vector
model.
class Vector {
/* x, y, add, minus, scaleBy */
// Add more vector operations that we need
dot = (other: Vector) =>
this.x * other.x + this.y * other.y
magnitude = () =>
Math.sqrt(this.x * this.x + this.y * this.y)
}
class RubberBand {
distanceTo(p: Vector): number {
/* a, b, s(t) */
const m = b.minus(a).magnitude()
const th = (p.minus(a).dot(b.minus(a)) / (m * m)
// ...
}
}
- Determine
This essentially clamps Math.min
and Math.max
as functions.
class Vector { /* ... */ }
class RubberBand {
distanceTo(p: Vector): number {
/* a, b, s(t), th */
const ts = Math.min(Math.max(th, 0), 1)
// ...
}
}
- Find the distance:
Finally, the distance just puts Vector
model methods to help make this line succinct.
class Vector { /* ... */ }
class RubberBand {
distanceTo(p: Vector): number {
/* a, b, s(t), th, ts */
return s(ts).minus(p).magnitude()
}
}
Put it all together, and we get the following code:
class Vector {
x: number
y: number
add = (other: Vector) =>
new Vector(this.x + other.x, this.y + other.y)
minus = (other: Vector) =>
new Vector(this.x - other.x, this.y - other.y)
scaleBy = (scalar: number) =>
new Vector(this.x * scalar, this.y * scalar)
dot = (other: Vector) =>
this.x * other.x + this.y * other.y
magnitude = () =>
Math.sqrt(this.x * this.x + this.y * this.y)
}
class RubberBand {
from: Peg
to: Peg
distanceTo(p: Vector): number {
const a = this.from.position
const b = this.to.position
const s = (t: number) => a.add(b.minus(a).scaleBy(t))
const m = b.minus(a).magnitude()
const th = (p.minus(a).dot(b.minus(a)) / (m * m)
const ts = Math.min(Math.max(th, 0), 1)
return s(ts).minus(p).magnitude()
}
}
This algorithm turns out to work much better! Using our same interactive, we can see that the balloon no longer collides with the "invisible" line formed by the rubber band.
Alternative Approaches?
Permalink to "Alternative Approaches?"Before we started looking into how to implement the collision detection, or domain model consisted of just positions for the balloon and pegs. Now, we have updated our understanding of the problem to include distance from the rubber band to various things and whether a balloon is in contact with a band.
In our code, the distance became a function in the RubberBand
class, and the collision detection a method on Balloon
. However, is there any particular reason to organize the code this way?
- Could we have instead implemented the distance function on
Balloon
? - Could we have implemented the collision detection on
RubberBand
instead?
First of all, these are very good questions to ask! Part of having a maintainable codebase is having an accurate domain model, and having an accurate domain model means representing core concepts in the right places in code.
I like to think of questions like these in two ways.
- At first, we had the task of converting our understanding of the balloon bounce into code. If we instead we flip our thinking and consider converting our code into what it means for a balloon to bounce, does anything change?
- Are we following good programming principles like encapsulation, single responsibility, and so forth?
Regarding whether determining distance belongs on Balloon
or RubberBand
, ultimately this came down to a concern for encapsulation and generality. If distance were implemented on Balloon
, then it would require the Balloon
class to know that a RubberBand
is composed of two pegs, which is an internal implementation. If in the future RubberBand
could be composed of three pegs, then we'd have to change the definition of a function in Balloon
, indicating coupled code.
Ideally, a change in the implementation of RubberBand
should require only changing code in RubberBand
, hence why it makes sense for the distance calculation to be there. Additionally, since distanceTo
is written taking a generic Vector
as a parameter, it could theoretically be used to determine distance to any object, not just Balloon
s.
Regarding whether collision belongs on the balloon or the rubber band, I landed on the logic being in Balloon
because it makes more sense to ask myself, "Am I as a person bumping into a wall?" rather than the question "As a wall, am I bumping into a person?" In particular, the first question makes sense because the one moving is the person, and the person is the one who will feel the effect of the collision.
Now that the logic is in place for detecting the bounce, the only thing left to do is code the bounce itself!
Bouncing off the Walls
Permalink to "Bouncing off the Walls"Having a model for determining when the interaction is occurring is useful, so now we need to ask about the effect.
Focusing on what changes can help in modelling the situation accurately. In the case of the balloon bouncing, only one thing is actually changing: the direction of the balloon's motion. And conveniently, we already have the direction encoded as the balloon's velocity.
So, the real challenge here is figuring out what the balloon's velocity should be after a bounce so that it feels like a real bounce.
As before, I like to think of the problem pictorially before analyizing possible abstractions. If we take the bounce from before, we can trace out its path and reveal the nature of the reflection:
In this case, the simplest way to represent the bounce is with a symmetrical reflection; in physics, this is known as specular reflection. If we compare a typical diagram for specular reflection against the diagram created by our simulated bounce, we can see the similarity:
So that's great and all, but how the heck do we actually code this?
Again, we have the internet to thank for providing a formula that models this exact kind of reflection.
Our goal is to find
- We already have a variable for
called balloon.velocity
- For
, however, we will need to append the RubberBand
model a little
The variable
We can actually add this to our model of RubberBand
with a descriptive function:
class RubberBand {
from: Peg
to: Peg
surfaceDirection(): Vector {
// Recall: our Vector class has minus defined already
return to.position.minus(from.position)
}
}
However, all we want is a direction, meaning that the length of the band is irrelevant. We can normalize the vector by dividing it by its own magnitude, essentially forcing its length to become 1.
class RubberBand {
from: Peg
to: Peg
surfaceDirection(): Vector {
return to.position.minus(from.position).normalized()
}
}
class Vector {
normalized = () =>
this.scaleBy(1 / this.magnitude())
}
Now, the mystical band.surfaceDirection()
! With that, we can utilize the other vector methods already created in the previous sections to model the bounce:
class Balloon {
bounce(band: RubberBand) {
const r = band.surfaceDirection()
const v = this.velocity
this.velocity = r.scaleBy(2 * r.dot(v)).minus(v)
}
}
And that's it! This changes the Balloon's velocity to match the velocity given by the formula representing a reflection on the rubber band.
Final Code
Permalink to "Final Code"And so, here is the final code we end up with after performing this exercise! This code represents the beginning of the Rubber Juggle domain model, capable so far of simulating a balloon bouncing off a rubber band.
During the game jam, this is essentially the core around which I built the rest of the code, adding to and updating the model as more complexity became required.
class Balloon {
position: Vector
velocity: Vector
radius: number
isCollidingWith(band: RubberBand): boolean {
return band.distanceTo(this.position) <= this.radius
}
bounce(band: RubberBand) {
const r = band.surfaceDirection()
const v = this.velocity
this.velocity = r.scaleBy(2 * r.dot(v)).minus(v)
}
}
class RubberBand {
from: Peg
to: Peg
distanceTo(p: Vector): number {
const a = this.from.position
const b = this.to.position
const s = (t: number) => a.add(b.minus(a).scaleBy(t))
const m = b.minus(a).magnitude()
const th = (p.minus(a).dot(b.minus(a)) / (m * m)
const ts = Math.min(Math.max(th, 0), 1)
return s(ts).minus(p).magnitude()
}
surfaceDirection(): Vector {
return to.position.minus(from.position).normalized()
}
}
class Peg {
position: Vector
}
class Vector {
x: number
y: number
add = (other: Vector) =>
new Vector(this.x + other.x, this.y + other.y)
minus = (other: Vector) =>
new Vector(this.x - other.x, this.y - other.y)
scaleBy = (scalar: number) =>
new Vector(this.x * scalar, this.y * scalar)
dot = (other: Vector) =>
this.x * other.x + this.y * other.y
magnitude = () =>
Math.sqrt(this.x * this.x + this.y * this.y)
normalized = () =>
this.scaleBy(1 / this.magnitude())
}
Summarizing the Key Questions
Permalink to "Summarizing the Key Questions"As it turns out, this entire ordeal in implementing the bounce for Rubber Juggle is really an exercise in domain modeling. Clearly, not every game is going to have bouncing the same way Rubble Juggle does, but every game is going to have its own domain, and our primary job as programmers is to pick out the core pieces of the problem and put them into code a computer understands.
To get to the final bounce solution, we asked some key questions:
- What are they key objects involved in my scenario?
- What properties of those objects are the most important to represent?
- How do I know when the scenario is starting or ending?
- During the course of the scenario, what changes?
These questions help put the focus on the key nouns and verbs, creating a domain-driven mindset. In a game with multiple mechanics and concerns, this can really help highlight the truly important aspects to focus on, but more than that, a domain-driven approach helps make the code readable, maintainable, and extensible.
At the end of the day, what matters most is creating a solid domain understanding, for which there are many useful techniques. Next time you get stuck with coder's block, see if slowing down and asking these key questions helps develop an approach!