Hi all,
I’ve a 3rd person camera similar to Mario64. Currently, I’m working on preventing the camera from getting stuck inside a mesh. The goal is to move the camera overhead & this much trickier than I’d expected.
Here’s the code:
using UnityEngine;
using System.Collections;
/// <summary>
/// This is a C# and VCS friendly conversion of ThirdPersonCamera.js found in
/// Unity's Character Controllers package. For any questions, contact
/// support@bitbybitstudios.com.
/// Appendeded camera clipping function
/// Camera culling set up:
/// 1. create 2 layers name them clip & clipNone
/// 2. use bitswitch to switch between layer masks
/// 3. ensure mesh in game has colliders
/// 4. create a tag & label environment - checked by RayCastHit
/// 5. if camera is occluded, move it overhead
/// 6. if an object will clip move it overhead
/// </summary>
public class VCThirdPersonCamera : MonoBehaviour
{
public Transform cameraTransform; //use this as raycast emitterPoint
private Transform _target;
// The distance in the x-z plane to the target
public float distance= 7.0f;
// the height we want the camera to be above the target
public float height= 3.0f;
public float angularSmoothLag = 0.3f;
public float angularMaxSpeed = 15.0f;
public float heightSmoothLag = 0.3f;
public float snapSmoothLag = 0.2f;
public float snapMaxSpeed = 720.0f;
public float clampHeadPositionScreenSpace = 0.75f;
public float lockCameraTimeout = 0.2f;
private Vector3 headOffset = Vector3.zero;
private Vector3 centerOffset = Vector3.zero;
private float heightVelocity = 0.0f;
private float angleVelocity = 0.0f;
private bool snap = false;
private VCThirdPersonController controller;
private float targetHeight= 100000.0f;
/// clipping variables
/// use the camera's slider to determine defaultFOV
public Vector3 cameraOffset = new Vector3(0,0.125f,-0.125f); //moves camera so player doesn't get clipped
public float fovDefault = 60f; //the desired default field of view set by user
public float camTurnTime = 0f; //use to compensate for the camera turning
public float headCamTimer = 1.5f; //how long should the camera stay overHead
public float fieldOfView = 95.0f; //set when camera is overHead
public bool camOccluded = false; //is the camera occluded
public bool clipped = true; //used for clipping
public Camera _camera;
public GameObject theCamera;
private LayerMask _mask;
private RaycastHit _hit;
/// end occlusionHandlers
private void Awake ()
{
if (cameraTransform == null && Camera.main != null)
cameraTransform = Camera.main.transform;
if (cameraTransform == null)
{
Debug.Log("Please assign a camera to the ThirdPersonCamera script.");
enabled = false;
}
_target = transform;
if (_target != null)
{
controller = _target.GetComponent<VCThirdPersonController>();
}
if (controller != null)
{
CharacterController characterController = _target.GetComponent<CharacterController>();
centerOffset = characterController.bounds.center - _target.position;
headOffset = centerOffset;
headOffset.y = characterController.bounds.max.y - _target.position.y;
}
else
Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached.");
Cut(_target, centerOffset);
/// instantiate occlusion culling handler through bitSwitch
_mask = 1 << LayerMask.NameToLayer("clip") | 0 << LayerMask.NameToLayer("clipNone");
}
public float AngleDistance ( float a , float b )
{
a = Mathf.Repeat(a, 360);
b = Mathf.Repeat(b, 360);
return Mathf.Abs(b - a);
}
public void Apply (Transform dummyTarget, Vector3 dummyCenter)
{
// Early out if we don't have a target
if (controller == null)
return;
Vector3 targetCenter = _target.position + centerOffset;
Vector3 targetHead = _target.position + headOffset;
// Calculate the current & target rotation angles
float originalTargetAngle= _target.eulerAngles.y;
float currentAngle= cameraTransform.eulerAngles.y;
// Adjust real target angle when camera is locked
float targetAngle= originalTargetAngle;
// When pressing Fire2 (alt) the camera will snap to the target direction real quick.
// It will stop snapping when it reaches the target
if (Input.GetButton("Fire2")) // VCS Note: For touch controls, you may want to change this to use a VCAnalogJoystick's TapCount or a VCButtonBase's Pressed properties.
snap = true;
if (snap)
{
// We are close to the target, so we can stop snapping now!
if (AngleDistance (currentAngle, originalTargetAngle) < 3.0f)
snap = false;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed);
}
// Normal camera motion
else
{
if (controller.GetLockCameraTimer() < lockCameraTimeout)
{
targetAngle = currentAngle;
}
// Lock the camera when moving backwards!
// * It is really confusing to do 180 degree spins when turning around.
if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ())
targetAngle += 180;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed);
}
// When jumping don't move camera upwards but only down!
if (controller.IsJumping ())
{
// We'd be moving the camera upwards, do that only if it's really high
float newTargetHeight= targetCenter.y + height;
if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5)
targetHeight = targetCenter.y + height;
}
// When walking always update the target height
else
{
targetHeight = targetCenter.y + height;
}
// Damp the height
float currentHeight= cameraTransform.position.y;
currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag);
// Convert the angle into a rotation, by which we then reposition the camera
Quaternion currentRotation= Quaternion.Euler (0, currentAngle, 0);
// Set the position of the camera on the x-z plane to:
// distance meters behind the target
cameraTransform.position = targetCenter;
cameraTransform.position += currentRotation * Vector3.back * distance;
Vector3 tempCameraTransformPos=cameraTransform.position;
tempCameraTransformPos.y = currentHeight;
cameraTransform.position = tempCameraTransformPos;
// Always look at the target
SetUpRotation(targetCenter, targetHead);
}
private void LateUpdate ()
{
Vector3 targetCenter = _target.position + centerOffset;
Vector3 targetHead = _target.position + headOffset;
Vector3 theTarget = transform.TransformDirection(Vector3.forward)* 3.5f;// <--original line
//Vector3 theTarget = transform.LookAt(targetHead);
Ray centerRay = _camera.ViewportPointToRay(new Vector3(.5f, 0.5f, 1f));
//cameraTransform.transform.LookAt(targetHead);//alwasy lookAt the player's head
// Raycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
if(Physics.Raycast(_camera.transform.position, theTarget, out _hit, 2.77f) && _hit.collider.CompareTag("Player"))
{
camOccluded = false;
}
if(Physics.Raycast(_camera.transform.position, theTarget, out _hit, 2.77f) && _hit.collider.CompareTag("environment"))
{
camOccluded = true;
}
/// creates ray from cameraViewPort to nearClipPlane
float camClipPlane = _camera.nearClipPlane;
/// sets up clipPoints from camera to viewPort
Vector3 pos1 = _camera.ViewportToWorldPoint(new Vector3(0,0,camClipPlane)); //btmL
Vector3 pos2 = _camera.ViewportToWorldPoint(new Vector3(.5f,0,camClipPlane)); //btmCtr
Vector3 pos3 = _camera.ViewportToWorldPoint(new Vector3(1,0,camClipPlane)); //btmR
//--------------------------
Vector3 pos4 = _camera.ViewportToWorldPoint(new Vector3(0,.5f,camClipPlane)); //ctrL
Vector3 pos5 = _camera.ViewportToWorldPoint(new Vector3(1,.5f,camClipPlane)); //ctrR
//--------------------------
Vector3 pos6 = _camera.ViewportToWorldPoint(new Vector3(0,1,camClipPlane)); //topL
Vector3 pos7 = _camera.ViewportToWorldPoint(new Vector3(.5f,1,camClipPlane)); //topCtr
Vector3 pos8 = _camera.ViewportToWorldPoint(new Vector3(1,1,camClipPlane)); //topR
/// visualizes clipPoints- can be deleted
//Debug.DrawLine(cameraTransform.position, pos1, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos2, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos3, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos4, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos5, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos6, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos7, Color.yellow);
//Debug.DrawLine(cameraTransform.position, pos8, Color.yellow);
bool clipLine01 = Physics.Linecast(cameraTransform.position, pos1, out _hit, _mask.value);
bool clipLine02 = Physics.Linecast(cameraTransform.position, pos2, out _hit, _mask.value);
bool clipLine03 = Physics.Linecast(cameraTransform.position, pos3, out _hit, _mask.value);
bool clipLine04 = Physics.Linecast(cameraTransform.position, pos4, out _hit, _mask.value);
bool clipLine05 = Physics.Linecast(cameraTransform.position, pos5, out _hit, _mask.value);
bool clipLine06 = Physics.Linecast(cameraTransform.position, pos6, out _hit, _mask.value);
bool clipLine07 = Physics.Linecast(cameraTransform.position, pos7, out _hit, _mask.value);
bool clipLine08 = Physics.Linecast(cameraTransform.position, pos8, out _hit, _mask.value);
///clip conditions are met & camera is occluded
if((clipLine01)||(clipLine02)||(clipLine03)||(clipLine04)||(clipLine05)||(clipLine06)||(clipLine07)||(clipLine08))
{
camOccluded = true;
//StartCoroutine(ResetCamera());
}
else
{
camOccluded = false;
}
if(!camOccluded)
{
Apply (transform, Vector3.zero);//default VCScamera method
}
else
{
StartCoroutine(ResetCamera());
}
print(camOccluded);
print(headCamTimer);
}
public void OverheadCamera()
{
Vector3 targetCenter = _target.position + centerOffset;
Vector3 targetHead = _target.position + headOffset;
_camera.fieldOfView = fieldOfView;
cameraTransform.position = (targetHead + cameraOffset) ;
cameraTransform.LookAt(targetHead);
}
IEnumerator ResetCamera()
{
//camOccluded = true;
OverheadCamera();
yield return new WaitForSeconds(headCamTimer);
_camera.fieldOfView = fovDefault;
Apply (transform, Vector3.zero);//default VCScamera position
camOccluded = false;
}
public void Cut ( Transform dummyTarget , Vector3 dummyCenter ){
float oldHeightSmooth= heightSmoothLag;
float oldSnapMaxSpeed= snapMaxSpeed;
float oldSnapSmooth= snapSmoothLag;
snapMaxSpeed = 10000;
snapSmoothLag = 0.001f;
heightSmoothLag = 0.001f;
snap = true;
Apply (transform, Vector3.zero);
heightSmoothLag = oldHeightSmooth;
snapMaxSpeed = oldSnapMaxSpeed;
snapSmoothLag = oldSnapSmooth;
}
public void SetUpRotation (Vector3 centerPos, Vector3 headPos)
{
// Now it's getting hairy. The devil is in the details here, the big issue is jumping of course.
// * When jumping up and down we don't want to center the guy in screen space.
// This is important to give a feel for how high you jump and avoiding large camera movements.
//
// * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth.
//
// So here is what we will do:
//
// 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
// 2. When grounded we make him be centered
// 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
// 4. When landing we smoothly interpolate towards centering him on screen
//--------------------------------------------------------------------------//
_camera.fieldOfView = fovDefault; // defaults camera to desired FOV
Vector3 cameraPos = cameraTransform.position;
Vector3 offsetToCenter= centerPos - cameraPos;
// Generate base rotation only around y-axis
Quaternion yRotation= Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
Vector3 relativeOffset= Vector3.forward * distance + Vector3.down * height;
cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
// Calculate the projected center position and top position in world space
Ray centerRay = _camera.ViewportPointToRay(new Vector3(.5f, 0.5f, 1f));
Ray topRay = _camera.ViewportPointToRay(new Vector3(.5f, clampHeadPositionScreenSpace, 1f));
Vector3 centerRayPos= centerRay.GetPoint(distance);
Vector3 topRayPos= topRay.GetPoint(distance);
float centerToTopAngle= Vector3.Angle(centerRay.direction, topRay.direction);
float heightToAngle= centerToTopAngle / (centerRayPos.y - topRayPos.y);
float extraLookAngle= heightToAngle * (centerRayPos.y - centerPos.y);
if (extraLookAngle < centerToTopAngle)
{
extraLookAngle = 0;
}
else
{
extraLookAngle = extraLookAngle - centerToTopAngle;
cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
}
}
public Vector3 GetCenterOffset ()
{
return centerOffset;
}
}
Using a while loops crash the game…
Any advise is greatly appreciated.