Skip to main content

5.7 - Static Members

So far, we've been working with instance members—fields, properties, and methods that belong to specific instances (objects) of a class. Now, we'll explore static members, which belong to the class itself rather than to any specific instance.

Understanding Static Members

Static members are associated with the class rather than with objects created from the class. This means:

  1. They're shared across all instances of the class
  2. They can be accessed without creating an instance of the class
  3. They're created when the program starts, before any objects are instantiated

Static members are useful for:

  • Utility functions that don't depend on instance state
  • Shared data that should be the same for all instances
  • Factory methods that create instances of the class
  • Constants and configuration values
  • Singleton patterns and global access points

Static Fields and Properties

Static fields and properties store data that is shared across all instances of a class.

Static Fields

public class Enemy
{
// Static field - shared across all Enemy instances
public static int EnemyCount = 0;

// Instance fields - each Enemy has its own copy
public string Name { get; private set; }
public int Health { get; private set; }

public Enemy(string name, int health)
{
Name = name;
Health = health;

// Increment the shared counter
EnemyCount++;
}

~Enemy()
{
// Decrement the shared counter when an Enemy is destroyed
EnemyCount--;
}
}

// Usage
Enemy goblin = new Enemy("Goblin", 50);
Enemy orc = new Enemy("Orc", 100);
Enemy troll = new Enemy("Troll", 200);

Console.WriteLine($"Total enemies: {Enemy.EnemyCount}"); // Output: Total enemies: 3

// Access through the class, not through instances
// Console.WriteLine(goblin.EnemyCount); // This would work but is discouraged
// Console.WriteLine(orc.EnemyCount); // Same value as above

In this example, EnemyCount is a static field that keeps track of how many Enemy objects exist. It's shared across all instances, so incrementing it in one constructor affects what all other instances see.

Static Properties

Like static fields, static properties are shared across all instances, but they provide controlled access with getters and setters:

public class GameSettings
{
// Private static backing field
private static float musicVolume = 0.7f;

// Static property with validation
public static float MusicVolume
{
get { return musicVolume; }
set { musicVolume = Mathf.Clamp01(value); }
}

// Auto-implemented static property
public static float SfxVolume { get; set; } = 0.8f;

// Static read-only property
public static bool IsMuted => musicVolume <= 0 && SfxVolume <= 0;

// Static property with side effects
public static bool FullScreen
{
get { return Screen.fullScreen; }
set
{
Screen.fullScreen = value;
Debug.Log($"Fullscreen mode set to: {value}");
}
}
}

// Usage
float currentVolume = GameSettings.MusicVolume; // Get the shared music volume
GameSettings.MusicVolume = 0.5f; // Set the shared music volume
GameSettings.SfxVolume = 0.6f; // Set the shared SFX volume

if (GameSettings.IsMuted)
{
Debug.Log("Audio is muted!");
}

GameSettings.FullScreen = true; // Changes the game to fullscreen mode

Static properties are useful for global settings, configuration values, and shared state that needs validation or side effects when changed.

Static Read-Only Fields

For values that should never change, you can use static read-only fields:

public class GameConstants
{
// Static read-only fields - can only be assigned in declaration or in a static constructor
public static readonly float GravityForce = 9.81f;
public static readonly int MaxPlayers = 4;
public static readonly string GameVersion = "1.0.0";

// Static readonly array
public static readonly string[] DifficultyLevels = { "Easy", "Normal", "Hard", "Nightmare" };

// For truly constant values, use const instead
public const float PI = 3.14159265359f;
public const int MaxLevel = 100;
}

// Usage
float gravity = GameConstants.GravityForce;
int maxPlayers = GameConstants.MaxPlayers;
string version = GameConstants.GameVersion;

string normalDifficulty = GameConstants.DifficultyLevels[1]; // "Normal"
float pi = GameConstants.PI;

The difference between static readonly and const:

  • static readonly fields can be assigned in a static constructor and can be complex types
  • const fields must be assigned at declaration and can only be primitive types, enums, or strings

Static Methods

Static methods belong to the class rather than to instances. They can't access instance members directly (without an object reference) and are often used for utility functions or operations that don't depend on instance state.

