Whats the ideal workflow for Rotation based ECS with Game Objects? Unity 6

My goal is to create a optimized look at camera for all 2D sprites placed in 3D level.

I was thinking of using ECS just for the parent objects transform rotation, while having game objects prefab attached to it with varying components like an animator or movement monobehavior script.

ECS object for rotation
GameObject Prefab
Sprite 2D renderer / character hierarchy
Other game objects and components

Ideally I would be able to edit the 2D sprites in scene view for visually editing of my levels.

I was thinking the ECS object would attached itself as the parent object to my 2D sprite prefab and rotate to face the camera, which would allow my sprite 2D prefab to be animated etc.

How can I achieve this or is there a better method?

If I understand correctly, you’re looking to use Entities just to accelerate some transforms operations. I would advise against doing that. Just stick to GameObjects for everything. Some thoughts:

  • The performance benefits of using Entities are centered around storing and processing data in batches. There is no concept of parenting GameObjects to Entities (not yet, at least), so you probably want to copy each Entity’s LocalToWorld component to the GameObject’s Transform component in every frame.
    • This is less efficient than just doing it the old fashioned way without Entities because the data is stored in multiple places and moved around a lot (first you calculate Entity transforms, and then read them back and apply them to GameObject transforms).
    • How are these sprites moved during gameplay? If your intention is to move them using the classic Transform component, then there’s even more overhead.
  • The workload of rotating a GameObject towards the camera is very lightweight in comparison. If you had more math or gameplay logic going on, maybe it would be worth it to offload some of that to Entities.
    • Entities isn’t very convenient for accelerating small parts of projects (not yet, at least), especially if those parts are supposed to interact with GameObject-based systems. If you’re looking to learn Entities, I think it’s a better idea to make a tiny game using Entities only, instead of layering it on top of a GameObject-based project.
  • This specific workload (calculating new rotation + applying it to the Transform component) can be optimized using IJobParallelForTransform. If you genuinely anticipate this to be a bottleneck, consider using that instead.
  • But also maybe don’t worry about it too much yet? Your most significant performance bottlenecks are likely to end up somewhere else, even assuming a trivial void Update() => transform.LookAt(Camera.main.transform.position); implementation.
  • Finally, with some billboard rendering methods you might not need to rotate any transforms at all.
2 Likes

The whole point of ECS is batching large amounts of things.

If I understand your question, you’re looking to use it to control one camera Transform.

I guess if you wanna do it the Hardest Way Possible™ that is a viable solution, and in this case go right ahead.

Meanwhile, in 2024, hardly anyone is writing camera controllers anymore. Camera stuff is pretty tricky… I hear all the Kool Kids are using Cinemachine from the Unity Package Manager.

There’s even a dedicated Camera / Cinemachine area: see left panel.

If you insist on making your own camera controller, do not fiddle with camera rotation.

The simplest way to do it is to think in terms of two Vector3 points in space:

  1. where the camera is LOCATED
  2. what the camera is LOOKING at
private Vector3 WhereMyCameraIsLocated;
private Vector3 WhatMyCameraIsLookingAt;

void LateUpdate()
{
  cam.transform.position = WhereMyCameraIsLocated;
  cam.transform.LookAt( WhatMyCameraIsLookingAt);
}

Then you just need to update the above two points based on your GameObjects, no need to fiddle with rotations. As long as you move those positions smoothly, the camera will be nice and smooth as well, both positionally and rotationally.

1 Like

Thanks for the detailed reply guys!

This should help paint the picture: My target platform is mobile, im using unity 3D URP, All 2D sprites in my game will be facing the camera, the camera follows the player via my script with lerps. There will be waves of enemies with hundreds of sprites, along with 2D sprites that are static props in world space like trees that need to face the camera.

Shiny-01

I thought there might be a hybrid ECS / DOTS solution or something simple to batch all the sprites rotations and eventually the move to player script. If not maybe i should make a something similar to the Sprites/Default shader that can handle billboarding.

Right now my bottleneck is the script facing the camera, especially when i use mathf clamp for the x axis of the rotation so the object can rotate a little more parallel to the camera but not clip into 3D walls.

My current setup:

I use RegisterLookAtCameraManager on my sprites or the parent object if theres multiple sprites forming a character. This registers themselves to the list to be rotated.
Then i have an instance LookAtCameraManager that uses the list of registered objects to look at camera, and processes them in update.

using UnityEngine;

public class RegisterLookAtCameraManager : MonoBehaviour
{
    private bool isRegistered = false;


    // For dynamically instantiated prefabs
    private void Start()
    {
        RegisterWithManager();
    }

    private void OnEnable()
    {
        RegisterWithManager();
    }

    private void OnDestroy()
    {
        UnregisterWithManager();
    }



