ID:265964
 
I'm currently trying to develop my own AI system for the hostile NPCs in my game, and I could use some help on how to go about doing this.

I have a little "mood" system going for them right now. There are 4 possible moods. The moods are as follows: happy, angry, scared, curious. These moods, along with hometown, affect how these hostile NPCs react towards other hostile NPCs and the players. If an NPC is angry, it will attack other mobs that come within aggro range. If an NPC is happy, it will ignore other mobs that come into aggro range. If an NPC is scared, it will run away from the strongest mob within aggro range. If an NPC is curious, it will just follow the closest mob that comes within aggro range.

I figured on just making it simple and making NPCs return to their original location when their target is out of range instead of just having them stay put. Also to make it simple, have each step taken by a mob run a loop through all other mobs in range to trigger any AI that's necessary, such as a player moving within aggro range of an angry NPC, or an NPC stepping within aggro range of a player or another NPC. However, should I have the AI target the strongest or weakest mobs (lowest or highest hp, lowest or highest damage output) within its aggro range, or have a list of priorities for it to check against (such as having the NPC try to kill the healer first in a group of players)?

My main question is: How should I go about programming everything? I need help coming up with a system that's flexible enough to account for any skills it may or may not have (which in my game are handled by obj instances with a universal proc to activate them), what range it primarily fights in, when to run away, and yet still remain modular enough to be easily edited.

I'm not asking for you to program me an entire system for me to use directly in my game, but to help with the concepts on how to handle everything and convert it to a nice and elegant DM code.
The system that is generally used in MMORPGs is a table of "threat" or "aggro", with the mob attacking whoever is at the top of the table, the person with the most threat (but still tracking what others are doing). One point of damage done to the monster would equal one point of threat. One point of damage healed to someone else on the threat table would equal one point of threat. Abilities might also have threat modifiers of various types (like a warrior would have abilities that give extra threat, and a healer would have abilities that reduce threat).

World of Warcraft also has a threshold of threat in order to make a monster change targets. It will continue attacking the same target even after it has been passed in threat, unless somebody in melee range has 10% more threat, or somebody at range has 30% more threat. This prevents a mob from being ping-ponged as easily.
In response to Garthor
That sounds like it'd be a lot of fun to attempt to program haha. How should I handle each NPC's threats? Just use a list and add (what?) to it along with the associated threat level? Should different attacks generate different threat levels? Such as a spellcaster's fireball generating 1 threat while a warrior's attack skills generate 2?
In response to Spunky_Girl
Each monster would have an associated list, with the keys being the player and the values being the threat. So, for example, monster.threat[player] = 1000.

Threat is generally based on damage done. So if you do 10 damage, you add 10 threat. It also tends to be modified by other factors, in order to promote to classic tank - healer - damage-dealer archetypes. For example, tanks might get double threat for doing damage but do half as much damage as damage-dealers.
In response to Garthor
So I'm adding player keys to the list? What would I add to the list if it were other NPCs? I'd like to be able to have other NPCs fight one another. Or are you saying add the mobs to the list?
In response to Spunky_Girl
I made a system like this in the past, and it is relatively simple to do.

Just have a list for each enemy mob.
If another mob generates any threat towards the enemy, they get added to the list, along with the threat they generated. If they are already in the list, then naturally you just increase the amount of threat they have.
When threat is changed, the enemy loops through it's threat list and finds the player with the highest threat. That is it's new target.

Naturally there is other things that need to be accounted for (players that die or leave combat have to be removed from the list and so on), but the basic system is actually fairly easy to set up if you know what list associations are and how to use them (look at help file for more detailed info).
In response to The Magic Man
I know how to use associative lists, but adding mobs to a list, doesn't that mess with the loc variable on the mobs?
In response to Spunky_Girl
You can use object references in assossiative lists like that, yes. If you want to use player's keys for some reason, such as leaving them in the list when they log out in case of internet connection hiccups or whatever, then you could use key text strings and object references both. This would increase the complexity slightly, and you'd have to make the NPC ignore players that are in their threat list but not logged in, for example.

