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.