adding new feature requires 99 code changes!

Started by
39 comments, last by Norman Barrows 10 years, 7 months ago
Here's the C++ translation of band member update all:


//    must call move_rafts first!
void BMupdate_all()
{
    int a;
    for (a=0; a<num_bandmembers; a++)
    {
        if (cm[a].alive == 0)
        {
            continue;
        }
        //    do update stuff here - do global frame
        BMdecrease_fatigue(a);
        BMmodel_attack(a);
        BMmodel_surprise(a);
        BMmodel_full_fatigue(a);
        run_bandmember(a);
        BMmodel_raft_movement(a);
        BMmodel_paddling_fatigue(a);
        BMmodel_falling(a);
        BMmodel_sneak_detection(a);
        if (frame != 0)
        {
            continue;
        }
        //  do global second
        BMmodel_fatigue_dueto_damage(a);
        BMmodel_fatigue_dueto_encumbrance(a);
        BMrun_quests(a);
        BMclear_rockshelter_enccouter_flags(a);
        BMcheck_rockshelter_encounters(a);
        BMclear_cave_encounter_flags(a);
        BMcheck_cave_encounters(a);
        BMclear_hut_encouter_flags(a);
        BMcheck_hut_encounters(a);
        BMcheck_hut_takeover(a);
        BMcheck_climbing_animals(a);
        if (second != 0)
        {
            continue;
        }
        //    do global minute
        BMcheck_animal_encounters(a);
        BMcheck_caveman_encounters(a);
        BMmodel_intox(a);
        BMextinguish_torches(a);
        if (minute%7 == 0)
        {
            BMreduce_hygiene_dueto_movement(a);
        }
        if (minute%10 == 0)
        {
            BMreduce_water(a);
            BMreduce_sleep(a);
        }
        if (minute%15 == 0)
        {
            BMreduce_food(a);
            BMaffect_mood(a);
        }
        if (minute != 0)
        {
            continue;
        }
        //    do global hour
        BMmodel_dehydration(a);
        BMmodel_food_spoilage(a);
        BMmodel_exposure(a);
        BMmodel_heatstroke(a);
        BMmodel_drown_in_flood(a);
        BMmodel_wearNtear(a);
        BMmodel_perm_shelter_raids(a);
        BMmoodboost_nature_lover(a);
        if (hour%2 == 0)
        {
            BMmodel_damage_dueto_illness(a);
        }
        if (((hour>7)&&(hour<19)))
        {
            BMcheck_quest_encounters(a);
        }
        if (hour != 0)
        {
            continue;
        }
        //    do global day
        BMmodel_starvation(a);
        BMmodel_getting_sick(a);
        BMmodel_background_radiation(a);
        BMmodel_perm_shel_weathering(a);
        BMzero_god_relations(a);
        BMreduce_social(a);
        BMmodel_traps(a);
        BMmodel_skill_reduction(a);
    }
}

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Advertisement
Is it really necessary to do all those state checks and updates for *every* agent every tick?

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

Here's the C++ translation of band member update all:


// snipping code...


I think it's appropriate that this was in comic cans. Is it really possible that one agent could be doing all of that at once? Is it not possible to consolidate certain heartbeat actions (skill reduction, god relations, background radiation, illness damage, etc...) into a single update? Those affected members could all be stuck in a common structure for cache coherency, and affected in a single HeartbeatUpdate call. This, here, seems to me to be the very epitome of the phrase "spaghetti code". It's especially funny that you have a comment saying "Must call move_rafts first" but you actually call BMmodel_raft_movement sixth in line. What does the method run_bandmember() do, if it doesn't handle any of all this? This whole thing just screams for refactoring.

Is it really necessary to do all those state checks and updates for *every* agent every tick?

excellent point.

technically, checks probably ought to be made as infrequently as possible.

as you can see, the code already splits up the checks into those done each frame, and those done once per game second, minute, hour, or day.

