Radical Cartridge Blog

How I used Unity for a 2D grid-based physics game

In designing Moving Type, I encountered technical challenges getting the 2D physics engine to work with the pseudo-physics of the game. In particular, the game involves blocks falling perfectly along a given column or row inside an arbitrary grid area. If a block is falling downwards, it shouldn’t drift left or right, or interact with any obstacles in neighboring columns.

Design constraints in depth

Moving Type works by allowing players to change the direction the blocks fall. Blocks can fall when a) the tilt of the board is changed, or b) other blocks are removed due to being spelled in a word.

The rules of the game, from a physics perspective, might be summarized as:

  1. Gravity will point in a cardinal direction at any given time
  2. Gravity will only change when all Rigidbodies are “settled”
  3. Rigidbodies should only collide when they are in the same row or column as the direction of Gravity
  4. Rigidbodies should not drift in a direction perpendicular to gravity

The easiest to solve was 4. Unity provides a RigidBodyConstraints2D attribute that allows you to freeze position along a particular axis.

The rest required some engineering.

Building boxes

The design of the game included several different game boards—each with unique wall sizes and shapes. Blocks need to collide with the walls, but avoid snagging along a corner. Also, for ease of prototyping, the final solution should be relatively painless to update.

I created an "invisible" grid square sized BoxCollider2D for each grid space occupied by a wall.

To ease prototyping, I added a mesh renderer so that I could see where I had misplaced colliders, as well as to quickly select the GameObject.

Side note, the Snap Settings are particularly helpful here (Ctrl+Drag will move the game object the number of units specified by Move X/Y/Z).

Sleep Debouncing

To satisfy constraint 2, the physics engine needs to correctly determine if a block is done moving. Moving Type is a turn-based game, where the physics interactions are a visual reward/explanation for the player. If they take too long, they will frustrate the player who is waiting for them to finish. If they go too quickly, they will be confusing and unsatisfying. Duration was tweaked via two approaches: 1) increasing the Gravity vector magnitude and 2) speeding up the simulation (i.e., increasing Time.timeScale). I found a mixture of both worked best; scaling too much or making Gravity too large would cause the simulation to break down.

Rigidbody2D has an IsSleeping() API. Its return value is affected (presumably) by a number of Project Settings, including: Time To Sleep, Linear Sleep Tolerance, Velocity Iterations, Position Iterations, and Velocity Threshold. My highly unscientific conclusion is that it is best to set the Project to aggressively consider an object asleep, then account for any errors by adding a short (~.3 second) "debounce" to checks to IsSleeping(). I was surprised to find that the physics simulation varies across platforms; it took some extra validation to make sure everything worked perfectly on iOS devices.

Enter the Layer Collision Matrix

Snagging was a gnarly problem where Colliders would get (incorrectly) stuck on the edges of a neighboring Collider. The game needs to dynamically determine which direction Gravity is going and split the blocks (and bounding box segments) into parallel lanes. Any collisions not in the same lane can be ignored.

First, I tried to use Physics2D.IgnoreCollision. This API allows you to specify 2 Collider2Ds so that the physics engine can do what it says on the tin and throw out any collisions between them. It turns out, for a modestly sized scene (I went with a game board of 31 blocks), the engine would start dropping frames like crazy. I needed an alternate solution.

The Physics2D Settings lets you specify particular layers which shouldn’t interact (a.k.a. the Layer Collision Matrix). This seemed like a good start. I could break the grid columns and rows into separate layers and make sure they don’t interact with each other. Unity allows you to define 23 “user” layers. In Moving Type, the game boards tended to have around 7-8 rows and 5 columns (+2 for the box boundary bookends).

This was a viable solution. I would create “lane” layers (as many as the max # of rows or columns) then assign them whenever Physics2D.gravity is set:

		
public void EvaluateDropLanes() 
{
    bool movingVertically = Mathf.Approximately(Physics2D.gravity.x, 0); 
    var laneSortedColliders = Enumerable.OrderBy(collidermanager.Instance.Colliders, 
        c => movingVertically ? c.transform.position.x : c.transform.position.y); 
    int layerIndex = -1; 
    float currentLanePosition = float.MaxValue; 
    foreach (var c in laneSortedColliders) 
    { 
        float newLanePosition = movingVertically ?  
            c.transform.position.x : c.transform.position.y; 
        if (Mathf.Abs(newLanePosition - currentLanePosition) > 1f) 
        {
            currentLanePosition = newLanePosition; 
            layerIndex += 1; 
        } 
        c.gameObject.layer = LayerMask.NameToLayer("grid" + layerIndex.ToString()); 
        var rigidBody = c.GetComponent<Rigidbody2d>(); 
        if (rigidBody) 
        { 
            ApplyRigidBodyConstraints(rigidBody); 
        } 
    }
} 
	
	

In Conclusion

Overall, I was happy with the performance of Unity. While this type of pixel-perfect physics isn't a first class citizen of the engine, it got the job done. The current solution has a hard limit of 23 rows or columns. If I were constrained by user layers, I'd probably use an odd/even grid layer, and alternate between them.