TRAIL BLAZER QUEST: ‘Procedural Generation’ - Solutions

Quest: Trail Blazer Quest
Challenge: Procedural Generation

Feel free to share your solutions, ideas and creations below. If you get stuck, you can find some ideas here for completing this challenge.

This was me tackling the challenge of Procedural Generation for the first time.
Since then, and after doing a few more quests, i’m getting a lot more comfortable with this concept, and have been making lots of things procedurally generate :smiley:
But this was my path as i figured it out for the first time on this challenge.

3 Likes

Finally got this working. My first step was to trash all the assets imported as part of the package (I really hate disorganization in my file structure) and create new assets with descriptive names. Once I had a few assets created I set to work creating a few block patterns to use with the generator script only to find out there was some issue with the player mover. I tracked that down, turned out I needed to create a Ground layer and assign my blocks to it, and set about writing my first procedural generator.

My final script

PlatformGenerator.cs

public class PlatformGenerator : MonoBehaviour
{
    [SerializeField] Transform myCamera;
    [SerializeField] float verticalShift;
    [SerializeField] float horizontalShift;
    [SerializeField] float generationPosition;
    [SerializeField] float maxElements;

    int elementCount = 0;
    bool generateEnding = false;

    BlockPicker blockPicker;
    Vector3 endPosition;
    string[] platformTypes = new string[] { "Multi", "Single" };


    #region "start routine"
    void Start()
    {
        blockPicker = GetComponent<BlockPicker>();

        GameObject startPlatform = GameObject.Find("Start Position Group");
        if (!startPlatform) CreateStartPosition();
        else
        {
            CountElements();
            endPosition = blockPicker.GetStartElementEndPoint();
        }
    }

    void CreateStartPosition()
    {
        var newBlock = Instantiate(blockPicker.GetStartElement(), new Vector3(), Quaternion.identity) as GameObject;
        newBlock.name = "Start Position Group";
        newBlock.transform.parent = this.transform;
        elementCount++;
        endPosition = blockPicker.GetStartElementEndPoint();

        Debug.Log("current end point" + endPosition);
    }

    void CountElements() { for (int i = 1; i <= this.transform.childCount; i++) elementCount++; }
    #endregion


    #region "Main Loop"
    void Update()
    {
        if (endPosition.x - myCamera.transform.position.x < generationPosition)
        {
            if (elementCount > (maxElements-1)) generateEnding = true;
            else GeneratePlatform();
        }
    }

    void GeneratePlatform()
    {
        string platformType = SelectPlatformType();
        Block generatedBlock = blockPicker.GenerateElement(platformType);

        GameObject newElement = blockPicker.GetDefaultElement();
        if (generateEnding) newElement = blockPicker.GetEndingElement();
        else if (generatedBlock.newBlock) newElement = generatedBlock.newBlock;

        Vector3 targetPosition = CalculateNewPosition();
        Debug.Log("Instantiating at: " + targetPosition);
        
        var newBlock = Instantiate(newElement, targetPosition, Quaternion.identity) as GameObject;
        if (platformType == "Single") newBlock.transform.localEulerAngles = new Vector3(0, 0, Random.Range(-45, 45));
        SetUpNewBlock(newBlock);
        AssignEndPoint(targetPosition, generatedBlock);
    }

    string SelectPlatformType() { return platformTypes[Mathf.RoundToInt(Random.Range(0f, platformTypes.Length-1))]; }

    Vector3 CalculateNewPosition()
    {
        float newXPos = endPosition.x + (Random.value * horizontalShift);
        float newYPos = endPosition.y + (Random.value * (verticalShift * 2)) - verticalShift;
        Debug.Log("Next Start: " + newXPos + "," + newYPos);
        
        return new Vector3(newXPos, newYPos, 0f);
    }

    private void SetUpNewBlock(GameObject block)
    {
        block.transform.parent = this.transform;
        elementCount++;
    }

