http://www.tesnexus.com/downloads/file.php?id=20878 - File "QQuix Conceptual - NPC ACE - Anvil Docks - Beta 0_1"
I've just finished this 'engine' to control NPCs and plan to release it later this month.
The engine itself is fully developed, but I still need a few weeks to complete the mod where I am using it.
I had the need for this engine while planning my The Evolving Society engine and decided to develop it as a stand-alone building block to be used later in the main project, but this time I decided to release the scripts in a playable mod (instead of proof-of-concept mod).
The engine is designed to have an exclusive, complete control over a group of NPCs. I suppose it can be adapted to control vanilla NPCs, but I did not put too much thought on this.
Follows a description of the engine. Hope it may be helpful to someone or, at least, raise some interesting discussions.
Main objectives
? Allow for new kinds of activities, beyond the standard AI packages (Find, Wander, Travel, etc)
? Allow for activities involving more than one NPC (Buy&Sell, dating, etc)
? Provide more elaborated ways to organize activities (as opposed to the standard AI package selection criteria)
? Have things happening when the PC is not around (NPCs are not in a loaded cell)
Initial comments
? Most of the following text describes the present implementation of this engine. Variations are (hopefully) easy to create, based on the initial code.
? All concepts presented here are public resources. Feel free to use them in total or in part, without asking for permission. Same goes for the code, when released
THE ACTIVITY SELECTION SYSTEM
Each NPC has a list of daily activities he may want to perform.
Each activity may be performed multiple times every day and has a minimum and maximum number of times it can be selected per day.
The list of activities has some similarities with an AI package list, but has a completely different selection algorithm.
An example may help here:
A city guard activity list might look like this
? Sleep for 7 hours - at least once a day - Max: once a day
? Patrol for 2 hours - Min: 3 times a day - Max: 5 times a day
? Drink at the bar for half an hour - Min: 0 - Max 3
? Chat at the bar for 1 hour - Min 1 - Max 2
? Wander around town for 20 minutes - Min 1 - Max 3
? Dinner with family for 1 hour - Min: 1 Max: 1
Every time an NPC finishes an activity, he picks a new one from the list, based on the result of the activity selection algorithm.
The selection algorithm is composed of a number of advisor/voter functions. Each function anolyses each activity and rates it based on a specific criteria.
The final selection is based on the combination of the 'opinions' of all the advisors.
Each activity also has a few attributes to be used by these functions, like location, ideal time of day, etc.
Implemented functions:
? Daily activities
Objective: make sure the NPC performs each activity a number of times within the Min and Max counts.
Votes high for activities that are below Min, low/negative for activities at Max and medium (interpolated) for other cases.
? Flow
Objective: prevent too many NPCs going to the same place at the same time.
Each activity has a Location attribute specifying where it takes place (Bar, Home, Town, etc). If here are very few NPCs at the Bar, this function will vote high for "Drink at the bar" and "Chat at the bar" and vice versa. Being mostly concerned with public areas, this function votes neutral for activities that takes place in private (like sleeping)
? Time of day
Objective: make things happen at the 'proper' time.
Votes low or high based on the activity and time of day. E.g. votes high for Diner at evening and Sleeping at night.
? Sequence
Objective: prevent selecting the same activity twice in a row and/or force a sequence
Votes negative for the activity that have just finished. Or votes to increase the chanco of some activities been selected in a sequence. E.g. drink at the bar, go home for dinner, go chat a bit with friends, go back home to sleep.
? Location
Objective: prevent the next activity being at the same location as the previous.
If two activities occur at the same location, like "Drink at the bar" and "Chat at the bar", you may want to prevent them from being chosen back to back. May be used to force it, instead of preventing it.
Many other voters may be created to fulfill specific needs. For example, I have an NPC that is supposed to do something at the bar at 6:00 PM. As the time approaches, there is an additional function that votes dramatically high for any activity at the bar and votes negative for everyone else. But only for that particular NPC, of course.
The votes are actually statistical chances of each activity being selected. All votes are combined and normalized, so all positive votes add up to 100% and negative votes add up to -100%.
Voters may also be weighed, giving different 'importance' to each criteria
At the end, the guard list may look like this:
? Sleep - (-50%)
? Patrol - 55%
? Drink at the bar - 20%
? Chat at the bar - 15%)
? Wander - 10%
? Dinner - (-50%)
Then an activity is selected base on the positive odds. In the above example, there is a 55% chance of Patrol being selected as the next activity.
There is also the possibility of forcing a new activity on an NPC, bypassing the described selection system. One activity on one NPC may need to force an activity on another NPC. For example, if the guard selects "Dinner with family", the process may force a similar activity on other members of the family, so they all arrive at the dinner table about the same time.
This algorithm creates a (kind of) more realistic decision process for NPCs, so they do not act as robots. They will, instead, decide differently each day (within imposed limits).
STAGES
Each activity is composed of one or more stages
An example up front may help: Let's use the "Chat at the bar for 1 hour" activity and break it down into its stages:
Stage 1 - Go to bar - the NPC will head for the bar (20 minutes)
Stage 2 - Select partner - NPC selects one of the presents to chat with (0 minutes)
Stage 3 - Go to partner - NPC approaches the selected partner (5 minutes)
Stage 4 - Chat - NPC starts conversation with partner (60 minutes)
When an activity is selected, the stages are played in sequence, one after the other. When the last stage finishes, the activity is done and a new activity will be selected as described earlier.
Duration:
The duration is actually an attribute of the Stage (and not of the Activity, as implied earlier)
Each Stage has a specified duration and a deviation value. If the Chat stage has a 60 minute duration and a 10 minute deviation, the engine will randomly pick a duration between 50 and 70 minutes. The deviation value may be zero.
The resulting duration plays a major role in the engine.
For this development, I self-imposed a challenge to create a NO-gamemode engine and implement an event-driven approach. The duration attribute allows the engine to 'forget about the NPC' during that period. When the time is up, a timer sub-engine (described later) generates an event that causes the engine to reevaluate the situation and react accordingly.
Let's anolyze the "Go to Bar" stage:
When the stage starts, the engine adds a Travel package to the NPC.
If the NPC arrives at the destination before the time expires, the OnPackageEnd block will generate an event that will finish the stage and the NPC goes to the next stage.
Otherwise, what happens when the duration expires (20 minutes, in this example) depends on the location of the NPC and player at the moment. Meaning, whether the NPC is in high level processing or not.
There are some possibilities:
(The description below is an example of how to react when time expires on a GoTo kind of stage. Other types will/may have different reactions, of course)
1 - Player and NPC are in the bar - Do nothing - give the stage some additional time to complete the action
2 - Player is in the bar and NPC is not - Force the NPC into the bar by making it activate the door to the bar, so he arrives normally (although a little late, maybe)
3 - Player and NPC are in the same area outside the bar (e.g, the PC is just outside the bar and the NPC is coming down the street) - Do nothing - give the stage some additional time to complete the action.
4 - Player is somewhere else - just move the NPC to the final destination, consider the stage completed and move on to the next stage.
This 4th scenario allows for the "virtual life" described later.
EVENTS
Each stage is event driven and reacts to them accordingly.
The Timer event generated by the timer sub-engine has already been mentioned. Other common, implemented events are:
- Start
- End
- Suspend
- Resume
- ActorAtMark
- A few others quite specific to the mod I am using for development
START
When a Stage starts, the engine triggers the stage code with a Start event.
This is used to initialize the stage with whatever is necessary. Typically selecting and adding an AI Package or adding a token to the NPC
END
The last event before finishing the stage. Not that useful.
SUSPEND
Happens the PC leaves the cell (generated by scripted doors). Useful to stop any heavy processing that may be going on.
RESUME
Happens the PC enters the cell (generated by scripted doors).
Under many circumstances the Start event does nothing if the player is not in the same cell. It just marks the Stage as "Start delayed". In this case, the Resume event either triggers the actions that would have been triggered by the Start event or, depending on how far we are in the duration, pretends the stage has already finished.
Also useful for repositioning NPCs when the player enters an interior cell, as the game engine has a nasty habit of positioning NPCs at odd places on occasion.
ActorAtMark
Typically generated by the OnPackacheEnd block activated when an AI packages completes
At the end of the event processing, it returns a value with one of the following alternatives:
- Not finished yet. Give me X additional minutes
- Stage finished
- Abort this activity and select a new one (e.g. if the NPC does not find a partner to chat with at the bar)
- Abort this activity and do not select a new one (used when a new activity has already been forced on the NPC)
THE TIMER SUB-ENGINE
Every stage has a time limit, either the initial duration or additional time set by event processing.
The Timer sub-engine is responsible for generating a Timer event at that particular moment.
All controls are based on a timestamp representing the time elapsed since the game started (gamedayspassed * 24 + gamehour).
When a duration is set or changed, the NPC is added to a queue ordered by timestamp, so the first one in the queue is always the smallest value / earliest timestamp.
With all this set up, all the Timer sub-engine has to do is keep checking the first one in the queue an generate the Timer event when the game time reaches that value. This is the only GameMode block that runns all the time (on a quest)
VIRTUAL LIFE
As mentioned earlier, the engine allows for NPCs to have a Virtual Live when the player is not around, meaning, NPC will (virtually) do things while not loaded.
Of course, this is not importance for actions that do not have any further consequence in game, I mean, who cares if John Doe went to the bar for half an hour yesterday evening?
This feature is designed for actions that do change something that affects future events on the mod.
A few examples of things that may happen, even if the player is up in the mountain meditating for a few days:
- NPC production - if a lumberjack spends a day at the woods, a number of logs would be added to the game. Same thing for other crafts.
- Buying and selling - if an NPC decides to buy food, at the end he will have less money and more food
- Pirate attack - a pirate may decide to attack town. Guards will defend. The outcome is calculated and, when the player returns, he will see the results.
ALTERNATIVE EVENT CONTROL SUB-ENGINE
I must have developed half a dozen ways of controlling events during this development.
The engine described above is the result of merging and improving all that experience.
From all of them, there was one that I had a very high expectation, but the implementation did not come out as elegant as I wanted. I have dropped it, but kept it the code along with one single activity that uses it. A quick description:
It is an Event Control System where one script filed requests to be informed of events and to where event generators directed their events.
Using the "Go to Bar" example:
A script adds a Travel package to the NPC John Doe and tells the Event Control System: "When the event "ActorAtMark" referring to John Doe is created, activate the reference so-and-so with the argument so-and-so" or "When the event "ActorAtMark" referring to John Doe is created, execute the User Function so-and-so with the argument so-and-so"
The Event Control System is composed pretty much of lists of pending requests and, when the NPC script OnPackageEnd block raises the event "ActorAtMark", the Event Control System checks if there are any listeners for that event and executes whatever was requested.
TOKENS
Tokens are used as a short term solution under specific circumstances when the NPC must be controlled on a frame-by-frame basis, like dancers and actors on stage (did I mention that there is a Vaudeville-like theater? Yeah . . I couldn't resist!!).
The code is what is called (I suppose) a State Machine: a gamemode block with "if state == A" + "elseif state == B" + "elseif state == C", etc, where each block does its stuff and sets the state to the next value.
I suppose it will be easier to understand looking at the code when it comes out later this month.
MISCELANEOUS NOTES ON THE CODE
? I am using extensively, all over the place, the concept I call an Array-Object. Not surprisingly, it is an array that represents an Object (at least as my non-professional understanding of Object Oriented concept). Some examples of things represented by AOs in this engine: Actors, dialogs, seats, tables, activities, a theater act.
? Most of the processing is done with User Functions. There are about 200 functions and less than 50 regular object/quest scripts
? I am also using stringmap arrays extensively, on purpose, just to see if there was a limit. Never had any problem.
? The beta release will keep a quite elaborated log system I have in place. Very useful for debugging.
I am sure parts of this text will prove not as clear as I intended. Please feel free to ask for clarification