Hard-coding each stage's map layout inside scripts makes the game hard to tweak. ScriptableObject separates data into Unity asset files (.asset) that you can edit right in the Inspector. PlayerPrefs is a simple persistent store that keeps the current stage number even after the app is closed and reopened.
Game Structure
| Script | Role |
|---|---|
StageScriptableObject |
Asset that holds the list of maps for one stage |
MapManager |
Reads the stage number from PlayerPrefs; generates maps from the ScriptableObject |
StageScriptableObject – Full Code
using UnityEngine;
[CreateAssetMenu(fileName = "Stage", menuName = "Stage Objects/Stage", order = 0)]
public class StageScriptableObject : ScriptableObject
{
public Map[] maps; // map prefabs that make up this stage, in order
}
MapManager – Full Code
using UnityEngine;
public class MapManager : MonoBehaviour
{
public static MapManager instance;
public GameObject goalObject;
public StageScriptableObject[] stages;
private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
}
else
{
instance = this;
}
}
public int GetStage()
{
return PlayerPrefs.GetInt("Stage", 1); // default 1 if key absent
}
void Start()
{
CreateStage();
goalObject = GameObject.FindWithTag("Goal");
GetGoalDistance();
}
private void CreateStage()
{
int currentStageIndex = GetStage() % stages.Length; // stay in bounds
StageScriptableObject stage = stages[currentStageIndex];
CreateMap(stage.maps);
}
private void CreateMap(Map[] stageMaps)
{
Vector3 mapPosition = Vector3.zero;
for (int i = 0; i < stageMaps.Length; i++)
{
Map selectedMap = stageMaps[i];
if (i > 0)
{
mapPosition.z += selectedMap.GetComponent<Map>().GetMapSize() / 2f;
}
Map nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, transform);
mapPosition.z += nowMap.GetComponent<Map>().GetMapSize() / 2f;
}
}
public float GetGoalDistance()
{
return goalObject.transform.position.z;
}
}
Key Concept Explanations
1. ScriptableObject – Data Assets
A ScriptableObject is a standalone data container, not a MonoBehaviour. It lives as a project asset file rather than being attached to a GameObject.
| Comparison | MonoBehaviour | ScriptableObject |
|---|---|---|
| Lives on | GameObject (component) | Project asset file |
| Instances | One per scene object | One asset shared everywhere |
| Purpose | Game logic, behavior | Configuration data, balance |
| Scene dependency | Yes | No |
// MonoBehaviour: attached to a GameObject, can have Update/Start
public class GameManager : MonoBehaviour { }
// ScriptableObject: asset file, no Update, data only
public class StageScriptableObject : ScriptableObject
{
public Map[] maps;
}
2. CreateAssetMenu – Registering an Asset Creation Menu
[CreateAssetMenu(fileName = "Stage", menuName = "Stage Objects/Stage", order = 0)]
public class StageScriptableObject : ScriptableObject { }
This single attribute adds an entry to the Unity Editor's asset creation menu:
Unity menu → Assets → Create → Stage Objects → Stage
fileName: default filename when the asset is created (Stage.asset)menuName: menu path (slashes create submenus)order: sort position within the menu
Open the generated .asset file in the Inspector to edit the maps array directly.
3. PlayerPrefs – Persistent Key-Value Storage
// Save
PlayerPrefs.SetInt("Stage", currentStage + 1);
// Load (second argument is the default value when the key is absent)
int stage = PlayerPrefs.GetInt("Stage", 1);
// Delete (useful for testing)
PlayerPrefs.DeleteAll();
PlayerPrefs.DeleteKey("Stage");
PlayerPrefs stores data that survives app restarts using a simple key-value interface.
| Methods | Type |
|---|---|
SetInt / GetInt |
Integer |
SetFloat / GetFloat |
Float |
SetString / GetString |
String |
Storage location (Windows): HKCU\Software\[CompanyName]\[ProductName] registry
Storage location (macOS/Linux): ~/.config/unity3d/[CompanyName]/[ProductName]/
Caveats:
- Do not store passwords or sensitive data (no encryption).
- For complex data structures, use JSON serialized to a file instead.
4. % (Modulo) – Cycling Through Array Indices
int currentStageIndex = GetStage() % stages.Length;
The modulo operator keeps the index within the valid range even when the stage number exceeds the array size:
stages.Length = 5 (Stage0 – Stage4)
Stage 1 → 1 % 5 = 1 → stages[1]
Stage 5 → 5 % 5 = 0 → stages[0] (wraps back to start)
Stage 7 → 7 % 5 = 2 → stages[2]
This lets the game loop back to the first stage automatically when the player finishes the last one.
5. FindWithTag vs Find
// FindWithTag: searches by tag hash (faster)
goalObject = GameObject.FindWithTag("Goal");
// Find: searches by name (slower)
GameObject.Find("GoalObject");
FindWithTag uses a hash lookup internally and is more efficient than name-based Find. Both methods should be called once in Start or Awake and cached. Calling either in Update every frame hurts performance.
6. Stage System Flow
PlayerPrefs.GetInt("Stage", 1) → stage number
↓
stages[number % stages.Length] → StageScriptableObject
↓
stage.maps → Map[] array
↓
CreateMap() → chain map prefabs along Z-axis
↓
FindWithTag("Goal") → cache goal object
When the goal is reached (see DinoController.DoorCheck):
PlayerPrefs.SetInt("Stage", MapManager.instance.GetStage() + 1);
GameManager.instance.StageClear();
After the scene restarts, MapManager.Start reads the new number and builds the next stage's map.
Setting Up Stages in the Unity Editor
- In the Project window, right-click →
Create > Stage Objects > Stageto create an asset. - Name the assets
Stage1,Stage2, … - In the Inspector, assign map prefabs to the
mapsarray in the desired order. - In the
MapManagerInspector, assign the stage assets to thestagesarray in order.
| Item | Setting |
|---|---|
stages array |
Assign Stage0.asset through Stage4.asset in order |
goalObject |
Set automatically by FindWithTag("Goal") in Start |
| Map prefabs | Each map must have a Map component with mapSize.z set correctly |