OK so… This will seem rushed out, and it kind of is, because this topic has been bugging me for a while (I don’t perform well in coding when I know someone is waiting on something from me. It adds pressure on me (no offence by the way)) and I want to focus on my farming system, so please bear with me. I’m sure you’ll find your way around things:
- Create a blend tree for your swimming animations (don’t rely on my values, as they’re dedicated to my system)
- Place an empty gameObject, called WaterFinder on the player, with a rigidbody (Kinematics on, Gravity off)
- Put on a box collider ON the player’s head (not on top of or below it, but on it), hide the mesh renderer and make it a trigger
- Add the ‘WaterFinder.cs’ script below on the box collider from step 3 (ignore the Malbers Stuff. This is an interface I’m using to deal with a mathematical issue):
using UnityEngine;
using RPG.Core;
using RPG.States.Player;
public class WaterFinder : RangeFinder<Water>
{
// This script sits on the Player's 'WaterFinder' component, allowing interaction
// between the Player and the Water. If true, the player can switch to his 'PlayerSwimmingState.cs'
// from 'PlayerStateMachine.cs', allowing Swimming in the game
private PlayerStateMachine playerStateMachine;
// (TEST) MALBERS TRACKER
private IMalbersInputHandler inputHandler; // same thing as 'PlayerStateMachine' I guess...
void Awake()
{
playerStateMachine = FindObjectOfType<PlayerStateMachine>();
// TEST:
inputHandler = GetComponentInParent<PlayerStateMachine>(); // same thing as 'PlayerStateMachine' I guess...
}
protected override void AddTarget(Water target)
{
base.AddTarget(target);
playerStateMachine.SetIsSwimmingOrUnderwater(true); // Solution to get the player to stop playing the dodging state whilst swimming, dodging his way to underwater
target.OnWaterDestroyed += RemoveTarget; // you've added the water, you'll need a pulse trigger when it's gone, as an event
// GetComponentInParent<MalbersInput>().DisableInput("Call Mount"); // replaced with 'TEST' below
Debug.Log("Adding WaterTarget");
// TEST - AVOID THE PLAYER FROM CALLING THE ANIMALS WHEN AT SEA:
inputHandler.DisableMalbersInput(); // stops the player from being able to call the animal when in sea (it's a tough solution, but it's the only one I can think of)
}
protected override void RemoveTarget(Water target)
{
base.RemoveTarget(target);
playerStateMachine.SetIsSwimmingOrUnderwater(false); // Solution to get the player to stop playing the dodging state whilst swimming, dodging his way to underwater
target.OnWaterDestroyed -= RemoveTarget; // you've already removed the water, you don't need the 'RemoveTarget()' function anymore...
// GetComponentInParent<MalbersInput>().EnableInput("Call Mount"); // replaced with 'TEST' below
Debug.Log("Removing WaterTarget");
// TEST - AVOID THE PLAYER FROM CALLING THE ANIMALS WHEN AT SEA:
inputHandler.EnableMalbersInput(); // stops the player from being able to call the animal when in sea (it's a tough solution, but it's the only one I can think of)
}
}
- Here’s my ‘PlayerSwimmingState.cs’ script:
using RPG.States.Player;
using UnityEngine;
public class PlayerSwimmingState : PlayerBaseState
{
// SWIM-LIMITER, ENSURES THE PLAYER DOES NOT REVIVE ABOVE A CERTAIN HEIGHT:
private BoxCollider swimLimiter;
// BOAT-FINDER (ALLOWS THE PLAYER TO DRIVE THE NEAREST BOAT):
private BoatFinder boatFinder;
// LAYERMASKS TO ENSURE ANIMALS AND SWIMLIMITER (FOR REVIVING) DO NOT INTERACT, CAUSING THE ANIMALS TO WALK ON BLOCKED NAVMESH:
private int animalLayer;
private int swimLimiterLayer;
public PlayerSwimmingState(PlayerStateMachine stateMachine) : base(stateMachine)
{
swimLimiter = GameObject.Find("SwimLimiter").GetComponent<BoxCollider>(); // NAME-BASED SEARCH (FOR ONE-OFF OBJECT)
boatFinder = stateMachine.BoatFinder;
animalLayer = LayerMask.NameToLayer("Animal");
swimLimiterLayer = LayerMask.NameToLayer("SwimLimiter");
// TEST - 24/6/2024 - CHECK BOTH LAYERS EXIST (-1 MEANS THEY DON'T EXIST):
if (animalLayer == -1 || swimLimiterLayer == -1)
{
// AVOID INTERACTION BETWEEN ANIMALS AND THE SWIM LAYER, IN 'ENTER'
Debug.Log($"Layers 'Animal' or 'SwimLimiter' not found");
}
}
private static readonly int FreeLookSwimBlendTreeHash = Animator.StringToHash("FreeLookSwimBlendTree");
private static readonly int FreeLookSwimSpeedHash = Animator.StringToHash("FreeLookSwimSpeed");
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(FreeLookSwimBlendTreeHash, stateMachine.CrossFadeDuration);
stateMachine.WaterFinder.OnTargetRemoved += InputReader_HandleFreeLookEvent; // RETURN TO FREELOOK IF NO WATER FOUND
stateMachine.ForceReceiver.SetIsSwimming(true);
stateMachine.InputReader.BoatDrivingEvent += InputReader_HandleBoatDrivingEvent; // BUTTON-BASED BOAT DRIVING
// boatFinder.OnTargetAdded += BoatFinder_OnTargetAdded; // PROXIMITY-BASED BOAT DRIVING
// TEST - 24/6/2024 - SET COLLISION IGNORANCE (BUT MAKE SURE THEY BOTH EXIST, FIRST):
if (animalLayer != -1 && swimLimiterLayer != -1)
{
Physics.IgnoreLayerCollision(animalLayer, swimLimiterLayer, true);
}
}
public override void Tick(float deltaTime)
{
Vector3 movement = CalculateMovement();
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.0f * movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
if (stateMachine.InputReader.IsSpeeding)
{
// the squared values in 2nd parameter are to compensate for the blend tree variable, some glitch I found (Bahaa stuff, Brian was not involved in this)
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.414f * 1.414f * movement.magnitude, AnimatorDampTime, deltaTime); // tuning the animation threshold, so he turns to swimming speed state
Move(movement * stateMachine.FreeLookSwimmingMovementSpeed * 1.5f, deltaTime); // actual movement speed
FaceMovementDirection(movement, deltaTime); // turning around when swimming, based on input
return;
}
if (stateMachine.InputReader.MovementValue == Vector2.zero)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f, AnimatorDampTime, deltaTime); // Float, no movement input
if (stateMachine.Animator.GetFloat(FreeLookSwimSpeedHash) < 0.1f)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f);
}
return;
}
if (stateMachine.InputReader.IsDiving)
{
// You're diving now:
stateMachine.ForceReceiver.SetIsDiving(true);
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 2f * 2f, AnimatorDampTime, deltaTime);
Move(movement * stateMachine.FreeLookDivingSpeed + movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
FaceMovementDirection(movement, deltaTime);
return;
}
else
{
stateMachine.ForceReceiver.SetIsDiving(false);
}
if (stateMachine.InputReader.IsReviving)
{
swimLimiter.enabled = true;
if (IsBelowReviveLimit())
{
stateMachine.ForceReceiver.SetIsReviving(true);
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 5f * 5f, AnimatorDampTime, deltaTime);
Move(movement * stateMachine.FreeLookDivingSpeed + movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
FaceMovementDirection(movement, deltaTime);
return;
}
else
{
stateMachine.ForceReceiver.SetIsReviving(false);
}
}
else
{
swimLimiter.enabled = false;
stateMachine.ForceReceiver.SetIsReviving(false);
}
Move(movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime); // actual movement speed
}
public override void Exit()
{
stateMachine.WaterFinder.OnTargetRemoved -= InputReader_HandleFreeLookEvent;
stateMachine.ForceReceiver.SetIsSwimming(false);
stateMachine.InputReader.BoatDrivingEvent -= InputReader_HandleBoatDrivingEvent; // BUTTON-BASED BOAT DRIVING
// boatFinder.OnTargetAdded -= BoatFinder_OnTargetAdded; // PROXIMITY-BASED BOAT DRIVING
// TEST - 24/6/2024 - RESET COLLISION IGNORE (AGAIN, FIRST MAKE SURE THEY BOTH EXIST):
if (animalLayer != -1 && swimLimiterLayer != -1)
{
Physics.IgnoreLayerCollision(animalLayer, swimLimiterLayer, false);
}
}
private bool IsCloseToBoat()
{
Boat nearestBoat = boatFinder.GetNearestTarget();
if (nearestBoat != null)
{
float distanceToBoat = Vector3.Distance(stateMachine.transform.position, nearestBoat.GetComponent<Collider>().ClosestPoint(stateMachine.transform.position));
float boatBoardingDistanceThreshold = 2f;
return distanceToBoat <= boatBoardingDistanceThreshold;
}
return false; // if you're not close to a boat, return this function as a false
}
/// <summary> (TEMPORARILY DISABLED)
/// Proximity-based Boat Driving Function. When activated, the player just needs to be close to the boat to be able to start driving it
/// </summary>
/// <param name="boat"></param>
private void BoatFinder_OnTargetAdded(Boat boat)
{
// no boat-mounting if you're diving, speeding or reviving (avoids jump and flying to space bugs)
if (IsCloseToBoat() && !stateMachine.InputReader.IsReviving && !stateMachine.InputReader.IsDiving && !stateMachine.InputReader.IsSpeeding) {
boat.GetComponent<BoxCollider>().isTrigger = true;
stateMachine.SwitchState(new PlayerBoatDrivingState(stateMachine));
}
// if you're diving or reviving, no boat-driving allowed (so you'll bump into the boat now)
else if (IsCloseToBoat())
{
// Write a debug here, asking the player to stop diving, reviving or speeding when arriving to the boat
boat.GetComponent<BoxCollider>().isTrigger = false;
}
}
private void FaceMovementDirection(Vector3 forward, float deltaTime)
{
if (forward == Vector3.zero) return;
Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
}
private Vector3 CalculateMovement()
{
Vector3 forward = stateMachine.MainCameraTransform.forward; // move forward relative to the camera
Vector3 right = stateMachine.MainCameraTransform.right; // move right relative to the camera
forward.y = 0; // don't go flying...
right.y = 0; // again, don't go flying...
forward.Normalize(); // regardless of the camera's orientation, forward is forward
right.Normalize(); // regardless of the camera's orientation, right is right
Vector3 movement = right * stateMachine.InputReader.MovementValue.x; // accumulating the horizontal direction of movement
movement += forward * stateMachine.InputReader.MovementValue.y; // summing up the horizontal and vertical directions of movement
return Vector3.Min(movement, movement.normalized); // get the minimum movement value, between the vector and the scalar equivalent
}
private bool IsBelowReviveLimit()
{
RaycastHit hit;
if (Physics.Raycast(stateMachine.transform.position, Vector3.up, out hit, stateMachine.ReviveLimitHeight))
{
// limit the reviving to the raycast limit (Vector3.Up ensures the Raycast is fired upwards):
Debug.Log("Can Revive");
return hit.point.y >= stateMachine.transform.position.y;
}
// below revive limit
Debug.Log("Can't revive");
return true;
}
private void InputReader_HandleFreeLookEvent(Water water)
{
stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
}
/// <summary>
/// Function called to enable driving a boat using the 'BoatDriving' ("F") Button
/// </summary>
private void InputReader_HandleBoatDrivingEvent()
{
// Hate to break it to you, but you need permission from a button (again) when you find a boat to drive:
if (stateMachine.BoatFinder.FindNearestTarget())
{
if (stateMachine.InputReader.IsReviving) // AVOIDS A BUG OF THE PLAYER POTENTIALLY FLYING WHEN HE DISMOUNTS THE BOAT LATER ON
{
MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification("Action Not Allowed: \n", "Stop Reviving First");
stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
return;
}
stateMachine.SwitchState(new PlayerBoatDrivingState(stateMachine));
}
}
}
You’ll also need a Swim Limiter, so you don’t revive your way into space. It’s a huge, game-world size box collider, which is invisible to the eye, and isn’t supposed to interact with any moving objects, that only activates when you hold the reviving button (i.e: when you go back to surface to catch some air to breathe). Based on my code, just name it “SwimLimiter” (if you get the name wrong, the ‘GameObject.Find()’ which string-searches for it won’t find it)
(You may have noticed there’s boat driving integrated too. That’s a whole other system for another day. It has it’s fair share of bugs, hence why I have disabled some parts of it)
And one last thing. In your Input Reader controls assignment, try stay away from Shift and Control Keys. They get sticky and don’t always respond for some reason (it’s a Unity problem, based on my testing)