when implementing each type of new update, i would ask myself, how often does this need to be done? the answer would usually indicate which of the general ranges of update frequency (each time step, once per game second, game minute, etc) seemed appropriate purely from a simulation point of view with no regard for performance. this led to the general ordering and slicing of things as seen above.

other than reducing the frequency of target selection from one per frame to once per second ( remember the thread about collision checks at 900x accelerate time? ),

i haven't gone back and re-evaluated the frequency at which things are being called. if / when needed, that could gain additional efficiency.

these are the updates done each time step:

BMdecrease_fatigue(a);
BMmodel_attack(a);
BMmodel_surprise(a);
BMmodel_full_fatigue(a);
run_bandmember(a);
BMmodel_raft_movement(a);
BMmodel_paddling_fatigue(a);
BMmodel_falling(a);
BMmodel_sneak_detection(a);

updating fatigue, doing the attack, incrementing surprise counters... run_bandmember is the actual AI and move routine. model_raft_movement just adds the raft's movement to the player's movement. falling is a special case move update, and of course LOS/LOF checks.

fatigue could probably be cut to once per second.

attack and movement obviously must be done every time step.

surprise lasts for so short a period of time (perhaps 2-3 seconds), that it too must be done every time step.

some savings might be found in the AI. But already i find the targeting a bit unresponsive at its current lower frequency (once per second as i recall). you can close a lot of distance in a second in melee at sprint speed.

LOS/LOF checks for sneak mode could probably be cut to once per second, but again, you have a responsiveness issue similar to targeting.

i'll probably end up increasing targeting responsiveness some before the game is released. perhaps to twice per second. a half second delay in enemy reaction shouldn't give the player too much of an advantage.

