How can I try to "DOTSify" my hybrid GameObjects? Ways to optimize MonoBehaviour usage?

(Sorry for the wall of text. It seemed a good idea to give some context.)

Having completed the conversion of my pathfinding and navigation system, I became a huge fan of the data oriented approach so I decided to start my game over from scratch. I was able to get most elements of it converted to pure ECS, but unfortunately, there are a few important aspects that have to remain MonoBehaviours for now (mostly because of PuppetMaster and Final IK).

I am wanting to try and get things as close as possible to the DOTS way of doing things. Since my NPCs use PM and FIK, I currently just have them as normal Prefabs with a GameObjectEntity, then in ECS I have an Entity for each one which has all of my waypoint, movement, navigation, etc and I was just using CopyTransformToGameObject for the moment to test and what not. Everything works as expected, for the most part.

Before I started over, I had some monitor/manager classs attached to each character (which used to handle storing the waypoints, doing the path finding, handle the movement, etc) and one thing that it did was use a fairly large trigger collider to detect if something is in range to potentially unpin the character and cause it to go into ragdoll, if so, it enables the PuppetMaster solvers and what not so that if whatever did come in range happens to come into contact with it, it is ready to react accordingly, otherwise they stay off so that everything isn’t calculating all the time.

This worked well enough before starting down the DOTS path, but now, it just feels… wrong/bad. Each character walking around with a few fairly decent sized classes, each with it’s own update checking a bunch of things constantly for itself only, running timers, checking distances etc. I very much want to try and use a more DOTS-like approach. So that is mostly what my questions pertain to.

What are some of the best ways to utilize the tools currently available in the DOTS packages with GameObjects/MonoBehaviours (if there even are any)? For example, you use IComponentData, DynamicBuffer, etc in ECS, and you can easily use those things with your systems and jobs, but what might be a good approach to use outside of ECS? Since I can’t throw some IComponentDatas on my MonoBehaviour character and have a system/job that is standing by waiting to do some work on them, are there any systems/tools that I can use which might help in trying to make better use of the memory in which these characters are allocated without them being Entities in the ECS system which would allow me to more effeciently iterate over all of the characters in a similar manner instead of having each one do it itself?

Perhaps instead of each character having the full “character monitor” script on it with all of the various data fields, functions, and update processes in it, I just make a small and simple character class (or more than one to split the characters data up more so it’s not one bigger class wasting cache misses on iterations?) that just has data in it and no functions or is just the fact that it is a GameObject/MonoBehaviour to begin with making it so that?

I suppose that each character that spawns could get added to a list on a MonoBehaviour manager, I could then do a GetComponent on each of them for the particular components that I need and iterate over each one of them as I would in an ECS system using the managers single update method, but would that approach even provide any sort of performance gain over just leaving it as it is now and having each one’s update handle it’s own processes?

Any tips or info in regards to these questions would be appreciated!
Thanks,
-MH

  1. You are never going to be able to efficiently iterate over managed data types (classes) like MonoBehaviours.
  2. You can attach a MonoBehaviour to an entity and iterate using Entities.ForEach.

So what I would do is split up all the data that can be stored in IComponentData and IBufferElementData and put those into ECS. Have systems and jobs do as much work on them as possible. Then have one system that queries the MonoBehaviour as well as whatever other ECS data it needs and manually updates the MonoBehaviours. This part will be slower, and there is no way to make it any faster other than to make it as small and minimal as possible.

2 Likes

I need to use on a small number of traditional components (like animator controllers, navmesh obstacles, etc), but I use a hybrid approach. What I do is I link a gameobject with traditional components to an entity. The components of the gameobject that I need to process are added to the entity using EntityManager.AddComponentObject(entity, object); These component objects can be queried and accessed in ComponentSystems. I sync the transform of the gameobject to the entity’s position and rotation (and vice versa).

The bulk majority of the logic for a hybrid is handled in pure ECS land and is jobified. I only ever access the component objects when I need to translate the results of computations to the traditional monobehavior world.

