using System.Collections.Generic;
using UnityEngine;
namespace Unity.Cinemachine.Samples
{
///
/// This is an add-on for SimplePlayerController that controls the player's Aiming Core.
///
/// This component expects to be in a child object of a player that has a SimplePlayerController
/// behaviour. It works intimately with that component.
//
/// The purpose of the aiming core is to decouple the camera rotation from the player rotation.
/// Camera rotation is determined by the rotation of the player core GameObject, and this behaviour
/// provides input axes for controlling it. When the player core is used as the target for
/// a CinemachineCamera with a ThirdPersonFollow component, the camera will look along the core's
/// forward axis, and pivot around the core's origin.
///
/// The aiming core is also used to define the origin and direction of player shooting, if player
/// has that ability.
///
/// To implement player shooting, add a SimplePlayerShoot behaviour to this GameObject.
///
public class SimplePlayerAimController : MonoBehaviour, IInputAxisOwner
{
public enum CouplingMode { Coupled, CoupledWhenMoving, Decoupled }
[Tooltip("How the player's rotation is coupled to the camera's rotation. Three modes are available:\n"
+ "Coupled: The player rotates with the camera. Sideways movement will result in strafing.\n"
+ "Coupled When Moving: Camera can rotate freely around the player when the player is stationary, "
+ "but the player will rotate to face camera forward when it starts moving.\n"
+ "Decoupled: The player's rotation is independent of the camera's rotation.")]
public CouplingMode PlayerRotation;
[Tooltip("How fast the player rotates to face the camera direction when the player starts moving. "
+ "Only used when Player Rotation is Coupled When Moving.")]
public float RotationDamping = 0.2f;
[Tooltip("Horizontal Rotation. Value is in degrees, with 0 being centered.")]
public InputAxis HorizontalLook = new () { Range = new Vector2(-180, 180), Wrap = true, Recentering = InputAxis.RecenteringSettings.Default };
[Tooltip("Vertical Rotation. Value is in degrees, with 0 being centered.")]
public InputAxis VerticalLook = new () { Range = new Vector2(-70, 70), Recentering = InputAxis.RecenteringSettings.Default };
SimplePlayerControllerBase m_Controller;
Transform m_ControllerTransform; // cached for efficiency
Quaternion m_DesiredWorldRotation;
/// Report the available input axes to the input axis controller.
/// We use the Input Axis Controller because it works with both the Input package
/// and the Legacy input system. This is sample code and we
/// want it to work everywhere.
void IInputAxisOwner.GetInputAxes(List axes)
{
axes.Add(new () { DrivenAxis = () => ref HorizontalLook, Name = "Horizontal Look", Hint = IInputAxisOwner.AxisDescriptor.Hints.X });
axes.Add(new () { DrivenAxis = () => ref VerticalLook, Name = "Vertical Look", Hint = IInputAxisOwner.AxisDescriptor.Hints.Y });
}
void OnValidate()
{
HorizontalLook.Validate();
VerticalLook.Range.x = Mathf.Clamp(VerticalLook.Range.x, -90, 90);
VerticalLook.Range.y = Mathf.Clamp(VerticalLook.Range.y, -90, 90);
VerticalLook.Validate();
}
void OnEnable()
{
m_Controller = GetComponentInParent();
if (m_Controller == null)
Debug.LogError("SimplePlayerController not found on parent object");
else
{
m_Controller.PreUpdate -= UpdatePlayerRotation;
m_Controller.PreUpdate += UpdatePlayerRotation;
m_Controller.PostUpdate -= PostUpdate;
m_Controller.PostUpdate += PostUpdate;
m_ControllerTransform = m_Controller.transform;
}
}
void OnDisable()
{
if (m_Controller != null)
{
m_Controller.PreUpdate -= UpdatePlayerRotation;
m_Controller.PostUpdate -= PostUpdate;
m_ControllerTransform = null;
}
}
/// Recenters the player to match my rotation
/// How long the recentering should take
public void RecenterPlayer(float damping = 0)
{
if (m_ControllerTransform == null)
return;
// Get my rotation relative to parent
var rot = transform.localRotation.eulerAngles;
rot.y = NormalizeAngle(rot.y);
var delta = rot.y;
delta = Damper.Damp(delta, damping, Time.deltaTime);
// Rotate the parent towards me
m_ControllerTransform.rotation = Quaternion.AngleAxis(
delta, m_ControllerTransform.up) * m_ControllerTransform.rotation;
// Rotate me in the opposite direction
HorizontalLook.Value -= delta;
rot.y -= delta;
transform.localRotation = Quaternion.Euler(rot);
}
///
/// Set my rotation to look in this direction, without changing player rotation.
/// Here we only set the axis values, we let the player controller do the actual rotation.
///
/// Direction to look in, in worldspace
public void SetLookDirection(Vector3 worldspaceDirection)
{
if (m_ControllerTransform == null)
return;
var rot = (Quaternion.Inverse(m_ControllerTransform.rotation)
* Quaternion.LookRotation(worldspaceDirection, m_ControllerTransform.up)).eulerAngles;
HorizontalLook.Value = HorizontalLook.ClampValue(rot.y);
VerticalLook.Value = VerticalLook.ClampValue(NormalizeAngle(rot.x));
}
// This is called by the player controller before it updates its own rotation.
void UpdatePlayerRotation()
{
var t = transform;
t.localRotation = Quaternion.Euler(VerticalLook.Value, HorizontalLook.Value, 0);
m_DesiredWorldRotation = t.rotation;
switch (PlayerRotation)
{
case CouplingMode.Coupled:
{
m_Controller.SetStrafeMode(true);
RecenterPlayer();
break;
}
case CouplingMode.CoupledWhenMoving:
{
// If the player is moving, rotate its yaw to match the camera direction,
// otherwise let the camera orbit
m_Controller.SetStrafeMode(true);
if (m_Controller.IsMoving)
RecenterPlayer(RotationDamping);
break;
}
case CouplingMode.Decoupled:
{
m_Controller.SetStrafeMode(false);
break;
}
}
VerticalLook.UpdateRecentering(Time.deltaTime, VerticalLook.TrackValueChange());
HorizontalLook.UpdateRecentering(Time.deltaTime, HorizontalLook.TrackValueChange());
}
// Callback for player controller to update our rotation after it has updated its own.
void PostUpdate(Vector3 vel, float speed)
{
if (PlayerRotation == CouplingMode.Decoupled)
{
// After player has been rotated, we subtract any rotation change
// from our own transform, to maintain our world rotation
transform.rotation = m_DesiredWorldRotation;
var delta = (Quaternion.Inverse(m_ControllerTransform.rotation) * m_DesiredWorldRotation).eulerAngles;
VerticalLook.Value = NormalizeAngle(delta.x);
HorizontalLook.Value = NormalizeAngle(delta.y);
}
}
float NormalizeAngle(float angle)
{
while (angle > 180)
angle -= 360;
while (angle < -180)
angle += 360;
return angle;
}
}
}