Easiest would probably be to just do references to the object. Then it could literally be as simple as altering your damage() function.
mob/proc/damage(amount, causedBy)
hp -= amount
if(causedBy in threat)
threat[causedBy] += amount
else
threat[causedBy] = amount
if(hp <= 0)
die(causedBy)

And, as someone else said, you just have to keep track of who is assossiated with highest threat. This is actually where you might want to put more effort, as it could slow things down if you have to search for highest threat often.

You should probably have a variable keeping track of target and just compare threat[target] to threat[causedBy] whenever threat amount is changed by causedBy.

Also, if threat can go down, such as if the threat automatically goes down slowly over time, or if you reduce threat by an enemy if the enemy is weakened (for example, if mob M is paralyzed, it's not as much of a threat now), then you will have to check it against others to see if the target should change. Easiest way would be to check against everything in the list to see what's highest now, but you could do better by keeping a backupTarget that has second highest threat and just switching to that and setting backupTarget to null, then only searching the entire list if backupTarget is null.
In response to Spunky_Girl
No. loc only gets automatically changed if you add it to a list which happens to be another object's contents list.
In response to Spunky_Girl
Only if you save and load it.
You don't need the actual mob if you use its reference number.
In response to Loduwijk
Loduwijk wrote:
> mob/proc/damage(amount, causedBy)
> hp -= amount
> if(causedBy in threat)
> threat[causedBy] += amount
> else
> threat[causedBy] = amount
> if(hp <= 0)
> die(causedBy)
>


You can even get away without the if-else he had. No big deal, just thought I'd point it out.

mob/proc/damage(amount, causedBy)
hp -= amount

threat[causedBy] += amount

if(hp <= 0)
die(causedBy)
In response to The Magic Man
Just to chime in here really quick, make sure to not initialize the threat list for all mobs, only define it. The list should only be created when the first player is added to it, and then deleted if it becomes empty.

Although it's unlikely to ever reach a point where the resources they use cause problems, failure to clear the unneeded threat lists could potentially allow them to build up from player deaths, abandoned hit and run tactics, and the occasional high level character just romping through. You'll probably want to add a simple time-out that just deletes the list entirely after x minutes of no activity.
Without looking at what others have suggested, here are a couple ideas.

A curious mob could end up attacking but perhaps after a random while or perhaps it just decides its not interested anymore. If it is faster than the PC, have it do circles and stay out of range. :)

Target out of range, randomly choose to mull about for a minute before returning to home. Pacing of sorts.

Target the closest. Target random if you have options. NPC gets hit hard which aggros naturally to strongest player which saves the weaker player and divides damage between players. This makes for great(er) battles.

NPC may be racist or attracted to the shortest PC in range.
In response to Vermolius
Vermolius wrote:
You can even get away without the if-else he had. No big deal, just thought I'd point it out.

Ah yes, of course.

Just to clarify what Vermolius said, not only will adding an assossiation to a list automatically add the key to the list if it's not present (that is, doing myList[something]=value will add something to myList if it's not already there), but it will also initialize to 0 so that you can do myList[something]+=value even if something is not in myList to set it initially to value.

So threat[causedBy] += amount will set threat[causedBy] to amount even if causedBy is not already there.
In response to Garthor
Basically this is a lot like fuzzy logic. The same concept of using a threshold to switch targets can handle switching moods too.

The four moods mentioned (angry, happy, scared, curious) can all coexist with different weights. One mood will be the current "dominant" mood, and might need a certain threshold to be passed before the dominant mood can change. That threshold could also be made to decay with time, so that a curious mob who is about equally aggressive will eventually switch to an attack.

ai
var/current_mood
var/mood_threshold // point at which the dominant mood can switch
var/list/moods
var/list/normal_moods

proc/TickMoods()
mood_threshold *= 0.99 // decay by 1%
var/top
for(var/mood in moods)
// slowly return to baseline personality
moods[mood] = moods[mood] * 0.99 + 0.01 * ((normal_moods && normal_moods[mood]) || 0)
if(!top || moods[mood] > moods[top]) top = mood
if(top && current_mood != top && moods[top] > mood_threshold)
ChangeMood(top)