    void AssignEndPoint(Vector3 startPos, Block newElement)
    {
        if (!newElement.endPoint) endPosition = new Vector3();
        else endPosition = startPos + newElement.endPoint.position;
        Debug.Log("Next end point is: " + endPosition);
    }
    #endregion
}

Block.cs

public struct Block
{
    public GameObject newBlock;
    public Transform endPoint;

    public Block(GameObject element, Vector3 rotation, Transform ending)
    {
        newBlock = element;
        newBlock.transform.localEulerAngles = rotation;
        endPoint = ending;
    }
}

BlockPicker.cs

public class BlockPicker : MonoBehaviour
{
    [SerializeField] GameObject defaultElement;
    [SerializeField] GameObject startElement;
    [SerializeField] GameObject endElement;

    [SerializeField] GameObject[] singleBlockPlatforms;
    [SerializeField] GameObject[] multiBlockPlatforms;

    public GameObject GetDefaultElement() { return defaultElement; }

    public GameObject GetStartElement() { return startElement; }

    public GameObject GetEndingElement() { return endElement; }

    public Vector3 GetStartElementEndPoint() { return FindEndPoint(startElement).position; }

    public Block GenerateElement(string type)
    {
        GameObject prefab;
        if (type == "Single") prefab = SelectSingleBlockElement();
        else if (type == "Multi") prefab = SelectMultiBlockElement();
        else prefab = defaultElement;
        Transform ending = FindEndPoint(prefab);
        return new Block(prefab, new Vector3(), ending);
    }

    GameObject SelectSingleBlockElement()
    {
        return singleBlockPlatforms[Mathf.RoundToInt(Random.Range(0, singleBlockPlatforms.Length - 1))];
    }

    GameObject SelectMultiBlockElement()
    {
        return multiBlockPlatforms[Mathf.RoundToInt(Random.Range(0, multiBlockPlatforms.Length - 1))];
    }

    Transform FindEndPoint(GameObject element)
    {
        return element.GetComponentInChildren<EndPoint>().transform;
    }

I had trouble with Unity refusing to automatically calculate the endpoint of each block so ended up remaking all of my assets so that I would only have 1 End Point Object in each and assigned an empty script (EndPoint.cs) to it in order to allow Unity to easily identify what I wanted it to find.

and after 18 hours of coding (14 of which were spent hunting the same problem) here is a short vid showing the final product.

1 Like

Looks great :slight_smile: nice work.
Like the single blocks mixed in there at random angles.

This challenge was my first time hitting Procedural Generating to. Felt things came together for me, and i’ve used the concepts i learnt from this in so many challenges and games moving forwards.
I hope you feel the same growth too :).

1 Like

So i made it much simple than the others (maybe not the right way but it works for me)
i added empty object on camera and gave him box collider and script.
on script i used onTriggerEnter and check if collider with trigger enter.

the box collider trigger was on empty object that was on the platform.
when trigger enter i generate another platform prefab on camera empty object position.

i used List to arrange all platform chunks and picked random from the list.
here is my simple code:

public class ProceduralGeneratePlatform : MonoBehaviour
{

    public List<GameObject> platforms = new List<GameObject>();
    public Transform generationPoint;

    private void Start()
    {
        Instantiate(platforms[Random.Range(0, platforms.Count)], new Vector3(generationPoint.position.x, generationPoint.position.y, 0), Quaternion.identity);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.GetComponent<BoxCollider>())
        {
          Instantiate(platforms[Random.Range(0, platforms.Count)], new Vector3(generationPoint.position.x, generationPoint.position.y, 0), Quaternion.identity);
        }
    }
}
2 Likes

Not a fan of this code so I might change it later, but it’s working for now

public class PlatformSpawner : MonoBehaviour
{
    [SerializeField] List<GameObject> spawnablePlatforms;
    int lastPlat;

    private void Start()
    {
        InvokeRepeating(nameof(SpawnPlatforms), 5, 2);
        lastPlat = spawnablePlatforms.Count - 1;
    }

