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