Chapter 5

Project Architecture & Folder Structure

Design the organizational backbone of our infinite runner. Folder conventions, namespaces, assembly definitions, ScriptableObjects, the Singleton pattern, and SOLID principles adapted for game development.

This chapter is about setting up the structure that will keep our project maintainable as it grows from a few scripts to dozens. We're making decisions now that will prevent pain later. None of this code does anything visible in the game yet, but it's the foundation everything else is built on.

Why Architecture Matters

Let's look at two versions of the same project to understand why spending time on architecture pays off.

The "Tutorial Code" Project (What We're Avoiding)

Spaghetti Project StructureBAD
Assets/
  GameManager.cs          (800 lines - handles state, scoring, UI, audio)
  PlayerMove.cs           (400 lines - movement, input, collision, animation)
  Spawner.cs              (300 lines - obstacles, coins, power-ups, all mixed)
  UIScript.cs             (200 lines - every UI screen in one file)
  Coin.prefab
  Obstacle1.prefab
  player model.fbx
  background_music.mp3
  SampleScene.unity
  coin_sound (1).wav
  final_final_v2.unity

This is what happens when you don't plan. Everything is in one folder. Scripts are massive monoliths that handle multiple unrelated concerns. File names are inconsistent. There's no way to find anything quickly. Changing the scoring system means editing the GameManager, which also controls audio and UI, so any change might break something unrelated.

The Production Project (What We're Building)

Our Project StructureGOOD
Assets/
  _Project/
    Scenes/
      GameScene.unity
      MenuScene.unity
    Scripts/
      Core/
        GameManager.cs        (manages game state only)
        EventBus.cs           (central event system)
        GameEvents.cs         (event definitions)
        Core.asmdef
      Player/
        PlayerController.cs   (lane switching, jump, slide)
        PlayerAnimator.cs     (animation control)
        PlayerCollision.cs    (collision handling)
        Player.asmdef
      World/
        ChunkManager.cs       (chunk lifecycle)
        WorldGenerator.cs     (procedural generation)
        ObjectPool.cs         (object recycling)
        OriginShifter.cs      (floating point fix)
        World.asmdef
      Obstacles/
        ObstacleBase.cs       (base class for all obstacles)
        BarrierObstacle.cs    (specific obstacle type)
        Obstacles.asmdef
      Collectibles/
        Coin.cs
        PowerUp.cs
        Collectibles.asmdef
      UI/
        HUDController.cs
        MenuController.cs
        GameOverController.cs
        UI.asmdef
      Audio/
        AudioManager.cs
        Audio.asmdef
      Camera/
        CameraController.cs
        CameraShake.cs
        Camera.asmdef
      Data/
        GameConfig.cs         (ScriptableObject)
        DifficultyConfig.cs   (ScriptableObject)
        Data.asmdef
    Prefabs/
      Player/
      Obstacles/
      Collectibles/
      UI/
    Materials/
    Audio/
      Music/
      SFX/
    Art/
      Models/
      Textures/
      Animations/
    Settings/
      GameConfig.asset
      DifficultyConfig.asset

Every script has a clear home. Every file type has its own folder. Each system is isolated in its own folder with its own assembly definition. Finding a file takes seconds. Changing the scoring system means editing Scripts/Core/ files only — nothing else is touched.

✅ The _Project Folder Convention

We put all our custom files inside a folder called _Project. The underscore makes it sort to the top of the Assets folder, keeping it visually separated from Unity's auto-generated folders (like Settings, Packages, TextMesh Pro). This is a common convention in professional Unity projects. You can name it _Game or _App instead — the important thing is consistency.

Creating the Folder Structure

Let's create this folder structure now. Open your InfiniteRunner project in Unity and follow these steps:

  1. In the Project window, right-click on Assets and choose Create → Folder. Name it _Project.
  2. Inside _Project, create these folders:
    • Scenes
    • Scripts
    • Prefabs
    • Materials
    • Audio
    • Art
    • Settings
  3. Inside Scripts, create these subfolders:
    • Core
    • Player
    • World
    • Obstacles
    • Collectibles
    • UI
    • Audio
    • Camera
    • Data
  4. Inside Prefabs, create subfolders: Player, Obstacles, Collectibles, UI.
  5. Inside Audio, create subfolders: Music, SFX.
  6. Inside Art, create subfolders: Models, Textures, Animations.
  7. Move your existing GameScene.unity from wherever it is into _Project/Scenes/. (Drag it in the Project window.)
⚠️ Move Files Inside Unity Only

Always move files by dragging them within the Project window. Never use Windows Explorer or macOS Finder to move Unity files. Unity tracks files using .meta files, and moving files externally breaks those references, causing missing asset errors.

Version Control with Git

Before we write a single line of game code, we need to set up version control. This is non-negotiable in production development. Version control tracks every change you make to your project, lets you undo mistakes, and keeps a history of your entire project's evolution.

Why this matters RIGHT NOW

Imagine you spend 3 hours building the player controller. It works great. Then you make a "small change" that breaks everything, and you can't remember what you changed. Without version control, you're stuck. With version control, you type one command and you're back to the working version. Every professional game studio uses version control. No exceptions.

What is Git?

Git is the most widely used version control system in the world. It tracks changes to your files over time. Think of it like an unlimited "undo" system, but much more powerful — you can see exactly what changed, when, and why. You can jump back to any previous state of your project at any time.

Key concepts you need to know:

Step 1: Install Git

  1. Windows: Download Git from git-scm.com. Run the installer. Accept all default options. When asked about the default editor, choose Visual Studio Code if you installed it.
  2. Mac: Open Terminal and type git --version. If Git isn't installed, macOS will prompt you to install it via Xcode Command Line Tools. Click Install.
  3. Linux: Open a terminal and run sudo apt install git (Ubuntu/Debian) or sudo dnf install git (Fedora).
  4. Verify installation: Open a terminal (Command Prompt on Windows, Terminal on Mac/Linux) and type:
    TerminalShell
    git --version

    You should see something like git version 2.43.0. The exact version doesn't matter.

  5. Set your identity (one-time setup). Git attaches your name and email to every commit:
    TerminalShell
    git config --global user.name "Your Name"
    git config --global user.email "your.email@example.com"

Step 2: Create the .gitignore File

Unity generates a LOT of temporary files — compiled code, cached textures, editor layouts, build outputs. We do not want to track these in Git. They can be regenerated automatically, and they change constantly, cluttering your history.

A .gitignore file tells Git which files and folders to ignore. Create this file in the root of your Unity project (the folder that contains Assets/, Packages/, and ProjectSettings/):

  1. Open your code editor (VS Code or Visual Studio).
  2. Create a new file in the root of your Unity project.
  3. Name it exactly .gitignore (with the dot at the start, no file extension).
  4. Paste the following content into it:
.gitignoreGit
# Unity generated folders
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/

# MemoryCaptures
[Mm]emoryCaptures/

# Recordings
[Rr]ecordings/

# Asset meta data should only be ignored when the corresponding asset is also ignored
!/[Aa]ssets/**/*.meta

# Autogenerated Crash Report
sysinfo.txt

# Builds
*.apk
*.aab
*.unitypackage
*.app

# Compiled code
*.dll
*.exe
*.pdb

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# IDE files
.idea/
.vs/
.vscode/
*.csproj
*.sln
*.suo
*.user
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db

# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta

# Gradle (Android builds)
ExportedObj/
.gradle/
build/
*.unitypackage

# Plastic SCM
ignore.conf
*.private.0
*.private
The .gitignore MUST be created before your first commit

If you commit the Library folder and then add the .gitignore later, Git will continue tracking those files. The .gitignore only affects untracked files. Always create it first. If you make this mistake, you'll need to run git rm -r --cached Library/ to untrack it — but let's avoid that by doing it in the right order.

Step 3: Initialize the Repository

Now let's turn your Unity project into a Git repository. Open a terminal and navigate to your project's root folder (the one containing Assets/):

TerminalShell
# Navigate to your project folder
cd "C:/Users/YourName/Unity Projects/InfiniteRunner"

# Initialize Git repository
git init

You should see: Initialized empty Git repository in .../InfiniteRunner/.git/

Step 4: Your First Commit

Let's save the current state of your project. This is the workflow you'll follow every time:

TerminalShell
# 1. See what files Git sees (check that Library/ etc. are NOT listed)
git status

# 2. Stage ALL current files for commit
git add .

# 3. Create your first commit with a descriptive message
git commit -m "Initial project setup with folder structure"

Let's break down those three commands — you'll use them hundreds of times:

Command What It Does Analogy
git status Shows which files have changed since the last commit Checking what's in your shopping cart before checkout
git add . Stages all changed files for the next commit. The . means "everything" Putting items in the shopping cart
git commit -m "..." Creates a snapshot with a message describing what changed Pressing "confirm purchase" — the save is permanent
Writing good commit messages

Your commit message should describe what you did, not what files changed. Git already tracks which files changed. Good messages:

  • "Add player controller with lane switching"
  • "Fix jump height being too low"
  • "Implement object pooling for obstacles"

Bad messages:

  • "updated stuff"
  • "fixed bug"
  • "asdfasdf"

Your future self will thank you when you're searching through history trying to find when something changed.

Step 5: Verify It Worked

TerminalShell
# See your commit history
git log --oneline

You should see something like:

a1b2c3d Initial project setup with folder structure

That's your first commit. Your project is now safely tracked. If anything ever goes wrong, you can always come back to this state.

The Daily Git Workflow

From now on, at the end of every chapter, we'll tell you to save your progress. The workflow is always the same three commands:

The Three Commands You'll MemorizeShell
# 1. Check what changed
git status

# 2. Stage the changes
git add .

# 3. Commit with a message
git commit -m "Your description of what you did"
How often should I commit?

Commit whenever you finish a logical piece of work that compiles and doesn't break anything. At minimum, commit after completing each chapter. In professional development, you'd commit multiple times per day — after adding a feature, fixing a bug, or reaching any stable milestone. Think of it like quicksaving in a game: do it before anything risky.

Useful Git Commands Reference

You only need the three commands above for now. We'll introduce more throughout the course, but here's a quick reference for when you're ready:

Command What It Does
git status See what's changed
git add . Stage all changes
git add filename Stage a specific file only
git commit -m "msg" Save a snapshot
git log --oneline See commit history (compact)
git diff See exactly what lines changed
git checkout -- filename Undo changes to a file (revert to last commit)

We'll introduce branches (for safely experimenting), GitHub (for backup and sharing), and git diff (for reviewing changes) in later chapters as they become relevant. For now, master the three-command workflow: status, add, commit.

Namespaces

A namespace is a way to organize your code into logical groups and prevent naming conflicts. Without namespaces, if you create a class called GameManager and a Unity asset store package also has a class called GameManager, they'll conflict and cause errors. With namespaces, your InfiniteRunner.Core.GameManager is completely separate from any other GameManager.

Scripts/Core/GameManager.csC#
using UnityEngine;

namespace InfiniteRunner.Core
{
    public class GameManager : MonoBehaviour
    {
        // All code for this class goes inside the namespace block
        void Start()
        {
            Debug.Log("GameManager initialized");
        }
    }
}
Scripts/Player/PlayerController.csC#
using UnityEngine;
using InfiniteRunner.Core; // Import the Core namespace to use GameManager

namespace InfiniteRunner.Player
{
    public class PlayerController : MonoBehaviour
    {
        void Start()
        {
            Debug.Log("PlayerController initialized");
        }
    }
}

Our namespace convention mirrors our folder structure:

Folder Namespace
Scripts/Core/ InfiniteRunner.Core
Scripts/Player/ InfiniteRunner.Player
Scripts/World/ InfiniteRunner.World
Scripts/Obstacles/ InfiniteRunner.Obstacles
Scripts/Collectibles/ InfiniteRunner.Collectibles
Scripts/UI/ InfiniteRunner.UI
Scripts/Audio/ InfiniteRunner.Audio
Scripts/Camera/ InfiniteRunner.GameCamera
Scripts/Data/ InfiniteRunner.Data
💡 Why InfiniteRunner.GameCamera Instead of InfiniteRunner.Camera?

Unity already has a class called Camera in the UnityEngine namespace. If we named our namespace InfiniteRunner.Camera, the C# compiler could get confused between our namespace and Unity's Camera class. By using GameCamera, we avoid any ambiguity. This kind of naming conflict is exactly what namespaces are designed to prevent, but sometimes the namespace name itself can cause issues.

Assembly Definitions

Assembly Definitions (asmdef files) tell Unity how to organize your code into separate compilation units. Without them, Unity compiles all of your scripts into a single assembly every time you change any script. With them, Unity only recompiles the assembly that changed.

Why This Matters

Imagine you have 50 scripts. You change one line in Coin.cs. Without assembly definitions, Unity recompiles all 50 scripts. With assembly definitions, Unity only recompiles the Collectibles assembly (maybe 3 scripts) and anything that depends on it. On large projects, this can cut compile times from 10 seconds down to 1–2 seconds. For a small project like ours, the difference is minor, but learning this now sets you up for good habits.

Creating an Assembly Definition

  1. In the Project window, navigate to _Project/Scripts/Core/.
  2. Right-click and choose Create → Assembly Definition.
  3. Name it InfiniteRunner.Core.
  4. Click on the new .asmdef file. In the Inspector, you'll see its settings.
  5. Under "Root Namespace", type InfiniteRunner.Core. This tells your code editor to use this namespace by default for scripts in this folder.

Repeat this process for each script folder. Here's the dependency chain — some assemblies need to reference others:

Assembly DependenciesReference
InfiniteRunner.Core       → (no dependencies - it's the foundation)
InfiniteRunner.Data       → references Core
InfiniteRunner.Player     → references Core, Data
InfiniteRunner.World      → references Core, Data
InfiniteRunner.Obstacles  → references Core, Data, World
InfiniteRunner.Collectibles → references Core, Data
InfiniteRunner.UI         → references Core, Data
InfiniteRunner.Audio      → references Core
InfiniteRunner.GameCamera → references Core, Player

To add a reference: click on an assembly definition file, find the "Assembly Definition References" section in the Inspector, click "+", and drag the referenced assembly definition into the slot.

⚠️ Assembly Definition Gotcha

Once you create an assembly definition in a folder, scripts in that folder can only see scripts in assemblies they explicitly reference. If your PlayerController needs to use GameManager from the Core assembly, the Player assembly definition must list Core as a reference. If you forget this, you'll get "type not found" compile errors. The error message will tell you exactly which type is missing, so you'll know which reference to add.

✅ Don't Worry If This Seems Complex

Assembly definitions are an advanced organizational tool. If they feel overwhelming right now, you can skip them and come back later. Your project will work perfectly without them — compile times will just be slightly slower. We include them here because they're part of production-grade architecture, and it's easier to set them up at the start than to retrofit them later.

ScriptableObjects: Data That Lives as Assets

A ScriptableObject is a special type of class in Unity that stores data as an asset file in your Project window. Instead of hardcoding values like float moveSpeed = 10f; directly in your scripts, you create a ScriptableObject that holds those values. Then you can edit them in the Inspector without touching any code.

Why ScriptableObjects Are Powerful

Creating a ScriptableObject

Let's create a configuration file for our game's core settings:

Scripts/Data/GameConfig.csC#
using UnityEngine;

namespace InfiniteRunner.Data
{
    // The CreateAssetMenu attribute adds an entry to Unity's
    // right-click Create menu, letting you create instances
    // of this ScriptableObject as asset files.
    [CreateAssetMenu(
        fileName = "GameConfig",
        menuName = "Infinite Runner/Game Config",
        order = 0
    )]
    public class GameConfig : ScriptableObject
    {
        [Header("Player Settings")]
        [Tooltip("Base forward speed of the player")]
        [SerializeField] private float baseSpeed = 10f;

        [Tooltip("Horizontal distance between lane centers")]
        [SerializeField] private float laneWidth = 2.5f;

        [Tooltip("How fast the player switches lanes")]
        [SerializeField] private float laneSwitchSpeed = 8f;

        [Tooltip("Force applied when jumping")]
        [SerializeField] private float jumpForce = 10f;

        [Header("World Settings")]
        [Tooltip("Length of each world chunk in units")]
        [SerializeField] private float chunkLength = 40f;

        [Tooltip("Number of chunks to keep ahead of the player")]
        [SerializeField] private int chunksAhead = 3;

        [Tooltip("Number of chunks to keep behind the player")]
        [SerializeField] private int chunksBehind = 1;

        [Header("Difficulty")]
        [Tooltip("How much speed increases per second")]
        [SerializeField] private float speedIncreaseRate = 0.1f;

        [Tooltip("Maximum speed the game can reach")]
        [SerializeField] private float maxSpeed = 30f;

        // Public read-only properties: other scripts can READ
        // these values but cannot CHANGE them.
        public float BaseSpeed => baseSpeed;
        public float LaneWidth => laneWidth;
        public float LaneSwitchSpeed => laneSwitchSpeed;
        public float JumpForce => jumpForce;
        public float ChunkLength => chunkLength;
        public int ChunksAhead => chunksAhead;
        public int ChunksBehind => chunksBehind;
        public float SpeedIncreaseRate => speedIncreaseRate;
        public float MaxSpeed => maxSpeed;
    }
}

Now create an instance of this ScriptableObject in Unity:

  1. Save the script and switch back to Unity. Wait for it to compile.
  2. In the Project window, navigate to _Project/Settings/.
  3. Right-click and choose Create → Infinite Runner → Game Config. (This menu entry was created by our [CreateAssetMenu] attribute.)
  4. Name the asset GameConfig.
  5. Click on it. In the Inspector, you'll see all the settings with their default values, neatly organized under headers. You can edit any value here.
✅ The Power of [Header] and [Tooltip]

Notice the [Header("Player Settings")] and [Tooltip("...")] attributes on our fields. [Header] adds a bold section label in the Inspector, keeping related values grouped together visually. [Tooltip] shows explanatory text when you hover over a field in the Inspector. These small touches make your ScriptableObjects much more user-friendly, especially when you come back to the project after a break and can't remember what each value does.

Using a ScriptableObject in a Script

Any MonoBehaviour script can reference a ScriptableObject. You drag the asset into a serialized field in the Inspector:

Scripts/Player/PlayerController.csC#
using UnityEngine;
using InfiniteRunner.Data;

namespace InfiniteRunner.Player
{
    public class PlayerController : MonoBehaviour
    {
        // Drag the GameConfig asset into this field in the Inspector
        [SerializeField] private GameConfig config;

        private float currentSpeed;

        void Start()
        {
            // Read values from the config asset
            currentSpeed = config.BaseSpeed;
            Debug.Log("Starting speed: " + currentSpeed);
            Debug.Log("Lane width: " + config.LaneWidth);
        }

        void Update()
        {
            // Use config values for movement
            transform.Translate(
                Vector3.forward * currentSpeed * Time.deltaTime
            );
        }
    }
}

How Our Systems Connect

Here's a conceptual overview of how our systems communicate. The key principle is that systems talk through events, not direct references. This keeps them decoupled — you can change or remove any system without breaking others.

System Communication DiagramArchitecture
                    ┌─────────────────┐
                    │   Game Config    │  (ScriptableObject)
                    │  (shared data)   │
                    └────────┬────────┘
                             │ read by all systems
            ┌────────────────┼────────────────┐
            │                │                │
    ┌───────▼───────┐ ┌─────▼─────┐ ┌────────▼────────┐
    │ Game Manager  │ │  Player   │ │ World Generator  │
    │  (state)      │ │Controller │ │  (chunks, pool)  │
    └───────┬───────┘ └─────┬─────┘ └────────┬────────┘
            │               │                │
            │         ┌─────▼─────┐          │
            │         │  Input    │          │
            │         │  System   │          │
            │         └───────────┘          │
            │                                │
    ┌───────▼────────────────────────────────▼───┐
    │              EVENT BUS                      │
    │  (GameStarted, PlayerDied, ScoreChanged,    │
    │   CoinCollected, StateChanged, etc.)        │
    └───┬──────────┬──────────┬──────────┬───────┘
        │          │          │          │
   ┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼──────┐
   │   UI   │ │ Audio  │ │Camera│ │ Scoring  │
   │ System │ │Manager │ │System│ │  System  │
   └────────┘ └────────┘ └──────┘ └──────────┘

Notice that the UI System doesn't know the Player Controller exists. It doesn't need to. It listens for a PlayerDied event and shows the game over screen. If we replaced the player with a spaceship, the UI would work exactly the same — it only cares about events, not where they came from.

The Singleton Pattern

A Singleton is a design pattern that ensures only one instance of a class exists, and provides a global access point to it. In Unity, this is commonly used for manager objects like GameManager and AudioManager — systems where having two copies would cause problems.

Scripts/Core/Singleton.csC#
using UnityEngine;

namespace InfiniteRunner.Core
{
    /// <summary>
    /// Generic Singleton base class for MonoBehaviours.
    /// Inherit from this to make any manager a Singleton.
    /// Usage: public class GameManager : Singleton<GameManager>
    /// </summary>
    public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
    {
        private static T instance;

        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    // Try to find an existing instance in the scene
                    instance = FindFirstObjectByType<T>();

                    if (instance == null)
                    {
                        Debug.LogError(
                            typeof(T).Name + " Singleton not found in scene! "
                            + "Make sure a GameObject with this component exists."
                        );
                    }
                }
                return instance;
            }
        }

        protected virtual void Awake()
        {
            // If an instance already exists and it's not this one, destroy this one
            if (instance != null && instance != this)
            {
                Debug.LogWarning(
                    "Duplicate " + typeof(T).Name + " found. Destroying duplicate."
                );
                Destroy(gameObject);
                return;
            }

            instance = this as T;

            // Optional: survive scene loads
            // DontDestroyOnLoad(gameObject);
        }

        protected virtual void OnDestroy()
        {
            if (instance == this)
            {
                instance = null;
            }
        }
    }
}

