Skip to main content
The score system implements an event-driven architecture for tracking player score, handling coin collection, and updating UI elements. This decoupled design allows multiple systems to react to score changes without direct dependencies.

Architecture Overview

The score system consists of four interconnected components:
  • ScoreSystem - Central score tracking with event broadcasting
  • Coin - Individual collectible items that trigger score events
  • ScoreUpdate - UI component that displays current score in real-time
  • ShowFinalScore - Displays the final score on game over screens
This system uses C# events for communication, creating a loosely coupled architecture where UI and gameplay logic remain independent.

ScoreSystem

The core component that manages score state and coordinates between coin collection and UI updates.

Properties

Score
int
default:"0"
Current score during active gameplay
FinalScore
int
default:"0"
Static property storing the final score after game completion or death

Events

ScoreSystem.cs:9
public static event Action<int> OnScoreUpdated;
This event broadcasts the new score value whenever it changes, allowing multiple listeners to react.

Event Subscriptions

The system subscribes to game events during its lifecycle:
ScoreSystem.cs:11-23
public void OnEnable()
{
    Coin.OnCoinCollected += UpdateScore;
    FinishGame.OnDeathEvent += WriteFinalScore;
    FinishGame.OnFinishedEvent += WriteFinalScore;
}

public void OnDisable()
{
    Coin.OnCoinCollected -= UpdateScore;
    FinishGame.OnDeathEvent -= WriteFinalScore;
    FinishGame.OnFinishedEvent -= WriteFinalScore;
}
Proper event cleanup in OnDisable() prevents memory leaks and ensures events don’t fire after the object is destroyed.

Score Update Logic

When a coin is collected, the score is updated and broadcast:
ScoreSystem.cs:25-29
private void UpdateScore(Coin coin)
{
    Score += coin.Value;
    OnScoreUpdated?.Invoke(Score);
}
The null-conditional operator (?.) safely invokes the event only if there are subscribers.

Final Score Recording

The score is saved to the static FinalScore property when the game ends:
ScoreSystem.cs:31-34
private void WriteFinalScore()
{
    FinalScore = Score;
}
Using a static property allows the final score to persist across scene changes, making it accessible on game over or victory screens.

Coin

Represents collectible coins in the game world that trigger score updates when collected.

Configuration

Value
int
required
Point value awarded when this coin is collected

Coin Collection Event

Coin.cs:7
public static event Action<Coin> OnCoinCollected;
This event passes the coin instance to listeners, allowing them to access the coin’s value.

Collision Detection

Coin.cs:9-13
private void OnTriggerEnter2D(Collider2D collision)
{
    OnCoinCollected?.Invoke(this);
    Destroy(gameObject);
}
When the player’s collider enters the coin’s trigger zone:
  1. The OnCoinCollected event is broadcast with this coin instance
  2. The coin GameObject is destroyed
Coins use trigger colliders (non-physical) so they don’t affect player movement while still detecting overlap.

Creating Different Coin Types

You can create coins with different values:
// In Unity Inspector:
// Bronze Coin: Value = 1
// Silver Coin: Value = 5
// Gold Coin: Value = 10

ScoreUpdate

UI component that displays the current score in real-time during gameplay.

Implementation

ScoreUpdate.cs:4-27
public class ScoreUpdate : MonoBehaviour
{
    private Text label;

    private void Awake()
    {
        label = GetComponent<Text>();
    }

    private void OnEnable()
    {
        ScoreSystem.OnScoreUpdated += UpdateText;
    }

    private void OnDisable()
    {
        ScoreSystem.OnScoreUpdated -= UpdateText;
    }

    private void UpdateText(int score)
    {
        label.text = score.ToString();
    }
}

How It Works

  1. Component Reference - Gets the Text component in Awake()
  2. Event Subscription - Subscribes to OnScoreUpdated when enabled
  3. Text Update - Updates the UI text whenever the score changes
  4. Cleanup - Unsubscribes when disabled
This component can be attached to any GameObject with a Text component. Multiple instances can exist to show the score in different locations.

ShowFinalScore

Displays the final score on game over or victory screens.

Implementation

ShowFinalScore.cs:4-13
public class ShowFinalScore : MonoBehaviour
{
    private Text labelFinalScore;

    private void Awake()
    {
        labelFinalScore = GetComponent<Text>();
        labelFinalScore.text = "FINAL SCORE: " + ScoreSystem.FinalScore;
    }
}

How It Works

Unlike ScoreUpdate, this component reads the static FinalScore once during initialization:
  1. Gets the Text component reference
  2. Reads ScoreSystem.FinalScore
  3. Formats and displays the final score
This component should be placed on game over or victory scene UI elements. It reads the score once and doesn’t update dynamically.

Event Flow Diagram

Here’s how the events flow through the system:
1. Player collides with Coin

2. Coin.OnCoinCollected event fires

3. ScoreSystem.UpdateScore() receives event

4. ScoreSystem.Score increases

5. ScoreSystem.OnScoreUpdated event fires

6. ScoreUpdate.UpdateText() receives event

7. UI text updates with new score

--- Game ends ---

8. FinishGame.OnDeathEvent or OnFinishedEvent fires

9. ScoreSystem.WriteFinalScore() executes

10. ScoreSystem.FinalScore is set

11. Scene changes to game over screen

12. ShowFinalScore.Awake() reads FinalScore

13. Final score displayed on UI

Integration Example

Scene Setup

// In Main Game Scene:
// 1. Create empty GameObject named "ScoreSystem"
//    - Add ScoreSystem component

// 2. UI Canvas → Text element named "CurrentScore"
//    - Add ScoreUpdate component

// 3. For each coin in the level:
//    - Add Coin component
//    - Set Value (1, 5, 10, etc.)
//    - Add 2D Circle Collider (Is Trigger = true)

// In Game Over Scene:
// 1. UI Canvas → Text element named "FinalScore"
//    - Add ShowFinalScore component

Creating Custom Score Sources

You can create other collectibles that award points:
using System;
using UnityEngine;

public class Gem : MonoBehaviour
{
    public int Value = 50;
    public static event Action<Gem> OnGemCollected;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        OnGemCollected?.Invoke(this);
        Destroy(gameObject);
    }
}
Then subscribe in ScoreSystem:
public void OnEnable()
{
    Coin.OnCoinCollected += UpdateScore;
    Gem.OnGemCollected += UpdateScoreFromGem; // New subscription
    // ... other subscriptions
}

private void UpdateScoreFromGem(Gem gem)
{
    Score += gem.Value;
    OnScoreUpdated?.Invoke(Score);
}

Best Practices

  1. Always unsubscribe - Match every += with a -= in OnDisable() to prevent memory leaks
  2. Use null-conditional operators - Always use ?.Invoke() when firing events to handle cases with no subscribers
  3. Pass relevant data - Events like OnCoinCollected pass the coin instance, allowing access to coin-specific properties
  4. Static for persistence - Use static properties like FinalScore when data needs to survive scene changes
  5. Single responsibility - Keep each component focused: ScoreSystem tracks, Coin collects, UI components display
  6. Extensibility - The event-driven design makes it easy to add new score sources or score listeners without modifying existing code

Advantages of Event-Driven Architecture

  • Decoupling - UI doesn’t need references to gameplay objects
  • Flexibility - Easy to add new score sources or displays
  • Testability - Components can be tested independently
  • Maintainability - Changes to one component rarely affect others
  • Multiple listeners - Many UI elements can react to the same score change

Build docs developers (and LLMs) love