public class MathUtils
{
// Static method for calculating distance between two points
public static float Distance(Vector2 a, Vector2 b)
{
float dx = b.x - a.x;
float dy = b.y - a.y;
return Mathf.Sqrt(dx * dx + dy * dy);
}

// Static method for linear interpolation
public static float Lerp(float a, float b, float t)
{
t = Mathf.Clamp01(t);
return a + (b - a) * t;
}

// Static method for generating a random point in a circle
public static Vector2 RandomPointInCircle(float radius)
{
float angle = Random.Range(0f, Mathf.PI * 2f);
float distance = Random.Range(0f, radius);
return new Vector2(
Mathf.Cos(angle) * distance,
Mathf.Sin(angle) * distance
);
}
}

// Usage
float distance = MathUtils.Distance(new Vector2(1, 2), new Vector2(4, 6));
float interpolated = MathUtils.Lerp(0, 100, 0.75f); // 75
Vector2 randomPoint = MathUtils.RandomPointInCircle(5f);

Static methods are called directly on the class, not on an instance. They're useful for utility functions, helper methods, and operations that don't need instance data.

Static Methods vs. Instance Methods

Here's a comparison to help understand when to use each:

public class Player
{
// Instance fields
private string name;
private int health;

// Constructor
public Player(string name, int health)
{
this.name = name;
this.health = health;
}

// Instance method - operates on the specific instance
public void TakeDamage(int amount)
{
health -= amount;
Console.WriteLine($"{name} takes {amount} damage. Health: {health}");
}

// Static method - operates on provided data, not instance data
public static int CalculateDamage(int baseDamage, int attackerLevel, int defenderLevel)
{
float levelDifference = attackerLevel - defenderLevel;
float multiplier = 1.0f + (levelDifference * 0.1f);
multiplier = Mathf.Max(0.5f, multiplier); // Minimum 50% damage

return Mathf.RoundToInt(baseDamage * multiplier);
}

// Static method that creates an instance (Factory method)
public static Player CreateWarrior(string name)
{
return new Player(name, 150);
}

// Static method that creates an instance (Factory method)
public static Player CreateMage(string name)
{
return new Player(name, 80);
}
}

// Usage
// Instance method - needs an object
Player hero = new Player("Hero", 100);
hero.TakeDamage(20); // Output: Hero takes 20 damage. Health: 80

// Static method - called on the class
int damage = Player.CalculateDamage(30, 5, 3); // Calculate damage for level 5 vs level 3
hero.TakeDamage(damage); // Use the calculated damage

// Factory methods
Player warrior = Player.CreateWarrior("Conan");
Player mage = Player.CreateMage("Gandalf");

When to use static methods:

  • For operations that don't depend on instance state
  • For utility functions that operate on provided parameters
  • For factory methods that create instances of the class
  • For operations that should be available without creating an instance

Static Constructors

A static constructor is used to initialize static fields or perform other one-time initialization for a class. It's called automatically before any static members are accessed or any instances are created.

public class GameManager
{
// Static fields
public static bool IsInitialized { get; private set; }
public static Dictionary<string, LevelData> Levels { get; private set; }

// Static constructor - called once before the class is used
static GameManager()
{
Debug.Log("GameManager static constructor called");

// Initialize static fields
Levels = new Dictionary<string, LevelData>();

// Load level data
LoadLevelData();

IsInitialized = true;
}

// Static method to load level data
private static void LoadLevelData()
{
// In a real game, this might load from files or a database
Levels.Add("Level1", new LevelData("Forest", 1, 10));
Levels.Add("Level2", new LevelData("Cave", 2, 15));
Levels.Add("Level3", new LevelData("Castle", 3, 20));

Debug.Log($"Loaded {Levels.Count} levels");
}

// Nested class for level data
public class LevelData
{
public string Name { get; private set; }
public int Difficulty { get; private set; }
public int EnemyCount { get; private set; }

public LevelData(string name, int difficulty, int enemyCount)
{
Name = name;
Difficulty = difficulty;
EnemyCount = enemyCount;
}
}
}

// Usage
// The first time GameManager is accessed, the static constructor runs
if (GameManager.IsInitialized)
{
LevelData level1 = GameManager.Levels["Level1"];
Debug.Log($"Level: {level1.Name}, Difficulty: {level1.Difficulty}");
}