Using the Singleton base class is simple. Any manager that should be a Singleton just inherits from it:

Scripts/Core/GameManager.csC#
using UnityEngine;

namespace InfiniteRunner.Core
{
    public class GameManager : Singleton<GameManager>
    {
        // Awake is called in the base Singleton class,
        // so if we override it, we must call base.Awake()
        protected override void Awake()
        {
            base.Awake(); // IMPORTANT: let Singleton do its setup
            Debug.Log("GameManager is ready");
        }

        public void StartGame()
        {
            Debug.Log("Game started!");
        }
    }

    // Any script can now access it globally:
    // GameManager.Instance.StartGame();
}

When to Use Singletons

⚠️ Singletons Are Controversial

Many experienced developers dislike Singletons because they create hidden global state, make testing harder, and can lead to tight coupling. These criticisms are valid. However, for a game of this scope, Singletons for core managers are a pragmatic choice. Our event-driven architecture mitigates most of the downsides — other systems communicate through events, not by directly calling GameManager.Instance.DoSomething(). We'll keep Singletons to a minimum: only GameManager, AudioManager, and EventBus.

SOLID Principles for Game Development

SOLID is a set of five design principles from software engineering. They sound academic, but they solve very real problems. Here's each one explained in practical game dev terms:

S — Single Responsibility Principle

