Best practice for associating Entity with a GameObject?

While I have all these systems, at the end of it all I still need to generate some GameObjects from entities so that they can be represented using traditional Unity stuff that doesn’t exist in DOTS yet.

Right now in my testing I’ve just blindly created these GOs without worrying about maintaining any link back, but that’s wasteful, as if I want to update the GO state I have to destroy everything and rebuild.

What would be better would be to maintain some form of connection, even if its just one way Entity->GO so I know what belongs to what.

At first I thought I could do something like just having a component that stores a string which is the name of a GO, but apparently I can’t use strings as components.

What is the best way to currently do this?

For Entity->GO, you could have a ComponentSystem that uses Entities.ForEach to iterate through a MonoBehaviour called “GameObjectReference” and then call gameObjectReference.gameObject.

For GO->Entity, you can make use of these two scripts:

using UnityEngine;
using System.Collections;
using Unity.Entities;

[RequiresEntityConversion]
public class EntitySharer : MonoBehaviour, IConvertGameObjectToEntity
{
    [SerializeField]
    private EntityStore store;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        store.entity = entity;
    }
}
using UnityEngine;
using Unity.Entities;

public class EntityStore : MonoBehaviour
{
    public Entity entity
    {
        get;
        set;
    }
}

Attach EntitySharer to your GameObject-Entity, and EntityStore to your GameObject. Drag the GameObject to your GameObject-Entity, and after conversion the entity will now be stored in the GameObject’s EntityStore script. Have your other scripts require a RequireComponent(typeof(EntityStore)) and they can use the entity as well

I’m sorry but I don’t understand what you mean. How is a Monobehaviour existing inside a Component System?

I do stuff like this now when I need to interact with gameobjects

public class GameObjectContainer : IComponentData // notice it is a class
{
    public GameObject GameObject;
}

public class SyncGameObject : JobComponentSystem
{
    public override JobHandle OnUpdate(JobHandle handle)
    {
        Entities
        .WithoutBurst()
        .ForEach((GameObjectContainer gameObjectContainer, in Translation translation) =>
        {
            gameObjectContainer.GameObject.transform.position = translation.value;
        })
        .Run();

        return handle;
    }
}

(Quickly written in notepad so probably has a syntax error, especially the lambda)

2 Likes

To enable serialization and deserialization of worlds while keeping track of my Entity<->GameObject relations, I have two classes
For static GameObjects that are created in-editor and never destroyed:

using EverydayEngine;
using KinematicCharacterController;
using System;
using System.Collections.Generic;
using System.Text;
using Unity.Entities;
using UnityEngine;
public static class GUIDGenerator {
    private static readonly Dictionary<string, GameObject> GUIDToObj = new Dictionary<string, GameObject>();
    private static readonly Dictionary<GameObject, string> ObjToGUID = new Dictionary<GameObject, string>();
    public static void GemerateGUID(GameObjectToEntity target) {
        EnsureInit();
        if (target.GUID != null && target.GUID != "") {
            if (GUIDToObj.ContainsKey(target.GUID)) {
                if (GUIDToObj[target.GUID] != target.gameObject || ObjToGUID[target.gameObject] != target.GUID) {
                    Debug.LogError("GameObjectToEntity has GUID but is not mapped. Mapping regenerated", target);
                } else {
                    return;
                }
            }
        }
        string guid = Guid.NewGuid().ToString();
        while (GUIDToObj.ContainsKey(guid)) {
            guid = Guid.NewGuid().ToString();
        }
        GUIDToObj[ObjToGUID[target.gameObject] = guid] = target.gameObject;
        target.GUID = guid;
    }

    private static void EnsureInit() {
        if (GUIDToObj.Count == 0) {
            UnityEngine.Object[] objs = UnityEngine.Object.FindObjectsOfType(typeof(GameObjectToEntity));
            foreach (UnityEngine.Object obj in objs) {
                GameObjectToEntity ote = ((GameObjectToEntity)obj);
                if (ote.GUID != null && ote.GUID != "") {
                    ObjToGUID[GUIDToObj[ote.GUID] = ote.gameObject] = ote.GUID;
                }
            }
            foreach (UnityEngine.Object obj in objs) {
                GameObjectToEntity ote = ((GameObjectToEntity)obj);
                if (ote.GUID == null && ote.GUID == "") {
                    GUIDGenerator.GemerateGUID(ote);
                }
            }
        }
    }

    internal static void Delete(GameObject gameObject) {
        EnsureInit();
        if (ObjToGUID.ContainsKey(gameObject)) {
            GUIDToObj.Remove(ObjToGUID[gameObject]);
            ObjToGUID.Remove(gameObject);
        }
    }

