Sign in to follow this  

too many select_target() routines!

This topic is 392 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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?

 

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites
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.

Edited by Norman Barrows

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites
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.

Edited by Norman Barrows

Share this post


Link to post
Share on other sites

 

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.

Share this post


Link to post
Share on other sites

shift your issues into data

 

whether you place constants in a text file, then read them in, or place them in a .cpp file and compile them makes little difference with respect to the fact that you have to define new constants every time. with almost every new select target routine, some new condition has been added to the "system". they are not just a different subset of existing conditions. they are typically a subset of existing conditions, plus one new type of condition. so somewhere in the code it has to go ok, if enum 1 is set, check condition 1, and so on.   So if you add a new check every time, you end up modding the system every time.  saves you a bit on  the cut and paste of the code that doesn't change, but you then have to go and set all those flags. probably easier with a "system" built upfront.  the reason why i didn't is because as i said, going into this i had no idea i'd end up with over a dozen different types of AI behavior. and thus lots of different "select a valid target" routines. If the AI wasn't done, and i anticipated adding more, i'd likely re-design.  but maybe not, the code doesn't really lack clarity yet. it just has some redundancy. Until i have to mod it in a major way, the price i pay is a few milisec longer for build and a few more bytes to the exe file. I'd guess half a day to re-factor it.   i could spend that time taking a shot at the rock toss mini-game using the combat engine.  it didn't quite make the cut-off line, due to complexity.  At the moment i'm working on SIMSpace, while I wait for the new PC to arrive so I can finish Caveman.

 

In the end it boils down to the fact that i don't refactor cause i don't have to. sure the code could be better, but its good enough for now.  re-factoring now gets me nothing right now, only maybe something in the future. and as the code is done, that's unlikely. and the next version - if there is one - would most likely use something like unreal engine. Its simply too much work for one person to build both a graphics engine and a reasonable sized game in any reasonable amount of time. So the code is basically one shot disposable, and is unlikely to ever be touched again.  If it is touched again in major way, then i'll consider re-factoring.  but at each step, there's the how long to refactor everything to the new system, vs simply adding the capability to the existing system.   As the exiting system grows in capabilities, the time to refactor can become significant vs the time to add a new capability. for example, the cut and paste took maybe an huor at most from the time i fired up the compiler, til it was done and tested and moved from the todo to the done list. refactoring everything would be half a day, maybe a day.  so sometimes not-refactoring can make you more productive, but with less elegant code. like i said, its not for everyone. i can get away with it case i'm a solo gamedev and have been coding for decades, so i have a pretty good idea when things need cleaning up and when they're good enough. obviously, this not a safe-for-work policy. So don't try this at work kids! <g>.

Share this post


Link to post
Share on other sites

Honest question, Norm: why do you bother asking for advice on these forums if all you ever do is argue that it doesn't apply to you?

 

Looking for silver bullets i guess.

 

In this case, it seems nothing will change the fact that as new condition checks are added to the system, the system will need to be modded.

 

The same way that an ECS can only use components that have been pre-defiinded in the engine,  A data driven or enum driven version of "select target" could only use conditions that had already been implemented in the system.   Since every new "select target" routine pretty much called for some new type of condition check, I would have been modding the system all the time.

 

The nature of the checks makes an all-in-one system not necessarily an obvious choice. 

 

Each routine checks (iterates though) 1 to 3+ lists - band members, animals (including NPCs), campfires, etc..

 

If i mash all of them into a single routine, i get two loops with a ton of branches in them to handle the dozen or so different cases.

 

the other option is a dozen or so dedicated routines with no branching in the loops.   faster execution, more readable code, but you have a lot of routines. 

 

Once I got to 3 or 4 select target routines I should have thought about  changing the design.  but do you go for two big ugly loops with tons of branching?  and adding a new branch every time?   or do you make it separate iterations for each enumerated or data driven check?  not very efficient...    a dozen dedicated loops may be the best compromise.    its entirely possible I split it into separate routines early on due to excessive spaghetti due to branching in a single select_target routine.

Share this post


Link to post
Share on other sites

In this case, it seems nothing will change the fact that as new condition checks are added to the system, the system will need to be modded.



I find your terminology really hard to follow, so I'm not sure what you're talking about here. Of course if you want to modify the system you have to modify the system; that's just common sense, not something you can "silver bullet" your way out of.

So I'm guessing there's more to this than "I want to add stuff to my game without touching the computer" ;-)



You talk at length about having too many branches and too complicated logic and all these downsides. But it seems to me like you don't really address the suggestion itself, i.e. put your relationships in a data table and don't hard code them!

Take every entity type in the world and assign it to a faction. Each faction has a numerical ID. Now you can express the relationships between any two entities (assuming they follow faction rules) in an O(1) lookup from a NxN matrix where N is your number of factions.


How does this turn into nested loops and branches and complicated lookups? If you want to add a faction, just fill in a new row and column in your data matrix. If you're really bad about dynamic allocation, increase the size of the matrix's static array dimensions in code. DONE.

Share this post


Link to post
Share on other sites

This topic is 392 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this