"Every class should have one reason to change."

Don't put player movement, scoring, and UI logic in the same script. If you need to change how scoring works, you shouldn't have to open the player movement file. Each script does one thing and does it well.

O — Open/Closed Principle

"Open for extension, closed for modification."

When you add a new obstacle type, you shouldn't have to modify existing obstacle code. Instead, you create a new class that extends the base obstacle. In our project:

L — Liskov Substitution Principle

"Child classes should work anywhere the parent class is expected."

If your world generator works with ObstacleBase, it should be able to use any subclass (BarrierObstacle, SpikeObstacle) without knowing or caring which specific type it is.

I — Interface Segregation Principle

"Don't force classes to implement methods they don't use."

If you have an ICollectable interface, it should only require methods related to collecting (Collect(), GetValue()). Don't add Move() or Animate() to the same interface — not all collectibles need those.

D — Dependency Inversion Principle

"Depend on abstractions, not concrete implementations."

Our UI system listens for events (an abstraction), not for specific methods on the GameManager (a concrete implementation). If we replaced GameManager with a completely different state system, the UI would still work because it depends on events, not on GameManager directly.

💡 Don't Over-Apply These Principles

SOLID principles are guidelines, not laws. In game development, pragmatism wins over purity. If following a principle makes your code harder to understand for no practical benefit, skip it. For our project, we'll apply these principles where they clearly help (like using a base class for obstacles) and keep things simple where they don't (like not creating an interface for something only one class implements). The goal is readable, maintainable code — not textbook-perfect architecture.

