too many select_target() routines!

Started by
12 comments, last by ApochPiQ 7 years, 5 months ago

The game in question: Caveman 3.0, a FPSRPG - stone age setting.

I recently added hunt behavior for pet dogs.

I did this by replacing graze() with hunt() at the end of their decision tree. hunt() calls graze() if there's nothing to hunt.

but hunt() uses get_predator_target(), which includes band members, so the dogs attack their masters! <g>.

The problem is i have a number of different types of entities:

band members (PCs)

friendly cavemen (NPCs)

friendly cavemen at shelters (NPCs who defend a location)

warriors (hired mercs)

companions (followers)

hostiles (badguys)

defend location hostiles (badguys that defend a location/object)

quest hostiles (badguys that fight to the death)

pet dogs (pets)

wild dogs

wild animals

predators

defend location predators

quest predators (fight to the death)

wild avians

avian predators

so for each of these i seem to need one or more select_target() routines. Its sort of like i have a bunch of factions and who is at war with who is different for everyone.

Any way to simplify this? Even re-using select_target() routines where possible, i still seem to have almost a dozen unique routines.

Maybe some sort of generic routine where you pass it a list of valid target types, and it selects from those?

That's really the only unique thing about these routines, is exactly what is considered a valid target. some exclude this, some exclude that, some exclude the other thing, some exclude all of the above...

Since this is the last one i'll have to write, should i just copy/paste/edit and be done with it?

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Advertisement

The usual answer is that the differences should be designed in data as far as possible. Then you get rid of functions like get_predator_target() and replace them with get_target(enum_predator) or whatever. Similarly, instead of having "pet dogs, wild dogs, wild animals" you have a species value (dog, wolf, mammoth, whatever) and a feralness value (wild, tame) and each animal has a setting for each of those.

On a different note:

I did this by replacing graze() with hunt() at the end of their decision tree. hunt() calls graze() if there's nothing to hunt.

That sounds a bit weird - choosing between hunting behaviour and grazing is exactly the sort of thing that should be in the decision tree, not implemented as a fallback within the code. This way, if you ever need to do hunting with an animal that never grazes, you'll have to create a new function or rework all your old trees. Ideally each activity should be able to work in isolation and be modular.

The usual answer is that the differences should be designed in data as far as possible. Then you get rid of functions like get_predator_target() and replace them with get_target(enum_predator) or whatever.

Yes, that's what I was thinking too, but then you just end up with a nasty bunch of "if this enum and that target type, check it" or the big switch: switch(enum) case 1 check this, case 2 check that, case 3 check the other one.

So it streamlines the API, but the guts of the code is no cleaner, just rearranged, and actually uglier.

About the cleanest way to do both would be an enum for the API, and then a switch that just calls the original dozen different select_target routines. That keeps the (public) API simple, and keeps each subroutine simple as well. You have to loop through 1 or 2 lists of targets (band members, and everyone else), checking for is_active, is_alive, is or is not some species, state (WILD, TAMED, WARRIOR, COMPANION, FRIENDLY, HOSTILE, etc), relations, range, etc. trying to combine 12 of those into one big loop is gong to be ugly....

Actually, the enum would determine if you even had to check bandmebers or not. And everything checks for active and alive (except predator target carcass for eating). So you could just do a big switch on enum in the middle of the loops to check for WILD, TAMED, predator ai, etc.

So it looks like the choices are a dozen routines with one or two loops and some hard coded checks in the loops. or a single routine with two loops, and a switch in the body of each loop that does checks based on enum, and execution of the band member loop based on the enum. if it wasn't already done 95% the first way, i might do it the second way instead. in the end it boils down to being able to say select_target(predator) instead of select_predator_target(), at the cost of having to refactor a dozen subroutines. If you think about it that way, it doesn't really seem to be worth it.

Similarly, instead of having "pet dogs, wild dogs, wild animals" you have a species value (dog, wolf, mammoth, whatever) and a feralness value (wild, tame) and each animal has a setting for each of those.

That's how it actually works. All entities have a species and state (wild, tamed, etc). Humans also have an NPC type (thief etc). So a wild dog is species 61, state=WILD, and a domesticated dog is species 61, state=TAMED. The 16 general entity types listed above are the result of the combo of species, state, is_avian, AI type, etc. There are actually 50 different species in the game. defending_location and is_quest_critter are additional variables that describe an animal's behavior. So species, state, AI type, is_avian, relations, defending_location, is_quest_critter, and NPC_type are all used to determine exactly what you're dealing with and what behaviors they follow.

That sounds a bit weird - choosing between hunting behavior and grazing is exactly the sort of thing that should be in the decision tree,

Hunt() IS a decision tree! <g>

I discovered you can write a decision tree for a given behavior, and if it triggers no move, you can then call another decision tree to see if it generates a move. So you have hunt AI, graze AI, and stand AI. You try hunting first, if there's nothing to hunt, you call graze. Graze transitions between wander, flock, and migrate at random, with stand as the default behavior when doing none of the above.

