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;
}