Created
March 16, 2020 02:36
-
-
Save Kleptine/db90f311501cd6bf3f624ed3fbb80d0b to your computer and use it in GitHub Desktop.
A Construction Script is a system to help generate procedural Gameobjects at Edit-Time in a Unity scene. It does /// three main things: /// <para>1. Generates a child gameobject wrapped in a <see cref="HideFlags.DontSave" /> gameobject.</para> /// <para> /// 2. Runs a set of <see cref="IOnConstruct" /> scripts on this generated object, whenever …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Linq; | |
using UnityEngine; | |
#if UNITY_EDITOR | |
using System; | |
using UnityEditor; | |
#endif | |
namespace Global.Scripts.ConstructionScript | |
{ | |
/// <summary> | |
/// A Construction Script is a system to help generate procedural Gameobjects at Edit-Time in a Unity scene. It does | |
/// three main things: | |
/// <para>1. Generates a child gameobject wrapped in a <see cref="HideFlags.DontSave" /> gameobject.</para> | |
/// <para> | |
/// 2. Runs a set of <see cref="IOnConstruct" /> scripts on this generated object, whenever this component's | |
/// gameobject updates in Edit mode. | |
/// </para> | |
/// <para>3. On Standalone build, bakes this game object (after running construct scripts) into the scene file.</para> | |
/// </summary> | |
/// <remarks> | |
/// There are two main types: <see cref="ConstructionScript" /> and <see cref="IOnConstruct" />. You can either | |
/// extend this class, implementing IOnConstruct, or add a IOnConstruct script on the child (within the prefab). Often | |
/// you may want to extend this script to store properties in the scene, but have a separate IOnConstruct script | |
/// running on the prefab to actually modify the gameobject. Having a separate IOnConstruct script allows that script | |
/// to directly reference objects within the prefab. | |
/// </remarks> | |
[ExecuteInEditMode] | |
public class ConstructionScript : MonoBehaviour | |
{ | |
public GameObject Child | |
{ | |
get | |
{ | |
// In standalone, we need to hook up the baked child object, as it's not generated at runtime. | |
#if !UNITY_EDITOR | |
if (child == null) | |
{ | |
child = transform.GetChild(0).GetChild(0).gameObject; | |
} | |
#endif | |
return child; | |
} | |
} | |
/// <summary>The Prefab to use as the base for generation, before running construction scripts.</summary> | |
[Header("Construction Script")] | |
[Tooltip("This prefab will be spawned when the child is regenerated.")] | |
public GameObject ConstructedPrefab; | |
/// <summary> | |
/// Stores a reference to the generated object. This object is not marked "DontSave", because marking it as such | |
/// keeps you from applying prefab changes in the editor. Instead we create an additional "DontSave" gameobject as the | |
/// parent of this object, which correctly lets you apply prefab changes. | |
/// </summary> | |
private GameObject child; | |
// The vast majority of code in this class only acts in the editor. At build time, we bake the generated | |
// objects directly into the scene files. | |
#if UNITY_EDITOR | |
/// <summary> | |
/// The immediate child object of this gameobject. In the editor, it is marked with | |
/// <see cref="HideFlags.DontSave" /> so that it is not serialized into the scene. | |
/// </summary> | |
private GameObject dontSave; | |
/// <summary>The previous value of ToSpawn in the editor.</summary> | |
private GameObject prevToSpawn; | |
private void Awake() | |
{ | |
// When a script is ExecuteInEditMode Awake is called in Editor as soon the component is created | |
// or when the scene is loaded. However, in edit mode Awake, we aren't able to do normal scene | |
// changes like creating gameobjects. Because of this, we have to recreate the prefab async, | |
// doing the actual operation on the next Editor GUI update frame. | |
// However, when Unity is building the player, it reloads all scenes (calling Awake here), | |
// and oddly, normal scene changes *are* allowed, so we're able to recreate the child immediately. | |
// This is lucky, because it allows us to save the generated GameObject into the scene at build time. | |
RecreateChild(!BuildPipeline.isBuildingPlayer); | |
} | |
private void RecreateChild(bool async) | |
{ | |
if (async) | |
{ | |
RunOnceDelayed(RecreateChildImmediate); | |
} | |
else | |
{ | |
RecreateChildImmediate(); | |
} | |
} | |
/// <summary>Re-creates the generated child object. This can only be done on the Unity main thread.</summary> | |
/// <remarks> | |
/// There are certain phases of the Unity editor (OnValidate, [ExecuteInEditMode]Awake) that do not allow scene | |
/// modifications. We can't run this function during those phases. | |
/// </remarks> | |
private void RecreateChildImmediate() | |
{ | |
if (dontSave != null) | |
{ | |
DestroyImmediate(dontSave); | |
} | |
dontSave = new GameObject("(Generated)"); | |
dontSave.transform.parent = transform; | |
dontSave.transform.localPosition = Vector3.zero; | |
dontSave.transform.localRotation = Quaternion.identity; | |
dontSave.transform.localScale = Vector3.one; | |
// Don't save the object that's created, in the editor. | |
// The only time we save the object is during the standalone build | |
// process, such that the constructed object is packaged into the scene file of the build. | |
if (!BuildPipeline.isBuildingPlayer) | |
{ | |
dontSave.hideFlags = HideFlags.DontSave; | |
} | |
if (ConstructedPrefab != null) | |
{ | |
// This should only happen in the editor, when creating a new component of this type. | |
child = (GameObject) PrefabUtility.InstantiatePrefab(ConstructedPrefab, dontSave.transform); | |
} | |
else | |
{ | |
if (BuildPipeline.isBuildingPlayer) | |
{ | |
child = new GameObject(gameObject.name); | |
child.transform.parent = dontSave.transform; | |
} | |
else | |
{ | |
child = new GameObject(gameObject.name); | |
child.transform.parent = dontSave.transform; | |
} | |
} | |
RunConstruction(); | |
} | |
/// <summary>Called when the inspector is modified.</summary> | |
private void OnValidate() | |
{ | |
if (ConstructedPrefab != prevToSpawn) | |
{ | |
// Can only modify the scene on the main thread. | |
// OnValidate isn't allowed to run any scene modifications. | |
RecreateChild(true); | |
} | |
prevToSpawn = ConstructedPrefab; | |
} | |
/// <summary>Runs a given action on the next editor gui update.</summary> | |
private void RunOnceDelayed(Action action) | |
{ | |
void OnDelayCall() | |
{ | |
action(); | |
EditorApplication.delayCall -= OnDelayCall; | |
} | |
EditorApplication.delayCall += OnDelayCall; | |
} | |
/// <summary>Runs in the editor whenever this gameobject is modified, such as dragging it or changing its properties.</summary> | |
private void Update() | |
{ | |
if (Application.isPlaying) | |
{ | |
return; | |
} | |
if (child == null) | |
{ | |
return; | |
} | |
// Skip updating the construction if we have the child selected. | |
if (Selection.activeGameObject == null | |
|| !Selection.activeGameObject.transform.IsChildOf(Child.transform) | |
|| Selection.activeGameObject == Child) | |
{ | |
RunConstruction(); | |
} | |
} | |
/// <summary> | |
/// Runs all construction scripts on this object and on the child. While this script controls the lifecycle of the | |
/// generated object, the Construction scripts actually setup the generated object and do the modification. | |
/// </summary> | |
private void RunConstruction() | |
{ | |
// Any scripts on this object or on the child. | |
var constructionScripts = child.GetComponents<IOnConstruct>().Concat(GetComponents<IOnConstruct>()); | |
foreach (IOnConstruct onConstruct in constructionScripts) | |
{ | |
onConstruct.OnConstruct(); | |
} | |
} | |
/// <summary>When this component is destroyed, remove the generated object.</summary> | |
private void OnDestroy() | |
{ | |
if (child != null) | |
{ | |
DestroyImmediate(dontSave); | |
} | |
} | |
#endif | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey there,
I know this is a few years old, but I came across it and thought it seemed interesting. I was wondering if you happened to have to remember what a good use case for this is?
I am trying to think of when I might want something in the scene during build, but not edit time, but nothing specific is coming to mind. Though, I am quite interested in finding out when it might, that I am not thinking of.
Thanks,
-MH