Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Setup of an interruptible, time-based event management system

Discussion in 'Scripting' started by Schneider21, Jan 17, 2016.

  1. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    I need to build an event management system to control NPC behavior, level events, and to simulate off-screen occurrences. For example, the player is in his office doing work. His coworker NPCs should arrive and leave as fits their assigned schedule. While working, they should do little busywork type tasks like type at their desk, get up and appear to talk to each other, etc. They should also be interruptible so they can respond to player interaction, adjust to new orders if necessary, and respond to events like if the room were to suddenly catch fire.

    I read through this great thread here, and I'm certain the method @JoeStrout describes is what I want to implement, but I'm stuck even visualizing the details of how this works. So let's say I have an EventManager object that has a SortedList event queue that will be checked each time tick to see if there are events that need to be processed. But what does that event class/struct look like? How do you store the variety of data needed to pass back to the appropriate entity to execute? And since it's not just characters that can execute events, but the level itself, how do you standardize events to be able to handle the different options?

    To add more complexity, only a limited number of characters will be on-screen at any given time. I'd like to roughly simulate actions for characters not in the currently loaded scene, so that if the character were to, for example, call that person and say "Come to work!" the amount of time it would take them to get there would depend on what they were currently doing. Coworkers at home asleep respond more slowly than ones who are across the street getting coffee.

    I know this isn't a ton of detail, but I guess I'm looking for guidance on how to really structure things. I think I have an idea of how it should work conceptually, but I don't know how to translate that into code.
     
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Nice problem. I read through the other thread. There are some subtle differences there from how I would handle it.

    I would use a master scheduler. However the master scheduler would not hold all of the npc actions. It would just maintain a sorted list with the next time each NPC should awake. It would expose a method for adding a new awake alert. Every frame it would check the first item on the list and send and awake command to the appropriate NPCs.

    Once an NPCs are awake they will do whatever they need to. When they are done they will go back to sleep and request a new awake time from the scheduler. The NPC maintains there own internal schedule of things to do, and only ever has one request with the scheduler at a time.

    As a side effect there is never a need to remove schedule items from the scheduler. If a awake call happens to an already awake NPC then there is no big deal.

    It also lets the player interact with an NPC at will. Simply give the NPC an awake call and let its AI take over. When the player looses interest the NPC goes back to sleep and puts in an appropriate wake request with the scheduler.

    I'll have a play with some code over the next few days and get back to you.
     
    Schneider21 likes this.
  3. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    That makes sense. So each entity would be responsible for knowing what it's supposed to be doing at any given time, but only ever "plan" to do the next item on its agenda? The scheduler keeps track and notifies entities it's their turn to do something without any knowledge of what that entity does. When an entity's action completes, it evaluates what it should do next, schedules a wake-up event with the scheduler, and sleeps.

    This sounds like it could be as simple as having a time check inside the entity's awake method (if time >= startWork && time < endWork) and a few conditions (office.status == notOnFire) to point them towards the right task. And those task methods could either simulate or manipulate game objects depending on the scene's status.

    This is something I can wrap my head around. Thanks for the starting point! I'd be happy to look at any code you can write up!
     
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Sounds about right to me. As for data structures, an event manager is mainly a list of events, e.g.:

    Code (CSharp):
    1. public class EventManager : MonoBehaviour {
    2.     public struct Event {
    3.         float time;
    4.         EventItem item;
    5.     }
    6.     public List<Event> events;
    7. }
    ...and you keep this list sorted by Event.time. In fact it might be even better to use SortedList rather than List; but it's also OK to just insert new items in the proper place (sorted by time) yourself.

    Then, in Update, you just have to check the .time of the first item in your list, and when if it's time, then call some method on .item to let it know its time has come (and remove that event from the list).

    If you want to get even fancier, there are ways to make this more efficient with a circular queue... but that's premature optimization, I think. Get it working first in the simplest way possible; optimize the snot out of it later, if you find that it actually matters!
     
    Kiwasi likes this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    I implemented my own using a linked list of the events nodes, I also included repeating events, and my ITimeSuppliers:

    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/Timers/Scheduler.cs

    Update should be called every frame by whatever script needs the scheduler (the brain for instance).

    As for interrupting... well I wouldn't exactly build that into your timed event scheduler. Rather while your AI is waiting for an event to occur (lunch, end of the work day, things that actually are scheduled), they just work moment to moment. They'll naturally just react to the random stuff going on around them.

    When a scheduled event comes along... it checks its own status. If for instance there is a fire, well, the event is ignored. Who cares about the agenda when I'm on fire!
     
  6. cjdev

    cjdev

    Joined:
    Oct 30, 2012
    Posts:
    42
    I had made one of these just like what JoeStrout is describing. It actually makes for a fairly simple construct if you don't need all the bells and whistles:

    Code (CSharp):
    1. using System;
    2. using System.Linq;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class EventManager : MonoBehaviour {
    7.  
    8.     private Queue<TimedEvent> events = new Queue<TimedEvent>();
    9.     private float totalTime = 0;
    10.  
    11.     public void Update()
    12.     {
    13.         totalTime += Time.deltaTime;
    14.         while (events.Count > 0 && events.Peek().time < totalTime)
    15.             events.Dequeue().action();
    16.     }
    17.  
    18.     public void AddEvent(Action delayedAction, float delayedSeconds)
    19.     {
    20.         TimedEvent newEvent = new TimedEvent { action = delayedAction,
    21.                                    time = delayedSeconds + totalTime};
    22.         events.Enqueue(newEvent);
    23.         events = new Queue<TimedEvent>(events.OrderBy(x => x.time));
    24.     }
    25.  
    26. }
    27.  
    28. public struct TimedEvent
    29. {
    30.     public Action action;
    31.     public float time;
    32. }
     
    Last edited: Jan 20, 2016
    JoeStrout likes this.
  7. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    Thanks, guys. This has given me a lot to go on.

    I'm thinking that for me, the best system will be one that uses the in-game clock (not Time, since there'll be times I want stuff going on without advancing time of day) and stores references to object methods with optional parameters. That way an event can be just a notice to an entity to do its regularly-scheduled task (Awake) or a specific instruction to make something happen at a given time (at 3:00pm, Bill is going to walk up to Mary and punch her in the face). The second method would help simplify characters' Awake methods, I think, as I could trigger events to keep things interesting and not necessarily based on a condition of whether or not Bill was mad enough to hit somebody, and deciding dynamically who he was going to hit on every call to Awake. It should also allow for events like breakRoom.startFire(intensity) or somesuch to be scheduled from an external Director class that's job is to keep things interesting.

    Hopefully I'll have time to put something together this weekend. I'd love to then get feedback on any obvious flaws in my system, stuff like that.
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    What is the 'in-game' clock if not 'Time'? Do you have your own method of keeping track of time in game? Or are you referring to the system-time of the device is running on?

    This is actually why I wrote 'ITimeSupplier' in my spacepuppy framework:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/ITimeSupplier.cs

    This way I can write code that uses the ITimeSupplier, rather than UnityEngine.Time. And I can implement it in any way I like. Of course also having base implementations for UnityEngine.Time:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/SPTime.cs

    Which is how I based everything in my 'Scheduler' class I linked above. This way the implementation is generalized and independent of the actual clock used.

    Other places I use it are in my tween, animation, timers, and coroutine libraries.

    You could consider doing something similar so as to avoid tethering your Scheduler to tightly to any given time definition. As for "3:00pm", that can be expressed as seconds anyways, no matter the time used. You could abstract that as a method that just takes in a 'Date' or 'TimeOfDay' value, and just convert it to seconds and store that. So that way you could have a Scheduler interface that handles various scheduling timing:

    Code (csharp):
    1.  
    2. void ScheduleXSecondsFromNow(...)
    3.  
    4. void ScheduleAtTimeOfDay(...)
    5.  
    6. ... etc
    7.  
     
  9. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    @lordofduct The in-game time I'm referring to would be the day, hour, minute of the game world. My GameTime Manager uses a coroutine to advance the time appropriately based on whether or not the current situation should be advancing game time or not. Like in Final Fantasy, the game time doesn't advance when in combat or dialog. So my event timing should be based on the game's world time, not the application timing.

    I'm not saying what I had in mind is the best way. It's just how I envisioned being able to handle minute-to-minute events in the same system as long-term events, like "In 3 days, the player gets fired UNLESS they've met their production quota".

    I guess I'll see once I actually get some code written, huh?
     
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    I'm not saying that's NOT the best way either.

    I'm just saying you can write a scheduler using interfaces like I described so that you can swap out what time is being used.

    (note - the complexity that might be recognized when looking at my Scheduler implementation has nothing to do with ITimeSupplier, and all to do with the fact I implement my linked-list by hand, as an optimization measure to speed up insertion while avoiding unnecessary garbage-collection).
     
  11. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    Yeah, not gonna lie... I was initially turned off by your solution in the originally-linked thread because it seems above my head and more complex than I had in mind. :p Sometimes I'm the wrong kind of lazy.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    To see what I was meaning, lets take cjdev's code and implement it with a more generalized implementation:

    Code (csharp):
    1.  
    2. using System;
    3. using System.Linq;
    4. using System.Collections.Generic;
    5. using UnityEngine;
    6.  
    7. public class MyGameTimeEventManager : MonoBehaviour
    8. {
    9.  
    10.     private EventScheduler _scheduler;
    11.  
    12.     public EventScheduler Scheduler { get { return _scheduler; } }
    13.  
    14.     void Awake()
    15.     {
    16.         //lets assume that your MyGameTimeManager is a singleton somewhere
    17.         //and that we've implement ITimeSupplier on it
    18.         _scheduler = new EventScheduler(MyGameTimeManager.Instance);
    19.     }
    20.  
    21.     void Update()
    22.     {
    23.         _scheduler.Update();
    24.     }
    25.  
    26. }
    27.  
    28. public class EventScheduler
    29. {
    30.  
    31.     #region Fields
    32.  
    33.     private Queue<TimedEvent> _events = new Queue<TimedEvent>();
    34.     private ITimeSupplier _timeSupplier;
    35.  
    36.     #endregion
    37.  
    38.     #region CONSTRUCTOR
    39.  
    40.     public EventScheduler(ITimeSupplier supplier)
    41.     {
    42.         _timeSupplier;
    43.     }
    44.  
    45.     #endregion
    46.  
    47.     #region Methods
    48.  
    49.     public void Update()
    50.     {
    51.         while(_events.Count > 0 && _events.Peek().time < _timeSupplier.TotalTime)
    52.         {
    53.             _events.Dequeue().action();
    54.         }
    55.     }
    56.  
    57.     public void ScheduleEventFromNow(Action callback, float timeFromNow)
    58.     {
    59.         TimedEvent ev = new TimedEvent() {
    60.             action = callback,
    61.             time = _timeSupplier.TotalTime + timeFromNow
    62.         };
    63.         _events.Enqueu(ev);
    64.         //this right here is what my optimization in my linked source resolves... this is SLOW
    65.         _events = new Queue<TimedEvent>(_events.OrderBy(x => x.time));
    66.     }
    67.  
    68.     #endregion
    69.  
    70.     #region Special Types
    71.  
    72.     public struct TimedEvent
    73.     {
    74.         public Action action;
    75.         public float time;
    76.     }
    77.  
    78.     #endregion
    79.  
    80. }
    81.  
    82. public interface ITimeSupplier
    83. {
    84.     float TotalTime { get; }
    85. }
    86.  
    Now lets say you created another scheduler that scheduled based on UnityEngine.Time, you just implement one of those:

    Code (csharp):
    1.  
    2.  
    3. public class UnityEngineTimeSupplier : ITimeSupplier
    4. {
    5.  
    6.     public float TotalTime { get { return Time.time; } }
    7.  
    8. }
    9.  
    And you could have this elsewhere:

    Code (csharp):
    1.  
    2. public class MyUnityTimeEventManager : MonoBehaviour
    3. {
    4.  
    5.     private EventScheduler _scheduler;
    6.  
    7.     public EventScheduler Scheduler { get { return _scheduler; } }
    8.    
    9.     void Awake()
    10.     {
    11.         _scheduler = new EventScheduler(new UnityEngineTimeSupplier());
    12.     }
    13.  
    14.     void Update()
    15.     {
    16.         _scheduler.Update();
    17.     }
    18.  
    19. }
    20.  
     
  13. Schneider21

    Schneider21

    Joined:
    Feb 6, 2014
    Posts:
    3,512
    I finally had some time to work on this, and I may have arrived at a solution that will work well for me. I don't have the actual code on this machine, so this will be more of a logic-behind-the-process post, but if anyone is interested in the actual code, I'd be happy to share it.

    My EventManager looks like SortedList<GameTime, GameEvent> , with both GameTime and GameEvent being custom classes.

    GameEvent has the following members:
    • Object eventOwner - A reference to the originator of the event. It's generic to allow Character and Building events in the same system.
    • int eventAction - An index the owner uses to check an enum of its own action types
    • int[] eventValues - null or array of varied length that would be additional details used in the event
    I surmised that I could get by with int values for the additional details based on my needs, but that is the one part I suspect I may run into issues later with ambiguity and having to force everything to work with ints. The idea would be, for example, that if the action was "GoHome", each house would have an id I could point to and pass that as the value for the character to find its home. I'm actually still not even sure these values are necessary, but I wanted to have the option just in case.

    GameTime is basically a custom Date object that matches up to the game world's time instead of real time. That way events aren't processed while the game is paused or in menus or whatnot, and it will also allow for the quick passage of time, say when the player sleeps. It implements the IComparable interface to allow it to be compared to other GameTimes.
    • Has int values for year, day, hour, minute, second. The GameTimeManager keeps track of in-game time using a GameTime object and increments/resets these values as needed as long as the clock is running. When an event is added to the scheduler, a GameTime is created with values set for a future time/date that the event should be executed
    • float timestamp and randStamp - Because SortedList doesn't like duplicate keys, these two are checked in order if everything else down to the second matches. So in order for a duplicate key to be generated, the events would have to be scheduled for the same second at the same second, with the same random number generated.
    Each tick of the clock in GameTimeManager, a call to EventManager's PerformEvents is made. This method checks the first item in the events SortedList, and if the key is at or before the current time, the event is processed, removed, and the next event checked.

    When an event is processed, the GameEvent's eventOwner object is checked for type, cast to that type (a strange way of doing it, I agree, but it works) and that object's DoEvent method is called with eventAction and eventValues passed back to it. The character (or other schedul-able object)'s own DoEvent method triggers the necessary methods for simulating or carrying out the orders as neccessary, as well as schedules the next needed event.

    - - -

    I've only run a few test cases so far, and just logging messages, so I'll report back once I have actual events executing. But thanks to everyone who posted for getting me on the right track!