More States

I’ll be returning to do more afterwards, but swimming isn’t in the game plan at all right now. Ranged Weapons, and Abilities are, however.

amazing (because one of my abilities bugged out, but I haven’t touched abilities in so long, so it doesn’t matter right now :sweat_smile:)

not the best start for me… I still can’t get the raycast to detect the water layer for some reason:

// in "PlayerFreeLookState.Tick()":

            // Debug.DrawRay(stateMachine.transform.position + new Vector3(0, 1.5f, 0), Vector3.forward, Color.red);
            if (IsSwimming()) 
            {
                Debug.Log("Player Entered Swimming Mode");
// if true, we shall switch to the swimming state
            }

// 'isSwimming():'
        bool IsSwimming() 
        {
            RaycastHit hit;
            if (Physics.Raycast(stateMachine.transform.position + new Vector3(0, 1.5f, 0), Vector3.forward, out hit, 10f, LayerMask.GetMask("Water"))) 
            {
                Debug.Log("Water Found");
                return true;
            }
            return false;
        }

I placed a ‘Debug.DrawRay()’ to see the raycast being emitted, but to my surprise… it works, but for god knows what reason (I checked the layer on my water, it is assigned to water. I also checked the project settings, water is connected to all sorts of layers), the debugger of either the ‘IsSwimming()’ and the one in tick under the true swim check won’t work…

The idea is simple, if the raycast hits the water layer, switch to the swimming state

It seems like you are reinventing the wheel here. Why not make a water detector that derives from RangeFinder. Adding a Swimming State should be similar to all the other states. That’s the beauty of using a state machine.

because if you’re on a surface and you detect water a few feet below you, the last thing I want is a swimming airborne animation :slight_smile: - currently trying to pass time by re-designing my game world though (mainly because I will need Level Streaming in the future, xD)

That’s not entirely true. You could create a RangeFinder with a small collider (say .4 radius), and place it at 0,1.5,0
Then it would only be true if the character was up to his hips in water

1 Like

OK if we’re taking that path, what will the generic inheritance of the RangeFinder refer to? :thinking: (I haven’t thought of that tbh… and now that I do, I have no idea :sweat_smile:)

If I go the ‘ResourceGathering’ path and do a ‘Swimming’ script, who do I put that script on, the player? (more importantly, what does that script contain?)

And suddenly, I have A LOT of questions… :sweat_smile: (hence why I want the Raycast path)

The simplest way to manage water is to set box collider for the water. When the RangeFinder OnTriggerEnters the water, you’re in water and should start swimming.When the RangeFinder finds you’ve left the water, you should fire an event and stop swimming. Just create a Water class that implements the ITarget interface.

OK I’m not exactly sure if what I did was right or wrong, but I wanted to give it a try. So here’s what I did:

I made a box collider, but when we enter or exit that, things don’t work out…

By “OnTriggerEnter()”, I assumed we were adding and removing targets (I honestly started peaking at ‘ResourceFinder.cs’ at this point in time), so I did that…:

    protected override void AddTarget(Water target)
    {
        base.AddTarget(target);
        target.OnWaterFound += RemoveTarget;
        Debug.Log($"WaterFinder: Adding {target.name}");
    }

    protected override void RemoveTarget(Water target)
    {
        base.RemoveTarget(target);
        target.OnWaterFound -= RemoveTarget;
        Debug.Log($"WaterFinder: Removing {target.name}");
    }

did that too, check above

I had no clue what to put in ‘IsValid()’ for that one, so I kept this class extremely simple:

using RPG.Core;
using UnityEngine;

public class Water : MonoBehaviour, ITarget
{

    public event System.Action <Water> OnWaterFound;

    bool ITarget.IsValid()
    {
        OnWaterFound?.Invoke(this);
        Debug.Log("Entering Water...");
        return true;
    }

}

and that was my entire ‘Water.cs’ script. For my ‘WaterFinder.cs’, here’s what I did:

using System.Linq;
using RPG.Core;
using UnityEngine;

public class WaterFinder : RangeFinder<Water>
{
    public Water GetNearestWater() 
    {
        CurrentTarget = Targets.OrderBy(w => Vector3.Distance(transform.position, w.transform.position)).FirstOrDefault();
        return CurrentTarget;
    }

    protected override void AddTarget(Water target)
    {
        base.AddTarget(target);
        target.OnWaterFound += RemoveTarget;
        Debug.Log($"WaterFinder: Adding {target.name}");
    }

    protected override void RemoveTarget(Water target)
    {
        base.RemoveTarget(target);
        target.OnWaterFound -= RemoveTarget;
        Debug.Log($"WaterFinder: Removing {target.name}");
    }

}

