!!! THIS IS NOT DONE YET !!!
Player
Right so the player is built up like this in unity;

Player

Character

Root

Capsule

Camera Target

Camera

Spring

CinemachineCamera

attackBoxCenter

This is how it is structured.
Let's begin with the camera.
Camera

Camera → PlayerCamera.cs

Spring → CameraSpring.cs & cameraLean.cs

CinemachineCamera
// The actual camera

attackBoxCenter
// This doesn't have a script but is used by another script

Let's go to PlayerCamera.cs first. This is taking care of 2 things mainly;
1. Cameras Position.
2. Cameras Rotation.
[SerializeField] private float sensitvity = 0.1f;
private Vector3 _eulerAngles;
public void Initialize(Transform target)
{
transform.position = target.position;
transform.rotation = target.rotation;

transform.eulerAngles = _eulerAngles = target.eulerAngles;
}

public void UpdateRotation(CameraInput input)
{
_eulerAngles += new Vector3(-input.lookVec.y, input.lookVec.x) * sensitvity;
_eulerAngles.x = Mathf.Clamp(_eulerAngles.x, -89.0f, 89.0f);
transform.eulerAngles = _eulerAngles;
}

public void UpdatePosition(Transform target)
{
transform.position = target.position;
}
It is a fairly small script.
But look at it with me.
Initialize, is handling just setting the camera where it should be and with what rotation it should have.
It takes a target transform to do this.
Keep in mind the target as a thing.

UpdateRotation, is where we update the angles of the camera.
We rotate it based on a CameraInput.
CameraInput, is a struct that contains a Vector2.
You can probably surmise that this is getting information from the mouse and rotating accordingly.
We do clamp the X axis cus other wise you can do this

// GIF OFF THE PROBLEM

This is all that PlayerCamera.cs is.
So let's look at the next script; *CameraSpring.cs*
Now remember the structure.
The camera, like the actual camera, in this case called CinemachineCamera, sits on this spring that hosts this script and the next one we will talk about.
public void UpdateSpring(float deltaTime, Vector3 up)
{
transform.localPosition = Vector3.zero;

Spring(ref _springPosition, ref _springVelocity, transform.position, halfLife, frequency, deltaTime);

var localSpringPosition = _springPosition - transform.position;
var springHeight = Vector3.Dot(localSpringPosition, up);

transform.localEulerAngles = new Vector3(-springHeight * angularDisplacement, 0.0f, 0.0f);
transform.localPosition = _springPosition * linearDisplacement;
}
The update spring function looks like this.
Fairly normal.
Let's move to the line that says Spring.
Spring is a custom math function that looks like this.
private static void Spring(ref Vector3 current, ref Vector3 velocity, Vector3 target, float halfLife, float frequency, float timeStep)
{
var dampingRatio = -Mathf.Log(0.5f) / (frequency * halfLife);
var f = 1.0f + 2.0f * timeStep * dampingRatio * frequency;
var oo = frequency * frequency;
var hoo = timeStep * oo;
var hhoo = timeStep * hoo;
var detInv = 1.0f / (f + hoo);
var detX = f * current + timeStep * velocity + hhoo * target;
var detV = velocity + hoo * (target - current);
current = detX * detInv;
velocity = detV * detInv;
}
Now for the life of me I can not explain this well at all.
But I will show you what this does below.

// make script showing this off here dumbass

if you want to learn more about this then here is the paper on it.

But the effect as you can see is a more “bouncy” camera.
Adding a nice effect to it.

The rest of the UpdateSpring function just updates the Spring's local position and local rotation.

Let's look at the other script called *cameraLean.cs*

Its update Spring function looks like this

public void UpdateSpring(float deltaTime, bool sliding, Vector3 acceleration, Vector3 up)
{
var planarAcceleration = Vector3.ProjectOnPlane(acceleration, up);
var damping = planarAcceleration.magnitude > _dampedAcceleration.magnitude ? attackDamping : decaykDamping;

_dampedAcceleration = Vector3.SmoothDamp(
current: _dampedAcceleration,
target: planarAcceleration,
currentVelocity: ref _dampedAccelerationVel,
smoothTime: damping,
maxSpeed: float.PositiveInfinity,
deltaTime: deltaTime
);

var leanAxis = Vector3.Cross(_dampedAcceleration.normalized, up);

transform.localRotation = Quaternion.identity;

var targetStrength = sliding ? slideStrength : walkStrength;

_smoothStrength = Mathf.Lerp(_smoothStrength, targetStrength, 1.0f - Mathf.Exp(-strengthResponse * deltaTime));

transform.rotation = Quaternion.AngleAxis(-_dampedAcceleration.magnitude * _smoothStrength, leanAxis) * transform.rotation;
}
So what does this function do and why do we do it?
Well the function starts by taking the player's current acceleration and projecting it onto the up direction of the target.
We use this acceleration twice.
Once to get the damping we want.
The other time we put it into the SmoothDamp vector3 function that unity has.