Key points about static constructors:

  • They don't take any parameters and can't be called directly
  • They run only once, before the class is first used
  • They run before any instance constructors
  • If a static constructor throws an exception, the class becomes unusable
  • The order in which static constructors run is not guaranteed between different classes

Static Classes

A static class is a class that can only contain static members and cannot be instantiated. It's marked with the static keyword and is useful for organizing related utility methods or constants.

// Static class - cannot be instantiated
public static class MathUtils
{
// Constants
public const float PI = 3.14159265359f;
public const float E = 2.71828182846f;

// Static methods
public static float Square(float x)
{
return x * x;
}

public static float Cube(float x)
{
return x * x * x;
}

public static bool IsEven(int x)
{
return x % 2 == 0;
}

public static bool IsPrime(int x)
{
if (x <= 1) return false;
if (x <= 3) return true;
if (x % 2 == 0 || x % 3 == 0) return false;

int i = 5;
while (i * i <= x)
{
if (x % i == 0 || x % (i + 2) == 0)
return false;
i += 6;
}

return true;
}
}

// Usage
float area = MathUtils.PI * MathUtils.Square(5); // Area of circle with radius 5
bool isPrime = MathUtils.IsPrime(17); // true

Static classes are useful for organizing related utility methods, constants, or extension methods (which we'll cover in a later module).

Common Patterns with Static Members

1. Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. In Unity, it's often implemented using a combination of static and instance members:

public class GameManager : MonoBehaviour
{
// Static instance - accessible globally
public static GameManager Instance { get; private set; }

// Instance properties and fields
public bool IsGamePaused { get; private set; }
public int CurrentLevel { get; private set; }

// Called when the script instance is being loaded
private void Awake()
{
// Singleton implementation
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
Debug.Log("GameManager initialized");
}
else
{
Destroy(gameObject);
Debug.Log("Duplicate GameManager destroyed");
}
}

// Instance methods
public void PauseGame()
{
IsGamePaused = true;
Time.timeScale = 0f;
Debug.Log("Game paused");
}

public void ResumeGame()
{
IsGamePaused = false;
Time.timeScale = 1f;
Debug.Log("Game resumed");
}

public void LoadLevel(int levelNumber)
{
CurrentLevel = levelNumber;
// Implementation details...
Debug.Log($"Loading level {levelNumber}");
}
}

// Usage from anywhere in the game
public class PlayerController : MonoBehaviour
{
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
if (GameManager.Instance.IsGamePaused)
{
GameManager.Instance.ResumeGame();
}
else
{
GameManager.Instance.PauseGame();
}
}
}
}

The Singleton pattern provides a global access point (GameManager.Instance) while still allowing instance methods and properties that can maintain state.

2. Static Utility Classes

Static utility classes group related utility methods together:

public static class StringUtils
{
public static string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value)) return value;
return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "...";
}

public static string Capitalize(string value)
{
if (string.IsNullOrEmpty(value)) return value;
return char.ToUpper(value[0]) + value.Substring(1);
}

public static string RemoveSpaces(string value)
{
return value.Replace(" ", "");
}
}

public static class GameUtils
{
public static string FormatTime(float seconds)
{
int minutes = Mathf.FloorToInt(seconds / 60);
int remainingSeconds = Mathf.FloorToInt(seconds % 60);
return $"{minutes:00}:{remainingSeconds:00}";
}

public static string FormatNumber(int number)
{
if (number >= 1000000)
{
return $"{number / 1000000f:0.#}M";
}
else if (number >= 1000)
{
return $"{number / 1000f:0.#}K";
}

return number.ToString();
}

public static Color GetRarityColor(int rarityLevel)
{
return rarityLevel switch
{
0 => Color.gray, // Common
1 => Color.white, // Uncommon
2 => Color.green, // Rare
3 => Color.blue, // Epic
4 => new Color(0.5f, 0, 0.5f), // Legendary (purple)
_ => Color.gray // Default
};
}
}

// Usage
string longText = "This is a very long text that needs to be truncated";
string truncated = StringUtils.Truncate(longText, 20); // "This is a very long..."

string name = "john";
string capitalized = StringUtils.Capitalize(name); // "John"

float gameTime = 125.5f;
string formattedTime = GameUtils.FormatTime(gameTime); // "02:05"

int score = 1250000;
string formattedScore = GameUtils.FormatNumber(score); // "1.25M"