Hunt was the original AI for lone predators. Graze was the original AI for herd animals.

So the AI becomes prioritized "try this, then try that", where each behavior you try is a decision tree that may or may not yield a "move" as output, such as "attack", or "wander" or "stand".

It just so happens that for every critter that hunts, the next behavior to try is graze. So if it gets to the end of hunt without triggering an action, it calls graze. Which actually turns the "hunt" behavior into "hunt if you can, graze if you can't" behavior.

Each AI type in the game, of which there are about a dozen, is simply a list of: "if do some behavior, return, else if do some other behavior, return, etc.". With behaviors like "i'm cornered", "i'm taking fire", "threat nearby", "time to hunt", "attack badguys within 100 feet" etc. Actually, they are more of the form: check for some condition, if condition exists, react to condition, else check next highest priority condition to react to.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

No, I completely disagree. Having a switch with 12 different routines to call is just hiding the fact that you have massive duplication of logic inside those 12 routines, and that when you add a 13th you have no effective way of reusing the existing code. And whenever you change things you have to fix somewhere between 1 and 13 routines. Sounds like a nightmare to me. You don't need "one or more select_target() routines" - you need different criteria being assessed inside select_target(). Find a sensible way to organise that.

you need different criteria being assessed inside select_target(). Find a sensible way to organise that.

that would be the enum with a switch on enum or a test of enum for each check performed in the loop.

if (enum1)

{

check_bandmemebrs

check_friendlies

etc

}

=or=

if (enum1 or enum3)

{

check_bandmembers

}

if (enum2 or enum4)

{

check_WILD

}

etc

So in the end I have a bunch of checks, and a bunch of enums that say which checks get done. Its kind of a toss up as to which of the above two forms would be easier to use and mod.

Using the second form, you can do each loop just once. with the first form, you loop over the list of active animals for each thing you check (except band members). So if they can target both "tamed" and "wild of different species", then you'd loop though the list twice, once looking for tamed animals, and once looking for wild animals of different species. So the second form seems to be more efficient.

But if i add a new enum, with the first form, all i do is add another case for the new enum and add the checks to that case. With the second form, i have to add the enum to the if statement of all required checks. i guess its a wash.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

I find it astonishing that you're so adamantly opposed to the root suggestion, which is to drive this with data instead of hard-coding everything.

Build a data table that describes the factions and relationships. Then write code once which interprets this table and feeds runtime game parameters in and gets decisions out.

Then configure your faction additions/changes in data instead of rewriting/duplicating code.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Any duplication of code is a design error.

Whether you want to use named constants (enums) or other data in your logic is not so important. The main difference is that the named constants effectively prevents modding via data, because a programmer has to define them and recompile the source when they change.

They are saying something more along the lines of, having your entities have an array of enums to identify what they fear, what they are allied with, what entities they eat, etc... and using the same (or close to the same) functions for all entities, regardless of type.

For example:


rabbit.threats = {lion, tiger, bear}; //oh my!
tigers.prey = {rabbit, gazelle, human};

You can even order those by priority, if you like. i.e. a tiger might prefer a gazelle, but would go for a rabbit if it's really near, or for a human if it's really hungry. Heck, it might go for a water buffalo if it it has help.


struct PreyBehavior
{
     Enum entityType; //I'll attack this entity...
     int maxPreyAllyRatio; //If they aren't too many of them, compared to me and my allies...
     float range; //And if it's close enough to even bother. (no point travelling a mile for a rabbit, but totally worth it to travel a mile for a gazelle).
     float hunger; //How hungry I have to be to go after this animal.
};
  
