Structuring Game Code To Avoid Null References

Why null references cause problems in games

Null references in game code can lead to NullReferenceExceptions and game crashes during gameplay. These exceptions disrupt the gaming experience and frustrate players. Some common ways that null references get introduced into game code include:

  • Forgetting to assign values to variables before using them
  • Incorrectly assuming another system assigned a value
  • Attempting to access a component that was destroyed
  • Failure to check if an API callback provided required parameters
  • Trying to utilize objects from an asynchronous operation before they have loaded

NullReferenceExceptions tend to surface randomly during gameplay, making them difficult to diagnose and fix. Defensive coding practices are necessary to guard against nulls that can sneak in from any part of a complex, interconnected game system.

Checking for null values before accessing members

The most straightforward way to avoid NullReferenceExceptions is to explicitly check for null values before accessing members. This defensive coding pattern prevents the runtime error by avoiding the invalid operation entirely.

For example, when utilizing a reference from another game system:


public class DamageSystem {

  private HealthSystem healthSystem;

  public void InflictDamage(Entity entity, int damage) {
    
    // Check for null to avoid crash
    if (healthSystem != null) {
      healthSystem.ModifyHealth(entity, -damage); 
    }
  }

}  

The reference healthSystem could be null if it has not been initialized correctly. The null check prevents the potentially crashing access of healthSystem.ModifyHealth().

This pattern should be used extensively throughout game code whenever instances or references could be null.

Using dependency injection to avoid nulls

Manually checking for nulls clutters code with boilerplate checks. An alternative approach is to utilize dependency injection. This design pattern injects external dependencies into classes through their constructors or properties rather than directly instantiating collaborators.

For example, rather than directly instantiating the HealthSystem, it can be injected via the constructor:


public class DamageSystem {
  
  private HealthSystem healthSystem;

  // Dependency injected via constructor
  public DamageSystem(HealthSystem healthSystem) {
    this.healthSystem = healthSystem;
  }

  public void InflictDamage(Entity entity, int damage) {
   
    // Null check is no longer needed
    healthSystem.ModifyHealth(entity, -damage); 
 
  }

}

This guarantees that healthSystem will not be null, eliminating the need for manual null checking in many cases.

Making fields and variables non-nullable

languages such as C# provide non-nullable reference types that prevent values from being assigned null after initialization. Fields and variables can be defined with the syntax MyType someVar ! to indicate they are not nullable.

Attempting to assign null to a non-nullable variable will generate a compile-time error, acting as an early warning system against null creeping in unintentionally:


public class DamageSystem {

  private HealthSystem healthSystem!;
  
  public DamageSystem(HealthSystem healthSystem) {
    // Compiler enforces non-null assignment
    this.healthSystem = healthSystem;
  } 

}

Non-nullable reference types require some restructuring of code but eliminate certain runtime null checks in return.

Using default values instead of null

Another way to avoid null is to utilize default values for references and components that may not have initialized yet. For example, health could default to maxHealth instead of leaving it null:

  
public class HealthSystem {

  // Default to max rather than null
  public float health = maxHealth; 
  
  // Other systems can now safely utilize
  // health without null checking
  public void TakeDamage(float damage) {
    health -= damage;
  }

}

Default values enable other systems to safely interact with potentially uninitialized systems until they are ready. This provides gracefulness in the face of asynchronous game system initialization.

Handling nullable values gracefully

When null values can’t be avoided, defensive coding can gracefully handle them rather than crashing. For example, weapon attacks could ignore null damage values:


public class WeaponAttackSystem {

  public void Attack(Entity attacker, Entity defender) {
      
    float? damage = GetDamageForEntity(attacker);
    
    // Gracefully handle null case
    if (damage != null) {
      ApplyDamage(defender, damage.Value) 
    }

  }

}

This avoids exceptions when damage hasn’t been initialized yet. Similar gracefulness should be employed whenever systems interact with unpredictable asynchronous subsystems.

Unit testing code to uncover null reference issues

Unfortunately not all null reference defects surface during normal gameplay. Unit testing game code can help expose these latent issues by simulating edge cases. Tests should specifically target potential null scenarios such as:

  • Unsafe usage of uninitialized references
  • Failure to validate API callback parameters
  • Asynchronous race conditions leading to null
  • Edge case combinations of internal systems

Exposing null prone areas of code through testing allows them to be addressed before they impact customers.

Using static analysis to detect possible nulls

In addition to testing, static analysis tools can automatically inspect code to detect possible null references. Custom annotations can also explicitly mark references as:@Nullable and @NonNull to have tooling enforce correct usage.

Continuous integration pipelines should utilize these tools to prevent known null causing anti-patterns from entering the codebase. Example analysis includes:

  • Validating proper null checking around external API calls
  • Detecting unsafe assumptions related to initialization order
  • Tracing unvalidated data flows to their usage

Regular static analysis provides another layer of protection against nulls beyond testing.

Configuring IDEs to flag null usage

IDE extensions can also detect null prone patterns as code is written, enabling real time fixes. Example configurations include:

  • Flagging local variables that could be null
  • Annotating public method parameters as @NonNull
  • Warning when calling methods without null checking

With disciplined usage of annotations, IDE analysis continuously reinforces best practices that prevent null references.

Leave a Reply

Your email address will not be published. Required fields are marked *