Color legendaryColor = GameUtils.GetRarityColor(4); // Purple color

3. Factory Methods

Static factory methods create and return instances of a class:

public class Enemy
{
// Properties
public string Name { get; private set; }
public int Health { get; private set; }
public int Damage { get; private set; }
public float Speed { get; private set; }

// Private constructor - forces use of factory methods
private Enemy(string name, int health, int damage, float speed)
{
Name = name;
Health = health;
Damage = damage;
Speed = speed;
}

// Static factory methods
public static Enemy CreateGoblin()
{
return new Enemy("Goblin", 50, 5, 3.5f);
}

public static Enemy CreateOrc()
{
return new Enemy("Orc", 100, 10, 2.0f);
}

public static Enemy CreateTroll()
{
return new Enemy("Troll", 200, 20, 1.5f);
}

public static Enemy CreateCustomEnemy(string name, int health, int damage, float speed)
{
return new Enemy(name, health, damage, speed);
}

// Static factory method with random variations
public static Enemy CreateRandomGoblin()
{
string[] prefixes = { "Small", "Agile", "Cunning", "Vicious" };
string prefix = prefixes[Random.Range(0, prefixes.Length)];

int healthVariation = Random.Range(-10, 11);
int damageVariation = Random.Range(-1, 2);
float speedVariation = Random.Range(-0.5f, 0.6f);

return new Enemy(
$"{prefix} Goblin",
50 + healthVariation,
5 + damageVariation,
3.5f + speedVariation
);
}
}

// Usage
Enemy goblin = Enemy.CreateGoblin();
Enemy orc = Enemy.CreateOrc();
Enemy troll = Enemy.CreateTroll();
Enemy boss = Enemy.CreateCustomEnemy("Dragon", 500, 50, 2.0f);
Enemy randomGoblin = Enemy.CreateRandomGoblin();

Debug.Log($"Spawned: {goblin.Name}, {orc.Name}, {troll.Name}, {boss.Name}, {randomGoblin.Name}");

Factory methods provide a controlled way to create objects, allowing for:

  • Descriptive method names that explain what's being created
  • Encapsulation of complex initialization logic
  • Creation of variations or random instances
  • Reuse of common initialization code

Static Members in Unity

Unity has several built-in static members that you'll use frequently:

1. Static Classes and Methods

// Examples of static classes in Unity
Debug.Log("This is a log message");
Mathf.Sin(0.5f);
Physics.Raycast(origin, direction);
Input.GetKeyDown(KeyCode.Space);
SceneManager.LoadScene("Level1");
PlayerPrefs.SetInt("HighScore", 1000);

2. Static Properties

// Examples of static properties in Unity
Vector3 up = Vector3.up; // (0, 1, 0)
Vector3 zero = Vector3.zero; // (0, 0, 0)
float deltaTime = Time.deltaTime;
bool isPlaying = Application.isPlaying;
int screenWidth = Screen.width;

3. Singleton MonoBehaviours

Many Unity systems use the Singleton pattern:

// Accessing Unity's built-in singletons
Camera mainCamera = Camera.main; // Shorthand for Camera.FindWithTag("MainCamera")
AudioListener listener = AudioListener.current;
EventSystem eventSystem = EventSystem.current;

4. Custom Singletons in Unity

When creating your own singletons in Unity, be careful about initialization order and scene loading:

public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }

[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource sfxSource;

private void Awake()
{
// Singleton implementation with scene persistence
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}

public void PlayMusic(AudioClip clip)
{
musicSource.clip = clip;
musicSource.Play();
}

public void PlaySFX(AudioClip clip)
{
sfxSource.PlayOneShot(clip);
}

public void SetMusicVolume(float volume)
{
musicSource.volume = volume;
}

public void SetSFXVolume(float volume)
{
sfxSource.volume = volume;
}
}

// Usage from anywhere
public class PlayerController : MonoBehaviour
{
[SerializeField] private AudioClip jumpSound;
[SerializeField] private AudioClip landSound;

private void Jump()
{
// Play jump sound
AudioManager.Instance.PlaySFX(jumpSound);

// Jump implementation...
}

private void Land()
{
// Play land sound
AudioManager.Instance.PlaySFX(landSound);

// Landing implementation...
}
}