proc/ChangeMood(mood)
current_mood = mood
mood_threshold = moods[current_mood] * 1.2


Lummox JR
In response to Tsfreaks
I was reminded by something. Don't forget the NPC who is just protecting vs attacking. A protector will not stray far from their treasure but will keep an eye on anyone getting close.

ts
In response to Tsfreaks
That's a good point. I guess a way for me to accomplish such a behavior for NPCs would be to create the protectors' AI to reference the protectee's threat list instead of create their own. Kind of like making them act as a group instead of individually (or exactly like o.O).
Spunky_Girl wrote:
and yet still remain modular enough to be easily edited.

I'm not going to write some tutorial on this or get into any kind of complex fuzzy-logic tutorial, but there is a simple way of achieving low-cost flexibility: /datum

First consider this basic AiLoop() example:
mob/npc
var
active = FALSE

proc/AiLoop()
if(active) return
active = TRUE

do
// stuff
while(active)

It's simple enough to add in conditional mood behavior:
mob/npc
var
active = FALSE
mood = HAPPY

proc/AiLoop()
if(active) return
active = TRUE

do
switch(mood)
if(HAPPY)
// wander aimlessly
if(MAD)
// attack
// etc.
// sleep
while(active)

However, any time you need to add, remove, or change the behavior of moods you have all of these disjointed places to look (AiLoop() being one of possibly many). You can simplify this by allowing the moods to handle themselves:
mood
proc/Tick(mob/npc/char)

happy
Tick(mob/npc/char)
step_rand(char) // wander aimlessly
// etc

mob/npc
var
active = FALSE
mood/my_mood

proc/AiLoop()
if(active || !my_mood) return
active = TRUE

do
my_mood.Tick(src)
// sleep
while(active && my_mood)

Now changing moods is as simple as replacing my_mood with another mood object (you could create a global cache of the available moods to prevent from having 1000 /mood/happy objects). All of the logic is contained within the moods, Tick() being one example; mood might also factor into any number of other actions, such as willingness to do something. Later, if I needed to add moods (sad, somber, vigilant, etc.), I wouldn't have to touch the AiLoop() or any other code using the mood value. This is an ideal trait, and can obviously be extended to much more than just mood switching.

This isn't anything but a basic demonstration; the code snippets given aren't necessarily intended to be included in any game. Unless, of course, you want to.
I'm having issues with trying to make it so the NPCs will retreat back to their spawn after they get so far from it. I've messed with get_dist and I've messed with if(!(npc in orange(x,npc.spawn_loc))) and have either yielded the NPC not moving at all, or not retreating back to their spawn at all.
In response to Spunky_Girl
If you use a model like the one I mentioned earlier, where moods all exist in a continuum with one of them taking the dominant focus, then what you basically need is another mood for desire to return to home territory, and keep the distance factored in to the mood calculations.

For instance, the raw distance between points A and B will be sqrt((A.x-B.x)**2+(A.y-B.y)**2). The square of that however is probably more interesting and easier to calculate, since past a certain boundary the creature will feel more and more uneasy. So all you need is the squared distance, divided by the square of the "safe" radius, and multiplied by a constant that represents a relatively significant mood. I'd say that constant should probably be set either equal to the most prominent "baseline" mood, or half the total of the baselines. For instance:

Baseline:
aggressiveness = 0.3
curiosity = 0.4
fear = 0.3

Then a good value for this distance constant would be 0.4 (the highest mood) or 0.5 (half the total).

var/dx = src.x - home.x
var/dy = src.y - home.y
var/dsq = (dx*dx + dy*dy) * 0.4 / (saferadius*saferadius)


And that value of dsq can then be used in the mood calculations to see if it surpasses the current dominant mood. You can make this a smidge faster BTW, by combining the 0.4/(saferadius*saferadius) into a single var since that shouldn't really change frequently per creature.

Lummox JR
Page: 1 2