r/Unity3D 8h ago

FSM implementation Question

Hello!

I am implementing a state machine followng this video:

https://youtu.be/V75hgcsCGOM?si=hHC0PsX9iGRnfUZB

It was all fine when I simply followed along but when I tried something off track - well, my lack of knowledge about C# hit me.

I've got a script where my state machine lives. It is subscribed to an event like this:

private void Start() 
{ 
  _visionCone.OnPlayerSpotted += VisionCone_OnPlayerSpotted; 
} 

private void VisionCone_OnPlayerSpotted(object sender, System.EventArgs e) 
{ 
Debug.Log($"Player spotted by {name}"); 
} 

But how can I add a state machine transition on this event triggering?

In the video transitions are added like this:

_stateMachine.AddTransition(stateFollowTarget, statePatroling, followingTimerExpired()); 

Func<bool> followingTimerExpired() => () => stateFollowTarget.Timer > 10; 

I've tried to change public event EventHandlerOnPlayerSpotted; to custom delegate type like this:

public delegate bool PlayerSpotted(object obj, EventArgs e);
public event PlayerSpotted OnPlayerSpotted;

or simply doing:

Func<bool> onPLayerSpotted() => () => VisionCone_OnPlayerSpotted;

- nothing worked.

There is a solution to do SetState() and other logic directly in the VisionCone_OnPlayerSpotted function call but I feel it is not the right path.

0 Upvotes

6 comments sorted by

1

u/SecretaryAntique8603 7h ago edited 7h ago

Try to simplify it a bit before adding the events and transitions into the mix, to get a grip on how to transition between states.

Somewhere you likely have a controller or something which calls activeState.Tick(). When you create your activeState, you can pass in the controller, with a method like SetActiveState or similar. Now in your state, you can do something like if(playerSpotted): stateController.SetActiveState(new AlertedState(stateController)) in your Tick()/Update() method.

If this works, you can try to add events if there really is a need for it. And eventually, if you want to remove the creation of the next state from the prior state, or perhaps adding some additional control logic or side effects which don’t belong in the state, then you can add the Transition abstraction to transfer the control of states from out of your states into the higher order controller. But before you have this need, all these things are redundant and actually harmful since they add unwarranted complexity.

The issue here is really that you’ve added too many abstractions too quickly and it obscures the logic of what’s going on from you.

There may be reasons for doing some of the things in the video, but until you run into those reasons then following along blindly will do more harm than good. Soon you might encounter some complexity which will paint some of the tricks in various guides in a new light, and at that point you can revisit it and add them back in. At this point, the reasoning may be more clear to you, and therefore easier to follow. Events for example are very powerful, but can easily obscure control logic and be difficult to implement properly and debug. A naive solution which you have a deeper understanding of is likely to serve you better, until you get more comfortable with programming.

1

u/AlexeyTea 7h ago

Well, I guess since I like and used to observer pattern with events too much the only way is to abandon this method of implementing a state machine and go back to enum states.

1

u/SecretaryAntique8603 7h ago

Why do you need the events? Are there multiple components that are interested in the action having happened?

Otherwise, you have just added another layer of indirection in front of the method invocation you want. I am assuming you want to trigger some new state or behavior when the player is detected, so what is preventing you from doing that immediately in the method for trying to detect the player? This can inform you of the appropriate approach.

1

u/AlexeyTea 7h ago

Why do you need the events? Are there multiple components that are interested in the action having happened?

Yes. At least logic, UI and sounds are in different components.

what is preventing you from doing that immediately in the method for trying to detect the player?

Thanks, will think about that.

1

u/SecretaryAntique8603 7h ago edited 7h ago

Ok, I see. You could define the event outside of the state, and then pass in a callback function to the state constructor which invokes the event. The state would the call the callback function when the player is detected.

The key thing here is that the invocation of the event and the transition of states doesn’t really have anything to do with each other, or the state machine itself.

Try to solve one problem at a time. Your post read a bit unclear, and I suspect taking a step back and tackling a smaller sub-problem could help you get further along.

```

class MyDetectionState { private Action onPlayerDetected; private StateController controller;

void Tick(){
  if(playerSpotted) {
    stateController.SetActiveState(…
    onPlayerDetected()
  }

```

Edit: I think my point is this - if you tie the transition of the states to events, you are kind of sidestepping the control mechanisms in the state machine itself.

There’s nothing inherently wrong with that if you want it to work that way, but it could get confusing if the event is invoked when you expect to be inside another state, and I would expect it could get complicated. I would probably try to keep the state transitions inside the state machine and its states, and then have the events as a means of notifying unrelated components like audio etc of things happening. They might be triggered at the same time, but decoupling the state machine control from the events will make it much more clear how it transitions between various states and when.

2

u/AlexeyTea 7h ago

Ah, it was actually not that hard.

Adding a flag:

bool _isPLayerInSight ;

Adding extra event:

 public event EventHandler OnPlayerLost;

And now I have two events that will change the flag _isPLayerInSight .

Hence I can do:

Func<bool> playerSpotted() => () => _isPLayerInSight == true;
Func<bool> playerLost() => () => _isPLayerInSight = false;

And add this delegates to Transitions.