ECS Third Person Camera Controller with Collision Example

Hi I have been working on this for two days for my project and I think it be helpful for other people so here is the code. :smile: Its using Hybrid ECS but the Player Component is a IComponentData(Iv seen other examples of hybrid where all components are Monobehaviours, don’t like it).

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms2D;
using UnityEngine;

public struct Player : IComponentData
{
}

public class PlayerComponent : ComponentDataWrapper<Player> { }
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Transforms2D;
using UnityEngine;
using UnityEngine.Experimental.PlayerLoop;

[UpdateAfter(typeof(PreLateUpdate))]
public class CameraSystem : ComponentSystem
{
    public struct Data
    {
        public readonly int Length;
        public GameObjectArray GameObject;
        public ComponentDataArray<Player> Player;
    }

    public class ClipPlanePoints
    {
        public Vector3[] points;
        public float hitDistance;
        public bool didCollide;
    }

    [Inject]private Data data;
    public LayerMask collisionLayers;
    private float sensitivity = 200;
    private float inputX;
    private float inputY;
    public float minRotatonY = -30;
    public float maxRotationY = 50;
    private float defaultDistance = 5;

    protected override void OnUpdate()
    {
        if (data.Length == 0)
            return;

        var dt = Time.deltaTime;
        var transform = Camera.main.transform;
        var target = data.GameObject[0].transform.GetChild(0).transform;
        collisionLayers = ~0;


        //Rotate
        inputX += Input.GetAxisRaw("RightHorizontal") * dt * sensitivity;
        inputY += Input.GetAxisRaw("RightVertical") * dt * sensitivity;
        inputY = Mathf.Clamp(inputY,minRotatonY,maxRotationY);
        transform.eulerAngles = new Vector3(inputY,inputX);
    
        //Move To Default Position
        transform.position = (target.position) - transform.forward * defaultDistance;

        //Collision
        ClipPlanePoints nearClipPlanePoints = GetCameraClipPlanePoints (target);
        DetectCollision(nearClipPlanePoints,target);

        //Move To Position based on collision
        transform.position = (target.position) - transform.forward * ((nearClipPlanePoints.didCollide) ? nearClipPlanePoints.hitDistance : defaultDistance);
    }

    private ClipPlanePoints GetCameraClipPlanePoints(Transform target)
    {
        //Variables
        ClipPlanePoints clipPlanePoints = new ClipPlanePoints();
        Transform transform = Camera.main.transform;

        float length = Camera.main.nearClipPlane;
        float height = Mathf.Tan((Camera.main.fieldOfView) * Mathf.Deg2Rad) * length;
        float width = height * Camera.main.aspect;
        clipPlanePoints.points = new Vector3[5];

        //Get Points
        clipPlanePoints.points[0] = (transform.position + transform.forward * length) + (transform.right * width) - (transform.up * height);
        clipPlanePoints.points[1] = (transform.position + transform.forward * length) - (transform.right * width) - (transform.up * height);
        clipPlanePoints.points[2] = (transform.position + transform.forward * length) + (transform.right * width) + (transform.up * height);
        clipPlanePoints.points[3] = (transform.position + transform.forward * length) - (transform.right * width) + (transform.up * height);
        clipPlanePoints.points[4] = (transform.position + transform.forward * length);
 
        return clipPlanePoints;
    }

    public void DetectCollision(ClipPlanePoints clipPlanePoints, Transform target)
    {
        RaycastHit hit;
        clipPlanePoints.hitDistance = -1f;
        for(int i = 0; i < clipPlanePoints.points.Length; i++)
        {
            if (Physics.Raycast (target.position, (clipPlanePoints.points[i] - target.position), out hit, Vector3.Distance(target.position,clipPlanePoints.points[i]),collisionLayers))
            {
                Debug.DrawLine (target.position, hit.point, Color.red);
                clipPlanePoints.didCollide = true;
                if (clipPlanePoints.hitDistance < 0 || hit.distance < clipPlanePoints.hitDistance)
                    clipPlanePoints.hitDistance = hit.distance;
            }
            else
                Debug.DrawLine (target.position, clipPlanePoints.points[i]);
        }       
    }
}

Edit : Cleaner using statements

using Unity.Entities;
using UnityEngine;
using UnityEngine.Experimental.PlayerLoop;

Edit : Image
https://drive.google.com/file/d/1hq5GBdmYzaDM3ViDX3EEGB-0e3JLm1JR/view?usp=sharing

Haven’t checked how it works exactly, but you could narrow using dependencies, to only 3

using Unity.Entities;
using UnityEngine;
using UnityEngine.Experimental.PlayerLoop;

It seams you very mixed OOP with ECS. Not sure if this is even can be called Hybrid ECS, since you store ClipPlanePoints in class, rather struct.

TwoStickShooter Hybrid show good example of implementation the concept.
Because of that, seams you loose most advantage of ECS.

But hey, if works then works.
I think is good starting point anyway.

1 Like

