The Bounce of Rubber Juggle

  • 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

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 NumbersAnchor for 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.

By the end, we should have a small list of questions that can help kick start a solution to many different programming problems!

Identifying ObjectsAnchor for 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.

All code in this article is in Typescript.

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 PropertiesAnchor for 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 WallsAnchor for 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 CollisionAnchor for 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:

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:

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

If you're wondering how the formula works, it essentially creates a triangle between the three points and uses the area for a triangle, , to figure out the "height". That height turns out to be the distance!

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 BugAnchor for 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.

Software testing might have caught this bug, but it isn't guaranteed. In theory, each scenario I pictured above could have become a unit test, but since I had failed to consider the case of the balloon passing the invisible line formed by the band, the bug would have manifested anyway.

Here, proper test driven development may have slowed me down enough to properly consider all of the edge cases and thereby catch the bug before it manifested. But I was doing a 48-hour game jam, and wasn't exactly in a TDD mindset at the time (:

Distance Between a Point and Line SegmentAnchor for 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 , and the two pegs in the band as and , then:

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

The variable represents a vector, which is shorthand for .

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

If you're wondering how this formula works, I recommend reading the original math exchange post, as it's summarized well, albeit rather concisely. I may decide to write something short on this topic specifically in the future.

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.

Feel free to look at the source code of the widget above to make sure I'm not cheating!

Alternative Approaches?Anchor for 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.

  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!

Bouncing off the WallsAnchor for 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.

The balloon bounce is admittedly simpler than most other scenarios, as only one thing is changing. The domain becomes vastly more complicated if multiple things change in interconnected ways.

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

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

In mathematical notation, the subtle difference between a vector marked with an arrow like and a "hat" symbol like is the difference between having a length of 1 or not. The hat symbol denotes a unit vector, meaning the vector must have a length of 1. Generally, this is used to indicate that it's only the direction of the vector that's important.

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.

There's no reason the reflection needed to be modeled with specular reflection. In fact, the angle of reflection could have been completely random if we wanted! During the game jam, I decided on a predictable model of reflection to make the game feel like it was more within the player's control, giving an opportunity to plan and strategize.

Final CodeAnchor for 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())
}

As an interesting note, we did not really need to know all that much math to arrive at this solution. Indeed, most of the formulas came from online sources! What we did need to know was what to research, and that required reducing the problem into something more familiar, breaking down the complexity into pieces that can be more easily searched.

Summarizing the Key QuestionsAnchor for 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:

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!