adding new feature requires 99 code changes!

Started by
39 comments, last by Norman Barrows 10 years, 7 months ago

so the next thing on my todo list for Caveman (open world RPG/ person sim w/ fps interface) is to draw dead PC's on the ground, instead of simply removing them from the game.

this means a PC will need an "alive" boolean, as well as an "active" boolean. PCs are stored in an array of structs. generally speaking, only active PC structs are processed in the game.

so its very common to have code of the form:

for (a=0; a<maxcavemen; a++)

{

if ! cm[a].active continue;

// do some stuff with active PC cm[a]

}

to make the change, this type of code will become:

for (a=0; a<maxcavemen; a++)

{

if ! cm[a].active continue;

if ! cm[a].alive continue;

// do some stuff with active and alive PC cm[a]

}

i did a search on cm[].active, and came up with 99 places that the above type of code appears!
one place is init, where i'll add cm[a].alive=1;
one is draw_bandmember (draw PC), which will remain unchanged, it won't skip dead bandmembers, it'll just draw them dead.
the other 98 places need "if ! (cm[a].alive) continue;" added to them.
i'm not sure if the number of places has to do with code design, depth of simulation or a little of both. most features have been implemented with their own iterator loops for processing PCs. i suppose one big loop that handles many features at once would reduce the number of loops, and therefore the number of checks for (! alive)....
but does 99 sound like a lot of things to be updating for each player entity? note that some of those are checks, not updates. also, some things like a list of their traps and the location of their house are stored in the PC's data, which accounts for some of the loops and checks.
is it just a big game?

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Advertisement

yes, it is a problem

you want to use classes better

the power and ease they give you is unsurpassed in programming:

1. create a class with member fields that make sense for your NPCs


class NPC
{
    bool enabled;      // 
    bool renderable; // can we see this unit in the camera?
    bool dead;          // draw a dead unit if true

public:
    inline bool isEnabled() const { return enabled; }
   /// etc ...
// so far so good, right? let's make better use of the class
    
    // returns true if the unit is enabled, and at the same time is dead
    inline bool isDead() const { return enabled && dead; }
};
 

this is one approach, where the unit exists in a list somewhere, but can be deactivated / disabled

another is to remove units that arent alive anymore

Typically you'd want your units inside a std::vector<>, which you could just call units

And even better, a manager class for units, not just all your units, but any amount of units for any purpose

You could have one vector for dead units one for alive units and any other combination

What you should choose to do is entirely up to you

There are books written, tutorials made, but you are making your own game in your own time

Experiment and learn from it smile.png

Here is a Units class example:


class UnitList
{
    std::vector<Unit> units;

public:
    void add(const Unit& unit);
    void remove(const Unit& unit);
    
    // returns the number of units in this list
    inline int count() const  { return units.size(); }
    // returns unit no# i
    inline Unit& operator [] (int i)
    {
         if (i < 0 || i >= units.size()) throw "tantrum";
         return units[i];
    }
};

Now that you have a list of units, any and all interactions with units must go through this list, and nowhere else!

That means you have only 2 places now where changes have to be made to accomodate a change.

Of course, if the game logic changes, such as in your example (adding dead units,) that still means you have to go over your outside code and make some changes

Yikes. It looks like you are building yourself a dependency nightmare there. Any time you have an internal member of a class or structure referenced in 99 places throughout your code, you should hear an alarm bell ringing in your head.

Why does the outside loop even care about if an object is alive, dead, active, etc? What happens if you add paralysis, sleep, hypnosis, and so on? Are you going to modify those 99 places yet again? Why can't the loop just iterate the objects and pass them a message to DoSomething, and let the objects themselves decide whether or not to actually DoSomething, based on their own status? That way, the enclosing loop doesn't need to know the internals of the objects, which you are free to modify to your heart's content without breaking 99 different locations.

You might want to start looking into alternative means for structuring your game. For example, you might want to look into object composition as a means for reducing dependencies. Functionality is broken out into components which are owned by objects, and which handle their own narrow, domain-specific tasks. For example, some sort of Renderable component, which is responsible for managing the renderable (sprite, mesh, whatever) that gets drawn in the world. The Render system iterates all Renderables and draws them according to their internal state, and that state is managed by other components. There are many ways you can structure such a system, and it can get complex, but if you do it correctly you can drastically reduce the occurrence of redundant, duplicate code and unnecessary connections between objects.