Now that we have done this we have our _dampedAcceleration.
We use this acceleration twice as well.
First we use it in a cross product between our up and acceleration.
This creates what axis we are going to lean on.
Think of it like this: if we are accelerating forwards then we want the camera to lean back.
To create the effect that the player is being pushed by the wind down.
This is also adaptive to whatever else we put in.
If we add a bit of strafing to this we will lean along with it.

In transform.localRotation = Quaternion.identity; we are setting the rotation of the local transform to be the same as the parents transform.

We get the targetStrength out and slap it into a lerp.
Lerping how strong the effect is over time.

Lastly we apply the rotation.
With this we now have this effect.

// Video of the lean effect

That's how the player's camera works.
It's simple but it's really useful.
It's good to add fluff to make the game feel good as well as look good.

This covers all camera scripts but where are all of these updated?
Well inside of the players late update.
Let me show you that before we move on to the PlayerCharacter.cs.
void LateUpdate()
{
var deltaTime = Time.deltaTime;
var cameraTarget = playerCharacter.getCameraTarget();
var state = playerCharacter.GetState();

PlayerCamera.UpdatePosition(cameraTarget);
CameraSpring.UpdateSpring(deltaTime, cameraTarget.up);
cameraLean.UpdateSpring(deltaTime, state.Stance is Stance.sliding, state.Acceleration, cameraTarget.up);
}
Here is where you can see what all of these are getting their inputs.
Let's go top to bottom.

PlayerCamera, only cares about getting the target

CameraSpring, wants both delta time and the target's up vector.
Remember it needed the up vector to calculate the springHeight.
deltaTime was used for the spring function.

cameraLean, needs the most out of all of them.
deltaTime for is lerp and damping functions.
state.Stance is Stance.sliding” just translates to a bool, which is true if you are sliding, otherwise it's false.
state.Acceleration, for the current acceleration.
Lastly cameraTarget.up, for axis calculation and acceleration projection.

That is almost everything *camera* related to this project.
But we will move on here to PlayerCharacter.cs
PlayerCharacter.cs
So this will be long, like the longest part of the player long.
PlayerCharacter.cs handles all of the movement stuff.
So what is the difference between the PlayerCharacter and the Player scripts?
Well let's look back at the hierarchy first.

Player → Player.cs

Character → KinematicCharacterMotor.cs
& PlayerCharacter.cs & CutAndParry.cs

Root // holds visual components of the player

Capsule // The visual of the player

Camera Target // the target the camera is going towards

So they are separated by hierarchy.
So what are their internal differences?
Well the Player.cs handles delegation.
Remember how the camera got all of its updates from the Player.cs script well Player.cs does that for all of the player stuff.
Inputs and updates are its domain.
It gets information and delegates it down to the other scripts as needed.

KinematicCharacterMotor.cs is what I am working with here.
Remember this is a specialisation project and I wanted to learn more about making movements in unity.
Using this motor was the point of this.

CutAndParry.cs is something I didn't finish but you'll get to see later.
(This thing builds towards a project im working on now *After Cut*)

Now because PlayerCharacter.cs is where movement is handled, so it is working a lot with the motor.
I will be going movement by movement.
We will look at 3 functions BeforeCharacterUpdate, AfterCharacterUpdate and UpdateVelocity.
There are like 17 functions in here but the other 14 functions are things like UpdateInput and UpdateBody.
They don't do enough interesting stuff or in interesting ways for me to show off.

Well will go in this order BeforeCharacterUpdateUpdateVelocityAfterCharacterUpdate.
BeforeCharacterUpdate, happens before velocity and physics are calculated.
UpdateVelocity, is called before the motor does its thing.
AfterCharacterUpdate, is called when the motor wants to finish up everything in the update cycle.

