Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Trying to figure out a routine / schedule system for NPCs in my RPG

Discussion in 'Scripting' started by PaperMouseGames, Nov 20, 2019.

  1. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Hello there! So I'm working on my first big project and I'm trying to figure out how to have NPCs that will have daily schedules.

    I've got a system that is working ok right now but I'm posting on here because I think it's a little brute force and I wonder that there isn't a more flexible way of doing this.

    Here is what I'm doing:

    I have a world clock that goes from 00:00 to 23:59 and then repeats as the day night cycle.

    Each NPC has an
    AI
    script and a
    Schedule
    script attached. The
    Schedule
    just holds 24 ScriptableObjects that work like enums to determine what kind of scheduled activity the NPC can do at that hour. For example:

    The first SO slot in the schedule refers to the hour of 00:00 - 01:00 and it might have the SO SleepActivity. This is nice, since I can go through each NPC and set up their schedule on the inspector.

    The
    AI
    script checks what time it is in the world clock and checks what activity is set up in the
    Schedule
    for that hour. So I can change activities every hour.

    This works fine, though I would like to extend it for days of the week eventually. I just wonder if there's a better way of doing this. I'd love to have more intervals than hourly for example, but I don't love the idea of having to set up 48 if statements to check the time of day if, for example, I use 30 minute increments.

    Any ideas on how this could be made a bit more flexible? Maybe I'm doing it fine already, but I figured I'd ask since this is my first time doing something like this.

    EDIT: Adding some of my code to make it a bit more clear.

    This is the
    Schedule
    script attached to each NPC.

    Code (CSharp):
    1.  
    2. public class Schedule : MonoBehaviour
    3. {
    4.     public ScheduleActivity blockOne;
    5.     public ScheduleActivity blockTwo;
    6. ...
    7.     public ScheduleActivity blockTwentyFour;
    8. }
    So the
    Schedule
    component takes in
    ScheduleActivities
    which are empty scriptable objects being used just like enums. Currently I have 3 types of
    ScheduleActivities
    ; sleep, work, freeTime. Each block represents an hour of the day, blockOne is 00:00 - 01:00, blockTwo is 01:00 to 02:00, etc... So I can determine what activity each NPC should be doing a specific hour.

    Then, the AI script which is also attached to each NPC checks the
    TimeManager
    and determines what block is currently active, and then prints out the name of the activity the NPC should be doing (but I can move the NPC to the activity location without too much hassle, this is just for prototyping) with the following code:

    Code (CSharp):
    1.  
    2. if (timeManager.Hours >= 0 && timeManager.Hours < 1)
    3. {
    4.         Debug.Log(schedule.blockOne.name);[/INDENT]
    5. }
    6. else if (timeManager.Hours >= 1 && timeManager.Hours < 2)
    7. {
    8.         Debug.Log(schedule.blockTwo.name);[/INDENT]
    9. ...
    10. else if (timeManager.Hours >= 23)
    11. {
    12.         Debug.Log(schedule.blockTwentyFour.name);[/INDENT]
    13. }
    So I skipped the ones in between, but yeah it's 24 if statements, and that's only checking per hour. If I wanted a system where I could change an NPC's schedule every 30 or 15 minutes it would be even more if statements. This is the part of the system that's really bugging me right now. I'm sure there's a better way to implement this while still being able to have NPCs have unique schedules that can be easily modified.
     
    Last edited: Nov 20, 2019
  2. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,010
    I see two serious problem with your enterprise.

    1. "big project"
    2. "RPG"
     
    Bunny83 likes this.
  3. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,386
    If I were doing what you describe, the most important thing is that I would need to have everyone's schedule indexed by time and location. I always know where the player is so anytime that the player changes locations i need to be able quickly get a list of which NPCs are supposed to be at that location at that time. After that I can figure out what they are supposed to be doing and simulate it.

    As for anyone who is not in the immediate area- their schedule is not important so I need to be able to ignore them. Iterating their schedule would just waist processing and memory.
     
  4. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    I appreciate the concern, but this really isn't helpful to anyone...
     
    MimiKitty, Fowi, ZZ_pf and 2 others like this.
  5. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,386
    By the way, if you are using even 24 if statements than you're doing something weird. That shouldn't be necessary at all. Can you explain your implementation?
     
  6. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Well if I only have the NPCs in the immediate area active, that should resolve that right? They're the ones whoa re checking their schedule so they won't check it unless I've activated them by being in the right location.

    I haven't actually implemented that part of the system yet so it could get complicated, but yeah I agree.

    Otherwise, mostly I'm just asking if there's an easier way to code / deal with a schedule system that is based on time of day, because like I said, the whole "I have an if statement for each hour of the day" thing seems really clunky, and I'm just trying to see if there's some technique that would be a little more sophisticated and allow for changes without having to hard code each possible time.

    If my current schedule system isn't clear enough from my post I'm happy to post some code, I just didn't want it to have too much unneeded info.
     
  7. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Yeah absolutely, I'll edit the original post with some code to make it more clear.
     
  8. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    I updated the original post, hopefully it's more clear what I'm doing / trying to do now, if it still doesn't make much sense let me know and I can see if I can be more clear, thanks!
     
  9. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,386
    I would make a schedule event class that stores at least 4 things:
    • the time of the event (as a float)
    • the location of the event
    • the NPC (or whatever subject) of the event
    • the type of event (or what the NPC is doing)
    Now make a List of these. Sort the list by time (I think you may need to implement iComparable if you want the list to sort automatically for you).
    Have an index on the list that you initialize somewhere
    int currentEvent=0


    Then, in your Update you can do something like:
    if (myEvents[currentEvent].time => TimeManager.time)
    { //process your event here
    currentEvent++;
    }


    That's assuming TimeManager.time is a float.

    This way you can schedule event's for any time that you want.
     
  10. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Interesting, so if I understand this correctly, you're saying the list would hold every event for every NPC, and then it would just play them all in order (once sorted) so, the 7:00 am events would play, then if the next events are at 7:15, those would play next regardless of what NPCs are associated with those events, etc.

    If I'm understanding that correctly, that does seem pretty streamlined. The only issue I have with it is that it seems more event based than NPC based. I don't think I'm articulating that well, but I guess it just seems a bit hard to manage with multiple NPCs, because I would have to create events and link them to an NPC, rather than being able to work with an NPC's individual schedule.

    Not sure I'm getting my concern across well. Certainly it's worth experimenting with, I guess it would just be nice to have a system where I can inspect an NPC and see what their full schedule for the day is (or once the system expands, for the week, etc.).
     
  11. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,386
    Yeah, I would prefer to have an individual schedule for each NPC too, when I were editing my content. You can set-it-up that way if you want, and then write some code that compiles all of the individual schedules into a master schedule at run-time.
     
  12. hyungjin_unity

    hyungjin_unity

    Joined:
    Jun 10, 2019
    Posts:
    3
    I actually just finished my own impl of this exact system. I used a node editor to link schedule elements together, but effectively it works the same as a list. To keep the scheduling dynamic, each element determines exactly how much time it will take. For example, a "sleep" element would have a "sleep amount" value that I could set which determines at what time the node finishes. The next node picks up from where the sleep node left off. Each character has it's own "Schedule" that my schedule manager iterates through continuously. And finally, every node has the responsibility of updating the "CharacterState" associated to the character, which holds data on what scene the character is in, what position/rotation the character is in, and what the progress of the current node is, - if asked for.
     
    eses likes this.
  13. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Ok so I came up with something that's at least a little easier to work with than what I had before thanks to some of the tips on here and on the suggestion of @kdgalla to use a list of events.

    So now I have a
    ScheduleActivity
    class which is this:
    Code (CSharp):
    1. [System.Serializable]
    2. public class ScheduleActivity
    3. {
    4.     [SerializeField] int startTime = 0;
    5.     [SerializeField] int endTime = 0;
    6.     [SerializeField] ScheduleActivityType type;
    7.  
    8.     public int StartTime
    9.     {
    10.         get { return startTime; }
    11.     }
    12.     public int EndTime
    13.     {
    14.         get { return endTime; }
    15.     }
    16.     public ScheduleActivityType Type
    17.     {
    18.         get { return type; }
    19.     }
    20. }
    And my Schedule class holds a
    List<ScheduleActivity> activities;
    .

    My NPCs still have Schedule as a component on them and I can now add more elements to the list based on how many different activities I want that NPC to have (rather than always having 24 elements, one for each hour block).

    And in the NPC's AI script I have this check running on Update:

    Code (CSharp):
    1. foreach (ScheduleActivity activity in schedule.Activities)
    2.             {
    3.                 if (activity.EndTime != 0)
    4.                 {
    5.                     if (timeManager.Hours >= activity.StartTime && timeManager.Hours < activity.EndTime)
    6.                     {
    7.                         currentActivity = activity;
    8.                         PrintActivity();
    9.                     }
    10.                 }
    11.                 else
    12.                 {
    13.                     if (timeManager.Hours >= activity.StartTime)
    14.                     {
    15.                         currentActivity = activity;
    16.                         PrintActivity();
    17.                     }
    18.                 }
    19.             }
    Which just checks the time with the activity start times and calls a method to print the activity's name to the console. The line
    if (activity.EndTime != 0)
    is there because otherwise activities that end at midnight we're getting called by the
    if (timeManager.Hours >= activity.StartTime && timeManager.Hours < activity.EndTime)
    statement.

    Definitely better than 24 if statements! I still don't have a way to change activities in increments under 1 hour. I could add startMinute and endMinute variables to the ScheduleActivity maybe, not sure, haven't tried it yet.

    My only concern here is calling that foreach statement on Update. It's just always going, and I worry it could be a performance issue with multiple NPCs.

    Thanks a bunch for all the ideas and tips so far, if anyone wants to chime on on improving this system further I'm open to suggestions.
     
  14. hyungjin_unity

    hyungjin_unity

    Joined:
    Jun 10, 2019
    Posts:
    3
    You could avoid both the for loop and the update if you incorporate callbacks for whenever a ScheduleActivity finishes or when the TimeManager hits a specific time. That way there's nothing actually checking up on the progress of each schedule and the activities will just happen one after another.
     
  15. PaperMouseGames

    PaperMouseGames

    Joined:
    Jul 31, 2018
    Posts:
    434
    Hmm I really don't see how I would do that. I don't see how I would know if an activity has ended without checking the time of day int he update method.
     
  16. hyungjin_unity

    hyungjin_unity

    Joined:
    Jun 10, 2019
    Posts:
    3
    Ah, my schedule elements work and finish independently so callbacks off of each element works just fine. Maybe you can go the TimeManager route with a dictionary of callbacks covering a span of time increments and have the current ScheduleActivity subscribe to the time it's supposed to finish.
     
  17. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @Nerevar7

    Interesting thread.

    I have been learning how to create my own NPC daily / weekly routine kind of system too.

    "I don't see how I would know if an activity has ended without checking the time of day int he update method."

    Based on what I've learned / tested so far, I would only think overlapping tasks were needed if there will be some interruption for current task, and then you would replace the current task with a new task (like flee / talk with player). So normally I would only run the current scheduled task, without need to check other tasks in the list. So run current task, when its done it can report it is done, then get next task and only run that in update.

    This is pretty artificial example, but I've done something like this. Time check can be length or day time check but you get the idea. You can add the callback method when you start the task.

    Code (CSharp):
    1. public class TimedTask : MonoBehaviour
    2. {
    3.     Task currentTask;
    4.  
    5.     void Start() {
    6.         currentTask = new Task() { name = "Test task" };
    7.  
    8.         currentTask.StartTask(3f, () => {
    9.             // Task is done, do what is needed here.
    10.             Debug.Log("Task finishes:" + currentTask.name);
    11.         });
    12.     }
    13.  
    14.     void Update() {
    15.         currentTask.Update();
    16.     }
    17. }
    18.  
    19. public class Task
    20. {
    21.     Action EndAction = delegate { };
    22.  
    23.     public string name;
    24.     float taskLength;
    25.     float timeUsed;
    26.     bool active;
    27.  
    28.     public void StartTask(float length, Action endAction) {
    29.         taskLength = length;
    30.         EndAction += endAction;
    31.         active = true;
    32.         Debug.Log("Task started: " + this.name);
    33.     }
    34.  
    35.     public void Update()
    36.     {
    37.         if (active)  {
    38.             timeUsed += Time.deltaTime;
    39.  
    40.             if (timeUsed >= taskLength) {
    41.                 EndAction.Invoke();
    42.             }
    43.         }
    44.     }
    45. }
    46.  
     
    Last edited: Nov 22, 2019
    Rallix and nduplessis1 like this.
  18. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,469
    Pretty much, schedule is a finite state machine, each state has to run and therefore can manage its own exit toward the next state.
     
    Bunny83 likes this.
  19. SundownStudio

    SundownStudio

    Joined:
    Apr 19, 2017
    Posts:
    14
    Hey OP - idk if you're still working on this (or if anyone sees it) but when you have alot of If/Else statements, it's much more performant to use a Switch statement instead.. the magic number is 3 or more If/Else statements in Java, not sure what in C# cuz I'm still a Unity noob but it's probably comparable.. btw great idea with the radiant schedule so it's not set in stone, I'm gonna give that a try in mine for more variety..

    BTW - if you use Switch you can also simplify your conditions based on the order.. for example -



    Code (CSharp):
    1.  
    2.  
    3.             switch (hours) {
    4.                 case int n when (n < 1):
    5.                     Debug.Log(schedule.blockOne.name);[/ INDENT]
    6.                     break;
    7.                 case int n when (n < 2):
    8.                     Debug.Log(schedule.blockTwo.name);[/ INDENT]
    9.                     break;
    10.                    
    11.                     ... etc
    12.  
    13.                 case int n when (n < 24):
    14.                     Debug.Log(schedule.blockTwentyFour.name);[/ INDENT]
    15.                     break;
    16.             }
    17.  
    18.  
    19.  
     
    Last edited: Nov 26, 2021