Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

 
Advanced search

1405812 Posts in 68545 Topics- by 62246 Members - Latest Member: KorgiGames

April 01, 2023, 05:38:53 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Characters ai code organization (old school not neural)
Pages: [1]
Print
Author Topic: Characters ai code organization (old school not neural)  (Read 728 times)
gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« on: February 11, 2023, 05:40:40 PM »

I want to create a an ai with finely customizable character. The ai should be able to handle generic behavior shared across all character, but have specific override for group of character called persona, and override for unique character behavior.

I thought about using an "if tree" with behavior tree pattern. Basically imagine it's a bunch of cards. Cards have a check, then a body of actions when the check is passed, cards can be nested as actions of other card, break() is a specific action that terminate the execution of the whole tree. But I'm stumbling on some issue.

Override for specific characters, could happen at any branch of the if tree, and it makes it harder to write code that separate specific and unique code, because a top level card can be specific to a character, but lower behavior be a mix of generic and specific, making it hard to untangle the two. It makes it hard to propagate change during dev, because number of behavior card can reach up to tens of thousands. That makes it that a lot of card would be identical variants save for a few actions, and updating them all would be a nightmare. On top of that, breaking the ai code into multiple files makes it harder to track the logic and debug specific vs generic behavior, especially when the tree becomes deep.

What architecture or programming design pattern you would recommend to cope with these difficulties? Specifically how to manage the explosion of card variants?
Logged

gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« Reply #1 on: February 14, 2023, 11:59:29 PM »

I was looking for advice from people who were in the same situation. I didn't explain the architecture, my problem is highly localized to the behavior variants problem.

The architecture is: a world manager that manage entities, a utility tree (UT) for perception (match concepts instead of actions) that query the world and internal state, a behavior tree like (BT) that sets "state variables", and a state machine that actually implement actions based on those vars. Only missing is inheritance, I couldn't figure out how to add it without exploding the maintenance cost and the solve the exposed problem. BT is great because it's an abstraction between a planner, a script and a state machine, but I separated the state machine to handle actions, it's simpler to handle temporal aspects (wait the action to finish) and make transitions more explicit. Same for separating UT and BT, it's convenience to follow the character logic better.

After staring at the problem for a while, I realize there is mostly 4 cases:

- Insertion: I insert behavior with card in the form of "isaX()" check, it append new behaviors in the action list.

- Inhibition: basically I prevent a card from firing off for certain character and is in the form of "& !isaX()" so when identifier X is detected, the card return false and the behavior is not started.

- Substitution: basically I need to replace actions and maybe checks, but keep subtree intact. Right now it's a bunch of inhibition checks for action in the list.

- Replacement: basically it's a whole new tree that bypass the behavior for another one. Typically implemented as inhibition, of the old tree and append of a new tree, with an "else" structure.

I haven't figured out how to abstract them yet, and I also have a lot of hardcoded interactions for multiple characters, I haven't figured out how to formalize that part yet. The key things seems that, if I put everything in an actual list, I can at least insert stuff to generic behavior without having the generic BT and the specific BT having to KNOW each other...
« Last Edit: February 15, 2023, 12:06:27 AM by gimymblert » Logged

[email protected]
Guest
« Reply #2 on: February 28, 2023, 03:46:31 PM »

I get confused by conceptual descriptions of AI state machines. I prefer to think in terms of objectives: What does a character want to do, and does the character always want to do it?

For an FPS it's easy: AI wants to kill your ass. All it has to do is pathfind to line-of-site and shoot until the clip is dry.

A game like Morrowind is a little more complex. Morrowind's humanoid characters have 2 common states and a number of less common states. Most characters are either neutral or in combat. If a character is neutral, it idles around and acts like a person going about its business. If a character is in combat, it usually fights, but less common and more interesting states are triggered if the character is at an extreme disadvantage.

