using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Physics; using Unity.Transforms; using Unity.CharacterController; [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(FixedStepSimulationSystemGroup))] [UpdateAfter(typeof(ThirdPersonPlayerVariableStepControlSystem))] [UpdateAfter(typeof(ThirdPersonCharacterVariableUpdateSystem))] [UpdateBefore(typeof(TransformSystemGroup))] [BurstCompile] public partial struct OrbitCameraSimulationSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll().Build()); } [BurstCompile] public void OnUpdate(ref SystemState state) { OrbitCameraSimulationJob job = new OrbitCameraSimulationJob { DeltaTime = SystemAPI.Time.DeltaTime, LocalTransformLookup = SystemAPI.GetComponentLookup(false), ParentLookup = SystemAPI.GetComponentLookup(true), PostTransformMatrixLookup = SystemAPI.GetComponentLookup(true), CameraTargetLookup = SystemAPI.GetComponentLookup(true), KinematicCharacterBodyLookup = SystemAPI.GetComponentLookup(true), }; job.Schedule(); } [BurstCompile] [WithAll(typeof(Simulate))] public partial struct OrbitCameraSimulationJob : IJobEntity { public float DeltaTime; public ComponentLookup LocalTransformLookup; [ReadOnly] public ComponentLookup ParentLookup; [ReadOnly] public ComponentLookup PostTransformMatrixLookup; [ReadOnly] public ComponentLookup CameraTargetLookup; [ReadOnly] public ComponentLookup KinematicCharacterBodyLookup; void Execute(Entity entity, ref OrbitCamera orbitCamera, in OrbitCameraControl cameraControl) { if (OrbitCameraUtilities.TryGetCameraTargetSimulationWorldTransform( cameraControl.FollowedCharacterEntity, ref LocalTransformLookup, ref ParentLookup, ref PostTransformMatrixLookup, ref CameraTargetLookup, out float4x4 targetWorldTransform)) { float3 targetUp = targetWorldTransform.Up(); float3 targetPosition = targetWorldTransform.Translation(); // Update planar forward based on target up direction and rotation from parent { quaternion tmpPlanarRotation = MathUtilities.CreateRotationWithUpPriority(targetUp, orbitCamera.PlanarForward); // Rotation from character parent if (orbitCamera.RotateWithCharacterParent && KinematicCharacterBodyLookup.TryGetComponent(cameraControl.FollowedCharacterEntity, out KinematicCharacterBody characterBody)) { // Only consider rotation around the character up, since the camera is already adjusting itself to character up quaternion planarRotationFromParent = characterBody.RotationFromParent; KinematicCharacterUtilities.AddVariableRateRotationFromFixedRateRotation(ref tmpPlanarRotation, planarRotationFromParent, DeltaTime, characterBody.LastPhysicsUpdateDeltaTime); } orbitCamera.PlanarForward = MathUtilities.GetForwardFromRotation(tmpPlanarRotation); } // Yaw float yawAngleChange = cameraControl.LookDegreesDelta.x * orbitCamera.RotationSpeed; quaternion yawRotation = quaternion.Euler(targetUp * math.radians(yawAngleChange)); orbitCamera.PlanarForward = math.rotate(yawRotation, orbitCamera.PlanarForward); // Pitch orbitCamera.PitchAngle += -cameraControl.LookDegreesDelta.y * orbitCamera.RotationSpeed; orbitCamera.PitchAngle = math.clamp(orbitCamera.PitchAngle, orbitCamera.MinVAngle, orbitCamera.MaxVAngle); // Calculate final rotation quaternion cameraRotation = OrbitCameraUtilities.CalculateCameraRotation(targetUp, orbitCamera.PlanarForward, orbitCamera.PitchAngle); // Distance input float desiredDistanceMovementFromInput = cameraControl.ZoomDelta * orbitCamera.DistanceMovementSpeed; orbitCamera.TargetDistance = math.clamp(orbitCamera.TargetDistance + desiredDistanceMovementFromInput, orbitCamera.MinDistance, orbitCamera.MaxDistance); // Calculate camera position (no smoothing or obstructions yet; these are done in the camera late update) float3 cameraPosition = OrbitCameraUtilities.CalculateCameraPosition(targetPosition, cameraRotation, orbitCamera.TargetDistance); // Write back to component LocalTransformLookup[entity] = LocalTransform.FromPositionRotation(cameraPosition, cameraRotation); } } } } [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(TransformSystemGroup))] [BurstCompile] public partial struct OrbitCameraLateUpdateSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll().Build()); } [BurstCompile] public void OnUpdate(ref SystemState state) { OrbitCameraLateUpdateJob job = new OrbitCameraLateUpdateJob { DeltaTime = SystemAPI.Time.DeltaTime, PhysicsWorld = SystemAPI.GetSingleton().PhysicsWorld, LocalToWorldLookup = SystemAPI.GetComponentLookup(false), CameraTargetLookup = SystemAPI.GetComponentLookup(true), KinematicCharacterBodyLookup = SystemAPI.GetComponentLookup(true), }; job.Schedule(); } [BurstCompile] [WithAll(typeof(Simulate))] public partial struct OrbitCameraLateUpdateJob : IJobEntity { public float DeltaTime; [ReadOnly] public PhysicsWorld PhysicsWorld; public ComponentLookup LocalToWorldLookup; [ReadOnly] public ComponentLookup CameraTargetLookup; [ReadOnly] public ComponentLookup KinematicCharacterBodyLookup; void Execute( Entity entity, ref OrbitCamera orbitCamera, in OrbitCameraControl cameraControl, in DynamicBuffer ignoredEntitiesBuffer) { if (OrbitCameraUtilities.TryGetCameraTargetInterpolatedWorldTransform( cameraControl.FollowedCharacterEntity, ref LocalToWorldLookup, ref CameraTargetLookup, out LocalToWorld targetWorldTransform)) { quaternion cameraRotation = OrbitCameraUtilities.CalculateCameraRotation(targetWorldTransform.Up, orbitCamera.PlanarForward, orbitCamera.PitchAngle); float3 cameraForward = math.mul(cameraRotation, math.forward()); float3 targetPosition = targetWorldTransform.Position; // Distance smoothing orbitCamera.SmoothedTargetDistance = math.lerp(orbitCamera.SmoothedTargetDistance, orbitCamera.TargetDistance, MathUtilities.GetSharpnessInterpolant(orbitCamera.DistanceMovementSharpness, DeltaTime)); // Obstruction handling // Obstruction detection is handled here, because we have to adjust the obstruction distance // to match the interpolated physics body transform (as opposed to the "simulation" transform). Otherwise, a // camera getting obstructed by a moving physics body would have visible jitter. if (orbitCamera.ObstructionRadius > 0f) { float obstructionCheckDistance = orbitCamera.SmoothedTargetDistance; CameraObstructionHitsCollector collector = new CameraObstructionHitsCollector(cameraControl.FollowedCharacterEntity, ignoredEntitiesBuffer, cameraForward); PhysicsWorld.SphereCastCustom( targetPosition, orbitCamera.ObstructionRadius, -cameraForward, obstructionCheckDistance, ref collector, CollisionFilter.Default, QueryInteraction.IgnoreTriggers); float newObstructedDistance = obstructionCheckDistance; if (collector.NumHits > 0) { newObstructedDistance = obstructionCheckDistance * collector.ClosestHit.Fraction; // Redo cast with the interpolated body transform to prevent FixedUpdate jitter in obstruction detection if (orbitCamera.PreventFixedUpdateJitter) { RigidBody hitBody = PhysicsWorld.Bodies[collector.ClosestHit.RigidBodyIndex]; if (LocalToWorldLookup.TryGetComponent(hitBody.Entity, out LocalToWorld hitBodyLocalToWorld)) { // Adjust the rigidbody transform for interpolation, so we can raycast it in that state hitBody.WorldFromBody = new RigidTransform(quaternion.LookRotationSafe(hitBodyLocalToWorld.Forward, hitBodyLocalToWorld.Up), hitBodyLocalToWorld.Position); collector = new CameraObstructionHitsCollector(cameraControl.FollowedCharacterEntity, ignoredEntitiesBuffer, cameraForward); hitBody.SphereCastCustom( targetPosition, orbitCamera.ObstructionRadius, -cameraForward, obstructionCheckDistance, ref collector, CollisionFilter.Default, QueryInteraction.IgnoreTriggers); if (collector.NumHits > 0) { newObstructedDistance = obstructionCheckDistance * collector.ClosestHit.Fraction; } } } } // Update current distance based on obstructed distance if (orbitCamera.ObstructedDistance < newObstructedDistance) { // Move outer orbitCamera.ObstructedDistance = math.lerp(orbitCamera.ObstructedDistance, newObstructedDistance, MathUtilities.GetSharpnessInterpolant(orbitCamera.ObstructionOuterSmoothingSharpness, DeltaTime)); } else if (orbitCamera.ObstructedDistance > newObstructedDistance) { // Move inner orbitCamera.ObstructedDistance = math.lerp(orbitCamera.ObstructedDistance, newObstructedDistance, MathUtilities.GetSharpnessInterpolant(orbitCamera.ObstructionInnerSmoothingSharpness, DeltaTime)); } } else { orbitCamera.ObstructedDistance = orbitCamera.SmoothedTargetDistance; } // Place camera at the final distance (includes smoothing and obstructions) float3 cameraPosition = OrbitCameraUtilities.CalculateCameraPosition(targetPosition, cameraRotation, orbitCamera.ObstructedDistance); // Write to LtW LocalToWorldLookup[entity] = new LocalToWorld { Value = new float4x4(cameraRotation, cameraPosition) }; } } } } public static class OrbitCameraUtilities { public static bool TryGetCameraTargetSimulationWorldTransform( Entity targetCharacterEntity, ref ComponentLookup localTransformLookup, ref ComponentLookup parentLookup, ref ComponentLookup postTransformMatrixLookup, ref ComponentLookup cameraTargetLookup, out float4x4 worldTransform) { bool foundValidCameraTarget = false; worldTransform = float4x4.identity; // Camera target is either defined by the CameraTarget component, or if not, the transform of the followed character if (cameraTargetLookup.TryGetComponent(targetCharacterEntity, out CameraTarget cameraTarget) && localTransformLookup.HasComponent(cameraTarget.TargetEntity)) { TransformHelpers.ComputeWorldTransformMatrix( cameraTarget.TargetEntity, out worldTransform, ref localTransformLookup, ref parentLookup, ref postTransformMatrixLookup); foundValidCameraTarget = true; } else if (localTransformLookup.TryGetComponent(targetCharacterEntity, out LocalTransform characterLocalTransform)) { worldTransform = float4x4.TRS(characterLocalTransform.Position, characterLocalTransform.Rotation, 1f); foundValidCameraTarget = true; } return foundValidCameraTarget; } public static bool TryGetCameraTargetInterpolatedWorldTransform( Entity targetCharacterEntity, ref ComponentLookup localToWorldLookup, ref ComponentLookup cameraTargetLookup, out LocalToWorld worldTransform) { bool foundValidCameraTarget = false; worldTransform = default; // Get the interpolated transform of the target if (cameraTargetLookup.TryGetComponent(targetCharacterEntity, out CameraTarget cameraTarget) && localToWorldLookup.TryGetComponent(cameraTarget.TargetEntity, out worldTransform)) { foundValidCameraTarget = true; } else if (localToWorldLookup.TryGetComponent(targetCharacterEntity, out worldTransform)) { foundValidCameraTarget = true; } return foundValidCameraTarget; } public static quaternion CalculateCameraRotation(float3 targetUp, float3 planarForward, float pitchAngle) { quaternion pitchRotation = quaternion.Euler(math.right() * math.radians(pitchAngle)); quaternion cameraRotation = MathUtilities.CreateRotationWithUpPriority(targetUp, planarForward); cameraRotation = math.mul(cameraRotation, pitchRotation); return cameraRotation; } public static float3 CalculateCameraPosition(float3 targetPosition, quaternion cameraRotation, float distance) { return targetPosition + (-MathUtilities.GetForwardFromRotation(cameraRotation) * distance); } } public struct CameraObstructionHitsCollector : ICollector { public bool EarlyOutOnFirstHit => false; public float MaxFraction => 1f; public int NumHits { get; private set; } public ColliderCastHit ClosestHit; private float _closestHitFraction; private float3 _cameraDirection; private Entity _followedCharacter; private DynamicBuffer _ignoredEntitiesBuffer; public CameraObstructionHitsCollector(Entity followedCharacter, DynamicBuffer ignoredEntitiesBuffer, float3 cameraDirection) { NumHits = 0; ClosestHit = default; _closestHitFraction = float.MaxValue; _cameraDirection = cameraDirection; _followedCharacter = followedCharacter; _ignoredEntitiesBuffer = ignoredEntitiesBuffer; } public bool AddHit(ColliderCastHit hit) { if (_followedCharacter == hit.Entity) { return false; } if (math.dot(hit.SurfaceNormal, _cameraDirection) < 0f || !PhysicsUtilities.IsCollidable(hit.Material)) { return false; } for (int i = 0; i < _ignoredEntitiesBuffer.Length; i++) { if (_ignoredEntitiesBuffer[i].Entity == hit.Entity) { return false; } } // Process valid hit if (hit.Fraction < _closestHitFraction) { _closestHitFraction = hit.Fraction; ClosestHit = hit; } NumHits++; return true; } }