Practical Examples

Example 1: Game Settings Manager

// Static class for game settings
public static class GameSettings
{
// Default values
private static float musicVolume = 0.7f;
private static float sfxVolume = 0.8f;
private static bool fullScreen = true;
private static int qualityLevel = 2;
private static bool invertYAxis = false;
private static float mouseSensitivity = 1.0f;

// Static constructor to load saved settings
static GameSettings()
{
LoadSettings();
}

// Properties with validation and side effects
public static float MusicVolume
{
get { return musicVolume; }
set
{
musicVolume = Mathf.Clamp01(value);
PlayerPrefs.SetFloat("MusicVolume", musicVolume);
PlayerPrefs.Save();

// Update audio if AudioManager exists
if (AudioManager.Instance != null)
{
AudioManager.Instance.SetMusicVolume(musicVolume);
}
}
}

public static float SfxVolume
{
get { return sfxVolume; }
set
{
sfxVolume = Mathf.Clamp01(value);
PlayerPrefs.SetFloat("SfxVolume", sfxVolume);
PlayerPrefs.Save();

// Update audio if AudioManager exists
if (AudioManager.Instance != null)
{
AudioManager.Instance.SetSFXVolume(sfxVolume);
}
}
}

public static bool FullScreen
{
get { return fullScreen; }
set
{
fullScreen = value;
PlayerPrefs.SetInt("FullScreen", fullScreen ? 1 : 0);
PlayerPrefs.Save();

// Apply immediately
Screen.fullScreen = fullScreen;
}
}

public static int QualityLevel
{
get { return qualityLevel; }
set
{
qualityLevel = Mathf.Clamp(value, 0, QualitySettings.names.Length - 1);
PlayerPrefs.SetInt("QualityLevel", qualityLevel);
PlayerPrefs.Save();

// Apply immediately
QualitySettings.SetQualityLevel(qualityLevel);
}
}

public static bool InvertYAxis
{
get { return invertYAxis; }
set
{
invertYAxis = value;
PlayerPrefs.SetInt("InvertYAxis", invertYAxis ? 1 : 0);
PlayerPrefs.Save();
}
}

public static float MouseSensitivity
{
get { return mouseSensitivity; }
set
{
mouseSensitivity = Mathf.Clamp(value, 0.1f, 5f);
PlayerPrefs.SetFloat("MouseSensitivity", mouseSensitivity);
PlayerPrefs.Save();
}
}

// Methods
public static void LoadSettings()
{
// Load without triggering side effects
musicVolume = PlayerPrefs.GetFloat("MusicVolume", 0.7f);
sfxVolume = PlayerPrefs.GetFloat("SfxVolume", 0.8f);
fullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1;
qualityLevel = PlayerPrefs.GetInt("QualityLevel", 2);
invertYAxis = PlayerPrefs.GetInt("InvertYAxis", 0) == 1;
mouseSensitivity = PlayerPrefs.GetFloat("MouseSensitivity", 1.0f);

// Apply settings
Screen.fullScreen = fullScreen;
QualitySettings.SetQualityLevel(qualityLevel);
}

public static void ResetToDefaults()
{
MusicVolume = 0.7f;
SfxVolume = 0.8f;
FullScreen = true;
QualityLevel = 2;
InvertYAxis = false;
MouseSensitivity = 1.0f;
}
}