Now lets actually start
public void BeforeCharacterUpdate(float deltaTime)
{
_tempState = _state;

if (_requestedRun && _state.Stance is Stance.Stand && !_requestedCrouch)
{
_state.Stance = Stance.Running;
}

if (_requestedCrouch && (_state.Stance is Stance.Stand || _state.Stance is Stance.Running))
{
_state.Stance = Stance.Crouch;
motor.SetCapsuleDimensions(
radius: motor.Capsule.radius,
height: crocuhHight,
yOffset: crocuhHight * 0.5f
);

root.localPosition = new Vector3(0.0f, 0.5f, 0.0f);
}
}
This is only really handling 2 things.
1. If you are running or if you are crouching.
2. What height you should be.

We do this here because a lot of things that come up late, like your velocity, is dependent on your state.

Now let's look at the big boy and the thing I'm going to be dividing up into movement mode segments.

There are 7 different movement modes

3 of which are;
1. sliding
2. wallruning
3. dashing

The rest are
1. Standing, as in standing still
2. Crouching
3. Running
4. airborn, being in air

As you can tell, it's a bit less interesting.
So I will be going through the first 3.
Starting with;
Slide
if (moving && crouching && shouldSlideStart && (wasStanding || wasInAir || wasRuning))
{
_state.Stance = Stance.sliding;

if (wasInAir)
{
Vector3 groundNormal = motor.GroundingStatus.GroundNormal;
Vector3 tangent = Vector3.ProjectOnPlane(currentVelocity, groundNormal);

// Keep tangential (momentum along slope), discard into-ground velocity
if (tangent.sqrMagnitude > 0.001f)
currentVelocity = tangent.normalized * currentVelocity.magnitude;
}

float effectiveSlideStartSpeed = slideStartSpeed;
if (!_lastState.Grounded && !_requestedCrouchInAir)
{
effectiveSlideStartSpeed = 0.0f;
_requestedCrouchInAir = false;
}

float slideSpeed = Mathf.Max(effectiveSlideStartSpeed, currentVelocity.magnitude);
currentVelocity = motor.GetDirectionTangentToSurface(
direction: currentVelocity,
surfaceNormal: motor.GroundingStatus.GroundNormal) * slideSpeed;
}
NOTE: this is all happeing only if you are currently grounded.

This part of the slide is setting up a few things.
It sets up the stance and the velocity.
That's really it.
What we get out from this function in the end is really just the slide velocity but projected onto the ground the player is standing on.
By projecting it onto the ground the player is on we get the slide to follow said ground.

The next part of the slide is here
if (_state.Stance is Stance.Stand or Stance.Crouch or Stance.Running){
/// CODE THAT DOESNT MATTER FOR SLIDE
} else
{
currentVelocity -= currentVelocity * (slideFriction * deltaTime);

Vector3 forceOnSlope = Vector3.ProjectOnPlane(
vector: -motor.CharacterUp,
planeNormal: motor.GroundingStatus.GroundNormal
) * slideGravity;

currentVelocity -= forceOnSlope * deltaTime;

float currentSpeed = currentVelocity.magnitude;
Vector3 targetVelocity = groundedMovement * currentVelocity.magnitude;
Vector3 steerVelocity = currentVelocity;
Vector3 steerForce = (targetVelocity - steerVelocity) * slideSteerAcceleration * deltaTime;
steerVelocity += steerForce;
steerVelocity = Vector3.ClampMagnitude(steerVelocity, currentSpeed);

_state.Acceleration = (steerVelocity - currentVelocity) / deltaTime;
currentVelocity = steerVelocity;

if (currentVelocity.magnitude < slideEndSpeed) _state.Stance = Stance.Crouch;
}
Now this function handles 4 things that are applied to the slide velocity.
Let's go down the list shall we?
First we apply friction to the slide, so that we slowly lose momentum.

After that we apply slideGravity forcing the player towards the slope.
Increasing the amount of force applied based on a projected vector between the players downvector and the ground's normal vector.

Now we have the strafing part.
We get the targetVelocity we want by taking the groundedMovement, which is our input project onto the ground's normal, and our current velocity's magnitude.
This basically just changes the direction of our velocity.
Magnitude has now gotten a new direction.

The steerVelocity we currently have is just our current velocity.

The steerForce is gotten by simply get the difference between our steerVelocity and our targetVelocity multiplied by our slide acceleration.
We add this force onto our steerVelocity, we are now basically moving our steer towards our targetVelocity.

