These should be a very obvious fix…
as much as I’d love to agree, I can’t find a single reference to ‘InteractWithWaterEvent’…
I thought we got rid of any connections to that event…
You may have a caching issue.
You may need to restart Unity and your Code Editor.
If that doesn’t work, you’ll need to delete the Library, obj, and Temp folders (with everything closed)
Then restart the editor and Unity.
OK so to keep it short and sweet, my player does remove water from his ‘WaterFinder’ detector, but he does NOT add any sort of water, so technically the Swimming State never gets called to begin with (I still don’t understand how you can remove something you never even added). I know because the Debugger of the ‘AddTarget()’ of this ‘WaterFinder.cs’ script never got called:
using RPG.Core;
using UnityEngine;
public class WaterFinder : RangeFinder<Water>
{
protected override void AddTarget(Water target)
{
base.AddTarget(target);
target.OnWaterDestroyed += RemoveTarget;
Debug.Log($"WaterFinder: Adding {target.name}");
}
protected override void RemoveTarget(Water target)
{
base.RemoveTarget(target);
target.OnWaterDestroyed -= RemoveTarget;
Debug.Log($"WaterFinder: Removing {target.name}");
}
}
And I have no idea why… I’m trying to figure it out, but I genuinely have no clue
The debugger for the 'RemoveTarget() does get called though
Once again you’re dragging me into working on something I clearly wasn’t planning on.
- Is the Water script on the collider for the water?
- Is the Water script’s IsValid returning True?
Go into play mode and move your character into the water. Pause the game and go into the scene view, are the colliders for the water and the WaterFinder intersecting?
I have no idea what to say - at least they’re fun to implement, right?
Yes (albeit it’s a little shallow, because the water box is a zero on the y-axis, but it gets detected)
based on debugging, no it doesn’t. It returns false, as shown here:
for this ‘isValid’ function:
bool ITarget.IsValid()
{
Debug.Log($"Water Validity: {Vector3.Distance(player.transform.position, transform.position) <= 0f}");
return Vector3.Distance(player.transform.position, transform.position) <= 0f;
}
and that’s what I got right after splashing into the water, somehow…
well… all the code above only occurs when I splash my player into the water (on the back of a horse for now), so yes I believe that they do intersect
That will never ever return true. Distance will never ever return a negative number, and it will only return 0 if the player is quite literally standing on the origin of the water which is statistically impossible.
It’s also unneeded. If the collider has caused a trigger, then the Player IS by definition in range. IsValid() is a function so that if the enemy is dead or some other strange circumstance is happening, it can filter out the dead wood. In this case, there is never a reason why the water would not be valid.
bool ITarget.IsValid() => true;
AT LAST, WE FINALLY GOT INTO THE SWIMMING STATE
For today I’ll give implementing the logic for it a go and see how we can improve it - I believe I have the animations for it. Before I do that though, I shall check the dropping item stuff gravity issue
kinda makes sense… I didn’t think of this before…
OK so… I developed my Swimming state as follows:
using RPG.States.Player;
using UnityEngine;
public class PlayerSwimmingState : PlayerBaseState
{
public PlayerSwimmingState(PlayerStateMachine stateMachine) : base(stateMachine) {}
private static readonly int FreeLookSwimBlendTreeHash = Animator.StringToHash("SwimBlendTree");
private static readonly int FreeLookSwimSpeedHash = Animator.StringToHash("FreeLookSwimSpeed");
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(FreeLookSwimBlendTreeHash, stateMachine.CrossFadeDuration);
stateMachine.WaterFinder.OnTargetRemoved += InputReader_HandleFreeLookEvent;
}
public override void Tick(float deltaTime)
{
Vector3 movement = CalculateMovement();
Move(movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
if (stateMachine.InputReader.MovementValue == Vector2.zero)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0, AnimatorDampTime, deltaTime);
if (stateMachine.Animator.GetFloat(FreeLookSwimSpeedHash) < 0.1f)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f);
}
return;
}
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
}
public override void Exit()
{
stateMachine.WaterFinder.OnTargetRemoved -= InputReader_HandleFreeLookEvent;
}
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;
Vector3 right = stateMachine.MainCameraTransform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
Vector3 movement = right * stateMachine.InputReader.MovementValue.x;
movement += forward * stateMachine.InputReader.MovementValue.y;
return Vector3.Min(movement, movement.normalized);
}
private void InputReader_HandleFreeLookEvent(Water water)
{
stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
}
}
(Any advise on optimizing this code is welcome )
I developed an entirely new blend tree, and variables to go with it, for the swimming state.
However, I still have one little problem:
Because this script looks a bit too similar to ‘PlayerFreeLookState.cs’, when my character enters into the water, he tends to want to stick to the ground as well, and because the way swimming currently works is by detecting the surface of the water, this means he switches to ‘PlayerFreeLookState.cs’ under water again (which is NOT what I want him to do), because… well… underwater is no longer close to the surface to allow the trigger to work anymore
My question now is, how do we get the player to safely stick to the surface of the water for ‘PlayerSwimmingState.cs’? Apart from that, this pretty much works well as expected (until I decide to get into diving, which sounds like a serious challenge to me, now that I think of it…)
Try putting an invisible non trigger collider in the water at a height that still allows the player’s WaterFinder to hit the water’s Water trigger collider, but doesn’t allow him to sink too far as to drown
ahh… this was a bad idea (I’m assuming all I had to do was place in a non-trigger box collider), mainly because it messes with both the player and the animals’ swimming systems, because they keep colliding with the invisible box. Any alternative solutions you can think of, or can we do it through code instead? That would be marvelous
That was my brilliant idea for the day. TBH, I’m not sure at the moment, and I have to go work.
ahh… can’t blame ya, I’ve been trying for a while as well but to no avail. Have a great day at work, but please don’t forget about this one
and whilst we’re at it, can we also have a look at why the swim idle animation never plays in idle positions? I checked the thresholds and what-not in the Animator, all seemed fine there:
The swimming state is your baby. So part 1 of your challenge is to figure out why the swim Idle isn’t working.
Part 2 of your challenge:
I’ve come up with an idea for a solution similar to how isblocking is manged in the blocking states and Health…
- You need a private variable in the ForceReceiver
bool isSwimming
- You need public setter for that variable.
- In ForceReciever.Update(), you need to check to see if isSwimming is true and make that the first if clause. If it’s true, the y speed (I’m at work, I don’t have the name of that variable, but you’ll find it) should be zero, otherwise continue on with the other two if statements (don’t forget to add else before the first if in that chain.
- In your Swimming states, you’ll need to tell the ForceReceiver in Enter that you’re swimming. In Exit, you’ll need to tell the ForceReceiver that you’re !swimming.
So that’s your challenge. I believe you have all the tools needed to make this happen.
smart solution. That actually solved it, thank you Brian. Here’s how I went through it:
- in ‘ForceReceiver.cs’, I created a check condition for ‘isSwimming’, created a ‘SetIsSwimming()’ function that sets it up, and then adjusted ‘Update()’ accordingly:
private bool isSwimming;
public void SetIsSwimming(bool isSwimming)
{
this.isSwimming = isSwimming;
}
// new 'Update()' function:
private void Update()
{
if (isSwimming)
{
verticalVelocity = 0f;
}
else if (verticalVelocity < 0f && controller.isGrounded)
{
verticalVelocity = Physics.gravity.y * Time.deltaTime;
}
else
{
verticalVelocity += Physics.gravity.y * Time.deltaTime;
}
impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);
// if the squared magnitude of the impact is below the 'minimumImpactVelocity',
// reset the impact and enable the agent (i.e: he took the impact, now get back to normal)
if (forceActive)
{
if (impact.sqrMagnitude < minimumImpactVelocity)
{
impact = Vector3.zero;
forceActive = false;
OnForceCompleted?.Invoke();
if (agent) agent.enabled = false;
}
}
}
and then in ‘PlayerSwimmingState.cs’:
// in 'Enter():'
stateMachine.ForceReceiver.SetIsSwimming(true);
// in 'Exit():'
stateMachine.ForceReceiver.SetIsSwimming(false);
and that did work marvelously
I honestly am clueless about this. Matter of fact, I have a very similar issue with my enemy, because he won’t play the movement animation properly when he’s patrolling, but because I got too occupied with more mechanics, I totally forgot about this one…
Anyway, I’ll go have a peak at what you mentioned regarding flying pickups before I get into diving (if you got swimming, you gotta have diving… I promise it’ll be fun xD). This one might take a bit of time before coding though, because I have absolutely no clue as of how to work the logic out yet
OK so I have a bit of an update about this one. First of all, my parameter I was using to tune the blend tree was the wrong one, so I fixed the name and got it to work again
The second problem I had was the magnitude limited to just 1, with my thresholds being set to 3 for normal swimming speed, and 6 for fast swimming. I tuned the values down to 1 and 1.5 (for the thresholds, the swimming speed force in the player inspector is not affected by this), and the animations work exactly as expected
The only problem is, and this one goes for the freeLook Blend tree as well, is that there is no speed multiplier state. In other words, I was hoping for something that when I HOLD DOWN the shift button, I can sprint (in other words, a running mode)
I have to be somewhere else right now, but as soon as I’m home I’ll work on trying to figure this out (I just know for a fact that this magnitude needs to crash up to 1.5 when that animation plays)
You know how to add a shift key to the Controls…
You know how to set a bool true if a button is pressed and expose that bool publicly in InputReader
You know how to read that bool from PlayerFreeLookState and your SwimmingState
You know how to multiply a value by 2 (say the freelookspeed)… do that when the bool is true.
for running, that was quite easy… still haven’t worked on trying to implement the same for my swimming speed, but here’s what I did for running (just posting it here to seek a code review. If there’s a mistake, please let me know):
-
Add ‘OnSpeeding’ (I called it ‘OnSpeeding’ because if we speed in multiple states, it needs a generic name) in your Action Map, and assign it any action button you want (for me personally, it was the ‘Left Shift’ key. If you’re going my route though, you may want to deactivate your sticky keys on your Windows 10, idk about Windows 11)
-
in ‘InputReader.cs’, add the following event, boolean and function:
// boolean (I am using a boolean to indicate that this function only works if you hold down the shift key):
public bool IsSpeeding {get; private set;}
// event:
public event Action SpeedEvent;
// function:
public void OnSpeeding(InputAction.CallbackContext context)
{
if (context.performed)
{
SpeedEvent?.Invoke();
IsSpeeding = true;
}
else if (context.canceled) IsSpeeding = false; // boolean, indicating that this action is only done when the key mapped to this is held down
}
in ‘PlayerFreeLookState.cs’:
// in the 'Tick()' method, right before setting the float and facing your movement direction in the end of the function, add this 'if' statement:
if (stateMachine.InputReader.IsSpeeding)
{
stateMachine.Animator.SetFloat(FreeLookSpeedHash, 2 * movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);
return;
}
I think this was it… (still don’t know why I got a ‘SpeedEvent’ event, but hey it’s there just in case
as for swimming, here’s what I did:
// in 'PlayerSwimmingState.Tick():'
if (stateMachine.InputReader.IsSpeeding)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.5f * 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
return; // bas khallas, don't need to move forward now...
}
else Move(movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
I placed this on top of the Tick function, right under declaring the ‘movement’ variable, before move (which, as you can tell, I placed it as an ‘else’ statement). This made more sense mathematically, so… I did it
khallas (I’m teaching you arabic as we go, khallas means ‘done’ ), now we can investigate the diving state (I’m not going to lie, I’ve been desperately wanting to get into this one, a little more than swimming). I’ll go do some research and see how I want to approach this one
so my ‘PlayerFreeLookState.Tick()’ now looks like this:
public override void Tick(float deltaTime)
{
// Uncomment the if statement below, if you want the player to be able to attack enemies outside of combat targeting mode:
/* if (stateMachine.InputReader.IsAttacking)
{
stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
return;
} */
Vector3 movement = CalculateMovement();
Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);
// IsAttacking links to the left mouse button, so we are dealing with player left mouse button expectations here (individually, for dialogues, shops, combat, banking, etc...):
if (stateMachine.InputReader.IsAttacking) // Change 'IsAttacking()' to 'IsInteracting()', by adding a button in the controls and then coding it in 'InputReader.cs', similar to attack, and then change it here (along with the function name)
{
HandleAttackButtonPressed();
return;
}
if (stateMachine.InputReader.MovementValue == Vector2.zero)
{
stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0, AnimatorDampTime, deltaTime);
if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < .1f) stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
return;
}
if (stateMachine.InputReader.IsSpeeding)
{
stateMachine.Animator.SetFloat(FreeLookSpeedHash, 2 * movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
Move(movement * stateMachine.FreeLookMovementSpeed, deltaTime);
return;
}
stateMachine.Animator.SetFloat(FreeLookSpeedHash, movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
}
and my ‘PlayerSwimmingState.Tick()’ looks like this:
public override void Tick(float deltaTime)
{
Vector3 movement = CalculateMovement();
if (stateMachine.InputReader.IsSpeeding)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 1.5f * 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
return; // bas khallas, don't need to move forward now...
}
else Move(movement * stateMachine.FreeLookSwimmingMovementSpeed, deltaTime);
if (stateMachine.InputReader.MovementValue == Vector2.zero)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0, AnimatorDampTime, deltaTime);
if (stateMachine.Animator.GetFloat(FreeLookSwimSpeedHash) < 0.1f)
{
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, 0f);
}
return;
}
stateMachine.Animator.SetFloat(FreeLookSwimSpeedHash, movement.magnitude, AnimatorDampTime, deltaTime);
FaceMovementDirection(movement, deltaTime);
}
Anyway, I’ll go research diving a bit, so I can see how I can create a diving state that comes from either jumping state (if you jump into the water, now you’re in diving, until you press whatever button you want to press to revive back to the surface. I think for this one, another ‘WaterFinder’ trigger can be placed a few more steps higher than the players’ head, and the trigger will be named ‘DiveTrigger’), or from swimming state (if you hit the control button, but no booleans for this one because… well… it’s not controlled by holding down a button. That would cause A LOT of strain on the players’ hands)
So my plan is as follows:
- Control button gets you to start diving (without any of the WASD or Arrow Keys, you’re vertically diving downwards)
- Space button gets you to revive to the water surface (again, without any of the WASD or Arrow Keys, you’re vertically diving upwards)
- (long pressing) Pressing any of the movement (Arrows or WASD Keys) in combination with (long pressing) the control means you dive and move in a direction, and in combination with the space key means you revive in whatever direction you’re pressing as well
Haven’t figured out a key layout for sprint-diving yet, without driving the players nuts in the key presses…
Any movement (WASD/Arrow Keys) underwater without pressing any of these keys (Control or Space) means you’re moving underwater at a constant height
I might find this useful though (problem is, I literally have no idea where to even start implementing this, mainly because it looks so big and scary…), at least the Blend Tree over there: UnderWater - Animal Controller
I was testing out diving, and I realized a problem with my Swimming State. Occasionally, the players’ WaterFinder will actually go underneath the water (I have waves of water coming at my player), and sometimes the difference in height between those waves and the players’ waterFinder collider are so big, he literally falls under the water and goes into FreeLook state. Any idea how to fix this? I can’t continue with developing the diving system before fixing this (and developing the diving system, to the requirements I have above, is quite hard if I’m being honest. I have already taken a few steps ahead, but… now it’s a complete mess)
Can we make that float (and the player will follow with it) over the water surface, by any chance? (I can expand the box collider, but then the player will slowly keep falling under the water over time, and let’s just say this will be a weird one. So… yup, will need help here PLEASE)
As for the diving state. Here’s what I did so far:
-
In the Action Map, I placed a Diving Button (Left Control) and a Revive from underwater (to rise back to the top of the water) button (Space Bar, which is occupied by the Jump state, but these two are not in contact, so I think I should be fine here)
-
In ‘InputReader.cs’, I added the following:
// ----- Diving Booleans ------
public bool IsDiving {get; private set;}
public bool IsReviving {get; private set;}
// ----------------------------
// ----- Diving Events -----
public event Action DiveEvent;
public event Action ReviveFromUnderwaterEvent;
// -------------------------
// --------------------- DIVING Events --------------------------------
public void OnReviveFromUnderwater(InputAction.CallbackContext context)
{
if (context.performed)
{
DiveEvent?.Invoke();
IsDiving = true;
}
else if (context.canceled) IsDiving = false;
}
public void OnDiveUnderwater(InputAction.CallbackContext context)
{
if (context.performed)
{
ReviveFromUnderwaterEvent?.Invoke();
IsReviving = true;
}
else if (context.canceled) IsReviving = false;
}
// --------------------------------------------------------------------
- I started integrating the diving state into my ‘PlayerSwimmingState.cs’, as follows:
// The last if statement check before setting the Animator Float (in 'Tick()')
if (stateMachine.InputReader.IsDiving)
{
stateMachine.SwitchState(new PlayerDivingState(stateMachine));
}
and then I invented my own ‘PlayerDivingState.cs’ (I am yet to invent the 'PlayerReviveFromUnderwaterState.cs script, which does the exact opposite of diving… Do I really need to invent that, or is diving enough and movement + animations can be inversed…?!):
using RPG.States.Player;
using UnityEngine;
public class PlayerDivingState : PlayerBaseState
{
private static readonly int DivingHash = Animator.StringToHash("DiveDownwards");
public PlayerDivingState(PlayerStateMachine stateMachine) : base(stateMachine)
{
}
public override void Enter()
{
stateMachine.Animator.CrossFadeInFixedTime(DivingHash, stateMachine.CrossFadeDuration);
stateMachine.ForceReceiver.SetIsDiving(true);
}
public override void Tick(float deltaTime)
{
if (stateMachine.InputReader.IsReviving)
{
stateMachine.SwitchState(new PlayerReviveFromUnderwaterState(stateMachine));
}
if (stateMachine.InputReader.IsReviving) {
Vector3 movement = CalculateMovement();
// Move(movement * stateMachine.FreeLookDivingSpeed, deltaTime);
FaceMovementDirection(movement, deltaTime);
}
else stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
}
public override void Exit()
{
stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
stateMachine.ForceReceiver.SetIsDiving(false);
}
private Vector3 CalculateMovement()
{
Vector3 forward = stateMachine.MainCameraTransform.forward;
Vector3 right = stateMachine.MainCameraTransform.right;
forward.y = 0;
right.y = 0;
forward.Normalize();
right.Normalize();
Vector3 movement = right * stateMachine.InputReader.MovementValue.x;
movement += forward * stateMachine.InputReader.MovementValue.y;
return Vector3.Min(movement, movement.normalized);
}
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);
}
}
The ‘Move’ line in tick is giving me wild Stack overflow errors, and one of them froze my computer before I restarted Unity. What is the cause for this, and how can I get diving to work?
- As for my Animator, I added the following:
The blend tree only contains two states, an idle state (for when you’re underwater), a SwimUnderwater state (these variables are supposed to be controlled through code, under variable ‘FreeLookDiveSpeed’), and then the animations have also added two action states, the DiveUpwards is what’s controlled by the Space Key (it’s the reviving from UnderWater), and the DiveDownwards, which is controlled by the Control Key (diving underwater), both supposedly controlled by the Input Action Map, through code
- Finally, I also added two new checks in ‘ForceReceiver.Update()’:
// variable
[SerializeField] private float divingSpeed = 3.0f;
// private booleans
bool isDiving;
bool isReviving;
// checks in the 'Update()' function:
if (isDiving)
{
verticalVelocity = -divingSpeed;
}
if (isReviving)
{
verticalVelocity = divingSpeed;
}
// setting swimming and diving (individual functions):
public void SetIsDiving(bool isDiving)
{
this.isDiving = isDiving;
}
public void SetIsReviving(bool isReviving)
{
this.isReviving = isReviving;
}
I also had plans to swim underwater in a whole new state, but I’m not sure if my current PlayerSwimmingState.cs can be used for that instead or not…
I’m sorry if that’s A LOT of information, and again I severely apologize for dragging you into this, I just want to get these two out of the way (and believe me, I’m trying so hard myself), mainly because that’s a whole other dimension in the game. So is it worth it? Probably yes. Is it easy? Nope