smish.dev
ball_simulation_3

This post aims to describe an accurate model of the physics behind the collisions of Rocket League, so that players and bot developers might better understand precisely what happens when the car and ball make contact.

Although these notes are presented on this personal website, this work was a collaboration with Nevercast and tarehart (both members of the RLBot community). Additionally, I also want to express my thanks to Bakkes for developing tools that greatly simplified the data collection process for this analysis.

## Hitboxes and Collision Detection

Rocket League doesn't use the actual car's geometry to determine when collisions take place, instead they use oriented bounding boxes (OBBs) for each preset type:

Although this geometry doesn't really match the car very well, it is convenient from a programming perspective because it is inexpensive to perform intersection tests on this simple shape. In particular, we can determine if a sphere is contacting the oriented bounding box by first finding the point on the OBB that is closest to the sphere's center, and then check if the distance between those points is less than the radius.

For a general shape, it can be pretty challenging to find the point on that shape that has the least distance to another point of interest. Fortunately, the orthogonality of the face normals on the OBB make it so we can find that nearest point in only a few lines of code.

Here's one possible implementation of a way to find that nearest point:

​xstruct sphere{  vec3 center;  float radius;};
struct obb{  vec3 center;  vec3 half_width;  mat3 orientation;};
vec3 nearest_point_on_obb(const obb & a, const vec3 b) {
// get the local coordinates of b  vec3 b_local = dot(b - a.center, a.orientation);
// clip those coordinates to find the closest point (in local coordinates)  vec3 closest_local = vec3{    fminf(fmaxf(b_local[0], -a.half_width[0]), a.half_width[0]),    fminf(fmaxf(b_local[1], -a.half_width[1]), a.half_width[1]),    fminf(fmaxf(b_local[2], -a.half_width[2]), a.half_width[2])  };
// transform back to world coordinates  return dot(a.orientation, closest_local) + a.center;
}
bool intersect(const obb & a, const sphere & b) {  vec3 p = nearest_point_on_obb(a, b.center);  return (norm(p - b.center) <= b.radius);}


Detailed information about hitbox dimensions and offsets for each preset type can be found here, courtesy of halfwaydead. Additionally, his video on hitboxes (and everything else on his channel) is also highly recommended.

Now that we have a way to detect when a collision happens, how does it affect the trajectory of the ball?

## Relevant Physics of Rigid Body Collisions

If a car collides with the ball, we need to know the following information to predict the outcome of the collision:

• $m$$m$ (mass)

• $\mathbit{I}$$\boldsymbol{I}$ (moment of inertia)

• $\mathbit{x}$$\boldsymbol{x}$ (position / pivot point)

• $\mathbit{v}$$\boldsymbol{v}$ (velocity)

• $\mathbf{\Theta }$$\boldsymbol{\Theta}$ (orientation matrix)

• $\mathbit{\omega }$$\boldsymbol{\omega}$ (angular velocity)

Some other useful notation:

• $\mathbit{J}$$\boldsymbol{J}$ (impulse)

• $\mathbit{L}$$\boldsymbol{L}$ (cross-product matrix satisfying $\mathbit{L}\mathbit{y}=\left(\mathbit{p}-\mathbit{x}\right)×\mathbit{y}$$\boldsymbol{L}\boldsymbol{y} = (\boldsymbol{p} - \boldsymbol{x}) \times \boldsymbol{y}$)

A symbol's subscript will be used to distinguish between quantities belonging to the car and ball (either 'c' for car or 'b' for ball). e.g. ${\mathbf{\Theta }}_{c}$$\boldsymbol{\Theta}_c$ will be the car's orientation matrix, ${\mathbit{\omega }}_{b}$$\boldsymbol{\omega}_b$ will be the ball's angular velocity, and so on. Symbols in bold represent vectors or matrices (e.g. $\mathbf{1}$$\mathbf{1}$ is the identity matrix, not the number 1), and all other symbols are scalars.

