The project timeline above details the different stages of our game development and research process.

Technologies Used

We used Unity, a freely available game engine, along with MATLAB and Visual Studio to code our game and run mathematical simulations.

Observer-Based Controller

For a single-input, single-output (SISO) system, the choice of K (and by extension, the derived controller) will be unique. Since we are working with a multi-input, multi-output (MIMO) system, we have to account for additional degrees of freedom when solving with a pole placement method. We used MATLAB’s pole placement algorithm to calculate the observer-based state space model for our MIMO system. In doing so, we essentially outsource handling the problem’s ambiguity to MATLAB.

The closed-loop poles will have minimal sensitivity to changes in either the A or B matrices. This is a double-edged sword: on the one hand, since our linear system is an approximation of our nonlinear system, this will prioritize the controller’s reliability. On the other hand, we have no say in which states stabilize faster than others. A set of poles meant to stabilize the angular dynamics faster than the lateral dynamics may not actually perform as expected, as MATLAB will prioritize placing poles where the sensitivity is lowest.

Linear Quadratic Regulator (LQR) Controller

To combat the inherent ambiguity that comes with using a pole-placement algorithm on a MIMO controller, we can instead substitute MATLAB’s cost function with our own that prioritizes some states over others, stabilizing them more quickly. Depending on the structure of our weighting matrices Q and R, we can apply a penalty to stabilizing some states faster than others. With an appropriate choice of Q and R, we can create a system response that stabilizes theta and theta-dot faster than any of the other states.

The key here is that both approaches (LQR and observer-based) attempt to account for the ambiguity inherent with using pole placement methods on a MIMO system by defining a cost function in order to generate a unique solution. In both cases, how one designs their cost function will affect the solutions they obtain. MATLAB’s LQR function gives the user less control over where the poles are placed, but more control over the cost of stabilizing the system. We choose to use the LQR function because we cared about being able to control which states should stabilize more quickly.

The biggest priority for us was to stabilize the theta dynamics as quickly as possible, so it has the largest associated Q value. We also wanted to stabilize x-dot and theta-dot, though they were not as crucial to controlling the system. Once our LQR controller was tuned, we generated the gain, G, and used it to calculate the closed loop A matrix for the Unity implementation of the controller.

Control Algorithms

Both our observer-based controller and our LQR controller rely on the same fundamental approach. Given an autonomous state-space model that represents each controller, we begin a coroutine that calculates the next state of the system by reading the current state and modifying theta and theta-dot appropriately. This coroutine then waits for all operations on the current frame to finish before repeating this process until either (a) the user turns the controller off, or (b) theta and theta-dot reach a predefined control threshold, at which the controller will deem the system stable and shut itself off.

The pseudocode for these algorithms, shown below, describes the specific procedures used to calculate a single step in the simulation and showcases the process control flow for the core physics loop. The full source code for each of these sections can be found on the Resources page.

Pseudocode for Generalized Controller Physics Calculations
Rigidbody2D _rb;  //Fetched from attached object.

LinearStateSpaceModel stateSpaceModel;  
//Object-oriented representation of f(x) = Ax + Bu, 
//y = Cx + Du. See algorithms below for further detail.

ApplyControllerPhysics()
{
 
    // calculate next states using stateSpaceModel //
    // set θ and dθ in _rb using newly calculated states //

    if(control thresholds are met) 
    {
        stop “ApplyControllerPhysics”
    }

    return WaitForEndOfFrame()
    //This causes the coroutine to pause execution until all
    //calculations in the current frame are finished, after which 
    //the line below will run, starting the coroutine again.

    start “ApplyControllerPhysics”
}
Algorithm to Calculate Each State
//LinearStateSpaceModels are object representations of a state-space
//model. Each LinearStateSpaceModel has an A, B, C, and D matrix that 
//can be referenced.

LinearStateSpaceModel stateSpaceModel 
          = new LinearStateSpaceModel(A, B, C, D)

//CalculateNextStates uses the current stateSpaceModel and 
//finds the change in states by multiplying the A matrix of
//stateSpaceModel to the currentStates.
Vector CalculateNextStates(LinearStateSpaceModel stateSpaceModel)
{
    Vector currentStates = CurrentStateVector();
    Vector changeInStates = stateSpaceModel.A * currentStates;
    return currentStates + changeInStates;
}
Process Control Flow for Main Script
//Runs once after script is initialized.
void Start()
{
    // instantiate variables //
    // check engine/UI configurations //
    // create state space models 
       for LQR and Autonomous systems//
}

//Runs every frame.
void Update()
{
    //death condition met when the rocket lands in the ocean
    //(i.e. the rocket misses the landing platform)
    if(rocket.position.y < deathThreshold) 
    {
        // reset the game //
    }
    // update the UI //
    // handle the particle systems //
}

//Operates on a fixed timer to account for differences in frame rate.
//Particularly useful for physics calculations.
void FixedUpdate()
{
    // handle player input //
    // clamp values to prevent infinite acceleration //
}