I got this working nicely with subscenes by writing a custom hybrid object pooling system. In a nutshell the system looks for entities with a Hybrid component data. The Hybrid component data contains an NativeString512 which is an address. The hybrid object pooling system uses the Addressables package and this address to instantiate the gameobject associated with the entity (asynchronously) and link up the gameobject’s components to the entity. When the entity is destroyed, the gameobject is returned to an object pool where it can be reused by other entities.

2 Likes

I appreciate the replies. I understand what you guys are getting at, I currently am using AddComponentObject(entity, object); with my vehicles. Each one was pure ECS at first, but since my characters needed to use PhysX because of PuppetMaster and Final IK, I took myvehicles and created a BoxCollider and attached it to the entity vehicles via AddComponentObject and used CopyTransformToGameObject. Then I also have my pedestrian entity, which I create from CreateEntity()

Pedestrian Creation

        private void CreatePedPrefab()
        {
            for (int i = 0; i < prefab.Count; i++)
            {
                var pedEntity = entityManager.CreateEntity(); /*GameObjectConversionUtility.ConvertGameObjectHierarchy(prefab[i], World.Active);*/
                pedEntities.Add(pedEntity);

                entityManager.AddComponentData(pedEntity, new PedestrianEntity
                {
                    entity = pedEntity
                });
                entityManager.AddBuffer<WaypointPathList>(pedEntity);
                entityManager.AddComponentData(pedEntity, new PedestrianTag());
                entityManager.AddComponentData(pedEntity, new MovableTag());
                entityManager.AddComponentData(pedEntity, new WaypointData
                {
                    wpTypeData = new WaypointTypeData
                    {
                        waypointType = WaypointType.Pedestrian,
                        pedWaypointType = waypointType
                    },
                    goalWaypoint = getPathSystem.pedDestWaypoints[0],
                    startWaypoint = getPathSystem.pedDestWaypoints[0],
                    currentWaypoint = getPathSystem.pedDestWaypoints[0],
                    previousWaypoint = getPathSystem.pedDestWaypoints[0]
                });
                entityManager.AddComponentData(pedEntity, new MoveSpeed { speed = PedestrianController.instance.RndSpeed(minspeed, maxspeed) });
                entityManager.AddComponentData(pedEntity, new WPPathStatus { PathStatus = pathStatus, needsPath = 1 });
                entityManager.AddComponentData(pedEntity, new WPGoalStatus { ReachedGoal = goalStatus });
                entityManager.AddComponentData(pedEntity, new WPMoveStatus { movementStatus = moveStatus });
                entityManager.AddComponentData(pedEntity, new SettingData
                {
                    currentIndex = 1,
                    CurrentTurnSpeed = turnSpeed,
                    DistanceFromWaypoint = distanceFromWaypoint
                });

                entityManager.AddComponent(pedEntity, typeof(Rotation));
                entityManager.AddComponent(pedEntity, typeof(Translation));
                entityManager.AddComponent(pedEntity, typeof(LocalToWorld));
                entityManager.AddComponent(pedEntity, typeof(CopyTransformToGameObject));
#if UNITY_EDITOR
                entityManager.SetName(pedEntity, $"Pedestrian_{i}");
#endif
                entityManager.AddComponent(pedEntity, typeof(Prefab));
            }
        }

Then I am using :

        public void SpawnPedestrian()
        {
            for (int i = 0; i < pedEntities.Length; i++)
            {
                NativeArray<Entity> entities = new NativeArray<Entity>(pedEntities.Length, Allocator.TempJob);
                var reference = Instantiate(prefab[i], new Vector3(12.5f, 0.5f, 16.5f), Quaternion.identity, transform);
                entityManager.Instantiate(pedEntities[i], entities);
                entityManager.AddComponentObject(entities[i], reference.transform);
                entities.Dispose();
            }
        }

The pedestrian entity then handles the pathfinding and movement and what not and just copies that to the GameObjects transform for the current moment (I will deal with animating and all that later).

I would very much like to be able to convert everything and be able to use it in ECS systems, but I was doing it this way because PuppetMaster relies entirely on Physx as it is an “active ragdoll” system. I didn’t think I would be able to convert the characters or use AddComponentObject on the PuppetMaster GameObject and retain proper functionality? All colliders and physics for it to function need to remain in the GameObject world.