At this point in time, I don’t want to know why it doesn’t work as much as I want to know if I’m even on the right track or not :sweat_smile:

I will also have to mention that ‘WaterFinder.cs’ went on another trigger placed on the player, and ‘Water.cs’ went on the Box Collider (on the water) that detects the player

You’re on the right track.
In this case, the OnWaterFound is unneeded (and sort of useless as you have it now anyways). Where we use these events in the other RangeFinders, it’s a bandaid for the fact that OnTriggerExit is never called if a collider leaves the trigger zone by being destroyed. So, for example, in Pickup, the pickup could leave the RangeFinder’s collider by the RangeFinder moving away, or by the Player picking up the item. If the picked up the Pickup, then we fire that event so that the target can be removed. Same with enemies. If an enemy rudely dies on us, the RangeFinder won’t know that until we rule out the target by subscribing to the Health’s OnDie.

The water should never be destroyed, so the event is not needed. Calling it in IsValid() makes little sense. If you were to call it, it would be in OnDestroy().

using RPG.Core;
using UnityEngine;

public class Water : MonoBehaviour, ITarget
{

    public event System.Action <Water> OnWaterFound;

    bool ITarget.IsValid()
    {
        Debug.Log("Entering Water...");    // Never gets called... :/
        return true;
    }

    private void OnDestroy() 
    {
    OnWaterFound?.Invoke(this);
    }

}

So ‘Water.cs’ is something like this…? Any other changes I need to make? :slight_smile: (I’m re-reading all you mentioned above to try and grasp what was being said)

EDIT: OHHH I GET WHAT YOU WERE SAYING NOW… So these events are basically the escape route that is used rather than ‘OnTriggerExit()’, to ensure that we can quit an event, but under “weird” circumstances (for “OnTriggerExit()”, that is… totally normal for us advanced human beings), like an enemy death or equivalent…?! (Basically I’m assuming ‘OnTriggerExit()’ won’t be able to detect that, hence we use the exit events) OK I’m a little rusty on ideas today, so please bear with me because I’m trying my best here :slight_smile:

Anyway, what other changes do we need to make? (It’s still not working)

LOL I got Malbers’ Horse to be able to swim and not my main character yet… :stuck_out_tongue:

Are you subscribing/unsubscribing to the WaterFinder in the PlayerFreeLookState?

wait, I can do that? I mean the swimming event takes no button inputs, it’s literally just entering a collider zone. How do I even create an event to subscribe to for this one?

My problem here is, there’s no button input that will go to trigger swimming state (it’s a trigger entrance), so probably no code will go into ‘InputReader.cs’, hence why I ask about how to subscribe and unsubscribe to the event in ‘PlayerFreeLookState’, based on everything I saw so far (I can’t find any option in the Action Map for entering triggers of any sort…)

So, how can we do that?

OK so I’m not sure if this is the right or wrong path, but here’s what I tried doing anyway

  1. I didn’t know what in the world to place into my Input Reader Action Map (where you bind keys to specific actions), so I left that part out (I don’t know how to communicate to it that it needs a trigger)

  2. In ‘InputReader.cs’, I injected this new code:

    // TEST
    public bool IsSwimming {get; private set;}

    // TEST
    public event Action InteractWithWaterEvent;

    // TEST (not sure if that's the right or wrong way, since I don't fully understand the context, but I'm trying anyway...)
    public void OnWaterEnter(InputAction.CallbackContext context) 
    {
        if (context.performed) 
        {
            InteractWithWaterEvent?.Invoke();
            IsSwimming = true;
        }

        else IsSwimming = false;
    }
  1. In ‘PlayerStateMachine.cs’, I added the following as well:
        // TEST
        [field: SerializeField] public WaterFinder WaterFinder {get; private set;}

        // TEST (in 'OnValidate()'):
            if (WaterFinder == null) WaterFinder = GetComponentInChildren<WaterFinder>();
  1. In ‘PlayerFreeLookState.cs’, here’s what I did:
            // TEST (in 'Enter()'):
            stateMachine.InputReader.InteractWithWaterEvent += InputReader_HandleSwimmingEvent;

            // TEST (in 'Exit()'):
            stateMachine.InputReader.InteractWithWaterEvent -= InputReader_HandleSwimmingEvent;

        // TEST
        private void InputReader_HandleSwimmingEvent() 
        {
            stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
        }

  1. I placed in a ‘PlayerSwimmingState.cs’, which just has an entrance debug so far:
using RPG.States.Player;

public class PlayerSwimmingState : PlayerBaseState
{
    public PlayerSwimmingState(PlayerStateMachine stateMachine) : base(stateMachine) {}

    public override void Enter()
    {
        UnityEngine.Debug.Log("Swimming State Entered");
    }

    public override void Tick(float deltaTime)
    {

    }