Putting It All Together

Let's verify our setup by creating the core files we'll build on in the next chapters. Create these three scripts in the specified locations:

Scripts/Core/GameEvents.csC#
namespace InfiniteRunner.Core
{
    /// <summary>
    /// Defines all game events as structs.
    /// Structs are lightweight, no-allocation event payloads.
    /// We'll populate this as we build each system.
    /// </summary>
    public struct GameStartedEvent { }

    public struct GameOverEvent
    {
        public int FinalScore;
        public float DistanceTraveled;
    }

    public struct GamePausedEvent
    {
        public bool IsPaused;
    }

    public struct ScoreChangedEvent
    {
        public int NewScore;
    }
}

This file defines our event types as lightweight structs. We'll add more events as we build each system. The key insight is that events are just data — they describe what happened, not how to respond to it. The response is up to whoever is listening.

Chapter Summary

We've laid the architectural foundation for our entire project. Here's what we accomplished:

This might feel like we haven't built "the game" yet. That's true — and that's intentional. This architecture is the skeleton. Starting in the next chapter, we'll flesh it out rapidly. The Game Manager comes first, followed by the Event System, and from there, each chapter adds a complete, working system. Because we invested in architecture now, adding each system will be clean and straightforward.

Save Your Progress

You've finished the architecture chapter. Let's commit everything we've set up. Open your terminal, navigate to your project folder, and run:

git status
git add .
git commit -m "Set up project architecture, folder structure, and core scripts"

Run git log --oneline to see your commits so far. You should see two entries now: your initial setup commit and this one. Get used to this workflow — you'll do it at the end of every chapter from here on out.

Let's build the Game Manager.