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.
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 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)
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
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.