One of the first things we need to understand in order to write a bot for Rocket League is how the controls we pass to the car affect what it does. These notes provide an accurate model for predicting how an airborn car will respond to the inputs we provide.

If you are not interested in any derivations, try skipping ahead to the example implementation.

Consider a scenario of a car in the air with center of mass velocity

While boosting, the car experiences an acceleration in the direction of the front of the car. However, as the car is tumbling through the air, this direction is constantly changing, so how can we keep track of the car's orientation?

There is no ambiguity about how to represent velocity and angular velocity.

Euler Angles: Orientation is parameterized by three angles, and an assumed order of application. The final orientation is achieved by applying the 3 rotations, in order. This is the representation that Rocket League uses internally.

Quaternions: In the same way that unit complex numbers are a natural representation of rotations in the plane, unit quaternions naturally can represent rotations in three dimensional space.

Proper-Orthogonal Matrices: This representation uses a 3-by-3 matrix to store the local coordinate system associated with an orientation.

I choose to represent orientations directly in their matrix form. Let

With that in mind, what happens to

We can see that the time rate of the individual directions

or, in matrix form

This matrix-valued ordinary differential equation in

It follows that the solution to our matrix-valued ODE has a similar form

Although most people are comfortable with the idea of taking the exponential of a number, many have not seen the matrix exponential before, so I will quickly review what it means. Fundamentally, the exponential function is defined by an infinite series:

So, if we pass in a matrix argument, we get

In general, it can be tricky to evaluate the matrix exponential, but luckily

So, to summarize: if we know our orientation at time

To verify, I recorded some data from Rocket League, where a car was tumbling with randomized inputs. This test predicts future car orientations, given the initial orientation of the car, and the exact (recorded) time history of angular velocities. Each of the 9 predicted entries (dashed lines) of the orientation matrix are plotted against their exact versions (solid lines) below:

Here, we see that the predicted values provide a reasonable approximation of the orientation, with some error. Comparing predicted and exact orientations at the final time step in this example shows that the two are off by a rotation of 5.92345 degrees.

From my experiments, it is noticeably more true-to-Rocket League to use the averaged angular velocity when evaluating the update procedure above:

But to get the

Now that we understand how to predict

where

At this point, it is a matter of understanding how Rocket League calculates

with

with numerical values for

Finally, we can update

Applying this procedure to the same dataset as before, this time with exact values for

The fact that the plots are nearly identical here is evidence that our assumption about the moment of inertia is not unreasonable. Furthermore, I briefly investigated what effect a realistic moment of inertia would have and found that the moment of inertia values that produced the best predictions were those of an isotropic tensor (which was our original assumption).

For the example comparisons, we made predictions about

Angular velocity:

Orientation:

Of these two predictions, it seems to be the case that the orientation update is the main source of error. This may be related to Rocket League's internal representation of orientation as Euler angles quantized to 16-bit integers.

With all of this information, we can try to figure out inputs that produce a desired car orientation for aerial hits, shots, and the recovery afterward.

`xxxxxxxxxx`

`const float omega_max = 5.5;`

`const float T_r = -36.07956616966136; // torque coefficient for roll`

`const float T_p = -12.14599781908070; // torque coefficient for pitch`

`const float T_y = 8.91962804287785; // torque coefficient for yaw`

`const float D_r = -4.47166302201591; // drag coefficient for roll`

`const float D_p = -2.798194258050845; // drag coefficient for pitch`

`const float D_y = -1.886491900437232; // drag coefficient for yaw`

```
```

`struct state {`

` vec3 omega; // angular velocity`

` mat3x3 theta; // orientation`

`};`

```
```

`state aerial_control(state current, float roll, float pitch, float yaw, float dt) {`

` `

` mat3x3 T{`

` {T_r, 0.0, 0.0},`

` {0.0, T_p, 0.0},`

` {0.0, 0.0, T_y}`

` }; `

```
```

` mat3x3 D{`

` {D_r, 0.0, 0.0},`

` {0.0, D_p (1.0 - fabs(pitch)), 0.0},`

` {0.0, 0.0, D_y (1.0 - fabs(yaw))}`

` };`

` `

` // compute the net torque on the car`

` vec3 tau = dot(D, dot(transpose(current.theta), current.omega));`

` tau += dot(T, vec3{roll, pitch, yaw}));`

` tau = dot(current.theta, tau));`

```
```

` // use the torque to get the update angular velocity`

` vec3 omega_next = current.omega + tau * dt; `

```
```

` // prevent the angular velocity from exceeding a threshold`

` omega_next *= fmin(1.0, omega_max / norm(omega));`

```
```

` // compute the average angular velocity for this step`

` vec3 omega_avg = 0.5 * (current.omega + omega_next);`

` float phi = norm(omega_avg) * dt;`

```
```

` mat3x3 Omega_dt = {`

` {0.0, -omega_avg[2] * dt, omega_avg[1] * dt},`

` {omega_avg[2] * dt, 0.0, -omega_avg[0] * dt},`

` {-omega_avg[1] * dt, omega_avg[0] * dt, 0.0}`

` };`

```
```

` mat3x3 R = mat3x3::eye();`

` R += (sin(phi) / phi) * Omega_dt;`

` R += (1.0 - cos(phi)) / (phi*phi) * dot(Omega_dt, Omega_dt);`

```
```

` return state{omega_next, dot(R, current.theta)};`

` `

`}`

also, to convert from Euler angles (in radians) to an orientation matrix:

`xxxxxxxxxx`

```
```

`mat3x3 convert_from_Euler_angles(float roll, float pitch, float yaw) {`

```
```

` float CR = cos(roll);`

` float SR = sin(roll);`

` float CP = cos(pitch); `

` float SP = sin(pitch);`

` float CY = cos(yaw);`

` float SY = sin(yaw);`

```
```

` mat3x3 theta;`

```
```

` // front direction`

` theta(0, 0) = CP * CY;`

` theta(1, 0) = CP * SY;`

` theta(2, 0) = SP; `

```
```

` // left direction`

` theta(0, 1) = CY * SP * SR - CR * SY;`

` theta(1, 1) = SY * SP * SR + CR * CY;`

` theta(2, 1) = -CP * SR; `

```
```

` // up direction`

` theta(0, 2) = -CR * CY * SP - SR * SY; `

` theta(1, 2) = -CR * SY * SP + SR * CY;`

` theta(2, 2) = CP * CR; `

```
```

` return theta; `

```
```

`}`