Intro

For my current prototyping ideas, I needed some big scale futuristic buildings - and nothing is more easy to place some big blocks in unity to get a feeling for scale, of course. Nevertheless, taking a step back it is worthwhile thinking about dynamically creating those buildings to save some time and have some flexibility afterwards with them.

What we will be creating

In this article, we will be looking at creating a quite simple building generator, which takes a prefab for a floor and a number of floors to generate a building - you guessed it - consisting of the prefab repeated on the y axis to achieve the goal.

As we need the visuality not only in-game but also in-editor, let's just do that and also visualize the generated building in the editor.
Scene View

Game View

So with low to little effort, this script allows us to create a 50 story building with the same assets as a 5 story building.

Prerequisites

So, lets set everything up: I am using Unity 2021.2.5f1 with Visual Studio 2019, although I do not believe this makes any difference in any other version.

We can start with an empty scene and add an empty game object there. I prefer some structure in my visual tree, though:

Visual Tree

As you can see, I did not start in an empty project, but the script is quite uninvasive. If you're curious around the 'BootStrapper' Prefab instance, I can highly recommend to dig yourself into Zenject and the likes 😉

https://github.com/modesttree/Zenject

On the empty GameObject 'Building Generator' you now create a new Script called 'SimpleBuildingGenerator' and switch to Visual Studio.

Implementation

Now we can start generating the code to achive the goal we have. Which goal? Let's recap:

  • we want to take a prefab floor
  • we need the number of floors
  • let's then instantiate the prefab and stack them all over
  • and don't forget to show it also in edit-time

SimpleBuildingGenerator.cs

Now we edit the 'SimpleBuildingGenerator.cs' Script. I left some open space which I point out with TODO-entries that you need to enhance and fit into your own implementation, e.g. Logging and some other things, which I'll highlight. The reason behind is that I have a fairly complex Dependency Injection set up where we have some dedicated services taking over the responsibility, which is visited another time.

using System;
using UnityEngine;

public class SimpleBuildingGenerator : BaseEditorEnabledBehaviour
{
    #region Properties

    [Tooltip("The prefab for a single floor")]
    public GameObject FloorPrefab;

    [Tooltip("The Height of one floor")]
    public float FloorHeight = 3f;

    [Tooltip("The number of floors to create")]
    [Range(1, 250)]
    public int NumberOfFloors;

    #endregion Properties

    #region Construction

    #endregion Construction

    #region Methods

    #region Awake: the building generation is being executed here
    /// <summary>
    /// the building generation is being executed here
    /// </summary>
    private void Awake()
    {
        this.Generate(this.FloorPrefab, this.FloorHeight, this.NumberOfFloors);
    }
    #endregion Awake

    #region Generate: The building is being created by instantiating the floor Prefab numberOfFloors times
    /// <summary>
    /// The building is being created by instantiating the floor Prefab numberOfFloors times
    /// </summary>
    /// <param name="floorPrefab">the prefab for a floor</param>
    /// <param name="floorHeight">the height of one floor (needs to be greater than zero)</param>
    /// <param name="numberOfFloors">the number of floors to iterate over (needs to be greater than one)</param>
    private void Generate(GameObject floorPrefab, float floorHeight, int numberOfFloors)
    {
        if (floorPrefab == null)
        {
            //floor prefab has to be filled
            //TODO: Log error for the above
        }
        else if (numberOfFloors < 1)
        {
            // number of floors needs to be greater than one
            //TODO: Log error for the above
        }
        else if (floorHeight < 0)
        {
            // floor needs to have a height
            //TODO: Log error for the above
        }
        else
        {
            Vector3 currentSpawnPosition = this.transform.position;  //start at the current position
            for (int currentFloor = 0; currentFloor < this.NumberOfFloors; currentFloor++)  //go from 0 to numberOfFloors-1
            {
                GameObject newFloor = GameObject.Instantiate(floorPrefab, currentSpawnPosition, Quaternion.identity, this.transform);  //instantiate the prefab on the desired position and s child of the current GameObject
                newFloor.name += " - Floor " + currentFloor.ToString("D4");  //add floor name for the visual tree
                currentSpawnPosition += new Vector3(0, floorHeight, 0);  //adding height to the next spawn position
            }
        }
    }
    #endregion Generate

    #region EditorValidateCallback: all children are being destroyed and the building is being regenerated
    /// <summary>
    /// all children are being destroyed and the building is being regenerated
    /// </summary>
    protected override void EditorValidateCallback()
    {
        this.gameObject.DestroyAllChildren(true);  //destroy all children to have no artifacts / leftover floors
        this.Generate(this.FloorPrefab, this.FloorHeight, this.NumberOfFloors);  //restart creation to get potentially new parameters
    }
    #endregion EditorValidateCallback