// Usage in a settings UI
public class SettingsUI : MonoBehaviour
{
[SerializeField] private Slider musicSlider;
[SerializeField] private Slider sfxSlider;
[SerializeField] private Toggle fullScreenToggle;
[SerializeField] private Dropdown qualityDropdown;
[SerializeField] private Toggle invertYAxisToggle;
[SerializeField] private Slider sensitivitySlider;

private void Start()
{
// Initialize UI with current settings
musicSlider.value = GameSettings.MusicVolume;
sfxSlider.value = GameSettings.SfxVolume;
fullScreenToggle.isOn = GameSettings.FullScreen;
qualityDropdown.value = GameSettings.QualityLevel;
invertYAxisToggle.isOn = GameSettings.InvertYAxis;
sensitivitySlider.value = GameSettings.MouseSensitivity;

// Add listeners
musicSlider.onValueChanged.AddListener(OnMusicVolumeChanged);
sfxSlider.onValueChanged.AddListener(OnSfxVolumeChanged);
fullScreenToggle.onValueChanged.AddListener(OnFullScreenChanged);
qualityDropdown.onValueChanged.AddListener(OnQualityChanged);
invertYAxisToggle.onValueChanged.AddListener(OnInvertYAxisChanged);
sensitivitySlider.onValueChanged.AddListener(OnSensitivityChanged);
}

private void OnMusicVolumeChanged(float value)
{
GameSettings.MusicVolume = value;
}

private void OnSfxVolumeChanged(float value)
{
GameSettings.SfxVolume = value;
}

private void OnFullScreenChanged(bool value)
{
GameSettings.FullScreen = value;
}

private void OnQualityChanged(int value)
{
GameSettings.QualityLevel = value;
}

private void OnInvertYAxisChanged(bool value)
{
GameSettings.InvertYAxis = value;
}

private void OnSensitivityChanged(float value)
{
GameSettings.MouseSensitivity = value;
}

public void ResetToDefaults()
{
GameSettings.ResetToDefaults();

// Update UI
musicSlider.value = GameSettings.MusicVolume;
sfxSlider.value = GameSettings.SfxVolume;
fullScreenToggle.isOn = GameSettings.FullScreen;
qualityDropdown.value = GameSettings.QualityLevel;
invertYAxisToggle.isOn = GameSettings.InvertYAxis;
sensitivitySlider.value = GameSettings.MouseSensitivity;
}
}

// Usage in game code
public class PlayerController : MonoBehaviour
{
private float lookSensitivity;
private bool invertY;

private void Start()
{
// Get settings
lookSensitivity = GameSettings.MouseSensitivity;
invertY = GameSettings.InvertYAxis;
}

private void Update()
{
// Check if settings have changed
if (lookSensitivity != GameSettings.MouseSensitivity)
{
lookSensitivity = GameSettings.MouseSensitivity;
}

if (invertY != GameSettings.InvertYAxis)
{
invertY = GameSettings.InvertYAxis;
}

// Use settings in player control
float mouseY = Input.GetAxis("Mouse Y") * lookSensitivity;
if (invertY)
{
mouseY = -mouseY;
}

// Apply camera rotation...
}
}

Example 2: Math and Physics Utilities

// Static utility class for game math and physics
public static class GameMath
{
// Constants
public const float GRAVITY = 9.81f;

// Projectile motion
public static Vector3 CalculateVelocityToHitTarget(Vector3 startPos, Vector3 targetPos, float angle)
{
Vector3 targetDir = targetPos - startPos;
float y = targetDir.y;
targetDir.y = 0;
float x = targetDir.magnitude;
float angleRad = angle * Mathf.Deg2Rad;

float v2 = (GRAVITY * x * x) / (2 * (y - x * Mathf.Tan(angleRad)) * Mathf.Cos(angleRad) * Mathf.Cos(angleRad));

if (v2 <= 0)
return Vector3.zero;

float v = Mathf.Sqrt(v2);

Vector3 velocity = targetDir.normalized * v * Mathf.Cos(angleRad);
velocity.y = v * Mathf.Sin(angleRad);

return velocity;
}

// Parabola calculation for jumping
public static float CalculateJumpVelocity(float height)
{
return Mathf.Sqrt(2 * GRAVITY * height);
}

// Bezier curve interpolation
public static Vector3 QuadraticBezier(Vector3 p0, Vector3 p1, Vector3 p2, float t)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;

return uu * p0 + 2 * u * t * p1 + tt * p2;
}

public static Vector3 CubicBezier(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float uuu = uu * u;
float ttt = tt * t;

return uuu * p0 + 3 * uu * t * p1 + 3 * u * tt * p2 + ttt * p3;
}

// Random point generation
public static Vector3 RandomPointInBounds(Bounds bounds)
{
return new Vector3(
Random.Range(bounds.min.x, bounds.max.x),
Random.Range(bounds.min.y, bounds.max.y),
Random.Range(bounds.min.z, bounds.max.z)
);
}

public static Vector2 RandomPointOnCircle(float radius)
{
float angle = Random.Range(0f, Mathf.PI * 2f);
return new Vector2(
Mathf.Cos(angle) * radius,
Mathf.Sin(angle) * radius
);
}