When objects collide, they exert (equal and opposite) forces on each other to prevent interpenetration. In general, there can be many points of contact (each with its own force), but the problem we care about is simple: a sphere hitting a box. In this case, the contact can be reasonably approximated by a force applied a single point. If we knew that force, $\mathbit{F}$$\boldsymbol{F}$, was applied to a rigid body at point $\mathbit{p}$$\boldsymbol{p}$ for a duration $\mathrm{\Delta }t$$\Delta t$, then the rigid body would respond in the following way (where quantities with a tilde are the post-collision values, and $\mathbit{J}=\mathbit{F}\mathrm{\Delta }t$$\boldsymbol{J} = \boldsymbol{F} \Delta t$):

$\begin{array}{}\text{(1)}& \left\{\begin{array}{l}\stackrel{\mathbf{~}}{\mathbit{v}}=\mathbit{v}+{m}^{-1}\mathbit{J}\\ \\ \stackrel{\mathbf{~}}{\mathbit{\omega }}=\mathbit{\omega }+{\mathbit{I}}^{-1}\left[\left(\mathbit{p}-\mathbit{x}\right)×\mathbit{J}\right]=\mathbit{\omega }+{\mathbit{I}}^{-1}\mathbit{L}\phantom{\rule{0.278em}{0ex}}\mathbit{J}\end{array}\end{array}$

So, the velocity change only depends on the impulse itself, not where it is applied. In contrast, the change in angular velocity cares about where that force is being applied, and the matrix $\mathbit{L}$$\boldsymbol{L}$ represents the "lever arm".

All that remains is to find an expression that tells us how to compute the appropriate value of $\mathbit{J}$$\boldsymbol{J}$ and we can use the equation above to figure out what the ball will be doing after the collision.

In Rocket League, this $\mathbit{J}$$\boldsymbol{J}$ is actually made up of two separate contributions: one from the game's underlying physics engine, and one that Psyonix added to make the game more predictable and fun. The following sections below describe how to calculate those different contributions.

## Impulse from the Physics Engine

This impulse from the physics engine is modeled as coming from an inelastic rigid-body collision with Coulomb friction.

Intuitively, the 'inelastic collision' part of the model means that the impulse $\mathbit{J}$$\boldsymbol{J}$ will be determined such that the post-collision surface velocities at $\mathbit{p}$$\boldsymbol{p}$ of the two bodies are the same. The surface velocity, $\mathbit{V}$$\boldsymbol{V}$ at $\mathbit{p}$$\boldsymbol{p}$ depends on the object's velocity, as well as how fast it is spinning:

$\begin{array}{}\text{(2)}& \mathbit{V}=\mathbit{v}+\mathbit{\omega }×\left(\mathbit{p}-\mathbit{x}\right)=\mathbit{v}-\mathbit{L}\phantom{\rule{0.278em}{0ex}}\mathbit{\omega }\end{array}$

So, the inelastic collision model demands that $\mathrm{\Delta }\stackrel{~}{\mathbit{V}}\equiv {\stackrel{~}{\mathbit{V}}}_{c}-{\stackrel{~}{\mathbit{V}}}_{b}=\mathbf{0}$$\Delta \tilde{\boldsymbol{V}} \equiv \tilde{\boldsymbol{V}}_c - \tilde{\boldsymbol{V}}_b = \boldsymbol{0}$, which when applying equations $\left(1\right)$$(1)$ and $\left(2\right)$$(2)$ gives us and expression in terms of the unknown impulse $\mathbit{J}$$\boldsymbol{J}$:

$\begin{array}{}\text{(1)}& \begin{array}{rl}\mathbf{0}& =\mathrm{\Delta }\stackrel{~}{\mathbit{V}}\\ \\ & ={\stackrel{~}{\mathbit{V}}}_{c}-{\stackrel{~}{\mathbit{V}}}_{b}\\ \\ & =\left[{\stackrel{~}{\mathbit{v}}}_{c}-{\mathbit{L}}_{c}{\stackrel{~}{\mathbit{\omega }}}_{c}\right]-\left[{\stackrel{~}{\mathbit{v}}}_{b}-{\mathbit{L}}_{b}{\stackrel{~}{\mathbit{\omega }}}_{b}\right]\\ \\ \mathbf{0}& =\left[\left({\mathbit{v}}_{c}+\frac{\mathbit{J}}{{m}_{c}}\right)-{\mathbit{L}}_{c}\phantom{\rule{0.167em}{0ex}}\left({\mathbit{\omega }}_{c}+{\mathbit{I}}_{c}^{-1}{\mathbit{L}}_{c}\mathbit{J}\right)\right]-\left[\left({\mathbit{v}}_{b}-\frac{\mathbit{J}}{{m}_{b}}\right)-{\mathbit{L}}_{b}\phantom{\rule{0.167em}{0ex}}\left({\mathbit{\omega }}_{b}-{\mathbit{I}}_{b}^{-1}{\mathbit{L}}_{b}\mathbit{J}\right)\right]\end{array}\end{array}$

Group similar terms and solve for $\mathbit{J}$$\boldsymbol{J}$:

$\begin{array}{}\text{(2)}& \mathbf{0}=\underset{\mathrm{\Delta }\mathbit{V}}{\underset{⏟}{\left(\left[{\mathbit{v}}_{c}-{\mathbit{L}}_{c}{\mathbit{\omega }}_{c}\right]-\left[{\mathbit{v}}_{b}-{\mathbit{L}}_{b}{\mathbit{\omega }}_{b}\right]\right)}}+\underset{{\mathbit{M}}^{-1}}{\underset{⏟}{\left(\left[\frac{1}{{m}_{c}}+\frac{1}{{m}_{b}}\right]\mathbf{1}-{\mathbit{L}}_{c}{\mathbit{I}}_{c}^{-1}{\mathbit{L}}_{c}-{\mathbit{L}}_{b}{\mathbit{I}}_{b}^{-1}{\mathbit{L}}_{b}\right)}}\phantom{\rule{0.278em}{0ex}}\mathbit{J}\end{array}$

So, when the dust settles, we are left with a 3 by 3 system of linear equations. This system can be solved to determine the impulse:

$\begin{array}{}\text{(3)}& \mathbit{J}=-\mathbit{M}\phantom{\rule{0.278em}{0ex}}\mathrm{\Delta }\mathbit{V}\end{array}$

where the matrix $\mathbit{M}$$\boldsymbol{M}$ could be interpreted as the reduced mass matrix for this problem.

The Coulomb friction model says that the maximum allowable frictional force for the collision is proportional to the normal component of $\mathbit{J}$$\boldsymbol{J}$. We call the constant of proportionality the 'coefficient of friction' and represent it by the symbol $\mu$$\mu$. Quantitatively, the constraint is simply:

$\begin{array}{}\text{(4)}& \begin{array}{c}\parallel {\mathbit{J}}_{\parallel }\parallel \le \mu \phantom{\rule{0.278em}{0ex}}\parallel {\mathbit{J}}_{\perp }\parallel \phantom{\rule{2em}{0ex}}\text{where}\phantom{\rule{2em}{0ex}}\left\{\begin{array}{l}{\mathbit{J}}_{\perp }=\left(\mathbit{J}\cdot \mathbit{n}\right)\phantom{\rule{0.278em}{0ex}}\mathbit{n}\\ \\ {\mathbit{J}}_{\parallel }=\mathbit{J}-{\mathbit{J}}_{\perp }\end{array}\phantom{\rule{2em}{0ex}}\text{and}\phantom{\rule{2em}{0ex}}\mathbit{n}=\frac{\mathbit{p}-{\mathbit{x}}_{b}}{\parallel \mathbit{p}-{\mathbit{x}}_{b}\parallel }\end{array}\end{array}$

$\mathbit{n}$$\boldsymbol{n}$ is the unit contact normal direction (depicted below), and ${\mathbit{J}}_{\perp },{\mathbit{J}}_{\parallel }$$\boldsymbol{J}_\perp, \boldsymbol{J}_\shortparallel$ are the perpendicular and parallel parts of $\mathbit{J}$$\boldsymbol{J}$, respectively.

This constraint is enforced by taking the impulse that comes from the inelastic collision model above, and rescaling ${\mathbit{J}}_{\parallel }$$\boldsymbol{J}_\shortparallel$ if it exceeds the maximum acceptable value:

$\begin{array}{}\text{(5)}& \mathbit{J}:={\mathbit{J}}_{\perp }+min\left(1,\mu \phantom{\rule{0.278em}{0ex}}\frac{\parallel {\mathbit{J}}_{\perp }\parallel }{\parallel {\mathbit{J}}_{\parallel }\parallel }\right)\phantom{\rule{0.278em}{0ex}}{\mathbit{J}}_{\parallel }\end{array}$

I hope to release a complete reference implementation of this procedure in C++ soon.

## Impulse from Psyonix

In order to tune the feel and control of the collisions in Rocket League, Psyonix applies an additional impulse to the center of ball (here $\mathbit{p}\equiv {\mathbit{x}}_{b}$$\boldsymbol{p} \equiv \boldsymbol{x}_b$) that is more predictable than the one directly from the physics engine.

This impulse is simple to describe, as it is given explicitly in terms of only the positions and velocities between the car and ball. Its value is given by:

$\begin{array}{}\text{(6)}& \mathbit{J}={m}_{b}\phantom{\rule{0.278em}{0ex}}\parallel \mathrm{\Delta }\mathbit{v}\parallel \phantom{\rule{0.278em}{0ex}}s\left(\parallel \mathrm{\Delta }\mathbit{v}\parallel \right)\phantom{\rule{0.278em}{0ex}}\mathbit{n}\end{array}$

where $\mathrm{\Delta }\mathbit{v}\equiv {\mathbit{v}}_{b}-{\mathbit{v}}_{c}$$\Delta \boldsymbol{v} \equiv \boldsymbol{v}_b - \boldsymbol{v}_c$, and the contact normal $\mathbit{n}$$\boldsymbol{n}$ can be computed by transforming the vector from the car to the ball in the following way:

xxxxxxxxxxvec3 f = car.forward();vec3 n = ball.position - car.position;n[2] *= 0.35f;n = normalize(n - 0.35f * dot(n, f) * f);

and the scaling function $s\left(\parallel \mathrm{\Delta }\mathbit{v}\parallel \right)$$s(\| \Delta \boldsymbol{v} \|)$ is depicted below:

Oddly enough, this part of the collision violates Newton's 3rd Law of Motion, since Psyonix applies this impulse to the ball without applying an equal and opposite impulse to the car. So, each time you hit the ball in Rocket League, momentum is not conserved!

## Example Predictions and Observations

Consider the simple situation where a car approaches the ball, jumps, and dodges forward to make contact (visualization of actual game data, not a simulation):

This scenario was repeated several times by a bot, with different initial separations between the car and ball. It timed the dodges such that when the car contacts the ball, each run has identical velocity and angular velocity, but different orientation. The figures below are closeups of the moment of contact for each run, along with the exact velocity change of the ball compared to the values predicted by the approach described in this article.

To put these values in perspective, the weakest hit in this set traveled for about 2500 units before hitting the ground again. The strongest hit was 40% faster, and traveled 5200 units (over twice as far).

So, even for a test where almost everything was constrained to be identical, varying the orientation by a few degrees can make a huge difference in the outcome of the collision.

## Final Thoughts

This model has been tested on a variety of different types of hits, including dribbling, wall shots, aerial hits, and dodge shots. For these examples, the error in the predictions is small enough to be negligible for practical purposes (on the order of 0.1% to 3% error).

This model is not suitable for wheel hits or pinches!