side note: why is it that every posing or email i write, i have to add a half dozen technical terms to the spellchecker (chrome's i assume) ?

anybody else get that a lot?

anyway, as you say, a second look at how often things get called might be called for, and will probably be happening when i try to go for that true 900x accelerate time game speed. right now its probably only managing perhaps 30x or so on a baseline box (IE an entry level PC).

and that will all be optimization of input() and update(), not render, it doesn't get called! should make for an interesting case study.

doubt i'll get true 900x, but i'll see what i can do. its not that bad right now, 8 hours of game time sleeping only takes about 7 seconds of real time, unless there's a lot of entities active in the simulation.

as you can infer from that last statement, some interesting optimizations challenges probably await. i actually almost look forward to it. at last some real engineering to sink my brain into. too much of programming is simply glorified word processing.

i have the timing test results from the two loop layouts. i'll post those.

next step is to hook up the findfirst/next iterator. is there a zero additional overhead c++ construct i can use in place of that?

wait, a vector comes with an iterator right? but they're implemented like a template, w/ rtt checking.....


Is it really necessary to do all those state checks and updates for *every* agent every tick?

another excellent point.

not only can the frequency of calls be reduced, but entities can be updated in round robin fashion as well.

a few of the updates do this already, as do the targeting routines. they get called every time step, but use "unit index mod current frame #" to divvy up the targets into groups. with only one group processed per time step. so the update function gets called every time step, but the update for a unit only occurs once per second. and only 1/fps of the units is updated per time step.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

I'm not talking about efficiency concerns at all. I mean does it actually make sense to do all those calls?

I'd probably structure it very differently, where each agent in the game world has a set of properties that can update over time. These can either be purely computed, or updated periodically via mutation. I'd tend towards preferring pure computation as much as practical, since it removes a lot of repetitive logic and cleanly encapsulates various concerns.

For things which absolutely need to have mutated state on a periodic basis, I'd implement a generic mechanism for triggering updates. You could make it as simple as a container of function pointers, where each function pointer is invoked in turn and given a pointer to the objects it needs to update. This could be trivially handled as a bitmask, where you set a bit for each update that needs to run.

For example:

enum UpdateTypes
{
    UPDATE_TYPE_MOVE,
    UPDATE_TYPE_HEAL_OVER_TIME,
    UPDATE_TYPES
};

typedef void (*UpdateFunction) (UpdatingObject * obj);

UpdateFunction Updaters[UPDATE_TYPES];
Updaters[UPDATE_TYPE_MOVE] = &MoveFunction;
Updaters[UPDATE_TYPE_HEAL_OVER_TIME] = &HealOverTimeFunction;

// Game loop
while(true)
{
    for(unsigned i = 0; i < UPDATE_TYPES; ++i)
    {
        for each updated object
        {
            if(object->UpdateFlags & (1 << i))
                (*Updaters[i])(object);
        }
    }
}

// Example updater
void HealOverTimeFunction(UpdatingObject* obj)
{
    obj->Health += 10;
}

You can also flip this around, and have each updating object store a container of update functions that should be called on it next tick. Then you just add an update function to that list every time you detect something needs updating, and dispatch those update calls across each object in your game loop:

for each game object
{
    for each update function in this object's update list
    {
        update_func(object);
    }
}

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


I think it's appropriate that this was in comic cans. Is it really possible that one agent could be doing all of that at once? Is it not possible to consolidate certain heartbeat actions (skill reduction, god relations, background radiation, illness damage, etc...) into a single update? Those affected members could all be stuck in a common structure for cache coherency, and affected in a single HeartbeatUpdate call. This, here, seems to me to be the very epitome of the phrase "spaghetti code". It's especially funny that you have a comment saying "Must call move_rafts first" but you actually call BMmodel_raft_movement sixth in line. What does the method run_bandmember() do, if it doesn't handle any of all this? This whole thing just screams for refactoring.

<BG>

what you see is the result of simply taking the guts of all those update loops and placing each in its own function, then calling them all. many (perhaps 2/3's) are very short 1-3 liners. further consolidation is almost definitely warranted. in the mean time i have fastcall and inline any suitable on my side - not that that necessarily means a whole lot.

OTOH, placing the body of the loop in its own function, and then placing the guts of all those routines (at least the smaller ones) in the loop body would have a negative impact on readability. not the loop body as a separate function, that actually makes things clearer - its the adding of all those little 1-3 liners to one big update_each() routine that would detract from the read-abilty of the loop body code.

the general design considerations in software engineering: read-ability, write-ability, maintain-ability, modify-ability, reuse-ability, speed, resource usage, etc. are often at odds with each other.

this is the nature of design considerations in general in all forms of engineering.

thus all design is an exercise in compromise.

good design finds that "sweet spot" trade-off point between competing considerations.

personally, i've always been big on read-ability. i used to give team mates grief in school for using shift instead of add due to lack of readability (it was less English-like, and we didn't need the speed).

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php


Those affected members could all be stuck in a common structure for cache coherency

rock shelter, cave, and hut encounter flags are in the list of caves, rock shelters, and huts.

everything else is in a caveman struct:

(gonna give slash code a try here....)


 
 
// cavemen list
struct cavemanrec
{
char name[100];                     
int str,intelligence,dex,con,chr,speed,   // core stats
food,water,sleep,mood,hp,dmg,has_disease,hygiene,     // current stats          
fatigue,   // fatigue * 1000
social,
xp[maxskilltypes],
mx,mz, // map sq             positive x is east. positive z is north.  x=0..4999.   z=0..4999.
active, // 0 or 1. whether the record is in use. 1=active.
current_action,     // what they're doing   0=do nothing.
action_counter,
action_x,
action_z,
moved,            // 1 if they moved this frame and dont recharge fatugue
wpndrawn,           // 1 if wpn drawn
sex,                 // 0=male 1=female
curwpn,             // what kind of weapon they're currently using      (an obj#)    
attacking,        // 0,1
attack_counter,
suprised,
suprise_counter,
using_stealth,
tracking_flag,   // 0=not using tracking. 1=using tracking to avoid encounters. 2=using tracking to find encounters.
running,          // 0 or 1
location,          // a pre-defined location. OUTSIDE, UPATREE, INSHELTER, etc
atk2subdue,        // 0 or 1
curammo,          // obj#   INACTIVE = no ammo selected
actiondata[10],
interests[3],
tgt,                 // used by clanmembers != cm0
rng,                //  rng to tgt
orders,              // used by clanmembers != cm0     0=no orders   1=follow me (ordersdata[0]=who to follow)
ordersdata[10],              // used by clanmembers != cm0     
has_perm_shelter,
shel_mx,
shel_mz,
shel_x,
shel_z,
paddling,
raftnum,
cur_quest,
god[8],
camoflaged,
camotype,
action_qual,
falling,
research_counter[maxskilltypes],
animals_killed,
cavemen_killed,
action_counter2[maxactions],
makeobj_counter[max_object_types],
stackptr,
perm_shel_quality,
aniplayerID,
currentani,
sneak_state,              // 0 not sneaking. 1 sneaking, undetected. 2 sneaking, detected.
collision_recovery,   // 0=not in collision recovery   1= in collision recovery
CR_counter,            // how many turns they've been in collision recovery
entangled,
climb_mode,
crh_index,         // which cave they are in
is_blocking,        // boolean
head_mesh,hair_mesh,skin_tex,face_tex,hair_tex,eye_tex,modelID,
alive;
 
float x,y,z,      // d3d location in the map sq.  pos x is east (0..4999).  pos z is north (0..4999).   pos y is up.
      xr,yr,zr,   // d3d orientation in radians
      vx,vy,vz,
      vxr,vyr,
      CRyr;          // desired heading for collision recovery
objrec2 stuff[maxobjrecs];
//psrec shel[psms][psms];
//psmap shel;
questrec quest[maxquests];
drugrec drug[maxdrugs];
traprec trap[maxtraps];
 
actionstackrec stack[actionstacksize];
 
};
 
 
 

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

ok, here's the results of the timing tests.

tests were conducted between run_simulation() as posted above, and the new run_simulation2() routine.

the new _runsimulation2 routine is identical, except that the many small player update loops have been replaced by one call to BMupdate_all at the beginning of do_global_frame.

to get meaningful numbers, i was required to measure the time in query performance counter ticks accumulated over of runs of 15 frames.

the old way with a bunch of loops clocked in at ~3000 ticks in 15 frames, or ~200 ticks per frame.

the new way with a single big loop clocked in at ~1000 ticks in 15 frames, or ~ 66 ticks per frame.

not surprising, perhaps a bit more difference than expected. eliminating loop overhead helped. not sure how much if any can be attributed to the code being more cache friendly.

the generic iterator is next, then consolidating all those one-liners.

so is there a zero overhead C++ alternative to a findfirst / findnext generic iterator design?

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Do not post code in blue comic sans on this forum, or anywhere on the Internet, ever again. We have [[size="1"]code] tags, use them.


It's especially funny that you have a comment saying "Must call move_rafts first" but you actually call BMmodel_raft_movement sixth in line.

BMmodel_raft_movement simply moves the player by the same amount as the raft he/she's on. gotta move the raft first, so you know how much to move the player! <g>

thus there is a call order dependency between move_rafts and BMmodel_raft_movement, and therefore between move_rafts (update rafts) and BMupdate_all (update PCs) in general.

this is due to the design choice of consolidating the player movement due to rafts into BMupdate_all. player movement due to rafts ( BMmodel_raft_movement ) is actually an "event handler" for a "raft moved" event.

the old way with lots of loops processed the "raft moved" event immediately, which required a loop to iterate over the list of players to move them if they were aboard the raft. but it was call order independent.

the new way moves the raft and stores the resulting dx,dz movement from the event for later processing by BMupdate_all. this eliminates the loop in move_rafts, and moves the body of that loop into a call from the loop body of BMupdate_all. but its not call order independent any longer. move_rafts must be called first to pickup the event, so BMupdate_all can then process it along with all the other band member updates in one big loop.

not sure which i favor. i tend to like to process events immediately and not leave stuff hanging around. it can often be simpler. in this case all it got me was one consolidated loop and a call order dependency i didn't have before. if call order becomes an issue i can always go back to processing the event immediately.

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