Lastly we clamp steerVelocity's magnitude to be that of the targetVelocity.
Then we just apply it and update our acceleration.

Last thing this function does is kick us out of sliding if we get to be too slow.

That's all for sliding.
And that was only sliding.
See why I split it?
Wallruning
// wallruning
if (!motor.GroundingStatus.IsStableOnGround && _wallrunColdownTimer < 0.0f && _state.Stance is not Stance.Dashing && !_requestedJump && _isGettingMovementInput && (currentVelocity.sqrMagnitude > wallrunFrechold || _state.Stance is Stance.Running) && (rightWall || leftWall))
{
{// start wallrun
_state.Stance = Stance.Wallruning;
_ungrounedDueToJump = false;
currentVelocity = new Vector3(currentVelocity.x, 0.0f, currentVelocity.z);
}
{// during wallrun
Vector3 wallNormal = rightWall ? rightHit.normal : leftHit.normal;
Vector3 wallForward = Vector3.Cross(wallNormal, motor.CharacterUp);

float chosenStartSpeed = Mathf.Max(wallrunStartSpeed, currentVelocity.magnitude);
float effectiveWallrunSpeed = Mathf.Lerp(
a: chosenStartSpeed,
b: wallrunEndSpeed,v t: 1.0f - Mathf.Exp(-walkResponse * deltaTime));

Vector3 effectiveForward = (motor.CharacterForward - wallForward).magnitude > (motor.CharacterForward - -wallForward).magnitude ? -wallForward : wallForward;

Vector3 tragetVelocity = (effectiveForward + groundedMovement).normalized * effectiveWallrunSpeed;
Vector3 moveVelocity = Vector3.Lerp( // not learping shit huh
a: currentVelocity,
b: tragetVelocity,
t: 1.0f - Mathf.Exp(-wallrunResponse * deltaTime)
);
_state.Acceleration = moveVelocity - currentVelocity;
currentVelocity = moveVelocity;
}
}
This one is long so Ill try my best to condecene it down.
When wallrunning the effect you want is just pushing the player towards the wall and chaninging the vellocity to fallow it intead of the ground or their forward.
Some checks are in there to solve for problems like the player looking to far away form the wall.
We also want the players speed to constanly lowerd as they wallrun and then end the wallrun if they end up going to slowly.

So how do we achive this?
Well lets look att “durring wallrun”
The first part is getting the the fraward direction we need to be moving.
A cross procuct of the players up and the walls normal achives this nicely.

Then to achive the slowing down of the player we just lerp our start speed towards our end speed.
We then use this multiplied with the forward we made earlier to get our target velocity.
We lerp down our current vel towards the traget and then update the acceleretion and the velocity variables.

Now you might have noticed the comment “// not lerping shit huh” well this should be working in therory but in practice well

// VIDEO OF WALLRUNING, -.-' im not home rn so please imagine a somewhat good video

But that is the main part of wallrunning explained.
Lets now look at the dash.
Dashing
Dash wants to achive one thing; push the player forward with some impulse of speed.
Now I have 2 verstion of dashing depending on if you are wallrunning or not.
Lets look at the not wallruning one first.
_state.Stance = Stance.Dashing;
Vector3 groundedForward = motor.GetDirectionTangentToSurface(direction: motor.CharacterForward, surfaceNormal: motor.GroundingStatus.GroundNormal);
motor.ForceUnground(time: 0.0f);

float effectiveDashSpeed = motor.GroundingStatus.IsStableOnGround ? groundDashSpeed : airDashSpeed;

if (groundedMovement.magnitude > 0.0f) currentVelocity += groundedMovement * effectiveDashSpeed;
else currentVelocity += groundedForward * effectiveDashSpeed;
So we do 3 things here;
1. We get the forward
2. We get the speed
3. We apply everything
Getting the forward is easy we callculate the tangent between the players forward and and the ground normal.
Tangent gets us this line that is dependent on the surface.
Then we take the direction of this line as our forward.

To get the speed we just check if we are currently grounded or not.

And then to apply all of this we just check if groundedMovement is currently giving us anything by checking in magnitude.
groundedMovement tells us if the player is currently giving any input and what direction that input is in.
So, after that we take the direction, we want to dash in multiplied by the speed we want to add and add it to the current velocity.

