Skip to main content

Architecture Overview

PlatformerGame follows Unity’s component-based architecture with these key patterns:
  • Event-Driven Communication - Decoupled systems using C# events
  • Component-Based Design - Single-responsibility components
  • ScriptableObject Pattern - Data-driven power-up system
  • Unity Input System - Modern input handling
  • Physics-Based Movement - Rigidbody2D-driven player control

Event-Driven Architecture

The project uses C# events extensively to decouple systems and maintain clean separation of concerns.

Event Pattern Implementation

Source: Assets/Scripts/Player/PlayerJump.cs:26
public class PlayerJump : MonoBehaviour
{
    public static event Action OnJumpChange;

    private void Jump()
    {
        Vector2 velocity = new(rb2D.linearVelocity.x, GetJumpForce());
        rb2D.linearVelocity = velocity;

        OnJumpChange?.Invoke();  // Notify subscribers
    }
}
Subscriber: Assets/Scripts/MusicManager.cs:14
public class MusicManager : MonoBehaviour
{
    [SerializeField] private AudioSource jumpSound;

    private void OnEnable()
    {
        PlayerJump.OnJumpChange += SetJumpEffect;
    }

    private void OnDisable()
    {
        PlayerJump.OnJumpChange -= SetJumpEffect;
    }

    private void SetJumpEffect()
    {
        jumpSound.Play();
    }
}
Benefits:
  • PlayerJump doesn’t need to know about audio
  • Audio system can be added/removed without modifying player code
  • Multiple systems can respond to the same event
Source: Assets/Scripts/Score/Coin.cs:7
public class Coin : MonoBehaviour
{
    public int Value;
    public static event Action<Coin> OnCoinCollected;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        OnCoinCollected?.Invoke(this);  // Pass coin reference
        Destroy(gameObject);
    }
}
Subscribers:
// ScoreSystem.cs:13 - Updates score
Coin.OnCoinCollected += UpdateScore;

// MusicManager.cs:15 - Plays sound
Coin.OnCoinCollected += SetCoinEffect;
Key Feature: Event passes the coin instance, allowing subscribers to access coin.Value
Source: Assets/Scripts/SceneLogic/FinishGame.cs:8
public class FinishGame : MonoBehaviour
{
    public static event Action OnDeathEvent;
    public static event Action OnFinishedEvent;

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (gameObject.CompareTag("Spike") || 
            gameObject.CompareTag("DeathDetector"))
        {
            OnDeathEvent?.Invoke();
        }
        else
        {
            OnFinishedEvent?.Invoke();
        }
    }
}
Subscribers:
  • MusicManager.cs:16 - Death sound
  • ScoreSystem.cs:14 - Finalize score on death
  • ScoreSystem.cs:15 - Finalize score on completion

Event Subscription Pattern

All event subscribers follow this lifecycle pattern:
private void OnEnable()
{
    // Subscribe to events
    PlayerJump.OnJumpChange += SetJumpEffect;
    Coin.OnCoinCollected += UpdateScore;
    FinishGame.OnDeathEvent += HandleDeath;
}

private void OnDisable()
{
    // Unsubscribe to prevent memory leaks
    PlayerJump.OnJumpChange -= SetJumpEffect;
    Coin.OnCoinCollected -= UpdateScore;
    FinishGame.OnDeathEvent -= HandleDeath;
}
Why OnEnable/OnDisable:
  • Prevents memory leaks from dangling references
  • Handles scene transitions correctly
  • Works with object pooling if implemented
  • Safer than Awake/OnDestroy for component lifecycle

Component-Based Architecture

Each script has a single, well-defined responsibility.

Player Components

The player GameObject uses multiple specialized components:
Location: Assets/Scripts/Player/PlayerInput.cs:4Responsibility: Horizontal movement and character orientation
public class PlayerInput : MonoBehaviour
{
    [SerializeField] private float speed = 5.0f;
    private float horizontalDirection;
    private Rigidbody2D rigid2d;

    private void FixedUpdate()
    {
        Vector2 velocity = rigid2d.linearVelocity;
        velocity.x = horizontalDirection * speed;
        rigid2d.linearVelocity = velocity;

        Animator.SetFloat("Walk", Mathf.Abs(horizontalDirection));

        if ((horizontalDirection > 0) && !facingRight ||
            (horizontalDirection < 0) && facingRight)
        {
            FlipCharacter();
        }
    }

