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.

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.