    private void RegisterWithManager()
    {
        if (!isRegistered && LookAtCameraManager.Instance != null)
        {
            LookAtCameraManager.Instance.Register(this);
            isRegistered = true;
        }
    }

    private void UnregisterWithManager()
    {
        if (!isRegistered && LookAtCameraManager.Instance != null)
        {
            LookAtCameraManager.Instance.Unregister(this);
            isRegistered = false;
        }
    }
}
using System.Collections.Generic;
using UnityEngine;

public class LookAtCameraManager : MonoBehaviour
{
    public bool isRotatingY = true;
    public float maxAngle = 10f; // Desired angle to cap (in degrees)
    
    private static LookAtCameraManager instance;
    private List<RegisterLookAtCameraManager> objectsToOrient = new List<RegisterLookAtCameraManager>();
    private Camera activeCamera;
    private Vector3 cameraPos;
    private Vector3 prevCameraPos;

    public static LookAtCameraManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject managerObject = new GameObject("LookAtCameraManager");
                instance = managerObject.AddComponent<LookAtCameraManager>();
            }
            return instance;
        }
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }
    }

    private void Update()
    {
        UpdateActiveCamera();

        if (activeCamera == null)
            return;

        OrientObjects();
    }

    private void UpdateActiveCamera()
    {
        if (Camera.main != activeCamera)
        {
            activeCamera = Camera.main;
            prevCameraPos = activeCamera.transform.position;
        }
    }

    private void OrientObjects()
    {
        cameraPos = activeCamera.transform.position;
        if(cameraPos == prevCameraPos)
            return;

        foreach (var obj in objectsToOrient)
        {
            if (obj == null) continue;

            // Get the current position of the object
            Vector3 targetPosition = activeCamera.transform.position;

            // Set the Y position of the target to be the same as the object’s Y (no vertical tilt)
            targetPosition.y = obj.transform.position.y;

            // Make the object look at the target position
            obj.transform.LookAt(targetPosition);

            // Apply the angle limit on the X-axis to prevent unwanted tilt
            //ClampTilt(obj.transform);
        }

        prevCameraPos = cameraPos;
    }

    // Method to clamp the tilt of the object to the max angle allowed on the X-axis
    private void ClampTilt(Transform objTransform)
    {
        // Get the current rotation in euler angles
        Vector3 currentRotation = objTransform.eulerAngles;

        // Clamp the X rotation (forward/backward tilt) within the specified range
        currentRotation.x = Mathf.Clamp(currentRotation.x, -maxAngle, maxAngle);

        // Set the new rotation while keeping Y and Z axis unchanged
        objTransform.eulerAngles = new Vector3(currentRotation.x, currentRotation.y, currentRotation.z);
    }

    public void Register(RegisterLookAtCameraManager obj)
    {
        if (!objectsToOrient.Contains(obj))
        {
            objectsToOrient.Add(obj);
        }
    }

    public void Unregister(RegisterLookAtCameraManager obj)
    {
        if (objectsToOrient.Contains(obj))
        {
            objectsToOrient.Remove(obj);
        }
    }
}

I would like to do this the most performant / easiest way possible ^ ^; Though ECS/Dots and shader code is required for batching this efficiently for mobile. I tried late update for my camera update script, but i didnt like how it made the rotation look choppy.

My camera controller is scripted and working well, i dont think ill use cinemachine.

The player is moved via rb linearVelocity . Since its the only player controlled movement i thought this would be critical for fine tuned movement for the platforming / speedrunning aspect while respecting collisions.

For enemies I figured i would do some simple transform calculations with raycast checks? Then have it batched with ECS / Burst compiler along with the rotation to face camera.

I have successfully tried all solutions, with my script being the best… IJobParallelForTransform turned out to be less efficient like you said lol… And the shader worked though performance was about the same since ive been testing with the Sprites/Default shader… ^ ^;

Unburstable jobs have much bigger overhead until your data set is large enough to outweigh it.

I’ve worked on a ECS game that depends on companion GameObjects for all presenters (Sprites, Particles, Damage Texts). It utilized IJobParallelForTransform to sync entities’ transform to GameObjects. The job is just a breeze even in the busiest time because it was Bursted.

1 Like

Yeah im pretty sure its user error on my end, i had it working but im sure the data wasnt separated properly. i heard using the 2022.3 LTS is easier with ECS because of the more stable documentation. I migrated to it from version 6. So far my monobehaviour scripts are working efficiently. The only real bottle neck is my time taken to optimize everything before getting my game made. The game object workflow is so tangible/visible, but i think it is wise to utilize ECS and lay the groundwork / have optimized workflows for mobile based on the magnitude of data being processed. Im going with the object workflow first then converting those parts to ECS where needed (if i can set it up right). The 2019 unite shooter demo made it look easy ^ ^;