That’s it for the non wallrun dash.
_state.Stance = Stance.Dashing;
_wallrunColdownTimer = wallrunColdownTime;
Vector3 wallNormal = rightWall ? rightHit.normal : leftHit.normal; // normals can at time point towards the wall fuck

Vector3 comebinedDir = (wallNormal * 0.3f) + motor.CharacterForward.normalized;

currentVelocity = new Vector3(0.0f, currentVelocity.y, currentVelocity.z);

currentVelocity += comebinedDir * airDashSpeed;
This one is only really different in the direction that is applied and the fact we take currentVelocity and remove the right/left force setting it to 0.
So to get the direction we take the normal of the wall and the character forward and add them together.
Note that we are making the input only 30% of its normal size, making it so it affects the outcome way less the direction the player is faceting.
After that we are doing basically just what we have done before.

And that is the dash.
And everything for the player that I wanted to show.

Now let’s see the Player.cs before we move onto the enemy
Player.cs
Like said before the Player.cs script handels delegation to the other scripts so im just going to show how it delegates and how inputs are updated.
var characterInput = new CharacterInput
{
// movement input
Rotation = playerCamera.transform.rotation,
Move = input.WASD.ReadValue<Vector2>(),
Jump = input.jump.WasPressedThisFrame(),
JumpSustain = input.jump.IsPressed(),
Crouch = toggleCrouchSlide ? (input.crouch_slide.WasPressedThisFrame() ? CrouchInput.toggle : CrouchInput.None) : (input.crouch_slide.IsPressed() ? CrouchInput.Crouch : CrouchInput.Uncrouch),
Run = toggleRun ? (input.run.WasPressedThisFrame() ? RunInput.toggle : RunInput.None) : (input.run.IsPressed() ? RunInput.Run : RunInput.Unrun),
Dash = input.dash.WasPressedThisFrame(),
};
Here a new character input is created and information filled in based on what the player is doing.
Lets look at a single line of this as that will be enough to explain most of what is happening here.

We do it like this because this means that all our calculations uses the same data.
From the players rotation to the inputs, all of it is the same across all of the math we are doing.
Run = toggleRun ? (input.run.WasPressedThisFrame() ? RunInput.toggle : RunInput.None) : (input.run.IsPressed() ? RunInput.Run : RunInput.Unrun),
Run is just a bool that is asking if a certain input was pressed this frame or not.
But run is also a togglable value so there is a check for that as well.
All of these inputs is then used down the line mainly as you saw in the character and camera.
playerCharacter.UpdateInput(characterInput);
playerCharacter.UpdateBody(deltaTime);
thta
Enemy Ai
EnemyBrain.cs
The EnemyBrain.cs was made to basicly just start up the ai. The actuall thinking is done in EnemyStateMachine.cs.
EnemyBrain.cs has only really 2 functions. Start and Update
Update calls the statemachine to tick function.
Start looks like this
This really just get the reference to the enemy, creates a new state machine and sets up a few states.
Lets look at the states that are bing made here

EnemyState_RunToCover.cs
before we go into to much of the what and the how of each state I should start by saying that all of these are inharenting from an interaface called iState.
iState contains 4 functions;
Tick, bascily this states update cycle
OnEnter, called once you enter the state
OnExit, called once you leave the state
GizmoColor, returns a color for debuging

This is an interface so that I am forced to implement all the functions needed for each state.

public void OnEnter()
{
Cover nextCover = this._coverArea.GetRandomCover(_enemyReferences.transform.position);
_enemyReferences._navMeshAgent.SetDestination(nextCover.transform.position);
}
Now how this state works is that once we have entered this state we get a random cover from the _coverArea.
_coverArea is just a holder of Cover(s) which are just points in space.
So in essesce, the enmey is just asks for a random point on this cover to walk to.
then the enemy walks there usuing the navmesh.

public bool HasArrivedAtDestination()
{
return _enemyReferences._navMeshAgent.remainingDistance <= 0.1f;
}
Now this is a very simple function, but this is not used any where inside of this state of the player. Instead you will see it
Here, inside the enmey brain
// Transition
At(runToCover, delayAfterRun, () => runToCover.HasArrivedAtDestination());
At(delayAfterRun, inCover, () => delayAfterRun.IsDone());
Guns
core
platform.m_time = std::clamp(platform.m_time += (std::clamp(_delta_time, 0.0f, 1.0f) * time_flow * platform.m_point_travel_time) / platform.diff, 0.0f, 1.0f);