as i said, some i believe is organizational, with regard to using one loop per update type instead of one loop per update frequency (IE one loop for all stuff that updates every frame, one for all stuff that updates once per game second, one for stuff that updates once per game every minute, etc). or using a single loop that updates every frame, then calls the less frequent updates using conditional control flow.

but even with one loop, and separating the list of traps and the "where's my house" info from the player info, i'm still looking at a PC entity type with something like 90 update and check methods!

whether its OO and all in one place or not, it still seems like a lot. i mean, know its an in-depth sim, but ~90 types of updates and checks?

OTOH, i wrote down the name of each of the 99 methods, while counting them, just in case i had to post some examples <g>. while looking over the names, each seemed to be something that an entity must have - at least for this game.

different updates occur at different rates, once per frame, second, minute, hour, or day, and also at other intervals like 5 minutes, 10 minutes, 15 minutes, etc. all the game's update() routine does is increment the clock and then call the appropriate update routines. since it appears that it models or checks ~99 things in relation to a PC, it would seem that i can reduce the number of loops, but not the number of methods.

consolidating the many small loops into fewer larger loops would make changes like this easier. might be less cache friendly though.

and none of the check loops could be consolidated. in OO terms, they are basically methods of the "list of PCs" object. so they have to iterate through the list of PCs to do their checks, stuff like "who's the closest player to player X's house", and stuff like that.

here's some of the types of updates (in the order they appear in the code):

WARNING! LONG LIST!

nuke_mood_friends_of(A). severely reduces mood (a la the sims) of all players who are friends with NPC "A" (who just died).

run_bandmember. update for a PC under AI control. the game is single player, but the player can control multiple PCs, like a household in The Sims. the player can switch between PCs at any time by hitting TAB. PCs not being controlled by the player are controlled by AI. this is the update routine for when they are under AI control.

nuke_band_mood. severely reduces the mood of all PCs (called when a PC dies).

kill_bandmember. hey, every object needs a dispose, right?

try_move_rafts. part of this routine adds a raft's movement to a PCs movement (you can walk around on a raft in motion!) it also calls automap for each PC onboard.

model_falling. does what it says. applies gravity, calculates velocity, checks for impact or glancing blow bounce off wall type thing, stops fall when hit ground, applies damage, kills player if needed.

model_paddling fatigue. increases fatigue when you're paddling a raft.

model sneak detection. does LOS/LOF checks for being spotted while in sneak mode.

model fatigue decrease. due to resting or not moving / attacking, or undertaking low energy actions like stargazing.

model player attacks. increment attack counters, apply attack results at appropriate point through the attack, reset "attacking flag" when attack completed.

model band member surprise. surprise is modeled in encounters. surprise gives you a few seconds to move before your opponent can, or causes you to be unable to move for a few seconds while your opponent can. this updates those counters.

resolve animal attack. a section of this routine checks for a PC in the line of fire of an animal's attack, and if found, checks for hit, applies damage, and kills the player if needed. so this the equivalent of where the game's "damage PC entity" method gets called.

check for 100% fatigue. checks for fatigue hitting 100%. when fatigue hits 100%, if the PC is doing an action (swimming, moving cross country, fixing a bow, gathering berries, etc), the action is pushed on the PC action stack. a "rest" action is then started. the rest action is designed to interrupt other actions, and therefore automatically pops the action stack if needed on completion.

check rock shelter encounters. checks for player near rock shelter. generates encounter if needed. removes encountered nps/animals, and resets "encounter checked" flag when no PCs nearby any longer. the game has 5000 rock shelters based on real world frequency distribution and the size of the game world.

check cave encounters - same idea, for caves. the game has 60,000 caves based on real world frequency distribution and the size of the game world.

check hut encounters - same idea, for huts. the game has 18,000 huts based on estimated population densities, the percentage of the population staying in one place at any given time, and the size of the game world.

check hut takeovers - checks for abandoned huts near the PCs. gives them the option to take them over. huts are typically "abandoned" because the player has killed or chased off the occupants.

model climbing animals - one of the actions in the game is "climb tree". if you get an encounter with a wild animal, you can climb a tree and wait for them to wander off. but some animals can climb up trees after you. this checks each PC to see of they're up a tree and in an encounter with an animal that can climb trees. if so, it forces them to jump to the ground to get away from the animal, at which point combat resumes as usual, although the player can always climb a tree again, not that it will do much good.