// Angle calculations
public static float SignedAngle(Vector2 from, Vector2 to)
{
float unsigned = Vector2.Angle(from, to);
float sign = Mathf.Sign(from.x * to.y - from.y * to.x);
return unsigned * sign;
}

// Distance calculations
public static float DistanceSquared(Vector3 a, Vector3 b)
{
return (a.x - b.x) * (a.x - b.x) +
(a.y - b.y) * (a.y - b.y) +
(a.z - b.z) * (a.z - b.z);
}

// Easing functions
public static float EaseInQuad(float t)
{
return t * t;
}

public static float EaseOutQuad(float t)
{
return t * (2 - t);
}

public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
}

// Usage
public class ProjectileLauncher : MonoBehaviour
{
[SerializeField] private GameObject projectilePrefab;
[SerializeField] private Transform target;
[SerializeField] private float launchAngle = 45f;

public void LaunchProjectile()
{
GameObject projectile = Instantiate(projectilePrefab, transform.position, Quaternion.identity);
Rigidbody rb = projectile.GetComponent<Rigidbody>();

if (rb != null)
{
Vector3 velocity = GameMath.CalculateVelocityToHitTarget(
transform.position,
target.position,
launchAngle
);

if (velocity != Vector3.zero)
{
rb.velocity = velocity;
}
else
{
Debug.LogWarning("Cannot hit target with this angle");
}
}
}
}

public class CharacterJump : MonoBehaviour
{
[SerializeField] private float jumpHeight = 2f;

private Rigidbody rb;

private void Start()
{
rb = GetComponent<Rigidbody>();
}

public void Jump()
{
float jumpVelocity = GameMath.CalculateJumpVelocity(jumpHeight);
rb.velocity = new Vector3(rb.velocity.x, jumpVelocity, rb.velocity.z);
}
}

Example 3: Object Pooling System

// Static object pool manager
public static class ObjectPool
{
// Dictionary to store pools for different prefabs
private static Dictionary<string, Queue<GameObject>> pools = new Dictionary<string, Queue<GameObject>>();
private static Dictionary<string, GameObject> prefabs = new Dictionary<string, GameObject>();

// Initialize a pool for a specific prefab
public static void InitializePool(GameObject prefab, int initialSize)
{
string key = prefab.name;

if (!pools.ContainsKey(key))
{
pools[key] = new Queue<GameObject>();
prefabs[key] = prefab;

// Create initial objects
for (int i = 0; i < initialSize; i++)
{
CreateNewInstance(key);
}

Debug.Log($"Initialized pool for {key} with {initialSize} instances");
}
else
{
Debug.LogWarning($"Pool for {key} already exists");
}
}

// Get an object from the pool
public static GameObject GetObject(string key, Vector3 position, Quaternion rotation)
{
if (!pools.ContainsKey(key))
{
Debug.LogError($"Pool for {key} doesn't exist");
return null;
}

GameObject obj;

// If pool is empty, create a new instance
if (pools[key].Count == 0)
{
obj = CreateNewInstance(key);
}
else
{
// Get existing object from pool
obj = pools[key].Dequeue();
}

// Position and activate the object
obj.transform.position = position;
obj.transform.rotation = rotation;
obj.SetActive(true);

// If the object has a poolable component, notify it
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnSpawnFromPool();
}

return obj;
}

// Return an object to the pool
public static void ReturnObject(GameObject obj)
{
string key = obj.name.Replace("(Clone)", "").Trim();

if (!pools.ContainsKey(key))
{
Debug.LogError($"Pool for {key} doesn't exist");
return;
}

// If the object has a poolable component, notify it
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnReturnToPool();
}

// Deactivate and return to pool
obj.SetActive(false);
pools[key].Enqueue(obj);
}

// Create a new instance for the pool
private static GameObject CreateNewInstance(string key)
{
GameObject obj = Object.Instantiate(prefabs[key]);
obj.name = prefabs[key].name + "(Clone)";
obj.SetActive(false);

// Add PooledObject component if it doesn't have IPoolable
if (obj.GetComponent<IPoolable>() == null)
{
obj.AddComponent<PooledObject>();
}

return obj;
}

// Clear all pools
public static void ClearAllPools()
{
foreach (var pool in pools.Values)
{
while (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
Object.Destroy(obj);
}
}

pools.Clear();
prefabs.Clear();

Debug.Log("All object pools cleared");
}
}