    internal static GameObject GetGameObjectFromGUID(string guid) {
        EnsureInit();
        return GUIDToObj[guid];
    }
}
[ExecuteInEditMode]
public class GameObjectToEntity : MonoBehaviour {
    [ReadOnlyAttribute]
    public string GUID;
    public Entity Entity;
#if UNITY_EDITOR
    public static bool DISABLE_CHECK;
    [HideInInspector]
    public bool justCreated;
#endif

    public GameObjectToEntity() {
#if UNITY_EDITOR
        justCreated = true;
#endif
    }
    private void Awake() {
        if (!UnityEngine.Application.isPlaying) {
            GUIDGenerator.GemerateGUID(this);
        }
    }
    private void Start() {
#if UNITY_EDITOR
        if (UnityEngine.Application.isPlaying) {
            if (justCreated) {
                Debug.LogError("GameObjectToEntity created on runtime. Use RuntimeGameObjectToEntity during runtime instead", gameObject);
            }
#endif
            EntityManager em = World.Active.EntityManager;
            Entity = em.CreateEntity();
#if UNITY_EDITOR
            DISABLE_CHECK = true;
#endif
            em.AddComponentObject(Entity, new EntityToGameObject(gameObject));
#if UNITY_EDITOR
            DISABLE_CHECK = false;
        }
        justCreated = false;
#endif
    }
    private void OnDestroy() {
        GUIDGenerator.Delete(gameObject);
#if UNITY_EDITOR
        if (UnityEngine.Application.isPlaying) {
#endif
            World.Active?.EntityManager.RemoveComponent<EntityToGameObject>(Entity);
#if UNITY_EDITOR
        }
#endif
    }
}

public class EntityToGameObject : Component, AutoSaveComponent {
    public byte[] GenerateData(Entity e) { return Encoding.UTF8.GetBytes(GameObject.GetComponent<GameObjectToEntity>().GUID); }
    public void LoadFromData(byte[] data, EntityRecoverer recover, Entity e) {
        GameObject = GUIDGenerator.GetGameObjectFromGUID(Encoding.UTF8.GetString(data));
        GameObject.GetComponent<GameObjectToEntity>().Entity = e;
        foreach (IReloadCacheOnLoad reloader in GameObject.GetComponents<IReloadCacheOnLoad>()) {
            Save.PostLoadCalls(() => reloader.ReloadCache());
        }
    }
    public GameObject GameObject;

    public EntityToGameObject() {
#if UNITY_EDITOR
        if (!GameObjectToEntity.DISABLE_CHECK && UnityEngine.Application.isPlaying) {
            Debug.LogError("GameObjectToEntity created on runtime. Use RuntimeEntityToGameObject during runtime instead");
        }
#endif
    }
    public EntityToGameObject(GameObject GameObject) : this() {
        this.GameObject = GameObject;
    }
}

public interface IReloadCacheOnLoad {
    void ReloadCache();
}

And for GameObjects created during runtime. In this case, the IComponentData needs to retain all the relevant information to recreate this GameObject from zero after loading it’s data, I’ve made a couple of other types and classes to help with that, but it’s irrelevant for this question

using KinematicCharacterController;
using System;
using System.Collections.Generic;
using System.Text;
using Unity.Entities;
using UnityEngine;
public class RuntimeGameObjectToEntity : GameObjectToEntity {
    private byte init;

    public RuntimeGameObjectToEntity() {
    }
    private void Start() {
        Init();
    }

    public void Init(Entity entity=default) {
        if (init == 0) {
            init = 1;
            EntityManager em = World.Active.EntityManager;
            if (entity == default) {
                Entity = em.CreateEntity();
            } else {
                Entity = entity;
            }
            em.AddComponentObject(Entity, new RuntimeEntityToGameObject(gameObject));
        }
    }

    private void OnDestroy() {
        World.Active.EntityManager.RemoveComponent<RuntimeEntityToGameObject>(Entity);
    }
}

public class RuntimeEntityToGameObject : Component {
    public GameObject GameObject;
    public RuntimeEntityToGameObject() {
    }
    public RuntimeEntityToGameObject(GameObject GameObject) {
        this.GameObject = GameObject;
    }
}

A ComponentSystem can iterate through entities that have a Monobehaviour attached to it. From that Monobehaviour you can access the GameObject that was injected into the entity

Are you still doing that in 1.0? :slight_smile:

Migrating from 0.17, and I used to keep a lookup table between entities and game objects (not using Entities.Graphics) that was populated during conversion, which is now done at editor time, so that’s kind of out of the question here.

Yes that is still the general way to do it.
I explained it in my tutorial/course while talking about animation and hybrid approach.

1 Like