All of this can be written in a single file in whatever programming language you use. My kill-your-ass FPS AI is just over 200 lines of C++ executed per frame. With a neutral state, the AI might be 400 lines (and harder to read). With all the complexity of Morrowind, it would be 500-1000 lines and split into multiple functions.
Logged
gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« Reply #3 on: March 02, 2023, 05:26:57 PM »

I get confused by conceptual descriptions of AI state machines. I prefer to think in terms of objectives: What does a character want to do, and does the character always want to do it?


Funnily that's exactly what my tree does, it's organize such that, going down the branch read like a sentence. Like

Ryan: 
hear(negative evaluation from his dad){perception layer}
-> react with anger {emotional layer}
-> make rude gesture {expression layer: broadcast data}
-> storms out the room {set pathfinding destination}
Logged

[email protected]
Guest
« Reply #4 on: March 10, 2023, 10:01:12 AM »

I get confused by conceptual descriptions of AI state machines. I prefer to think in terms of objectives: What does a character want to do, and does the character always want to do it?


Funnily that's exactly what my tree does, it's organize such that, going down the branch read like a sentence. Like

Ryan: 
hear(negative evaluation from his dad){perception layer}
-> react with anger {emotional layer}
-> make rude gesture {expression layer: broadcast data}
-> storms out the room {set pathfinding destination}

Too much indirection. I would probably code it like:

Code:
if(evaluation < 0.5){
playAnim(finger);
walkTo(hallway);
}

Below that would be lots of other conditionals. More structure would be nice, but then you confuse people who are used to thinking linearly.
Logged
gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« Reply #5 on: March 10, 2023, 01:58:14 PM »

I don't know, looks like pretty much what i'm doing, My example was high level
Logged

Action Tad
Level 0
**



View Profile WWW
« Reply #6 on: March 21, 2023, 06:27:47 PM »


What architecture or programming design pattern you would recommend to cope with these difficulties? Specifically how to manage the explosion of card variants?

I would use classes if possible.
In your example, the reaction itself could be a class,
some function could return that Ryan reacted with anger, if he made a rude gesture and storms out the room.
 
You could have a Reaction class, and a Character class for taking in evaluations and interacting.

Code:
var ryan = new Character();var dad = new Character();
var reaction = ryan.hearFrom(dad); //would cause dad.talkTo(ryan)
ryan.reactTo(reaction, reaction.negative ? hallway : room);

Character.reactTo = function( reaction, destination ) {


  if(reaction.self) { //for example dads reaction would not be self, it would be ryans reaction to what he said.
                       //so the hearFrom and talkTo methods would return a reaction with self flagged differently.
    if(reaction.negative) {
        this.gesture(-1);
        if(destination) this.walkTo(destination)
    }
    if(reaction.positive) {
         this.gesture(1);
         if(destination) this.walkTo(destination)
    }

  }


}


hearFrom(charac) {

  var reaction = charac.talkTo(this);
  if(reaction.evaluation() <= 0 ) {
    reaction.self = 1;
    reaction.positive = 0;
    reaction.negative = 1;

  }
  return reaction;


}

talkTo(charac) {

  //lets imagine textQue is an Array containing an integer denoting how positive and then the saying itself
  //[2, "Have a nice day Ryan", 1, "Your not cool Ryan", 1, "You should not go to your room right now Ryan", 4, "Here is your favorite food Ryan"]

  //and it just has each thing to say next in order

  this.displayText(textQue[this.next+1]);
  return new Reaction(textQue[this.next] - 1, textQue[this.next], 0);

}

Reaction(n,p,s) {

  this.negative = n;
  this.positive = p;
  this.self = s;

  this.evaluation = function() {

    return this.negative / this.positive;

  }
}

Logged
gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« Reply #7 on: March 22, 2023, 04:14:15 AM »

My code look roughly like that:

PSEUDO CODE
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)).
« Last Edit: March 22, 2023, 04:36:37 AM by gimymblert » Logged

Action Tad
Level 0
**



View Profile WWW
« Reply #8 on: March 23, 2023, 03:35:44 PM »

