1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | HandleStimulus( Entity, Stimulus ) { switch( stimulus->type ) { case Stimulus_SomeoneAttackedMe: { if( enemyTooStrong ) { Flee; } else { Attack him; } } case Stimulus_MurderHappened: { Entity* killed = stimulus->destEntity; Role role = GetRoleof( killed ); switch( role ) { case Sun: { if( entity->gender == female ) { SpawnStimulus(CallGuards); } else { StartAIPlan( DefendSun ); } } case Friend: ... case LandOwner: ... } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | BeginRace( Monsters ); OnActionSpawn( Attack, Stimulus_Attack ); CanReactTo( Stimulus_Murder, Role_Child, Protect ); BeginSubRace( Goblin ); OnActionSpawn( Attack_Stimulus_AttackSpecial ); CanReactTo( Stimulus_Murder, Role_EveryOne, Ignore ); EndSubRace(); BeginSubRace( Ghoul ); ... ... EndSubRace(); EndRace(); |
erpeo93
I can't say anything about those games, but in the game I'm currently developing I've designed a system that is very similar to this act/react system (simpler of course), only that I called it with a different name.
I'm using it only for AI decision making basically, but the concepts are the same I guess.
The only concern I have is that you end up with basically a giant switch statement, there's no way around it...
NelsonMandellaerpeo93
I can't say anything about those games, but in the game I'm currently developing I've designed a system that is very similar to this act/react system (simpler of course), only that I called it with a different name.
I'm using it only for AI decision making basically, but the concepts are the same I guess.
The only concern I have is that you end up with basically a giant switch statement, there's no way around it...
This system will likely end up a mess if your rpg is anything beyond modest in scope and scale. A far simpler and more powerful approach would be to use a stack-based goal architecture where you use only a single switch statement for the state at the top of the npc's behavior stack.
Say an npc is currently sleeping, and the player wakes him up. You'd simply push an "awoken_aistate" onto his behavior stack (along with the relevant data like expiration and who did it, etc.)...
|------|ai stack|------|
[1] awoken_aistate (expired in 2 min., player) <---- current state
[0] sleep_aistate (no expire)
then, if then if he happens to witness a crime while awoken (and he's flagged to respond to crimes against the perpetrator) just push a report_crime_aistate...
|------|ai stack|------|
[2] report_crime_aistate (expire: 3 min) <---- current state
[1] awoken_aistate (expired in 2 min.)
[0] sleep_aistate (no expire)
...you can probably already see the potential for sophisticated-looking emergent behavior. Also if your RPG has any kind of a scheduling system for npcs, it'll integrate easily and flawlessly with a system like this - as scheduling becomes just a matter of manipulating the behavior stack.
erpeo93
Sorry but I can't really see why having stimuli "suggesting" my entities what they should/could do would end up being a mess.
(Assuming I don't end up with a huge number of them of course)
edit: I should clarify that this is not the "main" AI system, I mean entities decides what to do in a totally separated way.
The "flee" and "attack him" were a bit misleading I have to say. What I wanted to say is that I want these stimuli to _possibly_ suggest some actions to entities, not that the entire ai should run this way :)
The decisions they take are already need-oriented, evaluating everything they can do around them and picking the best thing that matches their needs. Surely having a stack like you suggest (based on the current "routine" of the entity I guess?) is another totally viable option :)
Now on the example you've made: what if I want the npc's sun to perceive that his dad is awake and prepare him the breakfast?
How would you go to handle it without having a concept similar to a stimulus or "act"?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | void ai_goto(bot_t *bot, aistate_t *state) { if (dist(bot->loc, state->goal) < state->radius) pop_aistate(bot); else bot_set_goal(bot, state->goal)); } void ai_chase_nearest_guard(bot_t *bot, aistate_t *state) { if (dist(bot, get_nearest_guard() < state->radius) pop_aistate(bot); else push_aistate(bot, GOTO_AISTATE); } void ai_report_crime(bot_t *bot, aistate_t state) { if (dist(bot, get_nearest_guard() > state->radius){ push_aistate(bot, CHASE_NEAREST_GUARD_AISTATE); } else{ report_crime(); pop_aistate(bot); } } void run_bot_ai(bot_t *bot) { // you could check messages here for overriding the current behavior if permissible // obviously you could register npcs to listen for and respond to only particular // stimuli/events that they'd need to know about if (bot_receieved_crime_stimuli and can_override_curr_aistate) push_aistate(bot, REPORT_CRIME_AISTATE); aistate_t *state = get_top_aistack(bot); switch (state->type) { case GOTO_AISTATE: ai_goto(bot, state); break; case GOTO_AISTATE: ai_chase_nearest_guard(bot, state); break; case REPORT_CRIME_AISTATE: ai_report_crime(bot, state); break; default: break; } } |
I probably could have been more clear in my qualification. If your game has 20 NPCs (not including monsters/foes) you should be fine, but if it has over 200 and their behavior is sufficiently sophisticated, then explicitly spelling out behaviors in a nested way like that is probably going to lead to a lot of unnecessary and hard to maintain complexity down the road.
Obviously this also depends on the kind of game your doing. If your game is more like fable 2 than skyrim then this sort of need-oriented approach certainly makes sense (in theory) but if its more like skyrim, then it's probably over-engineering. And if what you're doing is more akin to skyrim, in terms of ai, then it's totally unnecessary to explicitly spell out father/son relationships - instead just assign them each a tag indicating that they're part of the same group (or something like that) so that they can respond to crimes against one another/conversation requests, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | UpdateAI( World, Entity, deltaTime ) { UpdateNeeds( Entity, deltaTime ); ListOfPossibleThingsToDo list = {}; for( everyEntity in sight ) { for( everything I am able to do with it ) { EvaluateAction; AddActionToList( list ); } } SortList( list ); entity->action = list[0].action; entity->destEntity = list[0].entity; } |
I get that your system is essentially for responding to "messages", or at least making behavioral response suggestions based on them. The only thing I'd suggest is that if your game is large in scale, shift as much of the responsibility for responding to stimuli to the actual behavior routines themselves - and then allow for a set of "overrides", like responding to a crime, or a conversation request, etc., that can "override" the npc's current behavior. This simplifies things massively, because you only ever have to think in the context of a single behavior at one time.
Stack-based doesn't have to be based on routines (I assume you mean routine as in schedule, not as in a function), though it can be. The advantage is that it remembers and can return to previous behaviors. Also building certain kind of complex and compounding behaviors becomes very natural. And above all it allows you to keep things "flat", there is an implied hierarchy for certain behaviors but you don't mentally have to step through it - and if you do need to understand what's going on hierarchically, just glance at the stack.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 void ai_goto(bot_t *bot, aistate_t *state) { if (dist(bot->loc, state->goal) < state->radius) pop_aistate(bot); else bot_set_goal(bot, state->goal)); } void ai_chase_nearest_guard(bot_t *bot, aistate_t *state) { if (dist(bot, get_nearest_guard() < state->radius) pop_aistate(bot); else push_aistate(bot, GOTO_AISTATE); } void ai_report_crime(bot_t *bot, aistate_t state) { if (dist(bot, get_nearest_guard() > state->radius){ push_aistate(bot, CHASE_NEAREST_GUARD_AISTATE); } else{ report_crime(); pop_aistate(bot); } } void run_bot_ai(bot_t *bot) { // you could check messages here for overriding the current behavior if permissible // obviously you could register npcs to listen for and respond to only particular // stimuli/events that they'd need to know about if (bot_receieved_crime_stimuli and can_override_curr_aistate) push_aistate(bot, REPORT_CRIME_AISTATE); aistate_t *state = get_top_aistack(bot); switch (state->type) { case GOTO_AISTATE: ai_goto(bot, state); break; case GOTO_AISTATE: ai_chase_nearest_guard(bot, state); break; case REPORT_CRIME_AISTATE: ai_report_crime(bot, state); break; default: break; } }
For the dad and son example, you'd have the son listen for a message that his dad has awoken, and you'd immediately push a MAKE_BREAKFAST_FOR_DAD_AISTATE onto his stack (if allowed by the son's current state). The actual make_breakfast() routine would be built out of the existing more general aistate building blocks like goto, just as with REPORT_CRIME_AISTATE example.
It's not perfect for everything, but it's a great fit for a large scale RPG which requires reasonably sophisticated and emergent looking npc behavior.
erpeo93
ok so the son is listening for a message, that means that the father has to send that message.
And that message could be seen as a stimulus, can't it? :)
The problem I see with "just send a message" is that now the son has to be inside the house to receive the message (or at least in the range of sight of the father), unless you send the message repeatedly for a certain amount of time...
At that point isn't better to just have an explicit stimulus that represents the fact that the father has awaken? This way the message has to be "created" only one time, and even if the son enters the home a couple of minutes later, is able to "perceive" that the father has aweken and goes prepare the breakfast. (of course not before having checked that the other son hasn't preceeded him :) )
NelsonMandellaerpeo93
ok so the son is listening for a message, that means that the father has to send that message.
And that message could be seen as a stimulus, can't it? :)
The problem I see with "just send a message" is that now the son has to be inside the house to receive the message (or at least in the range of sight of the father), unless you send the message repeatedly for a certain amount of time...
At that point isn't better to just have an explicit stimulus that represents the fact that the father has awaken? This way the message has to be "created" only one time, and even if the son enters the home a couple of minutes later, is able to "perceive" that the father has aweken and goes prepare the breakfast. (of course not before having checked that the other son hasn't preceeded him :) )
You by no means need to use messages/stimuli, that was just one example. But this really depends on the underlying system that's creating these behaviors. Are these behaviors determined by a scheduler or are they semi-randomly selected based on needs and/or desires? If the son makes or attempts to make breakfast for the dad each and every morning, then no message is necessary, as you'd simply pass the dad's info/id/ptr along through the make_breakfast_for_npc() behavior function and that's that - you'd just need a scheduler to push that state onto the son's stack. Polling is just fine, as long as its on a single entity like the dad and you're not combing through every npc looking for one who you can make breakfast for.
Take for example the simplest possible scheduler...
[11pm-7am][100%: sleep]
[10am-11pm][90%: go to school][10% fish[
[7am-10am] [50%: make breakfast for dad] [50%: go fishing]
...and then on top of that layer your overrides (crime reporting, conversation requests, etc.) and you've got a very sophisticated looking system that's already beyond what most AAA games are doing in terms of the perceived end result.
You can create very sophisticated looking behavior using simple behavioral building blocks in a stack-based system without having to explicitly spell out relationships between npcs.
Ultima 7 took these kinds of behaviors to the Nth degree, making food, serving patrons, etc. etc. and there's a reverse engineered open source version of the engine available here.
erpeo93
Do we agree that whatever is the way you choose to "manage" the ai, you have to "poll" the environment from time to time to know if there's something more interesting to do that what you're doing?
erpeo93
A stack seems to me like an optimization: instead of considering "all the possible things that you can do on everything you see" you just consider "the last thing you did" to be the best choice. For sure it's a really sane and good optimization, but I can't see how that can be more expressive than just mantaining a list of "active" behaviors, and you just switch on what you think is the best of them. Obviously a stack is easy to implement, faster, and easier to debug, but in my opinion it can't be more expressive than my option... And as I said you have to examine the sorrounding environment anyway, because otherwise your entities will be unable to react to sudden changes.
1 2 3 4 5 6 7 | push(drive_to_store) push(get_keys) push(check_pocket) pop(check_pocket) pop(get_keys) push(walk to door) etc... |
erpeo93
I didn't understood the part where you say "without having to explicitly spell out relationships between npcs"... what does it means? How can you make it without having the son storing whom is father is?
Not necessarily, you could in theory rely solely on message passing.
I wouldn't consider it an optimization, in the sense of improving performance. A stack is a natural and intuitive model for goal oriented behavior because it approximates nicely the way that our minds actually deal with goals. When someone wants to make take a drive they do something like...
1 2 3 4 5 6 7 push(drive_to_store) push(get_keys) push(check_pocket) pop(check_pocket) pop(get_keys) push(walk to door) etc...
1 2 3 4 | struct Plan { ACtion* firstAction; }; |
In the vast majority of cases it would not be necessary to explicitly spell out father-son relationship. They live in the same house, they're probably tagged as being part of the same group, they're particular ai schedules may reference one another so as to interlock for behaviors like making breakfast, they may have customized dialogue that they shout to one another - none of this requires creating a set of enums for father/son. What about the son's relationships to everyone else? Is he also a citizen in relation to a mayor? Is he a shopper in relation to a shopkeeper? You're drawing explicit relationships between npcs instead of letting the behaviors do the talking. If an npc is making breakfast for another npc, and the other npc is letting him do it, and they live together and one is a young man and the other is an old man then their behaviors become the expressions of these implicit relationships.
Sorry but I disagree.
I see it more like this:
1 2 3 4 struct Plan { ACtion* firstAction; };
And so the drive to store Plan is just a linked list of action or subplans: getkeys->checkpocket->walktodoor.
But maybe we're talking about the same thing in different terms, because when my entities encounters a "subplan" in a plan, they're effectively "pushing" onto the stack that plan... but I prefer to see a plan like a simple sequence of actions that have to be taken in series.
The role of the ai is to choose the best plan among some number that we mantain active.
In the vast majority of cases it would not be necessary to explicitly spell out father-son relationship. They live in the same house, they're probably tagged as being part of the same group, they're particular ai schedules may reference one another so as to interlock for behaviors like making breakfast, they may have customized dialogue that they shout to one another - none of this requires creating a set of enums for father/son. What about the son's relationships to everyone else? Is he also a citizen in relation to a mayor? Is he a shopper in relation to a shopkeeper? You're drawing explicit relationships between npcs instead of letting the behaviors do the talking. If an npc is making breakfast for another npc, and the other npc is letting him do it, and they live together and one is a young man and the other is an old man then their behaviors become the expressions of these implicit relationships.
This is absolutely true. As many behaviors as possible should "emerge" from attributes of the entities relative to their "community" and the entities that surround them. (age, personality, ecc)
But for certain things I think you're forced to have roles: for example the village has to know exactly whom is the king of the village: that has to be stored somewhere, and furthermore the members of the community have to have a way to know who the king his... you know maybe they want to spawn a quest that is like "kill the king" or something.
Obviously entities store only roles that are relative to them: the son will only remember father, mother, and maybe a couple of friends. The father could store also something like blacksmith, wife, innkeeper, king.
How would you go about a father that has broke his sword killing some goblins, and now has to go to the blacksmith to buy a new sword? I mean if the village is small enough, he _has_ to know exactly who the blacksmith is and where he lives. Without the blacksmith explicitly declaring "I'm a blacksmith" (or alternatively declaring "I offer swords" ), how can the father know where to go?
NelsonMandellaSorry but I disagree.
I see it more like this:
1 2 3 4 struct Plan { ACtion* firstAction; };
And so the drive to store Plan is just a linked list of action or subplans: getkeys->checkpocket->walktodoor.
But maybe we're talking about the same thing in different terms, because when my entities encounters a "subplan" in a plan, they're effectively "pushing" onto the stack that plan... but I prefer to see a plan like a simple sequence of actions that have to be taken in series.
The role of the ai is to choose the best plan among some number that we mantain active.
You're talking about building a plan from the top-down, whereas I'm talking about generating one automatically from the ground up using simple building block behaviors that push one another. With a stack-based approach, suppose your high level ai system wants some npc to go talk to another npc inside a locked room, just push talk_to_npc, which will in turn push walk_to_door which will push unlock_door, etc. and before you know it the stack is the plan - all done informally and automatically.
In the vast majority of cases it would not be necessary to explicitly spell out father-son relationship. They live in the same house, they're probably tagged as being part of the same group, they're particular ai schedules may reference one another so as to interlock for behaviors like making breakfast, they may have customized dialogue that they shout to one another - none of this requires creating a set of enums for father/son. What about the son's relationships to everyone else? Is he also a citizen in relation to a mayor? Is he a shopper in relation to a shopkeeper? You're drawing explicit relationships between npcs instead of letting the behaviors do the talking. If an npc is making breakfast for another npc, and the other npc is letting him do it, and they live together and one is a young man and the other is an old man then their behaviors become the expressions of these implicit relationships.
This is absolutely true. As many behaviors as possible should "emerge" from attributes of the entities relative to their "community" and the entities that surround them. (age, personality, ecc)
But for certain things I think you're forced to have roles: for example the village has to know exactly whom is the king of the village: that has to be stored somewhere, and furthermore the members of the community have to have a way to know who the king his... you know maybe they want to spawn a quest that is like "kill the king" or something.
Obviously entities store only roles that are relative to them: the son will only remember father, mother, and maybe a couple of friends. The father could store also something like blacksmith, wife, innkeeper, king.
How would you go about a father that has broke his sword killing some goblins, and now has to go to the blacksmith to buy a new sword? I mean if the village is small enough, he _has_ to know exactly who the blacksmith is and where he lives. Without the blacksmith explicitly declaring "I'm a blacksmith" (or alternatively declaring "I offer swords" ), how can the father know where to go?
Offload as much of the relationship expression and customized behaviors to scripting as possible. Create a scheduler, where you can assign an entire script in place of a behavior...
[10pm-7am][custom_script.lua]
[7am-10pm] [fish]
That way you can do explicit stuff like check your weapon condition, and go to a nearby blacksmith if it needs repair. Or else do something else. Even if you're not doing dialogue trees and are just keeping to "barking" or shouting, you absolutely need scripting in my opinion to do the highly customized stuff. You could use "triggers" on the floors to flag stuff like blacksmith's shop, town center, church, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | HandleAIState(Entity) { switch(entity->aiState) { case SearchingForFood: { if( there is food around) { ChooseBestFood(); AddState( entity, Eating, food ); RemoveState( entity, SearchingForFood ); } } case Eating: { if( nearEnough ) { entity->action = eat; } else { entity->acceleration = food->position - entity->position; } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ... case NeedMoney: { if( IsBad( entity->personality) { PushState( entity, SearchSomeoneToStealMoneyFrom ); } } case SearchSomeoneToStealMoneyFrom: { if( someoneElsePresent ) { PushState( StealFrom, destEntity ); } else { PushState( WanderAround ); } } |
erpeo93
I've been thinking about this during the morning, and I think that now I see what your point is.
Let's leave apart fom one second "how" the behaviors are handled, (stack, linked list, whatever)... you're saying that it's bad to define plans in "data", they are better defined "in code": in every moment every entity can only be doing "one thing". If we switch on that state, we arrive at the big switch statement that you were talking about:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 HandleAIState(Entity) { switch(entity->aiState) { case SearchingForFood: { if( there is food around) { ChooseBestFood(); AddState( entity, Eating, food ); RemoveState( entity, SearchingForFood ); } } case Eating: { if( nearEnough ) { entity->action = eat; } else { entity->acceleration = food->position - entity->position; } } } }
Then it's just a matter of choosing the right state in every moment.
And that can be done in different ways, depending on requirements/context.
(With a stack for example, you just assume that whatever is the top of the stack is your current state, while my proposal is to have a "list" of possible state, you evaluate them and you pick the best, and pass it to the switch statement).
erpeo93
And yeah with this switch we have a lot more freedom to also check attributes inside entities ecc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ... case NeedMoney: { if( IsBad( entity->personality) { PushState( entity, SearchSomeoneToStealMoneyFrom ); } } case SearchSomeoneToStealMoneyFrom: { if( someoneElsePresent ) { PushState( StealFrom, destEntity ); } else { PushState( WanderAround ); } }
And with this approach Stimuli/messages can just be implemented as simple pushes onto the stack/list.
I now absolutely agree with you on that, what I was not thinking is that at the end the entity can only be in a single ai state, so thank you :)
The cool thing is that it's a lot easier to debug and mantain, and also it's trivial to expand and "improve" a single ai state if you want.