    private void OnMove(InputValue value)
    {
        Vector2 input = value.Get<Vector2>();
        horizontalDirection = input.x;
    }
}
Features:
  • Receives input via OnMove() callback (Unity Input System)
  • Updates horizontal velocity only (doesn’t touch Y axis)
  • Handles character flipping based on direction
  • Updates walk animation parameter
Location: Assets/Scripts/Player/PlayerJump.cs:4Responsibility: Vertical movement, jumping mechanics, wall sliding
public class PlayerJump : MonoBehaviour
{
    public float JumpHeight;
    public float PressTimeToMaxJump;
    public float DistanceToMaxHeight;
    public float WallSlideSpeed;

    private CollisionDetection collisionDetection;
    public static event Action OnJumpChange;

    public void OnJumpStarted()
    {
        if (isGrounded || isWallSliding)
        {
            SetGravity();
            Jump();
            jumpStartedTime = Time.time;
            doubleJumpDelay = Time.time + 0.2f;
            doubleJumpDone = false;
        }
        else if (!doubleJumpDone && (Time.time > doubleJumpDelay))
        {
            doubleJumpDone = true;
            Jump();
        }
    }

    public void OnJumpFinished()
    {
        float fractionOfTimePassed = 1 / 
            Mathf.Clamp01((Time.time - jumpStartedTime) / PressTimeToMaxJump);
        rb2D.gravityScale *= fractionOfTimePassed;
    }

    private void SetGravity()
    {
        float gravity = 2 * JumpHeight * (SpeedHorizontal * SpeedHorizontal) / 
                       (DistanceToMaxHeight * DistanceToMaxHeight);
        rb2D.gravityScale = gravity / 9.81f;
    }
}
Advanced Features:
  • Variable jump height (hold for higher jump)
  • Double jump with 0.2s delay
  • Wall sliding mechanics
  • Physics-based gravity calculation
  • Separate OnJumpStarted() and OnJumpFinished() callbacks
Location: Assets/Scripts/Player/CollisionDetection.csResponsibility: Ground and wall detection for jump systemUsage:
private bool isWallSliding => collisionDetection.IsTouchingFront();
private bool isGrounded => collisionDetection.IsGrounded();
Provides clean collision queries for PlayerJump logic.
Location: Assets/Scripts/Player/PlayerDeath.csResponsibility: Death state handling and visualsSeparates death logic from movement logic for clarity.

Separation Benefits

Single Responsibility:
  • Each component can be tested independently
  • Easy to modify jump without affecting movement
  • Can disable wall-slide by removing component
Reusability:
  • CollisionDetection could be used by enemies
  • PlayerInput could control different objects
Maintainability:
  • Bug in jumping? Check PlayerJump.cs
  • Input issues? Check PlayerInput.cs
  • Clear file locations for each concern

Unity Input System Integration

The project uses Unity’s new Input System for modern, flexible input handling.

Input Actions Configuration

Location: Assets/InputSystem/BasicInput.inputactions
{
    "name": "BasicInput",
    "maps": [
        {
            "name": "Player",
            "actions": [
                {
                    "name": "Move",
                    "type": "Value",
                    "expectedControlType": "Vector2"
                },
                {
                    "name": "JumpStarted",
                    "type": "Button"
                },
                {
                    "name": "JumpFinished",
                    "type": "Button"
                }
            ],
            "bindings": [
                // WASD binding
                {"path": "<Keyboard>/a", "action": "Move", "part": "left"},
                {"path": "<Keyboard>/d", "action": "Move", "part": "right"},
                // Arrow keys binding
                {"path": "<Keyboard>/leftArrow", "action": "Move"},
                {"path": "<Keyboard>/rightArrow", "action": "Move"}
            ]
        }
    ]
}

Input System Features

Automatic Callbacks: Unity’s PlayerInput component automatically calls methods on scripts:
// Method name must match action name
private void OnMove(InputValue value)
{
    Vector2 input = value.Get<Vector2>();
    horizontalDirection = input.x;
}