I see, it can be very difficult trying to program on a small screen.
You could have different Structs; CharacterState, CharacterLocation, CurrentCharacter, anything you can think of to group ideas would help to lessen the amount of if/else statements you will need. The way you laid out those 8 points is actually not so bad, it may take a little while to write it all out, but that as a plain if/else chain would work.
Logged
gimymblert
Level 10
*****


The archivest master, leader of all documents


View Profile
« Reply #9 on: March 24, 2023, 05:05:49 AM »

To be frank the number of if else was never a problem. Though if I can better encapsulate them that's still a win.

On the practical scope of a dev, which isn't linear and is highly iterative, If are simple insertion, you don't spend one chunk of time adding all if in one go, and the structure is rather easy to grasp and navigate. That mean that constantly modifying the tree don't create problem, finding problem is equally easy, because if encapsulate behavior neatly, which also mean they are easy to move around.

Basically you have a few recurring mistakes that can happen:

- wrong logic checks, if enforce strong ordering, so that's easy to localized, you can follow step by step in the branching and avoid part of the code you don't need to look at.

- wrong ordering, if encapsulate logic in their body, fold and copy paste to reorder, copy paste is generally frown, but that's not the same type of copy paste (the laws vs the spirit of the laws).

- wrong logic in the body, since it's easy to localize, and that logic are self contained such that the scope doesn't bleed outside the if body.

The real benefit of if tree is the ordering in a branching structure, it has the same benefit that function or class for encapsulating logic, less so for encapsulating data BUT you can cheat by leveraging Lambda functions to wrap the If block to get some locality of variable (but not permanence of variable like object, which mean each frame you lose data), and you gain the function to break out at any point of the If block without going out the entire tree.

Code:
    //Breakable node
    () => {if (check){
           //code
           if(condition) return; //break from the node
           //code
    }}

If tree are typically bad in other programming context, where a simpler alternative generally always apply, and the context (edition, iteration, scope) are different.



BUT I figure out something and reorganize the code into a new architecture where I break responsibility further, it was basically a scope issues, redefining scene as scope instead of character modify how we can potentially organize the code:

- The global ai manager is stripped from some responsibility and is merely the manager of sub module. It may hold global generic behavior, but no longer track agents.

- The social area objects, hold by ai manager. Basically they hold area based "scene" called "social practice", for example the school hold practice like going to class, practicing sports, doing club activity. Another social area could be the mall, where "going to class and doing club activity" do not make sense. This represent the basic level background simulation.

- A story manager, which hold the structure of scenes that implement the progression of plots based on player's action and character states. It might inject Scene in social area to perform.

- A character sheet object, that implement the basic data of a character, such as appearance, stats and specific animations.

- A character script objects, that encapsulate the character sheets and the scene override of that character. The motivation in separating with sheets, is to be able to reuse character in different story.

- An actor agents, there is no longer a character object, actor takes a sheets to define their appearance, the stats to perform actions, and animation to display behavior. They also have a set of generic behavior such as moving and playing animation and hold the mutable states of the character. But the behavior are now injected by higher structure such as story, character scripts and social area objects, this can be implemented in many way, but for now I just keep a reference to these structure and call their behavior function on the character's state. This mean put the character in a mall or school, at a certain story point, and it would behave accordingly without modifying the actor, also it makes editing (authorship) collected in a a few places instead of many character.
Logged

Action Tad
Level 0
**



View Profile WWW
« Reply #10 on: March 27, 2023, 03:14:55 PM »

But the behavior are now injected by higher structure such as story, character scripts and social area objects, this can be implemented in many way, but for now I just keep a reference to these structure and call their behavior function on the character's state. This mean put the character in a mall or school, at a certain story point, and it would behave accordingly without modifying the actor, also it makes editing (authorship) collected in a a few places instead of many character.

I think that is a very good way to begin to tackle the problem, as many things as you can not have to re-code or go back and add to, will make overall coding easier. I think you can link a method to a struct by passing the struct as the first param of the method.
If that is not something you've tried already it might help.
Logged
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic