Skip to main content

8.1 - Delegates

In game development, you'll often need to create systems that can dynamically change behavior at runtime. For example, you might want different enemy types to react differently when they spot the player, or you might want to trigger various effects when a player levels up. This is where delegates come in - they're one of C#'s most powerful features for creating flexible, event-driven code.

What Are Delegates?

A delegate is essentially a type-safe function pointer. In simpler terms, it's a variable that holds a reference to a method. This allows you to:

  1. Pass methods as arguments to other methods
  2. Store methods in variables
  3. Call methods through these references
  4. Change which method is called at runtime

Think of a delegate as a messenger who knows exactly what type of message they can deliver (the method signature) and who can deliver it to different recipients as needed.

Declaring Delegates

To create a delegate, you define its signature - the return type and parameters of the methods it can reference:

// A delegate that takes no parameters and returns nothing
public delegate void GameEvent();

// A delegate that takes an int parameter and returns nothing
public delegate void PlayerDamaged(int damageAmount);

// A delegate that takes two parameters and returns a float
public delegate float DamageCalculation(float baseDamage, float armorValue);

The syntax looks similar to a method declaration, but with the delegate keyword and no method body.

Using Delegates

Once you've declared a delegate type, you can create instances of it and assign methods to them:

public class GameManager
{
// Declare a delegate variable
private GameEvent onGameStart;

public void Initialize()
{
// Assign a method to the delegate
onGameStart = DisplayWelcomeMessage;

// Call the method through the delegate
onGameStart();
}

private void DisplayWelcomeMessage()
{
Console.WriteLine("Welcome to the game! Press any key to start.");
}
}

Multicast Delegates

One of the most powerful features of delegates is that they can reference multiple methods simultaneously. These are called multicast delegates. When you invoke a multicast delegate, all the methods it references are called in sequence:

public class GameManager
{
private GameEvent onLevelComplete;

public void Initialize()
{
// Add multiple methods to the delegate
onLevelComplete = SaveGame;
onLevelComplete += DisplayLevelCompleteMessage;
onLevelComplete += CalculatePlayerScore;

// Later, when the level is completed:
onLevelComplete(); // Calls all three methods in sequence
}

// Remove a method from the delegate
public void DisableSaving()
{
onLevelComplete -= SaveGame;
}

private void SaveGame()
{
Console.WriteLine("Game progress saved!");
}

private void DisplayLevelCompleteMessage()
{
Console.WriteLine("Level complete! Great job!");
}

private void CalculatePlayerScore()
{
Console.WriteLine("Calculating final score...");
}
}

The += and -= operators are used to add and remove methods from a multicast delegate.

Practical Example: Damage System

Let's see how delegates can be used to create a flexible damage system in a game:

public class Character
{
public int Health { get; private set; } = 100;

// Delegate for damage calculation
public delegate int DamageCalculator(int baseDamage);

// Different damage calculation methods
public int CalculatePhysicalDamage(int baseDamage)
{
// Physical damage reduced by armor
int armor = 5;
return Math.Max(0, baseDamage - armor);
}

public int CalculateMagicalDamage(int baseDamage)
{
// Magical damage reduced by resistance
int resistance = 3;
return Math.Max(0, baseDamage - resistance);
}

public int CalculateTrueDamage(int baseDamage)
{
// True damage ignores all defenses
return baseDamage;
}

public void TakeDamage(int baseDamage, DamageCalculator damageCalculator)
{
// Use the provided delegate to calculate actual damage
int actualDamage = damageCalculator(baseDamage);
Health -= actualDamage;

Console.WriteLine($"Took {actualDamage} damage! Remaining health: {Health}");
}
}

// Usage:
public class Game
{
public void SimulateCombat()
{
Character player = new Character();

// Deal different types of damage using delegates
player.TakeDamage(20, player.CalculatePhysicalDamage); // Physical attack
player.TakeDamage(15, player.CalculateMagicalDamage); // Magic spell
player.TakeDamage(10, player.CalculateTrueDamage); // Special ability
}
}

In this example, the TakeDamage method accepts a delegate that determines how damage is calculated. This makes the system extremely flexible - you can add new damage types without modifying the TakeDamage method itself.

Predefined Delegate Types

C# provides several predefined delegate types that cover common scenarios:

Action<T>

The Action delegates represent methods that don't return a value (void return type):

// Action with no parameters
Action showGameOver = DisplayGameOverScreen;

// Action with parameters
Action<string> showMessage = DisplayMessage;
Action<int, string> updatePlayerStatus = UpdateStatus;

// Usage
showGameOver();
showMessage("Level starting!");
updatePlayerStatus(1, "Ready");

Func<T, TResult>

The Func delegates represent methods that return a value:

// Func with no parameters, returns int
Func<int> getRandom = GetRandomNumber;

// Func with parameters
Func<int, int, int> calculateDamage = CalculateDamage;
Func<string, bool> isValidName = ValidateName;

// Usage
int random = getRandom();
int damage = calculateDamage(10, 5);
bool isValid = isValidName("Player1");

Predicate<T>

The Predicate delegate represents a method that takes one parameter and returns a boolean:

// Predicate that checks if a number is even
Predicate<int> isEven = x => x % 2 == 0;

// Usage
bool result = isEven(4); // true

Using these predefined delegates can save you from having to declare custom delegate types for common patterns.

Delegates vs. Interfaces

You might wonder when to use delegates versus interfaces, as both can enable polymorphic behavior. Here's a quick comparison:

  • Use delegates when:

    • You need to invoke multiple methods in response to a single event
    • The behavior is a small, isolated piece of functionality
    • You want to change behavior dynamically at runtime
    • You need callback functionality
  • Use interfaces when:

    • You need to define a contract for a whole class
    • You have multiple related methods that should be implemented together
    • You want to leverage polymorphism for object hierarchies
    • You need to enforce a specific structure

Delegates in Unity

In Unity, delegates are commonly used for:

  1. Custom events: Notifying other components when something happens
  2. Callback functions: Executing code after an operation completes
  3. Input handling: Responding to player input in different ways
  4. AI behavior: Switching between different behavior patterns
Unity Relevance

Unity's event system heavily relies on delegates. For example, Unity's UI Button component uses delegates to determine what happens when a button is clicked. Understanding delegates will help you work effectively with Unity's event system and create your own custom events.

Best Practices for Delegates

  1. Always check for null before invoking a delegate:

    onPlayerDeath?.Invoke(); // The ?. operator only invokes if the delegate isn't null
  2. Use the predefined delegate types when possible to reduce code and improve readability.

  3. Be careful with multicast delegates that return values - only the value from the last method called will be returned.

  4. Consider memory management - delegates hold references to their target objects, which can prevent garbage collection.

Conclusion

Delegates are a powerful feature in C# that enable flexible, event-driven programming patterns. They allow you to write more modular code by decoupling the "what happens" from the "when it happens." As you progress in your game development journey, you'll find delegates indispensable for creating responsive, dynamic systems.

In the next section, we'll explore events, which build upon delegates to provide a more structured way of handling notifications between objects.