tigers.prey =
{
             {rabbit, 0 (no need for help), 200 ft (will travel 200 ft for a rabbit / can smell them from a mile away},
             {waterbuffalo, 3.0 (Better be at least 3 to 1), 5 miles (but I'll travel far to get a bite)}
             {human, 1.0 (Better be at least 1 to 1), 1 miles (I'll only travel a little ways), hunger = 0.9 (but I'm only going to go for a human if I'm desperate}
};

Mix in some other logic, and you can get things like "Bandit caveman will attack other cavemen if they are not part of same tribe and the bandits are in greater numbers than their targets."

By mixing in some randomness of who the entities spot, you can create scenario where bandits attack and then get overwhelmed not realizing the other cavemen had more friends nearby than they thought.


array<EntityHandle> Entity::scanForPrey(float chanceOfSpotting)
{
    array<EntityHandle> knownPrey;

    array<EntityHandle> entitiesInSearchRadius = getEntitiesWithin(my->position, preySearchRadius);
    nearbyEntities = nearbyEntities.sortByProximityTo(my->position);
    
    for(each nearby entity)
    {
          if(random() <= chanceOfSpotting)
          {
               if(my->entitiesIEat.contains(entity.type))
               {
                     knownPrey.push_back(entity);
               }
          }
    }

    return knownPrey;
}

array<EntityHandle> Entity::scanForThreats(float chanceOfSpotting)
{
    array<EntityHandle> nearbyEntities = getEntitiesWithin(my->position, threatSpottingRadius);

   //...etc...
}


Entity::think()
{
    array<EntityHandle> threats = my->scanForThreats(0.50f);
    if(threats.empty() == false)
    {
          my->state = run_like_hell;
          return;
    }
    
    array<EntityHandle> prey = my->scanForPrey(0.90f);
    if(prey.empty() == false)
    {
          if(prey->position < pounce_radius)
          {
               my->state = attack_target_enemy;
          }
          else
          {
               my->state = sneak_up_to_target_enemy;
          }

          return;
    }
}

I did this by replacing graze() with hunt() at the end of their decision tree. hunt() calls graze() if there's nothing to hunt.

That sounds a bit weird - choosing between hunting behaviour and grazing is exactly the sort of thing that should be in the decision tree, not implemented as a fallback within the code. This way, if you ever need to do hunting with an animal that never grazes, you'll have to create a new function or rework all your old trees. Ideally each activity should be able to work in isolation and be modular.

It kinda makes sense, if you think of "hunt()" as "search for food", with "graze()" being, "eat food on ground". But I'd absolutely give it a clearer function name to indicate this.

They are saying something more along the lines of, having your entities have an array of enums to identify what they fear, what they are allied with, what entities they eat, etc... and using the same (or close to the same) functions for all entities, regardless of type.

In the end, the extremely wide variety of types of conditions to check for, and the fact that i just needed one more, vs rewriting the dozen or so existing routines, caused me to take the easy way out and copy / paste / edit set_predator_target() to create set_dog_hunt_target(). dogs don't target: band members, companions, warriors, friendlies, other dogs, or critters bigger than they are. They will target critters near a campfire.

predators target anything not of the same species and not bigger than them, that's not near a campfire.

in the case of pack hunting, they target critters of different species that are not stronger than the pack, and not near a campfire.

The difficulty in building a "system" is you don't really know all the conditions you'll need to support til the game is done. Similar to building a mission or quest editor.

And when building a new kind of game, you don't exactly know what will end up being big enough to warrant a "system" in the first place. This is the first competed version of Caveman to feature full time FPSRPG game play. Version 1 only has FPS mode for settlements, cavern exploration, and combat, and was similar to The SIMs at other times. Version 2 was full time FPSRPG, but was never completed - a medical emergency in the family forced me to shelve the project and go get a day job.

So you have two choices, write it all, and be done with it, or write it all, then refactor, and then you're done. Once the code is done and working, if its unlikely it will be touched again, refactoring doesn't get you much.

You know how it is, you can codesmith (refactor) forever. I used to do it all the time, about 20 years ago. Usually didn't need much. Figured out it didn't really get games built faster. Out of the box code was usually well designed. So i only re-design as necessary now. I can only get away with this cause I'm a solo gamedev.

It kinda makes sense, if you think of "hunt()" as "search for food", with "graze()" being, "eat food on ground". But I'd absolutely give it a clearer function name to indicate this.

These are sort of "meta behaviors"

hunt actually consists of

if carcass nearby, goto carcass and eat, else hunt.live game.

hunt live game: if can pack hunt, and its to your advantage, do so, else solo hunt.

solo hunt: they will then target closest of same or less strength. move to tgt, attack.

if no target, they graze.

graze: stand, wander, flock, and migrate at random.

they go though this decision tree each time, so as soon as they get a kill and create a carcass, they will switch over to eating. And if the prey gets away, they will graze.

It just so happens that the desired meta behavior when you're hunting and have no targets is to graze - in all cases. If there was an animal type that didn't graze, i'd simply turn off graze at the end of hunt, and add it after the call for hunt to those critters that do graze. But since they all call it, i just have it at the end of hunt, instead of as a separate call for each critter type that hunts. I think there are actually only three calls to hunt, one for predators, one for packs, and now, one for dogs.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

The difficulty in building a "system" is you don't really know all the conditions you'll need to support til the game is done

That is your fault: try to build a system which supports your known requirements and try to utilize it for upcoming requirements.

Limit your system and be creative in utilizing it to find a solution for your upcoming issues (especially if you are the sole dev).

As mentioned before, shift your issues into data, as example I use the following query-approach completly based on data:

1. Every entity has normalized attributes (that is, you have a standard way to access them).

2. Every entity has a stereotype (for fast rooting).

3. My query consists of a list of conditions to attributes (los,range etc.):

3.1. Is an attribute present, does it have a certain value or is within a certain value range ?

3.2. Use optional,external parameters for thresholds, ranges and values.

3.3. Assign a type to a entity which fullfils certain filters/criteria ("danger","prey","ally")

So, now with this system established, I design my game in a way to utilize attributes which satisfy this system, instead of designing the game first and try to find a way to bend(break) the system to query all possible combinations.

This topic is closed to new replies.

Advertisement