Auroratide

Coding + Storytelling

The Bounce of Rubber Juggle

Game Dev

Topics
  • game
  • rubble juggle
  • domain driven design
  • typescript
  • technique
  • math
  • physics
  • bugs
  • reflection

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.

Animated image of a balloon bouncing off a rubber band.

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.

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.

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).

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.

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.

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:

Not colliding
Just colliding
Definitely colliding

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:

A blue circle marked (x, y) with radius r. Two small, red circles marked (ax, ay) and (bx, by) are connected with a brown line. The blue circle and brown line do not intersect. The distance between them is labeled d.
A blue circle marked (x, y) with radius r. Two small, red circles marked (ax, ay) and (bx, by) are connected with a brown line. The blue circle barely touches the brown line.
A blue circle marked (x, y) with radius r. Two small, red circles marked (ax, ay) and (bx, by) are connected with a brown line. The blue circle is on top of the brown line.
  • (x, y) represents the balloon's position, which in our code is balloon.position
  • (ax, ay) and (bx, by) represent the peg positions, which in or code is band.from.position and band.to.position
  • r represents the balloon's radius/size, which is actually not in our representation of Balloon yet! That's fine, if we need it, we can add it as a property called balloon.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 than r, and there is no collision
  • In the second diagram, d is equal to r, and there is a collision
  • In the third diagram, d is much shorter than r, 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...

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.

Two pegs connected with a rubber band and a ballon are lined up on a pegboard. A thick dashed line is drawn across all of them.
At the moment, the band internally stretches infinitely in both directions

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:

A blue circle marked (x, y) with radius r. Two small, red circles marked (ax, ay) and (bx, by) are connected with a brown line. The blue circle is above one of the red circles. The distance between them is labeled d.
A blue circle marked (x, y) with radius r. Two small, red circles marked (ax, ay) and (bx, by) are connected with a brown line. The blue circle is lined up with the brown line, but not going through it. The distance between them is labeled d.

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.

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 , and the two pegs in the band as and , then:

  1. Define
  2. Determine
  3. Determine
  4. Find the distance:

Let's modify the distanceTo() function to match this new formula one step at a time.

  1. 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))
		// ...
	}
}
  1. Determine

Defining requires using the dot product and vector magnitude. Again, since these are operations on vectors, we can put them onto the 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)
		// ...
	}
}
  1. Determine

This essentially clamps to be between 0 and 1. Javascript provides 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)
		// ...
	}
}
  1. Find the distance:

Finally, the distance just puts into the function defined earlier! We can utilize our 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.

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.

  1. 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?
  2. 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 Balloons.

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!

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:

A blue circle's path bouncing off a brown line is drawn, with a dashed line bisecting the angle formed.
Bounce Reflection
Two line segments QO and OP depect reflection on a mirror. A line bisects the angle formed.
Specular Reflection

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 , the new velocity for the balloon. Using its current velocity and the direction of the rubber band , we can do some vector math:

  • We already have a variable for called balloon.velocity
  • For , however, we will need to append the RubberBand model a little

The variable represents the direction of the rubber band's surface. This is essentially a vector created by sticking the rubber band onto a plane and drawing an arrow from one peg to the other.

Two pegs are drawn on a grid with one on the origin. A vector arrow is drawn from the origin to the other peg.
This vector equals the difference between the position vectors of each peg

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 variable is represented in our model with 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.

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())
}

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:

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?
Balloon

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!