Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unity 5: Clean way to manage dynamically created GameObjects

In Unity 5, what is "clean" way to manage dynamically created game objects?

I've written a component (MonoBehavior) that creates/destroys several GameObjects. The objects are loaded as part of custom customization system that selects portions of character - hair/clothes, etc. Meaning they're visible to player, visible in editor, but are not supposed to be editable in editor. The objects being loaded are meshes with skeletons.

The script behaves in this fashion:

  1. Loads GameObjects from Resources (exact object is determined in script, they are not prefabs)
  2. Attaches them to some portion of the scene (not necessarily to its own node)
  3. Deletes when destroyed.

Deletion:

protected void unloadLastResource(){
    if (lastResourceInstance){
        if (Application.isEditor){
            GameObject.DestroyImmediate(lastResourceInstance);
        }
        else
            GameObject.Destroy(lastResourceInstance);
        Resources.UnloadUnusedAssets();
        lastResourceInstance = null;
    }
}

Creation:

    GameObject target = getEffectiveTargetNode();
    Object resource = Resources.Load(newResourceName);
    instance = Instantiate(resource) as GameObject;

    instance.hideFlags = HideFlags.HideAndDontSave;
    instance.transform.parent = target.transform;
    instance.transform.localPosition = Vector3.zero;
    instance.transform.localRotation = Quaternion.identity;
    instance.transform.localScale = Vector3.one;

Destruction handler:

void OnDestroy(){
    unloadLastResource();
}

That seems to work fine in editor, but when I switch from gaming mode back to edito mode, I get a lot of warnings:

Destroying object multiple times. Don't use DestroyImmediate on the same object in OnDisable or OnDestroy.
UnityEngine.Object:DestroyImmediate(Object)

And I get bunch of new object trees (the ones that are supposed to be deleted - trees originate from object that was loaded along with original "resources" and was attached) at the top level of scene hieararchy.

So, how do I cleanly handle dynamically created gameobjects?

I need to know which flags I need to set, and which steps I should do to ensure that object does not "leak" into scene and is properly destroyed when I delete component that created it.

Advice?


Full base class used by "ResourceLoader"

public class BaseResourceLoader : MonoBehaviour {
    public GameObject targetNode = null;

    protected GameObject lastTargetNode{
        get{return lastTargetNodeInternal;}
    }
    private GameObject lastTargetNodeInternal = null;
    protected bool targetNodeChanged(){
        return targetNode != lastTargetNode;
    }
    protected string lastResourceName{
        get{return lastResourceNameInternal;}
    }
    private string lastResourceNameInternal = "";
    //private Object lastResource;
    private GameObject lastResourceInstance;

    protected GameObject getEffectiveTargetNode(){
        if (targetNode == null)
            return this.gameObject;
        return targetNode;
    }

    public void reloadResource(){
        loadNewResource(lastResourceNameInternal, true);
    }

    protected void unloadLastResource(){
        if (lastResourceInstance){
            if (Application.isEditor){
                GameObject.DestroyImmediate(lastResourceInstance);
            }
            else
                GameObject.Destroy(lastResourceInstance);
            Resources.UnloadUnusedAssets();
            lastResourceInstance = null;
        }
        lastResourceNameInternal = "";
    }

    protected void loadNewResource(string newResourceName, bool forceReload){
        if ((newResourceName == lastResourceNameInternal) && !forceReload)
            return;

        GameObject instance = null;
        if (newResourceName != ""){
            GameObject target = getEffectiveTargetNode();
            Object resource = Resources.Load(newResourceName);
            instance = Instantiate(resource) as GameObject;

            instance.hideFlags = HideFlags.HideAndDontSave;
            instance.transform.parent = target.transform;
            instance.transform.localPosition = Vector3.zero;
            instance.transform.localRotation = Quaternion.identity;
            instance.transform.localScale = Vector3.one;
        }

        unloadLastResource ();

        lastResourceInstance = instance;
        lastResourceNameInternal = newResourceName;
        lastTargetNodeInternal = targetNode;
    }

    void OnDestroy(){
        unloadLastResource();
    }
}
like image 597
SigTerm Avatar asked Mar 30 '15 19:03

SigTerm


2 Answers

I've figured it out.

The problem is caused by this line:

instance.hideFlags = HideFlags.HideAndDontSave;

In hierarchy, this flag affects only ONE object and does not affect object's children. As a result, if you set this flag for the root object in hierarchy and scene gets saved, root will be destroyed, but its children will get serialized (meaning they'll appear in inspector upon scene reload) AND you'll get tons of warnings about destroying same object many times.

To deal with the problem, it is necessary to walk through the entire hierarchy and set the flag to all objects in hierarchy. Which fixes the problem and eliminates all errors.

void makeHierarchyHidden(GameObject obj){
    obj.gameObject.hideFlags = HideFlags.HideAndDontSave;
    foreach(Transform child in obj.transform){
        makeHierarchyHidden(child.gameObject);
    }
}

.....
                makeHierarchyHidden (newObject);
.....

It is unclear if this only happens when loading non-prefabs from disk, or just in general case with all hierarchical objects.

like image 190
SigTerm Avatar answered Sep 21 '22 17:09

SigTerm


The error message means that your code would create an infinite recursion, because you are destorying an object from within OnDestroy() which would - again - call OnDestroy() on this object and so forth...

Let me share this piece of code which I use as the base class for all my Behaviors:

using UnityEngine;
using System.Collections.Generic;

namespace de.softfun.drawntogether {
    public class EnhancedBehavior : MonoBehaviour {
        private List<GameObject> linkedObjects = new List<GameObject>();

        protected void destroy(params GameObject[] objects) {
            foreach (GameObject o in objects) {
                try {
                    Destroy(o);
                } catch {
                    continue;
                }
            }
        }

        public void LinkObjects(params GameObject[] objects) {
            foreach (GameObject o in objects) {
                linkedObjects.Add(o);
            }
        }

        void OnDestroy() {
            foreach (GameObject o in linkedObjects) {
                destroy(o);
            }
        }

        protected T instantiate<T>(T prefab, bool addLink = true) where T : Object {
            T o =  (T)Instantiate(prefab);
            if (addLink && (o is GameObject)) {
                linkedObjects.add(o);
            }
            return o;
        }

    }
}

The trick here is that I collect a List of GameObjects which are bound to this MonoBehaviour and should be destroyed if the 'parent' gets destroyed. When using instantiate of this class, the created object gets automatically added to the List, unless addLink is set to false in this method.

Maybe you can use this or something similar.

like image 28
itchee Avatar answered Sep 22 '22 17:09

itchee