    public override void Exit()
    {

    }
}
  1. Here is my ‘Water.cs’ (for your convenience):
using RPG.Core;
using UnityEngine;

public class Water : MonoBehaviour, ITarget
{

    public event System.Action <Water> OnWaterFound;

    private WaterFinder player;

    private void Start() 
    {
        player = GetComponentInParent<WaterFinder>();
    }

    bool ITarget.IsValid()
    {
        return Vector3.Distance(player.transform.position, transform.position) <= 0f;
    }

    private void OnDestroy() 
    {
    OnWaterFound?.Invoke(this);
    }

}
  1. Here is my ‘WaterFinder.cs’ (and below the script is how I set it up under my player) - for your convenience as well:
using System.Linq;
using RPG.Core;
using UnityEngine;

public class WaterFinder : RangeFinder<Water>
{
    public Water GetNearestWater() 
    {
        CurrentTarget = Targets.OrderBy(w => Vector3.Distance(transform.position, w.transform.position)).FirstOrDefault();
        return CurrentTarget;
    }

    protected override void AddTarget(Water target)
    {
        base.AddTarget(target);
        target.OnWaterFound += RemoveTarget;
        Debug.Log($"WaterFinder: Adding {target.name}");
    }

    protected override void RemoveTarget(Water target)
    {
        base.RemoveTarget(target);
        target.OnWaterFound -= RemoveTarget;
        Debug.Log($"WaterFinder: Removing {target.name}");
    }

}

The last piece of the puzzle, is how do I get it to work based on entrance triggers? All I know about the Input Action Map, is that it only works for buttons… Never knew it can or cannot work with triggers. Apart from that, I assume that’s all, right? (at least until the debugger for the Swimming State works, I can figure the logic out later)

It’s going to have nothing to do with the input action map. It’s a matter of being within the water.

Within the RangeFinder class is an event

        public event System.Action<T> OnTargetAdded;

This is fired whenever a target is within the sphere of the RangeFinder.
If you subscribe to this method in PlayerFreelookState() then it will fire when you enter the water. That’s when you’ll want to switch to your swimming state.

OK so… I deleted step number 2, the part that has to do with the ‘InputReader.cs’ connected to any sort of Control Action Maps, and then changed step 4 to be as follows:

            // TEST (in 'Enter()'):
            stateMachine.WaterFinder.OnTargetAdded += InputReader_HandleSwimmingEvent;

            // TEST (in 'Exit()'):
            stateMachine.WaterFinder.OnTargetAdded -= InputReader_HandleSwimmingEvent;

        // TEST
        private void InputReader_HandleSwimmingEvent() 
        {
            stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
        }

but now I have a data conversion problem… what type of data should the ‘InputReader_HandleSwimmingEvent()’ function be? (I know for a fact it can’t be void, but setting it to ‘Water’ wasn’t working either, so now I’m confused, and the game won’t work without this fix…)

Have a think about this for a moment… you’re subscribing to the WaterFinder’s OnTargetAdded
The WaterFinder is a RangeFinder<T> of a certain type…

The event is that same type T that the WaterFinder is looking for…

So the HandleSwimmingEvent would need to take in a?

lol I literally didn’t even know that because our WaterFinder is a RangeFinder of type Water, that our ‘void’ type ‘InputReader_HandleSwimmingEvent()’ would need to take in a parameter for water, so it looks as follows:

        // TEST
        private void InputReader_HandleSwimmingEvent(Water water) 
        {
            stateMachine.SwitchState(new PlayerSwimmingState(stateMachine));
        }

I don’t see the point of the parameter existing except to silence the compiler issues. Is this correct?

This is the way

and… I have a new set of errors:

Assets\Project Backup\Scripts\State Machines\Player\PlayerFreeLookState.cs(30,38): error CS1061: 'InputReader' does not contain a definition for 'InteractWithWaterEvent' and no accessible extension method 'InteractWithWaterEvent' accepting a first argument of type 'InputReader' could be found (are you missing a using directive or an assembly reference?)
Assets\Project Backup\Scripts\State Machines\Player\PlayerFreeLookState.cs(72,38): error CS1061: 'InputReader' does not contain a definition for 'InteractWithWaterEvent' and no accessible extension method 'InteractWithWaterEvent' accepting a first argument of type 'InputReader' could be found (are you missing a using directive or an assembly reference?)

which basically both refer to these lines:


            // TEST
            stateMachine.WaterFinder.OnTargetAdded += InputReader_HandleSwimmingEvent;

            // TEST
            stateMachine.WaterFinder.OnTargetAdded -= InputReader_HandleSwimmingEvent;

(I’m already trying to fix them)

Remember when I told you I deleted step 2? Yup, here we go…

Privacy & Terms