public void OnJumpStarted()  // Called when jump pressed
public void OnJumpFinished() // Called when jump released
Convention: Method name = “On” + ActionName (e.g., “Move” → OnMove())
Actions support multiple control schemes:
  • WASD for movement
  • Arrow keys for movement
  • Both work simultaneously
  • Easy to add gamepad support
For immediate queries (like ESC key):
using UnityEngine.InputSystem;

public void Update()
{
    if ((Keyboard.current != null) && 
        Keyboard.current.escapeKey.wasPressedThisFrame)
    {
        Quit();
    }
}
Pattern: Device.current.control.wasPressedThisFrame

Input System Advantages

  • Rebindable: Change controls without code changes
  • Multiple Devices: Keyboard, gamepad, touch support
  • Action-Based: Think in game actions, not raw inputs
  • Event-Driven: Callbacks instead of polling in Update()
  • Variable Jump: Separate started/finished events enable hold mechanics

ScriptableObject Pattern

Power-ups use ScriptableObjects for data-driven, designer-friendly implementation.

Abstract Base Class

Location: Assets/Scripts/ScriptableObjectsScripts/Powerup.cs:3
using UnityEngine;

public abstract class PowerUp : ScriptableObject
{
    public abstract void Apply(GameObject target);
}
Design: Abstract base requires implementations to define Apply() logic.

Concrete Implementation

Location: Assets/Scripts/ScriptableObjectsScripts/JumpBoost.cs
[CreateAssetMenu(fileName = "JumpBoost", menuName = "PowerUps/JumpBoost")]
public class JumpBoost : PowerUp
{
    public float JumpMultiplier = 1.5f;

    public override void Apply(GameObject target)
    {
        if (target.TryGetComponent<PlayerJump>(out var jump))
        {
            jump.JumpHeight *= JumpMultiplier;
        }
    }
}

Collection System

Location: Assets/Scripts/ScriptableObjectsScripts/PowerUpPickUp.cs:3
public class PowerUpPickUp : MonoBehaviour
{
    [SerializeField] private PowerUp effect;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.TryGetComponent<PlayerJump>(out _))
        {
            effect.Apply(collision.gameObject);
            gameObject.SetActive(false);
        }
    }
}

Pattern Benefits

  • Create new power-ups in Unity Editor without code
  • Tune values (JumpMultiplier) in Inspector
  • Reuse effects across multiple pickup instances
  • No compilation needed for value changes
Assets/ScriptableObjects/
├── JumpBoost_Small.asset   (1.2x jump)
├── JumpBoost_Large.asset   (2.0x jump)
└── SpeedBoost.asset        (future power-up)
Same code, different data assets.
Add new power-up types:
[CreateAssetMenu(menuName = "PowerUps/SpeedBoost")]
public class SpeedBoost : PowerUp
{
    public override void Apply(GameObject target)
    {
        // Speed boost logic
    }
}
No changes needed to PowerUpPickUp.cs.

Coding Conventions

From CONVENTIONS.md, the project follows standard C# conventions:

Naming Conventions

ElementConventionExample
ClassesTitleCasePlayerInput, ScoreSystem
Public FieldsTitleCasepublic float Speed;
Private FieldslowerCaseprivate float horizontalDirection;
MethodsTitleCaseJump(), GetJumpForce()
ParameterslowerCasetarget, collision
ConstantslowerCaseconst int maxValue = 8;

Code Style

// Float values always use .xf notation
float gravity = 10.0f;
float multiplier = 1.5f;

// Control flow with spaces
if (condition) value = 0;

while (!WindowShouldClose())
{
    // Loop body
}

// Braces always aligned
void SomeFunction()
{
    // Function body
}

// Conditions in parentheses, booleans without
if ((value > 1) && (value < 50) && valueActive)
{
    // Code
}

Key Rules

  • Four spaces for indentation (no tabs)
  • No trailing spaces
  • All fields initialized
  • One public class per file
  • File name matches class name

System Communication Flow

Example: Collecting a coin Key Points:
  • Single trigger causes multiple system reactions
  • Systems don’t reference each other directly
  • Events enable loose coupling
  • Easy to add new subscribers without modifying existing code

Build docs developers (and LLMs) love