    #endregion Methods
}

This code will not work out of the box, as we are missing some references:

  • BaseEditorEnabledBehaviour: this base class helps us handling the editor callbacks to link the 'EditorValidateCallback' method
  • GameObject.DestroyAllChildren: this is an extension method I'll explain later on

I will briefly explain what happens in above script.

In both methods 'Awake' and 'EditorValidateCallback' we are calling the 'Generate' method to generate the building. Only difference is, that in 'EditorValidateCallback', we destroy all children before to have a clean set up.

The 'Generate' method then does all of the generation, which is a fair amount of parameter checking (you should always check the input parameters, there are many ways to do that, e.g. with Assertions). When the input parameters are within the expected values, we then can basically iterate over all floors and instantiate a new GameObject from the floor Prefab on the correct position and then give it a nice name. Don't forget to increase the next spawn position then!

GameObjectExtensions.cs

Let's first have a look at the extension method to destroy all children of a GameObject, fairly straightforward:

using System.Linq;
using UnityEngine;

public static class GameObjectExtensions
{
    #region DestroyAllChildren: Destroys all children of the current game object
    /// <summary>
    /// Destroys all children of the current game object
    /// </summary>
    /// <param name="gameObject">the affected game object</param>
    /// <param name="DoImmediate">should the children be destroyed immediately (Editor Scripts) or not (Game Scripts, end of Frame)</param>
    public static void DestroyAllChildren(this GameObject gameObject, bool DoImmediate = false)
    {
        var tempList = gameObject.transform.Cast<Transform>().ToList();  // this is important due to https://answers.unity.com/questions/678069/destroyimmediate-on-children-not-working-correctly.html
        foreach (Transform child in tempList)  //iterate through all child transforms
        {
            if (DoImmediate)  //check, if we should delete immediately or at the end of the frame
            {
                GameObject.DestroyImmediate(child.gameObject, true);  // needed in Editor scripts
            }
            else
            {
                GameObject.Destroy(child.gameObject);  //needed in game scripts
            }
        }
    }
    #endregion DestroyAllChildren
}

Only thing to notice here is that we have a switch 'DoImmediate' to distinguish the behaviour within the editor and within a game environment.

BaseEditorEnabledBehaviour.cs

Now, we can have a look at the base class 'BaseEditorEnabledBehaviour':

using UnityEngine;

/// <summary>
/// base behaviour, enabled to run in edit mode, e.g. for asset generating classes etc
/// </summary>
[ExecuteInEditMode]
public abstract class BaseEditorEnabledBehaviour : MonoBehaviour
{
    #region Methods

    #region OnValidate: invokes the ValidationCallback method after 0.1 seconds as a little delay is needed in the editor
    /// <summary>
    /// invokes the ValidationCallback method after 0.1 seconds as a little delay is needed in the editor
    /// </summary>
    private void OnValidate()
    {
        this.Invoke("ValidationCallback", 0.1f);  //the invoked method may not be called in Validate
    }
    #endregion OnValidate

    #region ValidationCallback: calls the EditorValidateCallback method to be used in child scripts
    /// <summary>
    /// calls the EditorValidateCallback method to be used in child scripts
    /// </summary>
    private void ValidationCallback()
    {
        this.EditorValidateCallback();
    }
    #endregion ValidationCallback

    protected abstract void EditorValidateCallback();

    #endregion Methods
}

After a quick look, only a few things to notice here: when using the Attribute 'ExecuteInEditMode' Unity will - surprise - execute a script in edit mode. To be able to react to property changes in the editor, we need to hijack the 'OnValidate' method, a builtin Unity method. There, life gets a bit tricky as we can not modify GameObjects in this method, so we need to Invoke a seperate method here with a small delay. That method can include also some logging or cleaning up and the class the abstract method 'EditorValidateCallback', which needs to be implemented by child classes anyway.

Results

So now you should see a nice script on your Game Object:

Script Parameters

You need to add a Prefab to be used as one floor of the building, I just used a stretched Unity 3d block:

Floor Prefab

Basically a block, stretched by 10, 3 and 10, and moved up by 1.5 to align to the ground floor.

Visual Tree after Generation

You can see now, that you can easily determine which Game Object is which floor - imho, this knowledge will pay off when we start creating the first bugs. You may notice that the length of the floor number in the Game Object Name is not in sync with the number of floors being able to be entered (four zeros vs. 250 max). One might align this if needed.

Next Steps

As you may have noticed, the visual diversity of the generated buildings is rather lacking. Next parts will focus on

  • how to use different prefabs for different floor heights
  • how to alternate between different prefabs in the same floor region
  • how to use the script to add other elements to the building as well
Category
Tags

No responses yet

Leave a Reply

Your email address will not be published.