Motive of this article is to lay a foundation on how to quickly set-up dynamic platform generation for an infinite 2D side scroller. Since it’s a prototype, I’m going solely going to be working with a rectangle. And, I’ll be using Unity engine as it’s really well suited for anything 2D. So, let’s dive in and take a look at some dynamic platform generation.
Check out this Youtube video to out to see this in action.
Getting Ready
We first need a platform with well defined dimensions. I created an extremely simple black rectangle prefab of 10 units wide.
For Obstacles, I created two prefabs: a small red rectangle that player should avoid by jumping and an orange blocking platform that player must slide/duck under.
For player itself, I’m using a very primitive green rectangle.
Everything is elementary, but I’ve employed a small trick when creating slide hazard prefab. The collider and sprite are at a slight offset so that the player can pass under it.
Now that our game objects are ready, you can store them inside a class like GameManager to be accessible when generating platforms.
My camera’s projection is set to orthographic and size to 15. Platforms start spawning from (0, 0, 0). So, I have the player sitting a few units above origin.
Coding IT!
Let’s first define a few constants and data structures for storing platforms. I’m going to call my script LevelGenerator.cs.
private readonly int TILE_DEACTIVATION_DISTANCE = 25; // Deactivate tile if distance to player is less than this
private float abyssLocation; // World X location of last spawned platform
private readonly int BLOCK_PADDING = 150; // Spawn a tile chain if (abyssLocation - playerPos) is less than this
private readonly int BLOCK_CHAIN = 10; // Spaws these number of tiles at once
public static LevelGenerator Instance;
[SerializeField]
private int numberOfLevels = 3; // This represents "floors". Limits how high platforms can be generated
public List<Tile> platforms; // List of all platforms "prefabs" used for spawning. Must be filled in editor
public List<Tile> hazards; // List of all available hazard "prefabs" used for spawning. Must be filled in editor
//object pool of type tiles
private List<Tile> tiles; // A cache of all active tiles(platforms and hazards) for object pooling.
private List<Tile> activeTiles; // Cache of active tiles
private PlayerMovement player;
private int currentLevel = 0;
private int previousLevel;
void Start () {
player = FindObjectOfType<PlayerMovement>();
//platforms = new List<Tile>();
tiles = new List<Tile>();
activeTiles = new List<Tile>();
previousLevel = currentLevel;
//Spawn a set of 10 blocks at the start of the game
InitialBlockSpawn();
}
void InitialBlockSpawn()
{
int BLOCK_CHAIN = 7;
for (int i = 0; i < BLOCK_CHAIN; i++)
{
Tile t = GetTile(0, true);
t.transform.position = new Vector3(abyssLocation, currentLevel * 7, 0);
t.transform.parent = this.transform;
abyssLocation += t.GetLength();
activeTiles.Insert(0, t);
t.Activate();
}
}
If everything is setup correctly, the code should spawn 10 blocks as soon as the game starts. The idea for platform initial generation is as follows:
- “GetTile” gets a tile from the object pool. If it can’t find any suitable objects, it creates a new object and returns it.
- “abyssLocation” is the position in the game beyond which no platforms exist. So, that’s where the player is headed and where the platform should be spawned next. Once we have the platform ready, we increment the “abyssLocation” with our platform’s length so that the next platform can go in that location.
- “currentLevel * 7” lets us spawn platforms at varying heights(Y). If currentLevel is 0, all platforms take the position (X, 0). If currentLevel is 1, they take up (X, 7) and so on. It’s a simple, hacky and definitely not a production quality code. But, it gets the job done for this simple tut.
- Once we have our X and Y, we have a location where we can spawn our platform.
We still haven’t looked at how to spawn tiles and get them from object pools. let’s do that now.
Tile GetTile(int id, bool platform)
{
//Debug.Log("Inside the get tile");
Tile t = null;
t = tiles.Find(x => x.Id == id && x.Platform == platform && !x.gameObject.activeSelf);
if(t == null)
{
GameObject g = Instantiate(platforms[id].gameObject);
t = g.GetComponent<Tile>();
t.Platform = platform;
t.Id = id;
tiles.Add(t);
tiles.Insert(0, t);
//Debug.Log("Tile is.......: " + t);
}
return t;
}
Tile GetHazard(bool hazard)
{
Tile t = null;
int id = Random.Range(0, hazards.Count);
t = tiles.Find(x => x.Id == id && x.Hazard == hazard && !x.gameObject.activeSelf);
if(t == null)
{
GameObject g = Instantiate(hazards[id].gameObject);
t = g.GetComponent<Tile>();
t.Hazard = hazard;
t.Id = id;
tiles.Add(t);
tiles.Insert(0, t);
//Debug.Log("Tile is.......: " + t);
}
return t;
}
void DeactivateTiles()
{
for(int i = 0; i < activeTiles.Count; i++)
{
if (activeTiles[i].transform.position.x - player.transform.position.x < -TILE_DEACTIVATION_DISTANCE)
{
activeTiles[i].gameObject.SetActive(false);
activeTiles.RemoveAt(i);
}
}
}
“GetTile” and “GetHazard” are pretty identical in their functionality. This is the basic idea:
- If we have multiple platforms and hazard prefabs, select one randomly and see if a game object exists in the “tiles” pool with similar parameters.
- If an object matches our requirements and it’s not currently active, return that.
- If no such object exists or is currently active, create a new game object and return that.
We now have all the tools at our feet and only thing left to do is coding the platform generation.
void Update () {
if(Mathf.Abs(player.transform.position.x - abyssLocation) < BLOCK_PADDING)
SpawnBlock();
DeactivateTiles();
}
void SpawnBlock()
{
//Get the block instantiation chain
int chain = Random.Range(1, BLOCK_CHAIN);
for(int i = 0; i < chain; i++)
{
Tile t = GetTile(0, true);
t.transform.position = new Vector3(abyssLocation, currentLevel * 7, 0);
t.transform.parent = this.transform;
abyssLocation += t.GetLength();
activeTiles.Insert(0,t);
t.Activate();
//instantiate random hazard
if (i != 0 && i != chain-1 && abyssLocation > 75)
{
if (Random.Range(0f, 1f) > 0.8f)
{
Tile h = GetHazard(true);
h.transform.position = new Vector3(abyssLocation, currentLevel * 7, 0);
h.transform.parent = this.transform;
activeTiles.Insert(0, h);
h.Activate();
}
}
}
//Generate a gap after a series of blocks
float randomGapVariable = Random.Range(0f, 1f);
if(randomGapVariable < 0.3f)
{
//Generate a gap of a random length
int gapLength = Random.Range(5, 12);
abyssLocation += gapLength;
}
//Change the path level with a certain random value
if(Random.Range(0f, 1f) < 0.15f)
{
if(currentLevel == 0)
{
currentLevel++;
}
else if(currentLevel == numberOfLevels - 1)
{
currentLevel--;
}
else
{
currentLevel = Random.Range(0f, 1f) > 0.5f ? currentLevel+1 : currentLevel-1;
}
}
}
- We check if the player is getting closer to the “abyssLocation” in update loop. If the distance crosses a certain threshold, SpawnBlock is called.
- A platform chain of previously specified number is spawned at once.
- When platform is spawned, there’s a 20% chance to spawn a hazard on it. I hardcoded this as “(Random.Range(0f, 1f) > 0.8f)” , but this can be exposed to editor to tweak around.
- To not make things too monotonous, there are going to be breaks after platform chain. There’s 30% chance for a gap to apprear.
- Finally, there’s ~15% chance to change the level(Y position) of platform spawns.
Here’s a snippet of the block generation in-game.
Hope this taught you something 🙂