Unless of course I am just not properly understanding what AddComponentObject’s purpose is? I tried looking up a more detailed explination of what it is, does, and what happens to an Object once it has been added using it, but I could not come up with much more than finding forum posts of people saying “Use AddComponentObject”, then they do, and that’s that.

From what it sounds like though based on what you guys said, I able to use AddComponentObject on all of the PuppetMaster and FinalIK components, and then that keeps their functionality with the GameObject in the world intact, but I can then access it from within an ECS system so that I can create my entire monitor system within ECS systems and just access the specific parts of the PuppetMaster system that I need to control?

Pretty much. That’s effectively what ConvertToEntity with inject gameObject option does. Just don’t destroy the GameObject and everything stays intact.

I was under the impression that if you ran your gameobject through the conversion process, things like rigidbodies, colliders, etc, were converted to use Unity.Physics? I guess I was unclear what it was actually injecting. Until now I was just dealing with pure ECS, a lot seems to have changed since I last tried to do something hybrid. Though, it sounds like it should all hopefully work out though. Thanks for the info, I do appreciate it.

That is one of the first things I am needing to do, as once the character is unpinned and goes to ragdoll, the entity movement would just continue moving as normal, so part of the system I need to make is something of a bidirectional thansform/translate, so that when the ragdoll stumbles, falls, or gets hit/pushed by something to a different location it syncs that back to the entity so that when the character regains its composure the entity can continue driving it along the waypoints. It will be much easier now that I know I can do most of this in a ComponentSystem.

I wasn’t aware that there was a hybrid component data, I though IComponentData was it, and it could not have “hybrid” type data in it? Is there some other type of component data that I am missing out on that I could/should be using in this particular use case?

I was also thinking about using addressables as it sounded like it might be quite useful, but I only simply installed the package and don’t know exactly how or what to do with it yet. I was going to do some research on that. Is there any information or articles that you might have came across or used to help with your system or anything that might be helpful in my trying to learn/implement it? The system you described sounds like a great idea.

Thank you both for the info and clarification. Sounds like things should work out how I am hoping they will.

This is coming from the perspective of a real game, emphasis on what has proven to perform the best and is easy to work with.

Our general approach is first separate data and logic. We had that before ECS, lessons learned even with using monobehaviors.

So what we end up with is data that is clearly ECS only, MB only, or shared. Shared data is always abstracted into a data only container and there aren’t that many of them. We put them into a global dictionary (one per container type), and they are classes not structs. In a MB context you often want to grab the data container and then hold on to it. You don’t want to be doing tons of dictionary lookups to get the latest. In our case we have 3 of these containers, agents representing all types of living things, vehicles, and items.

We also have a notion of who is authoritative for what data. We try to keep it on the ECS side as much as possible. Both sides having partial authority of some type for the same data is not allowed. There needs to be a clear and obvious distinction.

And then we just sync it all in a single job. Scheduled at our sync point, so no synchronization issues. Creation/destruction is also timed so there are no conflicts.

We have a large complex multiplayer game, with a lot of hybrid but zero synchronization of anything on the main thread. That includes network messages.

2 Likes

I think that this does occur; in this case you would probably want to have a separate gameobject that contains your PhysX things and a separate gameobject which is converted to an entity. I haven’t adopted Unity.Physics yet so this does not occur in my project.

There isn’t a hybrid component data included with Entities; I wrote it :).

public struct Hybrid : IComponentData
{
    public NativeString512 address; // use this to fetch prefabs with addressables package

    public BlittableBool addTransform; //blittablebool is a struct that I authored that functions like a bool, but is blittable (it's a byte)

    public BlittableBool addComponents;
}

EntityManager.AddComponentObject() will allow you to add any traditional Component.

I mostly just referenced the addressables package documentation. I will say that something that caught me off guard was that I had to include the UnityEngine.ResourceManagement.AsyncOperations namespace in order to use string based addresses with addressables.

1 Like