My code look roughly like that:
PSEUDO CODE
//Black board:
//(reference to global ai sets on initialization, hold world state)
AiManager Manager;
//hold all var and flags relative to interaction in the immediate sensing radius
Struct PerceptionBoard;
//character data such as persona, stats and other that can be modified, managed by the 3 decisions layer and consumed by the action state machine.
Struct CharacterState;
void Update(){
if (isActive & isLiving)
updatePerception();
//Kind of a Subsumption architecture
//select the proper behavior with 3 "layers" of if tree
//Idea proposed was to get the most complex one and find way to break it further in layers. By finding pattern to generalize.
updateSchedule();
updateEvents();
updateReaction();
//Actually do the behavior
performActions()
else if (!isLiving)
updateDeath();
}
void updatePerception(){
//Fallthrough pattern (concurrency: just DO don't exit, all node execute in order, order don't matter)
GetPlayerDistance();
UpdateDetection();
//...
}
void updateReaction(){
//priority pattern (exit on success)
if( gotPushed() ) return;
if( gotHit() ) return;
if( gotAlarmed() ) return;
//...
}
void updateSchedule(){
//Complex tree of functions with various ALTERNATING ai pattern as "actions", really should be renamed to "decision units", will clear some confusion
//behavior tree pattern (generally wrapped into function to resume to the proper node after exit,
//if it helps legibility or same type of decision is repeated accross node)
//sequence pattern (exit on fail)
if(!function1()) return;
if(!function2()) return;
if(!function3()) return;
//priority pattern (see updateReaction above)
//fallthrough pattern ("concurrency": see updatePerception)
//Preprocess units (Order matter, do stuff for nodes and units further down, generally before a nested node tree)
function4();
//Nested node, precise a specific steps into a sub selections,
//node CAN follow behavior tree pattern too with return on fail or success
//or breaking the whole tree traversal.
if(BlackBoard.check1){//prefix node comments indicating the task
//interleaving pattern implementing logic, in any necessary order
//->decisions units as sequence units
//->decisions units as priority units
//->decisions units as fallthrough units
//->decisions units as preprocess units
//->decisions units as nested node units
}//suffix comments telling how much line there is, should add number of units and nodes instead, now thinkig about it...
//more interleaved units and nested node
//Observation there is a lot of "preprocess units" in the actual code.
//problematic and specific node/units (happen at all hierarchy level)
if (functionOrCheck() && isNAMEcharacter) ...
if (functionOrCheck() && !isNAMEcharacter) ...
if (functionOrCheck() && isPersona) ...
if (functionOrCheck() && !isPersona) ...
}
Now the reason I have a "if-tree" like above, instead of breaking it into smaller function or class, is because legibility. It keep contextual stuff close together. With code folding I barely look at 100 lines at a times at worst and at the same time, despite it having thousands of code. Thousands of line is a bit misleading, a dozen of behavior at 100 lines go easily in thousands.
Legibility issues is compounded by the fact I have a single 720p screen, Im' working in the literal version of the meme "meanwhile two screen at home:" that is split windows set tup, with working one on the left, references on the right.
-> modern IDE allows for fancy options to look around the code, like peeking defintion and all, which I really can't leverage due to cramp space, using everything in one file, using folding, has been the best workflow so far. Long flat list of class, methods, rules and others, especially dispersed on multiple file don't help. Plus navigating thousands of file is way worse than navigating thousands of lines in a single place, code folding exist to snip the size.
That said, I came to realize something lately, It's not a character behavior code, it's a SCENE behavior code. The equivalent would be branching dialog sequence, in other game, except of text you have whole actions and behaviors. Joint actions between character, options, are decided by gameplay events during phases of the sequence.
It change the perspective on how to organize the code. Some data currently attributed to character, aren't for the character, but describe the SCENE states or transition from scene to another. That is SCENE contains character's behaviors, but aren't character behavior themselves, they direct character, and thus need specific encapsulation. That's why there was many characters within some branch of behavior, because it's misnamed and miscategorized.
Let's look at a mocked bath "behavior", not using a joint character scene
assuming it's the yandere simulator game as a reference:
0. is triggered by being dirty events
1. go to communal bath
2. go to locker
3. change cloth to swimsuits
4. take the bath
6. go to locker
7. change back to normal cloth
8. exit the communal bath
This would constitute the basic behavior.
The true purpose of the bath scene isn't to make the character realistic, but to offer gameplay opportunity in the game of social stealth and manipulation, it's the "level". That is the Dirty events is created as opportunities to move or remove characters from a place to another place. For example, the locker allow you to tamper with character belonging and do action like put a letter, stole the phone, stole the cloth, plant evidences, etc ...)
The actual (mocked) code would be more structured like that,
the (isX) denote added "if-cards" to create variants per character/persona (not joint behaviors) :
0. is triggered by being dirty events
(is rich girl: rage 1mn about the clothes, change mood to angry)
(is fragilePersona: start crying, change mood to sad)
(is nemesis: look around suspiciously, mood change to vigilance)
(is idiot AND judgementalPersona nearby: judgemental.talk, play embarassed animation)
1. go to communal bath
(is rich girl: call her bodyguard to follow her on the way)
(is rich girl AND is arrived at communal bath: the guard get posted at the entrance, intimidate anyone passing nearby)
(is nemesis AND arrive at communal bath: slowly push the door, scan the room, close the door carefully, tapped a bell to the door)
2. go to locker
(is rich girl: lament the cloth)
(is nemesis: open the locker carefully, double check the environement and inspect inside the locker)
3. change cloth to swimsuits
(is rich girl: look at the mirror to flatter herself, change mood to normal)
(is nemesis: tapped a bell inside the locker, pick the knife, close locker's door slowly)
(is idiot: look at the mirror do funny face and poses for a while)
4. take the bath
(is fragile: cry during bath)
(is nemesis: submerge whole body to hide the knife)
(is idiot AND time remaining is small: do hurry bath)
6. go to locker
(is nemesis: removed the tapped bell from locker)
7. change back to normal cloth
(is nemesis: removed the tapped bell from door, open slowly the door, scan the environement)
8. exit the communal bath
(is rich girl: the guard follow her to next place)
MISC interruptions and branches.
(is nemesis AND bell sound AND 3 < phase < 7: Speech("I was expecting you"), change to fight mode)
Letter at locker:
(is nemesis: dismiss and toss to trash)
(is rich girl: mood set to angry)
(is fragilePersona: mood set to scared)
(is idiot: Mood set to concerned)
Phone stolen: [...]
Evidence planted: [...]
Clothes stolen: [...]
Player detected: [...]
end.
BUT that doesn't solve my problem, which is how to SEPARATE different characters specific actions (variants) from the basic action toward their own files.
Basically the question is, how all these all (isX) insertions goes to their own file and merge back with the SCENE file.Especially when we have more complex scene where I have joint behavior between multiple character (example: reporting a murder in yandere simulator, scene has a character who see a corpse, report to a teacher, the teacher and the reporter goes to the scene, and react appropriately (scene can be cleaned before they come back)).