    private void SpawnPlatforms()
    {
        GameObject newPlatGO = Instantiate(ReturnRandomPlatform(), ReturnRandomPlatformPosition(), Quaternion.identity);
        spawnablePlatforms.Add(newPlatGO);
        int newIndexForPlat = spawnablePlatforms.Count - 1;
        lastPlat = newIndexForPlat;
    }

    private GameObject ReturnRandomPlatform()
    {
        int randPlatIndex = Random.Range(0, spawnablePlatforms.Count - 1);
        GameObject randPlat = spawnablePlatforms[randPlatIndex];
        return randPlat;
    }

    private Vector3 ReturnRandomPlatformPosition()
    {
        Vector3 newPos = new Vector3(spawnablePlatforms[lastPlat].transform.position.x + 7,
        Random.Range(spawnablePlatforms[lastPlat].transform.position.y - 2.5f, spawnablePlatforms[lastPlat].transform.position.y + 2.5f),
        spawnablePlatforms[lastPlat].transform.position.z);

        return newPos;
    }
}
2 Likes

My game turned out to be pretty fun! You can try it out here:

Enjoyed this one.

1 Like

I figured out that Instanciating new GameObjects over and over again can be taxing. Especially if you just go on endless. I had the Idea of doing something similar to the Chrome Offline Game. An Endless Runner . For the purpose of this tutorial I created 3 different ground prefabs (even tho on the lowest level it’s the same prefab with different a different material) and wrote a small Generator around it that creates a handful of GameObjects during Startup. And then I basicly reiterate through them. Whenever I need a new one I check if I already have one “on Hold” and if so, set it active, move it to positon (and give it the proper parent). If I don’t have one anymore I Instantiate a Handfull at once (might be helpfull for Bullets and other small particles that I need a handfull of at once)

If I no longer need one of the GameObjects I just put them on hold by turning it inactive and returning it to the pool

using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;

public class SpawnPool: MonoBehaviour
{
    [SerializeField] public float distanceToPlayer;
    
    private static Dictionary<string, List<GameObject>> _spawnPool;
    private static Dictionary<string, GameObject> _prefabDict;
    private static Dictionary<string, int> _initialPoolSizes;
    private const int PoolSize = 100;

    private static Dictionary<string, List<GameObject>> _activePool;
    
    private void Start()
    {
        _spawnPool = new Dictionary<string, List<GameObject>>();
        _activePool = new Dictionary<string, List<GameObject>>();
        if (!_activePool.ContainsKey("ground")) _activePool.Add("ground", new List<GameObject>());
        // I start out the scene with a handful of "groundBlue" Ground GameObjects
        _activePool["ground"].AddRange(GameObject.FindGameObjectsWithTag("groundBlue"));
        _prefabDict = new Dictionary<string, GameObject>();
        _initialPoolSizes = new Dictionary<string, int>();
    }

    public void PreSpawnPrefab(GameObject prefab, string type, int amount = PoolSize)
    {
        if (!_spawnPool.ContainsKey(type)) _spawnPool.Add(type, new List<GameObject>());
        if (!_prefabDict.ContainsKey(type)) _prefabDict.Add(type, prefab);
        if (!_initialPoolSizes.ContainsKey(type)) _initialPoolSizes.Add(type, amount);

        for (int i = 0; i < amount; i++)
        {
            GameObject spawnedPrefab = Instantiate(prefab, gameObject.transform, true);
            AddGameObjectBackToPool(spawnedPrefab, type);
        }
        _spawnPool[type].Shuffle();
    }

    private GameObject GetNextItemFromPool(string type)
    {
        if (!_spawnPool.ContainsKey(type)) throw new Exception("Prefab of type "
                                                               + type + " got never initialized via the SpawnPool");
        
        if (_spawnPool[type].Count > 0) return _spawnPool[type].RemoveAndGetRandom();

        PreSpawnPrefab(_prefabDict[type], type,  
            (_initialPoolSizes[type] == 0 ? PoolSize : _initialPoolSizes[type])/ 2);

        return _spawnPool[type].RemoveAndGetRandom();
    }

