smish.dev
aerial_control

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.

Problem Statement

Consider a scenario of a car in the air with center of mass velocity v, angular velocity ω, and orientation Θ. At that instant, a player can influence the car by providing inputs that exert torques on the car (to control its rate of rotation), as well as boost. The car can also air dodge, but we will save that mechanic for another discussion.

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?

Representing Orientation

There is no ambiguity about how to represent velocity and angular velocity. v and ω are simply vectors in R3. Orientation, on the other hand, has a number of different (but equivalent) common representations. Briefly, three of the most common ways to represent an orientation are:

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

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

  3. 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 f,l,u be the front, left, and up directions for the car. They appear in the following way:

Θ=[fxlxuxfylyuyfzlzuz],ΘΘ=1

car_axes

Time Rate of Orientation

With that in mind, what happens to Θ when it is subjected to a constant angular velocity ω? It will rotate about ω's axis (shown in purple), at ||ω|| radians per second:

We can see that the time rate of the individual directions f,l,u is given by

dfdt=ω×f,dldt=ω×l,dudt=ω×u

or, in matrix form

dΘdt=ΩΘ,whereΩa=[0ωzωyωz0ωxωyωx0][axayaz]=ω×a,aR3

This matrix-valued ordinary differential equation in Θ might look intimidating at first, but upon closer inspection, we can see that it is just a matrix version of the simplest differential equation in mathematics:

(dgdt=ag,g(t0)=g0)g(t0+Δt)=exp(aΔt)g0

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

(dΘdt=ΩΘ,Θ(t0)=Θ0)Θ(t0+Δt)=exp(ΩΔt)Θ0

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:

exp(x)=1+x+x22!+x33!+...

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

exp(ΩΔt)=1+ΩΔt+Ω2Δt22!+Ω3Δt33!+...

In general, it can be tricky to evaluate the matrix exponential, but luckily Ω is an antisymmetric matrix (i.e. Ω=Ω). For 3x3 antisymmetric matrices, we have a closed form expression for the matrix exponential:

exp(ΩΔt)=1+sin(ϕ)ϕΩΔt+1cos(ϕ)ϕ2Ω2Δt2,whereϕ=||ω||Δt

So, to summarize: if we know our orientation at time t, and the time step is small enough that ω is approximately constant over the time interval, then the new orientation is given by:

Θ(t+Δt):=(1+sin(ϕ)ϕΩΔt+1cos(ϕ)ϕ2Ω2Δt2)Θ(t)

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:

orientation_history

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:

useω:=12(ω(t)+ω(t+Δt))

But to get the ω(t+Δt) for this averaged angular velocity, we need to consider how ω evolves with time.

Time Rate of Angular Velocity

Now that we understand how to predict Θ(t+Δt), given ω(t) and Θ(t), we need a way to compute ω(t+Δt). We start with the rotational analogue of Newton's second law (for rotations about the center of mass of an object):

d(Iω)dt=τω×(Iω)

where I is the car's moment of inertia, and τ is the net torque applied on the car. In reality, the moment of inertia for an interesting shape like a car would be a 3x3 tensor with at least 3 unique entries. However, since there is no reason to believe that Rocket League is interested in realistic simulation, my first analysis is one where we consider the case where I=1 (i.e., the car has no direction in which it is harder or easier to rotate). In this case, the term ω×(Iω) vanishes, and we are left with just

d(ω)dt=τ

At this point, it is a matter of understanding how Rocket League calculates τ in terms of the user's input and the current orientation. Empirically, the following expression gives good results:

τ=Θ(Tu+DΘω),whereu=[urupuy]=[(input roll)(input pitch)(input yaw)]

with

T=[Tr000Tp000Ty],D=[Dr000Dp(1|up|)000Dy(1|uy|)]

with numerical values for Tr,Tp,Ty,Dr,Dp,Dy given in the example implementation. Note: the matrix D treats rotations in the "roll"-direction differently than the "pitch" and "yaw" directions. This means that when a user inputs a pitch or yaw of ±1, the damping in those directions is turned off. However, the damping torque in the "roll" direction is always on.

Finally, we can update ω:

ω(t+Δt)=ω(t)+τΔt

Applying this procedure to the same dataset as before, this time with exact values for Θ (which is needed to evaluate τ), we get very good agreement between predicted angular velocities (dashed lines) and exact values (solid lines). The red traces show the three components of input roll, pitch, and yaw, with their transitions.

angular_velocity_history

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

Final notes

For the example comparisons, we made predictions about Θ using exact values for ω, and used exact values of Θ to predict ω. In a realistic scenario, we will not have those exact values, and we must use only the initial orientation and angular velocity. This means that we go back and forth, updating Θ in order to update ω, in order to update Θ again and so on. This means that errors accumulate more rapidly, as shown below (using only exact initial onditions for Θ, ω):

Angular velocity:

angular_velocity_history_both

Orientation:

orientation_history_both

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.

Example Implementation

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