Tutorial 02

Structuring Your Game Project

The file structure decisions you make on day one determine whether your game is a joy to work on or a nightmare to maintain. Here is the mental model that keeps game projects clean as they grow.

Architecture File Structure Best Practices ~20 min

1The Problem With One Big File

When you start a game, everything in one file feels fine. Two hundred lines of code is easy to navigate. But games grow. Enemy logic gets more complex. You add UI. You add audio. You add a leaderboard. By 800 lines, a single file becomes a liability: scrolling endlessly to find the function you need, accidental variable name collisions, no clear place to put new things.

The solution is not clever engineering. It is simple separation: one file per concern. Each file knows exactly what it is responsible for, and nothing else.

๐Ÿ“
The Single Responsibility Principle

A file should have one reason to change. player.js changes when player movement changes. enemies.js changes when enemy behavior changes. state.js changes when scoring rules change. When you know exactly which file to open for a given problem, you are working with good structure.

2The Reference Structure

Below is the full recommended file structure for a Three.js/Vite microgame. This is not the only valid structure โ€” it is a structure that has proven to work for games of this scale without over-engineering.

my-game/
โ”œโ”€โ”€ index.html โ† canvas, HUD elements, overlay divs
โ”œโ”€โ”€ vite.config.js โ† dev server, build config
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ README.md โ† describe the game; helps on GitHub
โ”œโ”€โ”€ public/
โ”‚ โ””โ”€โ”€ favicon.ico
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ main.js โ† entry point: init + game loop
    โ”œโ”€โ”€ scene.js โ† renderer, camera, lights, floor
    โ”œโ”€โ”€ player.js โ† player mesh, input, movement
    โ”œโ”€โ”€ enemies.js โ† enemy spawn, update, pooling
    โ”œโ”€โ”€ state.js โ† score, lives, timer, game events
    โ”œโ”€โ”€ ui.js โ† HUD update, overlay show/hide
    โ””โ”€โ”€ utils/
        โ””โ”€โ”€ math.js โ† distance, clamp, lerp helpers

3What Each File Does

index.html โ€” The Shell

The HTML file is intentionally thin. Its only job is to provide the DOM structure: a canvas element, HUD divs (score, timer, lives), and an overlay div for start/end screens. All styling goes in a linked CSS file. All logic goes in the JS modules. HTML stays as a dumb skeleton.

htmlindex.html โ€” the important parts
<canvas id="c"></canvas>            <!-- Three.js renders here -->
<div id="hud">
  <span id="score">SCORE: 0</span>
  <span id="timer">60</span>
  <span id="lives">โ™ฅโ™ฅโ™ฅ</span>
</div>
<div id="overlay">               <!-- start / game-over / win screen -->
  <h1>My Game</h1>
  <button id="start-btn">PLAY</button>
</div>
<script type="module" src="src/main.js"></script>

main.js โ€” The Conductor

main.js imports everything and owns the game loop. It does not contain the logic for any system โ€” it just calls the update functions of each system in the correct order. If your main.js is longer than 80 lines, you probably have logic there that belongs somewhere else.

scene.js โ€” The Stage

Renderer, camera, lights, floor, and the resize handler. Created once at startup. Exported so other modules can add objects to the scene. Nothing in scene.js knows about gameplay โ€” it is pure Three.js setup.

player.js โ€” One Actor

The player mesh, its starting position, the key-state tracker, and the updatePlayer() function. It also exports helper state that other modules need: player.position for collision checks, and the setInvincible() function for when enemies score a hit.

enemies.js โ€” A System

The enemy array, the spawnEnemy() factory, and updateEnemies(). This module does not know about the player, the score, or the timer โ€” it only manages enemy meshes. Collision logic (which involves both the player and enemies) lives in state.js, where it can affect the score.

state.js โ€” The Rules

The score, lives, time remaining, and the running flag. This is the source of truth for game state. It also owns checkCollisions() because collisions produce game state changes (score increases, lives decrease). It fires endGame() when conditions are met.

ui.js โ€” The Display

All DOM manipulation lives here: updating the score text, changing the timer color when it turns critical, toggling the overlay visibility, and injecting the win/lose message. Separating this means your game logic never touches the DOM directly โ€” a clean separation that makes both sides easier to change.

Module Dependency Map
main.js scene.js player.js enemies.js state.js ui.js utils/math.js arrows = import dependency

4When to Break the Rules

The structure above is a guideline, not a religion. For a game under 300 lines total, two or three files is probably the right answer. The goal is to reduce cognitive load โ€” the number of things you have to hold in your head at once to make a change. If the structure is adding complexity rather than reducing it, simplify.

The most common mistake is over-engineering: creating an elaborate class hierarchy, an event bus, a component system, a service locator pattern. These are solutions to problems that only exist at a scale you have not reached yet. Start simple. Extract files when a file gets long enough that you have to scroll to find things. That is the right time, and no earlier.

๐Ÿ’ก
The Practical Test

Open any file in your project and ask: can I describe what this file does in one sentence? If the answer is "it handles some game stuff," that file needs to be split. If the answer is "it manages enemy spawning and movement," that is good structure. The ability to answer in one sentence is the measure.