using System;
using UnityEngine;
namespace Framework.Actors.Shooter {
/// Manages a shooter actor moving around on a 2D plane
public class ShooterActorController : ActorController {
/// Direction the actor is facing in
///
/// This is the direction the character is looking at. If
/// an is present, the direction will be
/// that of the head. Otherwise, it will simply point north and it's your
/// responsibility to update it :)
///
public Vector3 LookDirection = Vector3.forward;
/// Number of jumps the actor has done without touching the ground
public int ConsecutiveJumpCount;
/// Whether head movement beyond the allowed range is applied to the body
///
///
/// When the actor has one or more s added to it for
/// mouse look, moving the head beyond the allowed range will normally restrict
/// the angles the player can look at. If this is set to true, rotation beyond
/// the range allowed for the head will instead be applied to the body.
///
///
/// This design keeps all of the mouse look logic inside the head controller while
/// allowing for a wide range of character setups:
///
///
/// -
/// Apply = false; YawRange = -90.0 .. 90.0
///
/// Player can move the head in a 180 degree angle, body stays still
///
///
/// -
/// Apply = true; YawRange = 0.0 .. 0.0
///
/// Player can look around freely, body will change direction instead of head
///
///
/// -
/// Apply = true; YawRange = -90.0 .. 90.0
///
/// Player can look around freely, if player turns head more than 90 degrees,
/// the rotation beyond 90 degrees will be applied to the body instead.
///
///
///
///
public bool ApplyExcessHeadMovementToBody = true;
#if false // This can be done by deriving from the ShooterActorController instead
///
/// Whether the body aligns itself with the head after the head hit
/// its rotation limits
///
///
/// If this is enabled, the player can still freely rotate the head without any
/// response from the actor's body, until the head hits the limits of its
/// movement range and the body has to compensate. At that point, the body will
/// continue turning until it is aligned with the head again.
///
public bool AlignBodyAfterExcessHeadRotation = false;
#endif
/// Presenter driving the Mecanim animation state machine
public new ShooterActorPresenter Presenter {
get {
if(this.shooterPresenter == null) {
this.shooterPresenter = base.Presenter as ShooterActorPresenter;
}
return this.shooterPresenter;
}
}
/// Ability definitions that control what moves the actor can use
public Abilities Abilities {
get {
this.abilities = GetComponent();
if(this.abilities == null) {
this.abilities = gameObject.AddComponent();
}
return this.abilities;
}
}
/// Aligns the body with the direction the head is looking in
///
/// Whether to do the alignment instantly within this method (true) or
/// by honoring the field.
///
public void AlignBodyWithHead(bool instantaneously = true) {
if(instantaneously) {
RotateOnlyBodyOnYTowards(this.headController.GetGlobalLookDirection());
} else {
this.isAligningWithHead = true; // Rotation happens in FixedUpdate()
}
}
///
/// Rotates the body while counter-rotating the head to keep its orientation
///
/// Number of degrees the body should be rotated
public void RotateOnlyBodyOnY(float degrees) {
// With no head controller, we'll just rotate the body alone
if(this.headController == null) {
transform.Rotate(transform.up, degrees);
} else { // If there's a head controller, the head needs to counter-rotate
Quaternion globalHeadOrientation = (
this.headController.HeadBaseOrientation * this.headController.LocalHeadOrientation
);
transform.Rotate(transform.up, degrees);
// CHECK: This produces a little bit of wobble with the mouse controller
// This is likely from having a not perfectly vertical spine bone (defining
// the head's coordinate system) and the mouse controller converting to
// pitch/yaw internally, thus keeping the head vertically in line with it.
this.headController.LocalHeadOrientation = (
Quaternion.Inverse(this.headController.HeadBaseOrientation) * globalHeadOrientation
);
}
}
/// Rotates the body towards the specified direction in on the Y axis
/// Direction the body will rotate towards
/// Maximum number of degrees the body will be rotated
///
/// True if the body is now facing the desired direction completely,
/// false if further rotation is needed to reach the target direction.
///
public bool RotateOnlyBodyOnYTowards(Vector3 direction, float maximumDegrees = 180.0f) {
// Figure out the number of degrees we should rotate
float degreesToRotate;
{
Vector3 localTargetDirection;
{
Quaternion inverseActorRotation = Quaternion.Inverse(transform.rotation);
localTargetDirection = inverseActorRotation * direction;
}
float targetAngle = Mathf.Atan2(localTargetDirection.x, localTargetDirection.z);
degreesToRotate = targetAngle * Mathf.Rad2Deg;
}
// How far the body has to rotate to align with the head direction
if(degreesToRotate >= 180.0f) {
degreesToRotate -= 360.0f;
} else if(degreesToRotate < -180.0f) {
degreesToRotate += 360.0f;
}
// Limit the amount of rotation to the specified number of degrees,
// then apply the rotation to the body (and counter-rotate the head)
float unclampedDegreesToRotate = degreesToRotate;
degreesToRotate = Mathf.Clamp(degreesToRotate, -maximumDegrees, +maximumDegrees);
RotateOnlyBodyOnY(degreesToRotate);
// Report whether the angle was cut off or the target direction was achieved
return (degreesToRotate == unclampedDegreesToRotate);
}
/// Called when the component gets added to a gameObject
protected override void Awake() {
base.Awake();
// CHECK: Ugly. Hardcoded preferrence for a specific component type.
this.headController = GetComponent();
if(this.headController == null) {
this.headController = GetComponent();
}
if(this.headController != null) {
var onExcessHeadRotationDelegate = new Action(OnExcessHeadRotation);
this.headController.ExcessHeadRotation += onExcessHeadRotationDelegate;
}
}
/// Called once per physics frame to update the positions of game object
protected override void FixedUpdate() {
// Update the look direction if a mouse look handler is present on the actor.
// Do this before processing the active move so the move can respond to look direction
// without a 1-frame lag
updateLookDirection();
// If the actor is currently auto-aligning its body with the head (look direction),
// update the body orientation here (in FixedUpdate() because body orientation may
// have an effect on various things such as hitboxes, shooting, etc.)
if(this.isAligningWithHead) {
if(this.headController != null) {
Vector3 lookDirection = this.headController.GetGlobalLookDirection();
float maximumRotation = Abilities.TurnDegreesPerSecond * Time.fixedDeltaTime;
this.isAligningWithHead = !RotateOnlyBodyOnYTowards(lookDirection, maximumRotation);
}
}
base.FixedUpdate();
}
/// Called each visual frame to update the visible state of the actor
protected override void Update() {
// Update the look direction if a mouse look handler is present on the actor.
// Do this before processing the active move so the move can respond to look direction
// without a 1-frame lag
updateLookDirection();
base.Update();
// If a shooter presenter is present, update its velocities
// so the correct running, strafing and/or jumping animations can play.
ShooterActorPresenter presenter = Presenter;
if(presenter != null) {
// Try to obtain the velocity either from a rigidbody or an ActorPhysics component
Vector3 velocity;
{
if(Rigidbody != null) {
velocity = Rigidbody.velocity;
} else if(ActorPhysics != null) {
velocity = ActorPhysics.Velocity;
} else {
return;
}
}
// Forward the directional velocities to the presenter
float magnitude = velocity.magnitude;
presenter.ForwardVelocity = (
Vector3.Dot(transform.forward, velocity) * magnitude
);
presenter.StrafingVelocity = (
Vector3.Dot(transform.right, velocity) * magnitude
);
presenter.VerticalVelocity = (
Vector3.Dot(transform.up, velocity) * magnitude
);
}
}
///
/// Called when the player has tried to rotate the head farther than
/// its movement range allows
///
/// Excess rotation that was clamped off
protected virtual void OnExcessHeadRotation(Quaternion excessRotation) {
if(!this.ApplyExcessHeadMovementToBody) {
return;
}
updateLookDirection();
applyHeadRotationToBodyOnY(excessRotation);
}
/// Applies a rotation performed by the head to the body
/// Head rotation that will be applied to the body
///
/// This method translates the rotation from the head into an equivalent rotation
/// in the coordinate system of the body (because the actor might be bowing down
/// or be an animal whose head is at an angle relative to the body). Only
/// the rotation on the Y axis will be applied.
///
private void applyHeadRotationToBodyOnY(Quaternion headRotation) {
// Calculate the clamped and unclamped head orientations in actor space
Quaternion clampedHeadOrientation, unclampedHeadOrientation;
{
Quaternion inverseActorRotation = Quaternion.Inverse(transform.rotation);
Quaternion baseOrientation = this.headController.HeadBaseOrientation;
// Translate the current head orientation into the actor's coordinate system
clampedHeadOrientation = inverseActorRotation * (
baseOrientation * this.headController.LocalHeadOrientation
);
// Get the unclamped head orientation in the head's local coordinate system
Quaternion localUnclampedHeadOrientation;
{
Vector3 unclampedHeadEulers = (
this.headController.LocalHeadOrientation.eulerAngles + headRotation.eulerAngles
);
localUnclampedHeadOrientation = (
Quaternion.AngleAxis(unclampedHeadEulers.y, Vector3.up) *
Quaternion.AngleAxis(unclampedHeadEulers.x, Vector3.right)
);
}
// Translate the unclamped head orientation into the actor's coordinate system
unclampedHeadOrientation = inverseActorRotation * (
baseOrientation * localUnclampedHeadOrientation
);
}
Transform actorTransform = transform;
// And finally, we rotate the actor around its own Y axis by the amount of
// clamped-off head rotation that is present in the actor's coordinate system.
float yawRotation = (
unclampedHeadOrientation.eulerAngles.y - clampedHeadOrientation.eulerAngles.y
);
actorTransform.Rotate(actorTransform.up, yawRotation);
// If the head controller is virtual reality based we may need to counter-rotate
// the camera root to preserve the calibrated head position for the player.
Transform virtualRealityCameraRoot = this.headController.VirtualRealityCameraRoot;
if(virtualRealityCameraRoot != null) {
// Counter-rotation of the camera root is only needed if the camera root
// is a child of the actor (and was thus rotated /with/ the actor).
if(virtualRealityCameraRoot.IsChildOf(actorTransform)) {
virtualRealityCameraRoot.RotateAround(
actorTransform.position, actorTransform.up, -yawRotation
);
}
}
}
/// Updates the 'LookDirection' field if a head controller is present
private void updateLookDirection() {
if(this.headController != null) {
this.LookDirection = (
this.headController.HeadBaseOrientation * this.headController.LocalHeadOrientation
) * Vector3.forward;
}
}
/// Presenter driving the Mecanim animation state machine
private ShooterActorPresenter shooterPresenter;
/// Abilities of the actor
private Abilities abilities;
/// Optional head controller managing look direction and turning
private IHeadController headController;
/// Whether the body is currently aligning itself with the head
private bool isAligningWithHead;
}
} // namespace Framework.Actors.Shooter