    public void AddGameObjectBackToPool(GameObject poolItem, string type)
    {
        poolItem.SetActive(false);
        poolItem.transform.SetParent(gameObject.transform);
        _spawnPool[type].Add(poolItem);
        UpdatePool(type, poolItem, false);
    }

    public void SpawnItem(string type, Transform newParent, Vector3? position)
    {
        position ??= Vector3.zero;
        GameObject poolItem = GetNextItemFromPool(type);

        poolItem.transform.SetParent(newParent);
        poolItem.SetActive(true);
        poolItem.transform.position = (Vector3) position;
        UpdatePool(type, poolItem);
    }

    [CanBeNull]
    public GameObject GetFurthestGround(Vector3 position)
    {
        float dist = 0f;
        GameObject furthestGround = null;
        foreach (GameObject groundItem in _activePool["ground"])
        {
            float tmpDist = Vector3.Distance(groundItem.transform.position, position);
            if (tmpDist < dist || tmpDist < distanceToPlayer) continue;
            
            dist = tmpDist;
            furthestGround = groundItem;
        }

        return furthestGround;
    }

    private void UpdatePool(string type, GameObject poolItem, bool add = true)
    {
        if (type.StartsWith("ground")) type = "ground";
        if (!_activePool.ContainsKey(type)) _activePool.Add(type, new List<GameObject>());
        if (add)
        {
            _activePool[type].Add(poolItem);        
        }
        else if (_activePool[type].Contains(poolItem))
        {
            _activePool[type].Remove(poolItem);
        }
    }
}

private void OnTriggerEnter(Collider other)
{
    if (_spawnPool == null) SetupSpawnPool();
    if (!other.gameObject.CompareTag("Player")) return;
    
    _spawnPool.SpawnItem(_gameManager.possibleGroundVariations.GetRandom(), _gameManager.groundParent, _gameManager.newChunkPosition);
    _gameManager.newChunkPosition = new Vector3(_gameManager.newChunkPosition.x + 8,
        _gameManager.newChunkPosition.y,
        _gameManager.newChunkPosition.z);

    GameObject ground = _spawnPool.GetFurthestGround(other.gameObject.transform.position);
    if (ground == null) return;
    
    Debug.Log(ground);
    _spawnPool.AddGameObjectBackToPool(ground, ground.tag);
}

private void SetupSpawnPool()
{
    GameObject spawnPoolManager = GameObject.Find("SpawnPool");
    _spawnPool = spawnPoolManager.GetComponent<SpawnPool>();
}
public static class ListExtension
{
    private static Random rng = new Random();  

    public static void Shuffle<T>(this IList<T> list)  
    {  
        int n = list.Count;  
        while (n > 1) {  
            n--;  
            int k = rng.Next(n);  
            (list[k], list[n]) = (list[n], list[k]);
        }  
    }
    
    public static T RemoveAndGet<T>(this IList<T> list, int index)
    {
        lock(list)
        {
            T value = list[index];
            list.RemoveAt(index);
            return value;
        }
    }
    
    public static T RemoveAndGetRandom<T>(this IList<T> list)
    {
        int n = list.Count;
        int k = rng.Next(n);
        return RemoveAndGet(list, k);
    }
    
    public static T GetRandom<T>(this IList<T> list)
    {
        int n = list.Count;
        int k = rng.Next(n);
        return list[k];
    }
}

Now I just need to figure out a nice way to spawn in the ground at different heights and distances based on difficulty / players abilities (i.e. double Jump and so on) and come up myself with some interesting prefabs to use

1 Like

Well, I used 3 different List. 2 list to store platform prefab and another List to cache in the prefab to destroy it. Used the endpoint to spawn at the designated transform position. Tried it myself its not perfect but it works. Can i reagrd myself as intermediate level what do u think ?



Privacy & Terms