model fatigue due to damage - most shooters don't model this. but you DO get tired faster when wounded. this might be an example of one of the update methods required due to the depth of the simulation.

model fatigue due to encumbrance - another real world effect most shooters don't model.

model effects of intoxicants - somewhat akin to potion effects in skyrim etc.

make torches go out - an update on a PCs time limited inventory item.

check caveman encounters. encounter checks are done for each PC or group of PCs.

reduce water

reduce food

reduce sleep

affect mood

check animal encounters

reduce hygiene due to movement

model carried food spoilage

model raids on PC's caves / rock shelters / huts

model exposure

model heatstroke

model wear and tear of clothing and similar equipment. all objects in the game have a quality level 0-100%, and wear out / weather away.

check quest encounters. does any encounter checks needed for all active quests of all PCs, and generates the encounters. i know for sure oblivion does this.

model intoxicant OD

do wildfire. checks if PCs are in a map square with a wildfire. if so, does an animation of the player caught in the wildfire, applies damage, kills player if needed.

mood boost nature lovers: each PC and NPC has 3 interests. doing stuff related to your interests boosts your mood. this does a mood boost on all PCs with nature as an interest, who are outside.

reduce hygiene

model dehydration

model damage from disease

reduce food for cold weather (burn more calories to maintain body temp).

model drowning in floods - the game tracks water tables and flooding, driven by the weather engine.

model background radiation (IE old age - permanent reduction of hit points, until you eventually die).

zero god relations - move relations with the gods towards zero.

model skill reduction - skills go down over time. you must practice / research / refresh to keep up. only seen this when you get locked up in oblivion.

model starvation

model getting sick (catching an illness, not the effects thereof).

reduce social stat (a la the sims)

add band member - add a new PC to the list of PC entities.

selected band member - returns true if the player selected a PC with the spacebar to interact with them.

mood boost nearbys - does mood boost on all nearby PCs when a PC does an action like sing, play flute, or play drums.

talk to band member - does a check to stop talking if they die. "jeez, i didn't think i was that boring!" <g>.

listen to band member - does a check to stop listening if they die.

rungame - contains code to reset all PC's "moved" flags, and reset horizontal velocities for PCs not falling. basically clearing flags before calling update_all();

and thats it for the update type stuff. a few things in there may not technically be PC updates. it looks like a lot of the methods are checks such as "band member in way of band member", "band member near dropped object", "someone paddling" - returns true if a PC is aboard a raft and is doing the "paddle" action, and so on.

there are also render calls for drawing a PC in the world, on the world map, and the local map.

the AI uses six different type of target selection routines that iterate though the list of PCs looking for potential targets. I'm surprised it isn't more, given that there's something like 14 types of AI in the game.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

I think if its the same tests in all 99 loops you could sort all those entitys once into active and inactive. Then you can delete 98 of those 99 "if", loop only until maxactivecavemen instead of maxcaveman and it gets even more cachefriendly.

but does 99 sound like a lot of things to be updating for each

when I was writing some things like that I got it much the

same, some field in character structure was touched, referenced in a 99 places - I am not sure if this could be

redesigned, but it is good question

Having logically duplicate code even once is a terrible habit.

You can continue removing cavemen as you used to, but spawn new "corpse" entitties and process them in your render pass.

If you want for some reason to keep corpses same entities as alive ones, you can make isActive function (active & not dead) instead of plain field, while in render pass you can opt dead in.

And there are even better ways if you implement suitable architecture.

when making a change to one piece of functionality effects a lot of other things, this is generally a bad sign.

usually, it makes sense to try to organize things when possible, that making changes does not have widespread effects.

usual strategies are what I call "modularizing", "layering", and "pipelines".

modularizing is basically breaking parts into independent "black boxes", where code in one "module" ideally minimize dependencies and interactions with code within another module.

layering is basically building an API such that a range of common functionality can essentially be "abstracted out" of the code.

ideally, code building on top of the API needs to not know or care how things work inside the API, nor should the API code care what happens in client code or how it is used.

pipelines are basically where the code is broken up into a number of stages, typically with each stage transforming input into output in some well-defined way. typically either, each stage is stateless, or the existence of state is handled explicitly in the design.

in this case, a piece of code to achieve a task then becomes a number of interconnected stages, with each operating independently of the others.

