ActionComplete / onActionComplete / onInteractComplete etc

I love this course, but I’m started to get a little bit confused with all the delegates and events, especially with so many parts of the code being every so slightly different. It got me thinking of ways to drive some consistency to make it easier to understand the flow of the code. As I’m not an engineer professionally, simplicity is absolutely key for me.

Ok so here’s my idea:

Since we already have OnAnyActionCompleted?.Invoke(…) inside of BaseAction ActionComplete would it be OK to remove the onActionComplete callback and instead have UnitActionManger and Enemy AI subscribe to the OnAnyActionCompleted event? The only nuance I see is each of those subscribers would have to parse the event to see if it came from either a player or enemy action being completed. UnitActionSystem should ignore Enemy events and EnemyAI should ignore Player events. The rest of it would seem to work.

Want to make sure I’m not seeing any big concerns. I imagine the reason we did both EventHandlers and callbacks is because this is a teaching course. I heard Hugo many times emphasize this is a teaching course and he wants to introduce us to many different ways of doing things.

I’m getting all the concepts individually and I love learning everything but I’m worried I won’t be able to make progress on my own games without reducing the “variety” when it comes to my own creations.

This can work, though as you point out, the UnitActionSystem and EnemyAI would have to get info from the sender and determine if the Action was from an enemy or a player. It would also have to check it’s own state to make sure that onAnyActionCompleted even makes sense at this point. (It’s also possible that the handler could just check the state, since UnitActionSystem and EnemyAI should never both be waiting for an Action to finish.

Personally, my concern would be that this actually introduces more complexity rather than removing complexity. A single delegate like onActionComplete is laser focused, while still being generic. The UnitActionSystem passes the delegate to the Action, and when the Action is finished, the call goes directly to the UnitActionSystem’s method to finish the action and move on. The same happens with the EnemyAI. Under the hood, the Action needs to know nothing but “do this thing when we’re done”, regardless of the caller of the event.

That is not saying that you should not do this, only pointing out the reason that where available, I always prefer a callback delegate over an event for operational control.

1 Like

Thank you for the very quick reply and advice here. This is telling me that I need a little more experience to get comfortable with having a more varied mix of patterns. Which other courses have examples of callbacks and EventHandlers?

I’m trying to build a mobile / touch enabled game so that was going to be my next course but OK taking a small detour in this one targeted area.

P.S. The RPG Course looks amazing. I really want to take it at some point but I also want to get started on my own project soon.

I went back to the code after I wrote this. On some scratch paper, I inlined (almost) everything and it clicked.

I think one of the things that kept confusing me was passing a reference to ActionComplete within the call to .Interact() and then later calling ActionStart(onActionComplete). I couldn’t help but think from reading the code that the action is starting within TakeAction but really it’s an entirely different flow.

Ok so I think it’s really three distinct flows. Something like this?

First:

  • setup the thing (e.g. interactable / grenade / etc) that we’re going to take action on
  • setup the action object itself
  • let all other listeners to the action object know that the action started (or is about to start)

*** then some point later on ***

  • the action actually starts to take effect via Update()s on MonoBehaviours

*** then some point later on ***

  • The interactable/ grenade/ etc finishes and let’s the action object “know”
  • which results in the action object letting its caller “know”
  • which ends with the action object letting other listeners “know”

It makes a ton of sense now but it was hard to grasp initially.

I’d still love to see a few other examples in practice to really get this pattern down.

This is a pattern that shows up a lot in asynchronous operations, often with internet requests.

We use this pattern a lot in the Abilities section of the RPG Shops and Abilities course. The Ability has several phases: Targeting, Filtering, and Effects, and each of those phases may or may not take time to finish. Since we don’t want the game to stop responding in the meantime, we let the strategies play out, but pass them a callback delegate. The callback is called when the strategy is finished, and the main game loop is allowed to carry on as normal.

Thanks Brian. I am definitely familiar with the pattern from web development but from my much more limited experience the sync and async parts of the code were very separate. It is a bit more complex to write (just as you mentioned, you have to do a lot of state checking). It happened to be a little easier for me to read it though.

I’m hoping that by working on this pattern in many more places it will come more naturally to me.

To follow up on what was tricky for me… When I read this I kept thinking “Oh no… what happens if an IInteractable decides to fully complete its job within the Interact method?”

    public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    {
        IInteractable interactable = LevelGrid.Instance.GetInteractableAtGridPosition(gridPosition);

        interactable.Interact(OnInteractComplete);

        ActionStart(onActionComplete);
    }

    private void OnInteractComplete()
    {
        ActionComplete();
    }

I keep thinking that there’s no escape from state checking. :slight_smile:
But this is where my background in web services is working against me. All I can think of are 10,000 disparate parties not reading my API spec and running the async code where the sync code should run and vise versa. :stuck_out_tongue_winking_eye:

And here in Unity, the game is generally run on one thread… Even coroutines are faking async operations, while you’re executing a coroutine, it’s still on the main thread, so if it’s blocking, the game hangs.

As long as the result is instant, then it can just call the callback and you’re done.

Well wouldn’t we have a bug in our case if Interact completes instantly? Because the onActionComplete reference is set in the following line, the interactable may call a null or a stale callback.

It seems the synchronous flow (i.e. TakeAction) should do something like

  • setup its own state (e.g. isActive = true, store onActionComplete reference) first
  • call any dependencies (e.g. Interact) which may complete synchronously or asynchronously
  • notify its caller and any listeners that it started. (e.g. OnStartMoving for MoveAction, OnSwordActionStarted for SwordAction, the camera for ShootAction)

You could end up with situations that the onActionComplete gets triggered before OnAnyActionStarted gets Invoke’d but that seems Ok.

Sorry for all the noise on this thread, but I’m trying to work through the pattern of how to add more Actions

I meant instantly meaning “in that frame”. No matter what, you want to make sure you cache the callback, or if you’re doing something instant, use the callback passed into the method instead of the cached callback.

1 Like

Gosh I often forget this about Unity. Yes.

The risk here seems if a callback calls a callback right? (and you haven’t had the chance yet to link one to the other)

OK thanks Brian. I appreciate this so much. I honestly had to relearn how the Action works every time we added a new subclass to BaseAction. I was able to conceptualize the whole code base quite well except this one piece which was driving me crazy. I understand it now after talking through it.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms