Tile / Chunk System
Learn how to break your infinite world into modular, reusable sections called chunks — the fundamental building blocks of procedural world generation.
What Is a "Chunk"?
In an infinite runner, the world stretches out endlessly in front of the player. But you obviously can't build an infinitely long level in the Unity editor — that would require infinite memory and infinite time. Instead, we use a clever trick: we build a handful of short sections of the world, and then stitch them together end-to-end at runtime, recycling old ones as the player passes them. Each of these sections is called a chunk (also sometimes called a "tile" or "segment").
Think of chunks like LEGO bricks. Each brick is a self-contained piece — it has its own ground surface, its own obstacle positions, and its own collectible positions. You snap them together in a line to create a road. When the player runs past a brick, you pick it up from behind them and place it at the front of the line. The player never notices that the world behind them is disappearing, because they're always looking forward.
What's Inside a Chunk?
A typical chunk in our 3-lane infinite runner contains:
- A ground mesh — The flat surface the player runs on. This is usually a simple stretched cube or a plane with a road texture. It spans all three lanes.
- Obstacle spawn points — Empty GameObjects placed at specific positions where obstacles can appear. For example, you might have a spawn point on the left lane at the midpoint of the chunk.
- Collectible spawn points — Empty GameObjects placed where coins or power-ups can appear. These are often along the center of a lane at regular intervals.
- A defined length — How many units long this chunk is (measured along the Z-axis, which is our "forward" direction). This is crucial for knowing where to place the next chunk.
Empty GameObjects are invisible in the game but visible in the editor. They act as position markers — "put an obstacle here" or "put a coin here." When the chunk spawns at runtime, our code reads these positions and decides what to actually place there. This gives us enormous flexibility: the same chunk can have different obstacles every time it appears, because the spawn points are just positions, not specific objects.
Why Modular Chunks?
You might wonder: why not just generate the world procedurally, placing ground tiles and obstacles one at a time? The chunk-based approach has several major advantages:
- Reusability — You design a chunk once and it can appear hundreds of times during a single play session. With just 10-15 chunk variants, you can create a world that feels varied enough that players don't notice the repetition.
- Visual variety — Each chunk can have unique visual features: maybe one chunk has road markings, another has street lamps on the sides, another has a slightly different ground color. This makes the world feel alive without expensive runtime generation.
- Memory efficiency — Because chunks are prefabs, Unity stores them efficiently. At runtime, we only need a handful of active chunks (the ones visible to the player). Everything else is in the object pool, waiting to be reused.
- Design control — You can hand-design the spawn point layouts for each chunk to ensure they're fair and fun. Maybe one chunk is "easy" with spawn points only on the edges, while another is "hard" with spawn points clustered in the center.
- Easy to extend — Want to add a new type of world section? Create a new chunk prefab. No code changes needed. The world generator will pick it up automatically.
For a polished game, aim for 10-20 chunk variants. For our tutorial, we'll start with 3-4 and add more later. Even with just a few chunks, the randomized obstacle and collectible placement makes each playthrough feel different. Remember: the chunks define where things can appear, but the spawner system (Chapter 15) decides what actually appears there.
Designing the Chunk Prefab Structure
Let's establish a clear hierarchy for our chunk prefabs. Every chunk follows the same structure so our code can work with any variant.
Here is the GameObject hierarchy for a single chunk:
Chunk_Straight (parent - has ChunkData script)
|-- Ground (3D mesh - stretched cube or plane)
|-- SpawnPoints
| |-- Obstacle_L_1 (empty GO at left lane, position 1)
| |-- Obstacle_C_1 (empty GO at center lane, position 1)
| |-- Obstacle_R_1 (empty GO at right lane, position 1)
| |-- Obstacle_L_2 (empty GO at left lane, position 2)
| |-- Obstacle_C_2 (empty GO at center lane, position 2)
| |-- Obstacle_R_2 (empty GO at right lane, position 2)
| |-- Coin_L_1 (empty GO at left lane, coin position 1)
| |-- Coin_C_1 (empty GO at center lane, coin position 1)
| |-- Coin_R_1 (empty GO at right lane, coin position 1)
|-- SnapPoint_Start (empty GO at Z = 0, the "back" of the chunk)
|-- SnapPoint_End (empty GO at Z = chunkLength, the "front")Let's break down each part:
- Chunk_Straight — The root GameObject. This is what gets moved around when the chunk is positioned in the world. It holds the
ChunkDatacomponent. - Ground — A visible 3D mesh. For a 3-lane runner, this is typically about 6 units wide (2 units per lane) and 20-30 units long. We'll use a simple cube scaled to (6, 0.1, 20).
- SpawnPoints — An organizer object that contains all the spawn point markers. The naming convention
Obstacle_L_1means "obstacle spawn point, left lane, position 1." This naming helps you keep things organized in the editor. - SnapPoint_Start / SnapPoint_End — These mark the exact start and end positions of the chunk. When we place chunks end-to-end, we align one chunk's
SnapPoint_Endwith the next chunk'sSnapPoint_Start. This ensures seamless connection even if chunks have different lengths.
Make sure your lane positions are consistent across all chunk variants. If the left lane is at X = -2 in one chunk, it must be at X = -2 in every chunk. If they don't match, players will see objects "jump" sideways when transitioning between chunks. We'll define lane positions as constants: Left = -2f, Center = 0f, Right = 2f.
The ChunkData Script
Every chunk prefab needs a script that tells our code what's inside it. The ChunkData script holds references to the chunk's spawn points, its length, and its snap points. This is the script that the World Generator (Chapter 13) reads when it spawns a chunk.
using UnityEngine;
namespace InfiniteRunner.World
{
/// <summary>
/// Holds all metadata for a single world chunk.
/// Attach this to the root GameObject of every chunk prefab.
/// </summary>
public class ChunkData : MonoBehaviour
{
[Header("Chunk Dimensions")]
[Tooltip("The length of this chunk along the Z-axis (forward direction).")]
[SerializeField] private float chunkLength = 20f;
[Header("Snap Points")]
[Tooltip("The transform at the very start (back) of this chunk.")]
[SerializeField] private Transform snapPointStart;
[Tooltip("The transform at the very end (front) of this chunk.")]
[SerializeField] private Transform snapPointEnd;
[Header("Spawn Points")]
[Tooltip("Positions where obstacles can be placed on this chunk.")]
[SerializeField] private Transform[] obstacleSpawnPoints;
[Tooltip("Positions where collectibles (coins, power-ups) can be placed.")]
[SerializeField] private Transform[] collectibleSpawnPoints;
// ---- Public Properties ----
/// <summary>
/// How long this chunk is along the forward (Z) axis.
/// Used by the WorldGenerator to know where to place the next chunk.
/// </summary>
public float ChunkLength => chunkLength;
/// <summary>
/// The world-space position of the back edge of this chunk.
/// </summary>
public Vector3 StartPosition => snapPointStart.position;
/// <summary>
/// The world-space position of the front edge of this chunk.
/// The next chunk's StartPosition should align with this.
/// </summary>
public Vector3 EndPosition => snapPointEnd.position;
/// <summary>
/// All obstacle spawn point transforms on this chunk.
/// The ObstacleSpawner reads these to decide where to place obstacles.
/// </summary>
public Transform[] ObstacleSpawnPoints => obstacleSpawnPoints;
/// <summary>
/// All collectible spawn point transforms on this chunk.
/// The CollectibleSpawner reads these to decide where to place coins.
/// </summary>
public Transform[] CollectibleSpawnPoints => collectibleSpawnPoints;
// ---- Validation ----
private void OnValidate()
{
// Auto-calculate chunk length from snap points if both are assigned
if (snapPointStart != null && snapPointEnd != null)
{
float calculatedLength = snapPointEnd.localPosition.z
- snapPointStart.localPosition.z;
if (calculatedLength > 0f)
{
chunkLength = calculatedLength;
}
}
}
// ---- Editor Visualization ----
private void OnDrawGizmos()
{
// Draw obstacle spawn points in red
if (obstacleSpawnPoints != null)
{
Gizmos.color = Color.red;
foreach (Transform point in obstacleSpawnPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.3f);
Gizmos.DrawLine(point.position,
point.position + Vector3.up * 2f);
}
}
}
// Draw collectible spawn points in yellow
if (collectibleSpawnPoints != null)
{
Gizmos.color = Color.yellow;
foreach (Transform point in collectibleSpawnPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.2f);
}
}
}
// Draw chunk bounds in green
if (snapPointStart != null && snapPointEnd != null)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(snapPointStart.position, snapPointEnd.position);
// Draw start marker
Gizmos.color = Color.cyan;
Gizmos.DrawWireCube(snapPointStart.position,
new Vector3(6f, 0.1f, 0.1f));
// Draw end marker
Gizmos.color = Color.magenta;
Gizmos.DrawWireCube(snapPointEnd.position,
new Vector3(6f, 0.1f, 0.1f));
}
}
}
}Let's walk through what each part does:
[SerializeField] private— We use private fields with[SerializeField]instead of public fields. This is a best practice: the fields appear in the Unity Inspector so you can assign them, but other scripts can only read them through the public properties. This prevents accidental modifications.chunkLength— How long this chunk is. TheOnValidatemethod automatically calculates this from the snap points whenever you change something in the Inspector, so you don't have to set it manually.snapPointStart/snapPointEnd— Transforms marking the back and front edges. When the World Generator places chunks, it positions each new chunk so that its start aligns with the previous chunk's end.obstacleSpawnPoints/collectibleSpawnPoints— Arrays of Transforms that mark where things can appear. You drag the empty GameObjects from the hierarchy into these arrays in the Inspector.OnDrawGizmos— This draws visual markers in the Scene view so you can see spawn points without running the game. Red spheres for obstacles, yellow spheres for collectibles, and colored lines for chunk boundaries. This is incredibly helpful when designing chunks.
OnValidate() is a special Unity method that runs whenever you change a value in the Inspector (and when the script first loads). We use it to automatically calculate the chunk length from the snap point positions. This means if you move a snap point, the length updates instantly — no manual math needed. It only runs in the editor, never in a built game, so there's zero performance cost.
ChunkConfig ScriptableObject
Each chunk variant has properties beyond what's stored in the prefab itself — things like how likely it is to appear, what difficulty level it requires, and a human-readable name. We store these in a ScriptableObject called ChunkConfig.
If you haven't used ScriptableObjects before: they're data containers that live as assets in your project. Unlike MonoBehaviours, they don't need to be attached to a GameObject. Think of them as configuration files that you can edit in the Inspector.
using UnityEngine;
namespace InfiniteRunner.World
{
/// <summary>
/// Configuration data for a chunk variant.
/// Create instances via: Assets > Create > Infinite Runner > Chunk Config
/// </summary>
[CreateAssetMenu(
fileName = "NewChunkConfig",
menuName = "Infinite Runner/Chunk Config",
order = 1
)]
public class ChunkConfig : ScriptableObject
{
[Header("Identity")]
[Tooltip("A human-readable name for this chunk variant.")]
[SerializeField] private string chunkName = "New Chunk";
[Tooltip("The prefab to instantiate for this chunk.")]
[SerializeField] private GameObject chunkPrefab;
[Header("Spawn Settings")]
[Tooltip("How likely this chunk is to be selected. Higher = more common. " +
"A weight of 10 is twice as likely as a weight of 5.")]
[Range(1, 100)]
[SerializeField] private int spawnWeight = 10;
[Tooltip("The minimum difficulty level before this chunk can appear. " +
"0 = available from the start, 5 = only appears later in the game.")]
[Range(0, 10)]
[SerializeField] private int minimumDifficulty = 0;
[Header("Chunk Properties")]
[Tooltip("Maximum number of obstacles that can spawn on this chunk.")]
[Range(0, 10)]
[SerializeField] private int maxObstacles = 3;
[Tooltip("Maximum number of collectibles that can spawn on this chunk.")]
[Range(0, 20)]
[SerializeField] private int maxCollectibles = 8;
[Tooltip("If true, this chunk will never have obstacles " +
"(useful for starting chunks or rest chunks).")]
[SerializeField] private bool isSafeZone = false;
// ---- Public Properties ----
public string ChunkName => chunkName;
public GameObject ChunkPrefab => chunkPrefab;
public int SpawnWeight => spawnWeight;
public int MinimumDifficulty => minimumDifficulty;
public int MaxObstacles => maxObstacles;
public int MaxCollectibles => maxCollectibles;
public bool IsSafeZone => isSafeZone;
// ---- Validation ----
private void OnValidate()
{
if (chunkPrefab != null)
{
// Verify the prefab has a ChunkData component
ChunkData data = chunkPrefab.GetComponent<ChunkData>();
if (data == null)
{
Debug.LogWarning(
$"ChunkConfig '{chunkName}': Assigned prefab " +
$"'{chunkPrefab.name}' is missing a ChunkData component!",
this
);
}
}
}
}
}Key points about this ScriptableObject:
[CreateAssetMenu]— This attribute adds an entry to Unity's Create menu. After writing this script, you can right-click in the Project window and choose Create > Infinite Runner > Chunk Config to make new configuration assets.spawnWeight— Controls how often this chunk appears relative to others. If Chunk A has weight 10 and Chunk B has weight 5, Chunk A will appear roughly twice as often. We'll use this in the World Generator (Chapter 13) for weighted random selection.minimumDifficulty— Prevents hard chunks from appearing at the start of the game. The difficulty system (Chapter 18) increases a difficulty value over time. A chunk withminimumDifficulty = 3won't appear until the difficulty reaches 3.isSafeZone— Marks chunks where no obstacles should spawn. This is useful for the very first chunk (the player needs time to get oriented) and for occasional "rest" chunks that give the player a breather.OnValidate— Checks that the assigned prefab actually has aChunkDatacomponent. If you forget to add it, you'll see a warning in the console immediately rather than a crash at runtime.
ScriptableObjects let designers tweak game parameters without touching code. Once you have the ChunkConfig system set up, adding a new chunk variant is entirely a Unity Editor workflow: create the prefab, create a ChunkConfig asset, fill in the values, and drag the prefab in. Zero lines of code. This is the kind of production workflow that separates hobby projects from professional ones.
Creating Your First Chunk Prefabs
Now let's build actual chunk prefabs in Unity. We'll create four variants: a basic straight chunk, a chunk with lots of spawn points, a narrow passage chunk, and a safe starting chunk.
Step 1: Set Up the Folder Structure
- In the Project window, navigate to
Assets/Prefabs. If this folder doesn't exist, create it. - Inside
Prefabs, create a subfolder calledChunks. - Inside
Assets, create a folder calledScriptableObjects, and inside that, createChunkConfigs.
Your folder structure should look like:
Assets/
Prefabs/
Chunks/
Chunk_Straight.prefab
Chunk_Dense.prefab
Chunk_Narrow.prefab
Chunk_Safe.prefab
ScriptableObjects/
ChunkConfigs/
ChunkConfig_Straight.asset
ChunkConfig_Dense.asset
ChunkConfig_Narrow.asset
ChunkConfig_Safe.asset
Scripts/
World/
ChunkData.cs
ChunkConfig.csStep 2: Build the Basic Straight Chunk
- In the Hierarchy, right-click and choose Create Empty. Name it
Chunk_Straight. Set its position to (0, 0, 0). - Add the
ChunkDatascript toChunk_Straight(Component > Add Component > search for ChunkData). - Right-click
Chunk_Straightand choose 3D Object > Cube. Name itGround. - Select
Groundand set its Transform:- Position: (0, -0.05, 10)
- Scale: (6, 0.1, 20)
- Right-click
Chunk_Straightand choose Create Empty. Name itSpawnPoints. - Inside
SpawnPoints, create the following empty GameObjects with these positions:Obstacle_L_1— Position: (-2, 0, 5)Obstacle_C_1— Position: (0, 0, 5)Obstacle_R_1— Position: (2, 0, 5)Obstacle_L_2— Position: (-2, 0, 15)Obstacle_C_2— Position: (0, 0, 15)Obstacle_R_2— Position: (2, 0, 15)Coin_L_1— Position: (-2, 1, 3)Coin_C_1— Position: (0, 1, 8)Coin_R_1— Position: (2, 1, 13)
- Right-click
Chunk_Straightand create two more empty GameObjects:SnapPoint_Start— Position: (0, 0, 0)SnapPoint_End— Position: (0, 0, 20)
- Select
Chunk_Straightand in the ChunkData component in the Inspector:- Drag
SnapPoint_Startinto the Snap Point Start field. - Drag
SnapPoint_Endinto the Snap Point End field. - Drag the six Obstacle GameObjects into the Obstacle Spawn Points array.
- Drag the three Coin GameObjects into the Collectible Spawn Points array.
- Drag
- Drag
Chunk_Straightfrom the Hierarchy into theAssets/Prefabs/Chunksfolder to create the prefab. - Delete the
Chunk_Straightfrom the scene (the prefab is saved in the Project window).
After creating the prefab, double-click it in the Project window to open it in Prefab Edit Mode. You should see red wire spheres at obstacle spawn points, yellow wire spheres at collectible spawn points, and colored lines marking the chunk boundaries. If you don't see them, make sure Gizmos are enabled (the Gizmos button in the top-right of the Scene view).
Step 3: Build the Dense Chunk
This variant has more spawn points, creating opportunities for tighter obstacle patterns.
- Duplicate the
Chunk_Straightprefab (select it in Project window, Ctrl+D). Rename it toChunk_Dense. - Double-click
Chunk_Denseto open it in Prefab Edit Mode. - Add additional obstacle spawn points inside the SpawnPoints object:
Obstacle_L_3— Position: (-2, 0, 10)Obstacle_C_3— Position: (0, 0, 10)Obstacle_R_3— Position: (2, 0, 10)
- Add more collectible spawn points:
Coin_L_2— Position: (-2, 1, 6)Coin_C_2— Position: (0, 1, 11)Coin_R_2— Position: (2, 1, 16)
- Update the ChunkData component arrays to include the new spawn points.
- Save the prefab (Ctrl+S) and exit Prefab Edit Mode.
Step 4: Build the Narrow Chunk
This variant only has spawn points on the left and right lanes, forcing the player to stay in the center or dodge side-to-side.
- Duplicate
Chunk_Straightagain. Rename itChunk_Narrow. - Open in Prefab Edit Mode. Remove all center-lane obstacle spawn points (delete Obstacle_C_1 and Obstacle_C_2).
- Move the collectible spawn points so they form a line down the center lane, rewarding the player for staying in the middle.
- Update the ChunkData arrays. Save and exit.
Step 5: Build the Safe Chunk
This is used at the very start of a run. No obstacles, just collectibles to teach the player to collect coins.
- Duplicate
Chunk_Straight. Rename itChunk_Safe. - Open in Prefab Edit Mode. Delete ALL obstacle spawn points.
- Leave the collectible spawn points (or add a nice line of coins down the center).
- Clear the Obstacle Spawn Points array in ChunkData (set its size to 0).
- Save and exit.
Tags and Layers for Spawn Points
To help our spawner scripts identify what kind of spawn point they're dealing with, we use Unity's Tag system. Tags are string labels you can attach to any GameObject.
- Go to Edit > Project Settings > Tags and Layers.
- Under Tags, click the + button and add:
ObstacleSpawnCollectibleSpawn
- Under Layers, add a new layer:
SpawnPoint(pick any unused layer number, e.g., layer 8)
- Open each chunk prefab and assign:
- All
Obstacle_*spawn points: Tag =ObstacleSpawn, Layer =SpawnPoint - All
Coin_*spawn points: Tag =CollectibleSpawn, Layer =SpawnPoint
- All
Tags let us distinguish between different types of spawn points (obstacle vs. collectible). The shared SpawnPoint layer lets us find all spawn points quickly using layer masks, and ensures they don't interfere with physics. Spawn point GameObjects have no colliders, but setting them to their own layer is a good organizational practice that prevents accidental raycast hits.
SpawnPoint Helper Script
While tags work fine, a small helper script on each spawn point gives us more type safety and extra configuration per point.
using UnityEngine;
namespace InfiniteRunner.World
{
/// <summary>
/// Defines what kind of object can spawn at this point.
/// </summary>
public enum SpawnPointType
{
Obstacle,
Collectible
}
/// <summary>
/// Identifies which lane this spawn point belongs to.
/// </summary>
public enum Lane
{
Left = -1,
Center = 0,
Right = 1
}
/// <summary>
/// Attach this to every spawn point empty GameObject inside a chunk.
/// Provides type-safe metadata about the spawn point.
/// </summary>
public class SpawnPoint : MonoBehaviour
{
[SerializeField] private SpawnPointType pointType = SpawnPointType.Obstacle;
[SerializeField] private Lane lane = Lane.Center;
[Tooltip("Optional: Override the spawn height. " +
"0 = use default for the object type.")]
[SerializeField] private float spawnHeight = 0f;
public SpawnPointType PointType => pointType;
public Lane Lane => lane;
public float SpawnHeight => spawnHeight;
/// <summary>
/// Returns the world position where an object should be spawned,
/// accounting for the optional height override.
/// </summary>
public Vector3 GetSpawnPosition()
{
Vector3 pos = transform.position;
if (spawnHeight > 0f)
{
pos.y = spawnHeight;
}
return pos;
}
private void OnDrawGizmos()
{
// Color-code by type
Gizmos.color = pointType == SpawnPointType.Obstacle
? Color.red
: Color.yellow;
Gizmos.DrawWireSphere(transform.position, 0.25f);
// Draw a small label showing the lane
#if UNITY_EDITOR
UnityEditor.Handles.Label(
transform.position + Vector3.up * 0.5f,
$"{pointType} ({lane})"
);
#endif
}
}
}This script is optional but recommended. Instead of relying on string-based tags, we now have strongly-typed enums. The spawner code in later chapters can do things like:
// Get all spawn points on a chunk
SpawnPoint[] points = chunk.GetComponentsInChildren<SpawnPoint>();
// Filter to just obstacle points on the left lane
foreach (SpawnPoint point in points)
{
if (point.PointType == SpawnPointType.Obstacle && point.Lane == Lane.Left)
{
// Spawn an obstacle here
SpawnObstacle(point.GetSpawnPosition());
}
}Creating ChunkConfig Assets
Now let's create the ScriptableObject assets that configure each chunk variant.
- In the Project window, navigate to
Assets/ScriptableObjects/ChunkConfigs. - Right-click and choose Create > Infinite Runner > Chunk Config.
- Name it
ChunkConfig_Straight. Select it and fill in:- Chunk Name: "Straight"
- Chunk Prefab: drag in
Chunk_Straightfrom Prefabs/Chunks - Spawn Weight: 10 (most common)
- Minimum Difficulty: 0 (available from start)
- Max Obstacles: 3
- Max Collectibles: 8
- Is Safe Zone: unchecked
- Create
ChunkConfig_Dense:- Chunk Name: "Dense"
- Chunk Prefab:
Chunk_Dense - Spawn Weight: 5 (less common)
- Minimum Difficulty: 2 (appears after difficulty ramps up)
- Max Obstacles: 5
- Max Collectibles: 10
- Is Safe Zone: unchecked
- Create
ChunkConfig_Narrow:- Chunk Name: "Narrow"
- Chunk Prefab:
Chunk_Narrow - Spawn Weight: 3 (rare)
- Minimum Difficulty: 3 (mid-game and later)
- Max Obstacles: 4
- Max Collectibles: 6
- Is Safe Zone: unchecked
- Create
ChunkConfig_Safe:- Chunk Name: "Safe Zone"
- Chunk Prefab:
Chunk_Safe - Spawn Weight: 1 (very rare — occasional rest)
- Minimum Difficulty: 0
- Max Obstacles: 0
- Max Collectibles: 12
- Is Safe Zone: checked
Chunk Registry: Collecting All Configs
The World Generator needs a way to know about all available chunk configs. We'll create a simple registry ScriptableObject that holds an array of all ChunkConfig assets.
using System.Collections.Generic;
using UnityEngine;
namespace InfiniteRunner.World
{
/// <summary>
/// Central registry of all available chunk configurations.
/// The WorldGenerator references this to know what chunks it can spawn.
/// Create via: Assets > Create > Infinite Runner > Chunk Registry
/// </summary>
[CreateAssetMenu(
fileName = "ChunkRegistry",
menuName = "Infinite Runner/Chunk Registry",
order = 2
)]
public class ChunkRegistry : ScriptableObject
{
[Tooltip("All chunk configurations available for world generation.")]
[SerializeField] private ChunkConfig[] chunkConfigs;
/// <summary>
/// Returns all registered chunk configurations.
/// </summary>
public ChunkConfig[] AllChunks => chunkConfigs;
/// <summary>
/// Returns only chunks that are eligible at the given difficulty level.
/// </summary>
/// <param name="currentDifficulty">The current game difficulty (0-10).</param>
/// <returns>List of chunk configs whose minimumDifficulty is met.</returns>
public List<ChunkConfig> GetAvailableChunks(int currentDifficulty)
{
List<ChunkConfig> available = new List<ChunkConfig>();
foreach (ChunkConfig config in chunkConfigs)
{
if (config == null)
{
Debug.LogWarning("ChunkRegistry contains a null entry!", this);
continue;
}
if (config.ChunkPrefab == null)
{
Debug.LogWarning(
$"ChunkConfig '{config.ChunkName}' has no prefab assigned!",
config
);
continue;
}
if (currentDifficulty >= config.MinimumDifficulty)
{
available.Add(config);
}
}
if (available.Count == 0)
{
Debug.LogError(
"No chunks available at difficulty " + currentDifficulty +
"! Returning all chunks as fallback.",
this
);
// Fallback: return everything so the game doesn't break
foreach (ChunkConfig config in chunkConfigs)
{
if (config != null && config.ChunkPrefab != null)
{
available.Add(config);
}
}
}
return available;
}
/// <summary>
/// Returns a random ChunkConfig using weighted selection.
/// Chunks with higher SpawnWeight are more likely to be selected.
/// </summary>
/// <param name="currentDifficulty">Current game difficulty level.</param>
/// <returns>A randomly selected ChunkConfig.</returns>
public ChunkConfig GetWeightedRandomChunk(int currentDifficulty)
{
List<ChunkConfig> available = GetAvailableChunks(currentDifficulty);
if (available.Count == 0)
{
Debug.LogError("No chunks available at all!", this);
return null;
}
// Calculate total weight
int totalWeight = 0;
foreach (ChunkConfig config in available)
{
totalWeight += config.SpawnWeight;
}
// Pick a random value within the total weight
int randomValue = Random.Range(0, totalWeight);
// Walk through the list, subtracting weights until we find our pick
int cumulativeWeight = 0;
foreach (ChunkConfig config in available)
{
cumulativeWeight += config.SpawnWeight;
if (randomValue < cumulativeWeight)
{
return config;
}
}
// Fallback (should never reach here due to math, but safety first)
return available[available.Count - 1];
}
// ---- Validation ----
private void OnValidate()
{
if (chunkConfigs == null || chunkConfigs.Length == 0)
{
Debug.LogWarning(
"ChunkRegistry has no chunks configured!",
this
);
}
}
}
}The ChunkRegistry gives us two key capabilities:
GetAvailableChunks(difficulty)— Filters chunks by the current difficulty level. Early in the game, only easy chunks appear. As difficulty increases, harder variants become available.GetWeightedRandomChunk(difficulty)— Picks a random chunk using weighted selection. A chunk withSpawnWeight = 10is 10 times more likely to be selected than one withSpawnWeight = 1. This is the method the World Generator will call.
Notice how the code checks for null entries, missing prefabs, and empty available lists at every step. In a real project, someone will accidentally leave a field empty or delete a prefab without updating the registry. Defensive coding like this prevents cryptic NullReferenceExceptions and instead gives clear, actionable error messages. Never assume your data is correct — always validate.
Testing Your Chunks in the Editor
Before moving on to the World Generator, let's verify everything works by placing a few chunks manually in the scene.
- Drag
Chunk_Straightfrom the Project window into the Scene. Position it at (0, 0, 0). - Drag another
Chunk_Straightinto the Scene. Position it at (0, 0, 20) — this places it right at the end of the first chunk. - Drag
Chunk_Denseinto the Scene. Position it at (0, 0, 40). - Switch to the Scene view and look at the gizmos. You should see:
- Three flat ground surfaces lined up end-to-end with no gaps.
- Red spheres at obstacle spawn points.
- Yellow spheres at collectible spawn points.
- Cyan and magenta lines at chunk boundaries.
- If there are gaps between chunks, adjust your SnapPoint_End positions so they match the next chunk's SnapPoint_Start.
- Once verified, delete all chunks from the scene. The World Generator will handle spawning at runtime.
What We Built
In this chapter, we created the fundamental building block of our infinite world: the chunk system. Here's what we now have:
- ChunkData.cs — A MonoBehaviour that stores spawn points, chunk length, and snap points on each chunk prefab. Includes editor gizmos for visual debugging.
- ChunkConfig.cs — A ScriptableObject that stores metadata about a chunk variant: spawn weight, difficulty requirement, and obstacle limits.
- SpawnPoint.cs — A helper component on each spawn point with type-safe enums for point type and lane.
- ChunkRegistry.cs — A central ScriptableObject that collects all chunk configs and provides weighted random selection based on difficulty.
- 4 chunk prefabs — Straight, Dense, Narrow, and Safe variants, each with appropriate spawn point layouts.
In the next chapter, we'll build the Object Pooling system that lets us reuse these chunks efficiently instead of creating and destroying them at runtime.
Git Branching: The Production Workflow
Up to now, we've been committing straight to main. That works for solo beginners, but it's not how professional teams work. Starting with this chapter, we'll use branches — the feature that makes Git truly powerful.
What is a Branch?
A branch is a parallel timeline of your project. You create a branch, make changes on it, and your main branch stays completely untouched. If your experiment works, you merge it back into main. If it doesn't, you delete the branch and nothing was harmed.
Think of it like this: main is your "known working" version. Branches are where you build new things. You never break main.
Production Branch Naming Conventions
Professional teams use a prefix/description naming pattern for branches. The prefix tells you what kind of work is happening:
| Prefix | When to Use | Example |
|---|---|---|
feature/ |
Adding new functionality | feature/world-generation |
fix/ |
Fixing a bug | fix/player-falls-through-ground |
hotfix/ |
Urgent fix on a released build | hotfix/crash-on-startup |
refactor/ |
Restructuring code without changing behavior | refactor/pool-manager-cleanup |
experiment/ |
Trying something that might not work | experiment/new-obstacle-type |
Branch names use lowercase with hyphens (not spaces or underscores). Keep them short but descriptive. feature/world-generation is good. feature/adding-the-new-world-generation-system-to-the-game is too long. feature/stuff is too vague.
The Two-Branch Model
For our project, we'll use a simple but effective workflow:
main— Always stable, always works. This is the version you could build and ship at any time. You never commit directly to main (after this chapter).feature/*,fix/*— Short-lived branches where work happens. Created frommain, merged back when done.
Each letter is a commit. Feature branches split off from main, get several commits, then merge back. Main only moves forward when complete, tested features are merged in.
The Branch Workflow (Step by Step)
- Create a branch from main when starting a new feature:
git checkout -b feature/world-generationThis creates the branch AND switches to it. You're now on
feature/world-generation. - Work and commit on the branch. Multiple commits are fine — commit after each logical step:
git add . git commit -m "Add chunk system with modular tile design" # ... more work ... git add . git commit -m "Implement object pooling for chunks" - Check which branch you're on at any time:
git branchThe current branch has an asterisk (*) next to it.
- Merge back to main when the feature is complete and working:
git checkout main git merge feature/world-generationThis brings all your feature commits into
main. - Delete the branch (optional, keeps things clean):
git branch -d feature/world-generation
If you accidentally commit to main instead of your feature branch, don't panic. Git has tools to fix this. But the easiest fix is prevention: always run git branch before you start working to confirm you're on the right branch. Make it a habit.
When to Use Branches in This Course
We'll create a new feature branch for each Part of the course:
| Branch | Chapters | Merge After |
|---|---|---|
feature/world-generation |
Ch 11–14 | Ch 14 (Origin Shifting) |
feature/gameplay |
Ch 15–18 | Ch 18 (Difficulty) |
Direct to main |
Ch 19–25 | Each chapter (polish & ship are small, self-contained changes) |
In professional projects, you'd branch for every individual feature, no matter how small. For learning purposes, we're grouping related chapters to practice the full branch-commit-merge cycle twice before going back to simpler main-only commits.
Let's put this into practice right now. Create your first feature branch and commit the chunk system:
git checkout -b feature/world-generation
git add .
git commit -m "Add chunk system with modular tile design"
Verify you're on the new branch by running git branch. You should see:
main
* feature/world-generation
The asterisk (*) shows your current branch. Everything we commit from here through Chapter 14 stays on this branch, safe from main.