typically, all this is then applied at various levels. this allows generally getting more things done with less effort, and basically more freedom with going mix-and-match with various parts of the project while minimizing need for large-scale changes.

for game logic, this would generally mean trying to abstract the logic from the entities from the handling of entities themselves.

for example, does the entity-system logic even need to know or care whether the entity is alive or dead? generally not, and instead being alive or dead would be a property of certain types of entities (such as AI controlled mobs). so, the entity system will primarily concern itself with things which are fairly universal.

generally, we also want to isolate any loops or iteration over entities from the logic for the entities themselves. this would mean that general update loops will primarily consist of calling methods in the specific entity objects (this is independent of language choice), which may itself implement the relevant behavior logic.

but, yeah, if things are organized well, than more types of similar work can be done more quickly and with less effort, even if potentially at some cost in terms of additional up-front effort.

though, yes, there is also a problem that too much up-front generalization can also be a problem, resulting in a lot of work for not a whole lot of gain (or a project that is 90% infrastructure and 10% program logic), or use of inappropriate abstractions which don't really fit the use-case, ..., so striking a balance is needed.

some types of tasks are easier to abstract in this way than others.

Having had this sort of problem in the past (more common with my SOA organized data when I had to do AOS access style in specific cases), I generally used a helper+filtered iterator approach to simplify things, though it does not completely remove the 99 cases, it should at least reduce the code overlaps:

Original
for (a=0; a<maxcavemen; a++)
{
if ! cm[a].active continue;
// do some stuff with active PC cm[a]
}
Abstract iteration version:
typedef std::function< void ( Caveman& ) > CavemanUpdate_t;
IteratateCavemen( Cavemen& cm, CavemanUpdate_t update ) {
for( Caveman& c : cm ) update( c );
}
This is not really going to do any good but it is the start to writing:


typedef std::function< bool ( Caveman& ) > Filter_t;
typedef std::function< void ( Caveman& ) > CavemanUpdate_t;
IteratateCavemen( Cavemen& cm, Filter_t filter, CavemanUpdate_t update ) {
for( Caveman& c : cm ) if( filter( c ) ) update( c );
}
So, the benefit of this is to get rid of that "if( active ) etc" stuff and move it to a helper class. So you can write something as follows:


struct UpdateFilters
{
static bool IsLiving( Caveman& cm ) {return cm.alive && cm.active;}
};

IterateCavemen( cavemenList, std::bind( &UpdateFilters::IsLiving, std::placeholders::_1 ), []( Caveman& c )
{
// Do your update code here.
} );
};
This does not attempt to correct code structure though I tend to agree with others that you might consider this is a problem, but that's completely up to you. What it does do though is allow you to break up the functionality such that in the future at least the key pieces of the code beyond the localized "update call" are nicely organized somewhere central.

NOTE: Obviously I'm using C++11 in the above. You can of course replace it all with TR1, Boost or just plane old functors. Of course each of the other solutions has it's own downfalls, generally in terms of typing.

I think if its the same tests in all 99 loops you could sort all those entitys once into active and inactive. Then you can delete 98 of those 99 "if", loop only until maxactivecavemen instead of maxcaveman and it gets even more cachefriendly.

actually, the trick there is to the old "Swap with last and decrement count" trick.

IE if num_PCs = 5, and you get a kill_PC(2) call, you do:

pc[2]=pc[4];

num_PCs--;

that way there's no sorting, and you only iterate over 0 through num_PCs-1. this only works cause the list is unsorted.

but for the moment, i haven't had to get into optimizing update() too much. like all good simulators, the game supports accelerated time: 2x, 20x,and (up to) 900x. but 900x currently only runs at about 30x on a baseline PC. so some optimization is probably in my future.

all the updates don't happen every frame. some happen every frame, some every second, some every 5 seconds, some every minute, some every 10 minutes, some every 15 minutes, and so on. this lead to the following type of code for update():

 
void run_simulation()                           // the "model stuff" engine
{
do_global_frame();
if (frame==0)  
    {
    do_global_second();
    if (second==0) 
        {
        do_global_minute();
        if (minute==0) 
            {
            do_global_hour();
            if (hour==0)
                {
                do_global_day();
                if (day==0) 
                    {
                    do_global_year();
                    }
                }
            }
        }
    }
}
 
 

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

This topic is closed to new replies.

Advertisement