Below is a sample of code from PlayerController.cs. it has gone through a series of updates upon adding Inverse Kinematics. One of my goals was to make the player's weighty movement a mechanic itself. I ended up separating the player camera from the body, creating a floating component, which acted as a rig, with the parent controlling the mechanics like leaning. It was a long process and a few rewrites to get the movement to feel right. Through playtesting, I was able to refine the values of look and move acceleration and speed.
Sample of Player Movement:
//Turn player body based on camera orientation
internal void Turn()
{
//Get Camera orientation, ignoring the Up/Down camera swivel
Quaternion cameraOrientation2D = Quaternion.Euler(new Vector3(
transform.eulerAngles.x,
cameraParent.transform.eulerAngles.y,
cameraParent.transform.eulerAngles.z));
//Player's body rotation trails camera rotation
transform.rotation = Quaternion.Lerp(transform.rotation, cameraOrientation2D, Time.deltaTime * 6f);
}
//Get angular difference from body.forward and camera.forward
float GetFacingAngularDifference()
{
Vector3 currentPosition = transform.position,
cameraTargetPosition = cameraTarget.position;
//Remove Y factor to get 2D direction
currentPosition.y = cameraTargetPosition.y = 0;
//Get Unit Direction
Vector3 direction = (cameraTargetPosition - currentPosition).normalized,
//Get Player's forward as 2D vector
forward = new Vector3(transform.forward.x, 0, transform.forward.z);
return Vector3.Angle(forward, direction);
}
//Move the player
internal void Move()
{
//Get input from current input device
InputModule.CheckInput(this);
//Slow player down if leaning
if (CamMovement.IsLeaning)
Walk(leanSpeedPercent);
else if (IsSprinting && CanSprint) //Currently Sprinting
{
timeSprinting += Time.deltaTime;
//Start sprint audio
if (timeSprinting > 1f)
sprintAudio.volume += .01f;
//Second Audio source ("Outtro") for sprint is still active
if(endSprintAudio.volume > 0)
endSprintAudio.volume -= .05f;
Sprint(); //Accelerates player to sprint speed
}
else
{
//Begin end of sprint audio
sprintAudio.volume = Mathf.Max(sprintAudio.volume - .01f, 0f);
if (timeSprinting > 2f)
{
//BUTT TO outtro sprint audio clip if constant sprint clip is playing
if (sprintAudio.volume > 0f && !endSprintAudio.isPlaying && CanSprint)
{
endSprintAudio.Play();
}
//Reset for next sprint audio trigger
else if (endSprintAudio.volume >= 1f)
timeSprinting = 0f;
endSprintAudio.volume = Mathf.Min(endSprintAudio.volume + .005f, 1f);
}
Walk(WalkSpeed); //Normal Movement
}
//Trigger sound event for enemies to hear
if (CurrentSpeed >= WalkSpeed / 1.5f)
SoundObjectManager.NotifyPlayerSoundTriggered(
gameObject, WalkSoundRange);
}
Here is a section of code displaying the vertical look. I have a Utils class, which has a function to clamp the euler angles of a rotation. Currently, the normal vertical angle is set to 75°, sprint is set to 30°:
internal void UpdateVerticalLook()
{
//Get the next intended angle
float verticalAngle = cameraTargetSwivel.eulerAngles.x + (vertLookSpeed * controller.InputModule.CamSensitivity);
//Clamp vertical angle between next angle and +-current angle
Utils.ClampAngle(ref verticalAngle, -currentAngleView, currentAngleView);
//Check if joystick is not moving or vertical look is close to hitting the max look angle bounds
if (Mathf.Abs(controller.Look.x) <= Mathf.Epsilon)
{
vertLookSpeed = Mathf.MoveTowards(vertLookSpeed, 0f, controller.InputModule.CamLookDecel * Time.deltaTime);
}
else if ((vertLookSpeed >= 0 && controller.Look.x > 0) ||
(vertLookSpeed <= 0 && controller.Look.x < 0))
vertLookSpeed += controller.Look.x *
controller.InputModule.CamLookAccel *
Time.deltaTime;
else
vertLookSpeed = controller.Look.x *
controller.InputModule.CamLookAccel *
Time.deltaTime;
//Clamp the current speed to +-max speed
vertLookSpeed = Mathf.Clamp(
vertLookSpeed,
-CurrentLookSpeedClamp,
CurrentLookSpeedClamp);
//Set the X angle (up/down swivel) to the current look posigion
cameraTargetSwivel.eulerAngles = new Vector3(
verticalAngle,
cameraTargetSwivel.eulerAngles.y,
cameraTargetSwivel.eulerAngles.z);
}
Early in preproduction, we decided that in order to give the player a fighting chance, he/she would be given a device that would have several functions, and would be upgradable over the course of the game. We really wanted a radar, and traditional to the genre, a battery-powered flashlight. We packaged it all in a single military device called the Multi-Function-Display (MFD).
This is the abstract class MFDMode, slotted into the MultiFunctionDisplay:
namespace MFD.Modes
{
public abstract class MFDMode : MonoBehaviour
{
internal float changeModeCooldown = 0.25f;
internal float currentChangeModeTime = 0.25f;
internal abstract float BatteryDrain { get; }
internal abstract string ModeName { get; }
int currentAbilityIndex = 0;
protected internal IMFDAbility CurrentAbility { get; protected set; }
internal bool HasAbilities => Abilities.Count > 0;
protected List<IMFDAbility> Abilities { get; set; }
public void Install(IMFDAbility ability, MultiFunctionDisplay mFD)
{
Abilities.Add(ability); //Add ability to list
Abilities[Abilities.Count - 1].Connect(mFD); //Connect required components from MFD to ability
//If no abilities have been installed yet, automatically set incoming ability as Current
if (CurrentAbility.GetType() == typeof(EmptyMFDAbility))
{
CurrentAbility = Abilities[currentAbilityIndex = (Abilities.Count - 1)];
//Check if this mode is the current active mode before setting ability in MFD
if (mFD.CurrentMode.GetType() == GetType())
mFD.SetAbility();
}
}
internal virtual void ActivateAbility(MultiFunctionDisplay mFD) => CurrentAbility.Activate(mFD);
internal virtual void Run(IInputModule inputModule, MultiFunctionDisplay mFD)
{
//Ability has been added to mode
if(Abilities.Count > 0)
{
//Check input for ability swapping
TryChangeAbility(mFD, inputModule.ChangeAbility);
CurrentAbility.Run(mFD); //Run ability core functions
}
}
protected void TryChangeAbility(MultiFunctionDisplay mFD, int direction)
{
currentChangeModeTime -= Time.deltaTime;
//Checks if ability axis has been moved left or right
if (direction == 0 || Abilities.Count == 0 || currentChangeModeTime > 0) return;
switch (direction)
{
case 1:
//Ability index sets to 0 if direction moves past ability count
currentAbilityIndex = currentAbilityIndex == 0 ?
Abilities.Count - 1 : currentAbilityIndex - 1;
break;
case -1:
//Ability index sets to count - 1if direction moves below 0
currentAbilityIndex = currentAbilityIndex == Abilities.Count - 1 ?
0 : currentAbilityIndex + 1;
break;
}
Off(); //Disable exited mode
CurrentAbility = Abilities[currentAbilityIndex]; //Set ability
On(); //Enable entered mode
mFD.SetAbility(); //Setup ability in MFD
currentChangeModeTime = changeModeCooldown; //Reset cooldown
}
internal virtual void On() => CurrentAbility.On();
internal virtual void Off() => CurrentAbility.Off();
}
}
Here is the interface for the mode abilities:
using Entities.Player;
using System.Collections;
namespace MFD.Abilities
{
public interface IMFDAbility
{
string AbilityName { get; } //Display name on device
float BatteryDrain { get; } //Battery usage while selected
bool IsOn { get; }
bool CanUse(MultiFunctionDisplay mFD); //Ability check
void Run(MultiFunctionDisplay mFD); //Constant update when ability is selected
void Activate(MultiFunctionDisplay mFD); //Use ability
void On(); //Initialize before Running
void Off(); //Initialize before Running another ability
//Pass all variables needed from MFD on install
void Connect(MultiFunctionDisplay mFD);
}
}