Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Discussion Characters ai code organization

Discussion in 'Scripting' started by neoshaman, Feb 17, 2023.

  1. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,492
    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, in that order: generic -> persona -> unique character. It allows a mix of simulation and scripting (ie story, unique situation, unique interaction and behavior).

    I thought about using an "if tree" (KISS principle) 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, return; (or break() if I implement as class) is a specific action that terminate the execution of the whole tree. But I'm stumbling on some issue.

    Override for specific, could happen at any branches of the if tree, and it makes it harder to write code that separate generic 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 (KILL principle, folding IF is just more efficient) and debug specific vs generic behavior, especially when the tree becomes deep. my problem is highly localized to the behavior variants problem.

    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.

    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, like through a linking object... That's an idea, but that mean implementing the card idea properly as class and not a metaphor, but I don't see it the direct benefit for the problem. I haven't figured out how to abstract them yet. And I also have a lot of hardcoded interactions for multiple character interactions, I haven't figured out how to formalize that part yet BUT I leave that for later, one problem at a time.

    For informations:
    The whole architecture so far is: a world manager that manage entities, a utility like 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.

    BT is great because it's an abstraction between a planner, a script and a state machine, but I use a separated state machine to handle actions, it's simpler to handle temporal aspects (wait the action to finish) and makes transitions more explicit. Same for separating UT and BT, it's convenience to follow the character logic better.

    Only missing is inheritance, I couldn't figure out how to add it without exploding the maintenance cost (KISS & KILL) and the solve the exposed problem. Inheritance works great when the structure are quite atomic, but what if the you want to patch a tree? I'm not sur how using it would help.

    I'm not sure building a tools for edition will help, it will just translate the problem to a new structure and have a cost to build and maintain for no advantage for a solo dev.

    What architecture or programming design pattern you would recommend to cope with these difficulties? Specifically how to manage the explosion of card variants? Have you ever been in such situation? How did you manage it?