I used a class because I’m just used to it. My bad. Here is the updated code with it being a struct and only 3 dependencies. I assure you that the rest of my project only uses structs except for one scriptable object class that holds prefabs in my Bootstrap code, and when I need to use Unity’s legacy components such as Transforms, Rigidbodys, and Animator.

Thanks for the feedback.

using Unity.Entities;
using UnityEngine;
using UnityEngine.Experimental.PlayerLoop;

[UpdateAfter(typeof(PreLateUpdate))]
public class CameraSystem : ComponentSystem
{
    public struct Data
    {
        public readonly int Length;
        public ComponentArray<Transform> Transform;
        public ComponentDataArray<Player> Player;
    }

    public struct ClipPlanePoints
    {
        public Vector3[] points;
        public float hitDistance;
        public bool didCollide;
    }

    [Inject]private Data data;
    public LayerMask collisionLayers;
    private float sensitivity = 200;
    private float inputX;
    private float inputY;
    public float minRotatonY = -50;
    public float maxRotationY = 80;
    private float defaultDistance = 5;

    protected override void OnUpdate()
    {
        if (data.Length == 0)
            return;

        var dt = Time.deltaTime;
        var transform = Camera.main.transform;
        var target = data.Transform[0].GetChild(0).transform;
        collisionLayers = ~0;


        //Rotate
        inputX += Input.GetAxisRaw("RightHorizontal") * dt * sensitivity;
        inputY += Input.GetAxisRaw("RightVertical") * dt * sensitivity;
        inputY = Mathf.Clamp(inputY,minRotatonY,maxRotationY);
        transform.eulerAngles = new Vector3(inputY,inputX);
      
        //Move To Default Position
        transform.position = (target.position) - transform.forward * defaultDistance;

        //Collision
        ClipPlanePoints nearClipPlanePoints = GetCameraClipPlanePoints (target);
        DetectCollision(ref nearClipPlanePoints,target);

        //Move To Position based on collision
        transform.position = (target.position) - transform.forward * ((nearClipPlanePoints.didCollide) ? nearClipPlanePoints.hitDistance : defaultDistance);
    }

    private ClipPlanePoints GetCameraClipPlanePoints(Transform target)
    {
        //Variables
        ClipPlanePoints clipPlanePoints = new ClipPlanePoints();
        Transform transform = Camera.main.transform;

        float length = Camera.main.nearClipPlane;
        float height = Mathf.Tan((Camera.main.fieldOfView) * Mathf.Deg2Rad) * length;
        float width = height * Camera.main.aspect;
        clipPlanePoints.points = new Vector3[5];

        //Get Points
        clipPlanePoints.points[0] = (transform.position + transform.forward * length) + (transform.right * width) - (transform.up * height);
        clipPlanePoints.points[1] = (transform.position + transform.forward * length) - (transform.right * width) - (transform.up * height);
        clipPlanePoints.points[2] = (transform.position + transform.forward * length) + (transform.right * width) + (transform.up * height);
        clipPlanePoints.points[3] = (transform.position + transform.forward * length) - (transform.right * width) + (transform.up * height);
        clipPlanePoints.points[4] = (transform.position + transform.forward * length);
   
        return clipPlanePoints;
    }

    public void DetectCollision(ref ClipPlanePoints clipPlanePoints, Transform target)
    {
        RaycastHit hit;
        clipPlanePoints.hitDistance = -1f;
        for(int i = 0; i < clipPlanePoints.points.Length; i++)
        {
            if (Physics.Raycast (target.position, (clipPlanePoints.points[i] - target.position), out hit, Vector3.Distance(target.position,clipPlanePoints.points[i]),collisionLayers))
            {
                Debug.DrawLine (target.position, hit.point, Color.red);
                clipPlanePoints.didCollide = true;
                if (clipPlanePoints.hitDistance < 0 || hit.distance < clipPlanePoints.hitDistance)
                    clipPlanePoints.hitDistance = hit.distance;
            }
            else
                Debug.DrawLine (target.position, clipPlanePoints.points[i]);  
        }         
    }
}

I have to disagree with what Antypodish said. I feel he has mixed up ECS with the job system. While generally used together, they are not the same thing.

There’s nothing wrong with ClipPlanePoints being a class, if you’re not using it in a burst job it doesn’t matter, especially as you have managed types within it anyway. It’s not like classes are obsolete, the sames reasons you used classes before still exist and there are still many uses for managed types within ECS.

What you’ve done is pretty much exactly what the Unity team suggested as the first step for converting old projects onto ECS, and that’s just replacing MonoBehaviour.Update() with ComponentSystem. It is correct to say that you won’t really get many performance benefits for what you’ve done, but it will provide you better laid out code.

You should totally look into taking it further though and seeing if you can jobify everything!

You haven’t done wrong. Just thought about certain ECS pattern for consistency. And as tertle stated

is indeed valid point. I am also new to it, so I can get wrong :wink:

But since converting to structs, I think gives you good exerciser, for further jobbing anything you need.