// Interface for poolable objects
public interface IPoolable
{
void OnSpawnFromPool();
void OnReturnToPool();
}

// Default implementation of IPoolable
public class PooledObject : MonoBehaviour, IPoolable
{
public void OnSpawnFromPool()
{
// Default implementation - can be overridden
}

public void OnReturnToPool()
{
// Default implementation - can be overridden
}

// Automatically return to pool after a delay
public void ReturnToPoolAfter(float delay)
{
StartCoroutine(ReturnToPoolCoroutine(delay));
}

private IEnumerator ReturnToPoolCoroutine(float delay)
{
yield return new WaitForSeconds(delay);
ObjectPool.ReturnObject(gameObject);
}
}

// Example usage - bullet that implements IPoolable
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float speed = 20f;
[SerializeField] private int damage = 10;
[SerializeField] private float lifetime = 3f;

private Rigidbody rb;

private void Awake()
{
rb = GetComponent<Rigidbody>();
}

public void OnSpawnFromPool()
{
// Reset state
rb.velocity = transform.forward * speed;

// Return to pool after lifetime
Invoke("ReturnToPool", lifetime);
}

public void OnReturnToPool()
{
// Clean up
rb.velocity = Vector3.zero;
CancelInvoke();
}

private void ReturnToPool()
{
ObjectPool.ReturnObject(gameObject);
}

private void OnCollisionEnter(Collision collision)
{
// Apply damage if hit something damageable
IDamageable damageable = collision.gameObject.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.TakeDamage(damage);
}

// Return to pool
ObjectPool.ReturnObject(gameObject);
}
}

// Example usage - weapon that uses the object pool
public class Weapon : MonoBehaviour
{
[SerializeField] private GameObject bulletPrefab;
[SerializeField] private Transform firePoint;
[SerializeField] private int initialPoolSize = 20;

private void Start()
{
// Initialize bullet pool
ObjectPool.InitializePool(bulletPrefab, initialPoolSize);
}

public void Fire()
{
// Get bullet from pool instead of instantiating
ObjectPool.GetObject(bulletPrefab.name, firePoint.position, firePoint.rotation);

// Play sound, effects, etc.
}
}

Best Practices for Static Members

  1. Use Static Members Judiciously: Static members are powerful but can make code harder to test and maintain if overused.

  2. Prefer Instance Methods for operations that depend on object state or might have different implementations in derived classes.

  3. Use Static Classes for Utility Functions that don't depend on instance state and are purely functional.

  4. Be Careful with Static Mutable State: Shared state can lead to unexpected behavior and threading issues.

  5. Initialize Static Fields in Static Constructors to ensure proper initialization before use.

  6. Consider Thread Safety if your static members might be accessed from multiple threads.

  7. Use Singletons Carefully: While convenient, singletons can make testing and dependency management harder.

  8. Document Static Members Thoroughly: Since they can be accessed from anywhere, clear documentation is essential.

  9. Avoid Circular Dependencies between static constructors, which can lead to initialization problems.

  10. Consider Alternatives to Singletons: Dependency injection and service locators can provide similar benefits with better testability.

Conclusion

Static members are a powerful feature in C# that allow you to create class-level functionality and shared state. They're particularly useful for utility functions, constants, factory methods, and global access points.

In Unity, static members are commonly used for singletons, utility classes, and global game state. By understanding when and how to use static members effectively, you can create more organized, maintainable, and efficient code for your games.

In the next section, we'll explore inheritance, which allows you to create hierarchical relationships between classes.

Practice Exercise

Exercise: Create a static utility class for a game with the following requirements:

  1. Create a static class called GameUtils with the following functionality:

    • A method to calculate damage based on attack power, defense, and a random factor
    • A method to generate a random item with different rarity levels
    • A method to format time in minutes and seconds (e.g., "05:32")
    • A method to calculate the distance between two game objects
    • Constants for common game values (max level, max health, etc.)
  2. Create a simple Player class that uses the GameUtils class for various calculations.

  3. Create a singleton GameManager class that:

    • Keeps track of the game score
    • Manages the game state (playing, paused, game over)
    • Provides methods to start, pause, and end the game

Think about how these static members improve your code organization and reusability.