![]() |
|||
Behavioral Rules Artificial Intelligence Node System (B.R.A.I.N.S). A Behavioral Tree-driven, Kismet powered, UnrealScript Plug-in B.R.A.I.N.S is a controlled open-source Behavior Tree AI ideal for use in Action, Tactical, Stealth and many other games. It combines several state-of-the-art AI techniques , leveraging existing UDK AI & Navigation, Unrealscript JSON import/export for Behavior Tree Hierarchies, and Kismet as a Visual Editor. Its currently being developed with priority to the requirements for SanctityLost, with intention to expand features for other AI application. Design Documentation and Source Code will be made available for public download. The project is currently under Source Control via Perforce: Server: electric-lizard.servegame.com:1666 Client/Workspace: UDK I'll promptly add interested parties to the P4 Access List with a PM, Title containing desired username/password (8 character minimum). Example: Give Me BRAINS P4 Access techlord/t3CHl0rd#1 |
|||
|
|||
Announcement
Collapse
No announcement yet.
B.R.A.I.N.S: Behavior Tree AI Plugin powered by Kismet
Collapse
X
-
B.R.A.I.N.S: Behavior Tree AI Plugin powered by Kismet
Tags: None
-
In my game there will be segments where AI allies will be cooperating with the player. I was planning on giving them several float values to represent emotional states and having certain events modify these. I was also planning on each AI having an array of "goals" which would be ranked by priority. However, which goal to act on would also take into account the emotional state. For example, high anger may cause "Do damage to enemy" to take priority over "Do not allow health to reach 0." You would also be able to give different AI different emotional predispositions to make them have more "personality." This also brings up possibilities of taunting enemy AI to cause them to behave recklessly.
Those are really great references you listed. Lots of food for thought...
What are your thoughts on navigation? My problem with path nodes is that they are static and if you have carrier vehicles like Halo 3's Elephant, you need another way of providing navigation info to an AI that is onboard.
Comment
-
Originally posted by skwisdemon666 View PostIn my game there will be segments where AI allies will be cooperating with the player. I was planning on giving them several float values to represent emotional states and having certain events modify these. I was also planning on each AI having an array of "goals" which would be ranked by priority. However, which goal to act on would also take into account the emotional state. For example, high anger may cause "Do damage to enemy" to take priority over "Do not allow health to reach 0." You would also be able to give different AI different emotional predispositions to make them have more "personality." This also brings up possibilities of taunting enemy AI to cause them to behave recklessly.
Those are really great references you listed. Lots of food for thought...
What are your thoughts on navigation? My problem with path nodes is that they are static and if you have carrier vehicles like Halo 3's Elephant, you need another way of providing navigation info to an AI that is onboard.
Thanks for your inquiry. Are you using Finite State, Hierarchical FSM or a Behavioral Tree? Have you considered a Planner implementation? Navigation on a large moving platform such as Halo 3 Elephant, would require a custom path node system. I would use Skeletal Joints or Sockets to create the nodes.
Comment
-
Originally posted by skwisdemon666 View PostI haven't gotten into actually coding the A.I. just yet, I'm still trying to write out all my pseudo-code to keep things from getting messy when I start. Planners and animation driven behavior definately interest me. I want to keep the number of states very small.
I'm a big advocate of design docs. Drawing up your plan and psuedo-code is a good idea. I personally use Powerpoint or equivalent. For myself, I've started with a short list of plugin requirements: 1) Modularity, 2) utilize UDK's AI and Navigation as much as possible - no external dependencies, 3) Ease of integration.
Modularity. I've divided the AI into 3 primary Subsystems: Motor, Sensor, and Planner. The Motor (actions) handle path-finding and animation. The Sensor (conditions) handles collision detection, internal and exterior state/status checks. The Planner (tasks) generates a Plan based on Goals and Sensor Input to drive Motor output. These subsystems will be broken down into further components if necessary.
Motor Navigation is a hot topic. Dynamic Navigation is a concern because it requires a custom solution. I suspect your writing psuedo-code for dynamic navigation. The situation you posed with Halo3 Elephant also brings up some good questions. I mentioned using joints or sockets as path nodes, however. the representation of a path node can be a 3D vector, a scalar value, a key. Its the cost (heuristic, priority, weight) that's required by search algo.
I would prefer a generic Search Algo that could be used in multiple situations such as the Planner itself. The cost may be pre-assigned (priority) or calculated on-the-demand (heuristic). Just need to settle on a data structure to map nodes to cost and feed the Search Algo. I'm a lazy programmer, so, I'm searching for a pre-written A* that can be converted to UnrealScript.
Comment
-
B.R.A.I.N.S: A Tactical AI UnrealScript Plugin
Idea:
Some pawns will be placed on the mAp. But most will use a spawn point.
Pawn class;
The pawn class holds certain decision components by weight between 1 and 10. These are
Flankingweight, coverweight and chargingweight.
So a pawn with:
Code:Flankingweight=10 Coverweight= 0 Chargingweight= 0
Ai class:
When the pawn sees the player, it makes a random decision based on the decision component if the pawn class.
{Weighted state take cover }
If it chooses to take cover, it runs to a random cover node ( it had to be a cover node) between itself and the player and starts firing. If it takes a certain amount of damGe, it retreats to a covernode away from the player.
{Weighted state flankplayer }
If the bot chooses to flank it goes into this state. It pretty much moves in a wide arc around the player, trying to get behind him, while shooting. It might be wise to use path ode-like actors so the bot knows what paths are areas for flanking instead of just running around the player in the open. So they might use those nodes to move around the player. If they take too much damage, they go to the retreat state, if they do make it bejind the player, they go to the charging state...or stand and shoot.
{Weighted state charge }
Bots with heavy charge weight in their pawn class will be used for enemies with low combat skill.
These bots will immediately charge straight at the player, ignoring cover and firing at him. If they take too much damage though, they go to the retreat state.
{state retreat}
After taking damage, the bot falls back to a covernode away from the player. And starts shooting again until he gets killed.
{state stand and shoot}.
The bot stands in place and shoots at the player. Retreats when damage gets high.
Comment
-
Originally posted by TechLordThe B.R.A.I.N.S Modular Behavior Tree. Use Kismet as Visual Tree Editor. Use JSON for import/export data format and serialization.
Comment
-
Originally posted by Demestotenes View PostI would also like to use Kismet as an editor for my custom object structure and import/export it to JSON. Could you tell me how did you achieve that?
Disclaimer: Work in Progress, subject to change without notice, has not been tested at this time.
The Goal: Utilize Kismet as Visual Editor to build Behavior Tree Nodes Hierarchies. The Approach: Extend classes from Unrealscript Kismet SequenceObject Classes {SequenceCondition, SequenceAction, SequenceVariable}. Kismet Sequences has many similar semantics found in Behavior Trees Task: Actions & Conditions. I'm extending from the Kismet Sequence Class the provides the closest match by functionality to a BT Task.
Below are some useful links w/ example: JSONObject, Kismet Custom Nodes, Kismet Gems. A majority of the whats visible in UnrealEd Kismet is assigned in the default properties. My next step is to change the shape/fillcolor or add sprite Icons for BRAINS Kismet nodes, modifiable to label Behaviors/Task to add some visual distinction for BRAIN Kismet Nodes.Kismet Sequence B.R.A.I.N.S Subsystem B.R.A.I.N.S Task Description Event Planner Root Tree Search Initialization Condition Planner Composite Task: Parallel, Sequence, Selector, Decorator Comparative Node Selection Action Planner Decorator Component Manipulate Output Condition Sensor Leaf Task Condition Action Motor Leaf Task Action
BrainsPlannerSeqCond_CompositeNode.uc:
Code:/** * Plugin: b.r.a.i.n.s - behavioral rules artificial intelligence nodes system * Version: 1.0 * Name: BrainsPlannerSeqCond_CompositeNode * Authors: F.L.Taylor (techlord) 2014 * References: http://unrealdb.com/the-hidden-powers-of-kismet-pt-2-custom-nodes/ * Purpose: Kismet Composite Node */ /* Kismet Design Notes InputLinks = ParentNode OutputLinks = ChildNode States manage Latency and Concurrency */ class BrainsPlannerSeqCond_CompositeNode extends SequenceAction abstract; /** Holds IndexValue of NodeSelectionBehaviors[], CheckNodeBehaviors[] */ var int NodeType; /** Holds CurrentOutputLink; used in Node Selection and Processing */ var array<int> ValidOutputLinksIdx; var SeqOpOutputLink CurrentOutputLink; var int CurrentValidOutputLinkIdx; var BrainsPlannerSeqCond_CompositeNode CurrentChildNode; var int OutputLinkIterator; var bool bAllChildNodesActivated; /** Holds Reference to Composite Node State Task */ var BrainsPlannerNodeTask Task; /** Holds Success/Fail Result */ var int Result; /* Bail */ var bool bBailout; /** Indicates whether or not this latent action has been aborted */ var bool bAborted; /** Keep track of Activation Time in Latent Actions so that we don't activate them twice in the same frame */ var float LatentActivationTime; /** NodeSelectionBehaviors[] Points to Delegate Functions */ var Array< delegate<OnNodeSelect> > NodeSelectionBehaviors; /** NodeSelectionBehaviors[] Points to Delegate Functions */ var Array< delegate<OnNodeCheck> > NodeCheckBehaviors; /** NodeSelectionBehaviors[] Delegate Functions Parallel, Sequence, Selector {Random, Weighted, Priority}, Gate {And,Or,Xor,Not} */ delegate OnNodeSelect(int stub); /** OnNodeCheck Delegate Functions */ delegate OnNodeCheck(int stub); /** Kismet Node Activation */ event Activated() { local SeqOpOutputLink EachOutputLink; local int EachOutputLinkIdx; `log("Activating"@Name); //add to Parent Activated Node /* if(InputLinks[0].LinkedOp != None)//IsA('BrainsPlannerSeqCond_CompositeNode' { `log("Adding to Activate ParentNode:"@InputLinks[0].LinkedOp.Name); //BrainsPlannerSeqCond_CompositeNode(InputLinks.LinkedOp).ActivatedOutputLinks.AddItem(Self); } */ //validate Child Node OutputLinks foreach OutputLinks(EachOutputLink,EachOutputLinkIdx) { if(EachOutputLink.Links.Length > 0) { `log("Adding to Valid ChildNode["$EachOutputLinkIdx$"]:"@EachOutputLink.Links[0].LinkedOp.Name); ValidOutputLinksIdx.AddItem(EachOutputLinkIdx); } } //Activate Node Task Task = GetWorldInfo().Spawn(class'BrainsPlannerNodeTask'); Task.CompositeNode = Self; //debug if(bOutputObjCommentToScreen) { GetWorldInfo().Game.Broadcast(Task, ObjComment, 'Say'); } //Note2Self: Move InitNodeSelectionBehaviors, NodeSelectDelagation to static (global cscope) class or var, RootNode? InitNodeSelectionBehaviors(); InitNodeCallBehaviors(); `log("Activated"@Name); } /** Child Node Finish Processing */ function CheckNodeProcessing() { } /** Initialize NodeSelectionBehaviors Delegate Array */ function InitNodeSelectionBehaviors() { NodeSelectionBehaviors.AddItem(NodeSelectionBehaviorNone); NodeSelectionBehaviors.AddItem(NodeSelectionBehaviorSequence); NodeSelectionBehaviors.AddItem(NodeSelectionBehaviorSelector); NodeSelectionBehaviors.AddItem(NodeSelectionBehaviorParallel); NodeSelectionBehaviors.AddItem(NodeSelectionBehaviorDecorator); } /** Initialize NodeCallBehaviors Delegate Array */ function InitNodeCallBehaviors() { NodeCheckBehaviors.AddItem(NodeCheckBehaviorNone); NodeCheckBehaviors.AddItem(NodeCheckBehaviorSequence); NodeCheckBehaviors.AddItem(NodeCheckBehaviorSelector); NodeCheckBehaviors.AddItem(NodeCheckBehaviorParallel); NodeCheckBehaviors.AddItem(NodeCheckBehaviorDecorator); } /** Call NodeSelectionBehaviors Delegate by Index */ function SelectNode(optional int idx = -1) { local delegate<OnNodeSelect> NodeSelectionBehavior; //`trace(); //assign delegate from array. see InitNodeSelectionBehaviors NodeSelectionBehavior = NodeSelectionBehaviors[idx != -1 ? idx : NodeType]; //call delegate NodeSelectionBehavior(0); } /** Call NodeSelectionBehaviors Delegate by Index */ function CheckNode(optional int idx = -1) { local delegate<OnNodeCheck> NodeCheckBehavior; //`trace(); //assign delegate from array. see InitNodeSelectionBehaviors NodeCheckBehavior = NodeCheckBehaviors[idx != -1 ? idx : NodeType]; //call delegate NodeCheckBehavior(0); } /** NodeSelectionBehaviors Delegate Functions */ function NodeSelectionBehaviorNone(int stub) { `trace(); } /** This NodeSelectionBehavior activates Nodes one at time (non-blocking), in numerical order. */ function NodeSelectionBehaviorSequence(int stub) { `trace(); `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); //Active Next Child Node if(!bAllChildNodesActivated) { //set CurrentValidOutputLinkIdx CurrentValidOutputLinkIdx = ValidOutputLinksIdx[OutputLinkIterator]; //Set CurrentValidOutputLink CurrentOutputLink = OutputLinks[CurrentValidOutputLinkIdx]; //validate OutputLink; if OutputLink connects to child node, activate Child `log("Activate ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); ForceActivateOutput(CurrentValidOutputLinkIdx); //For_Next: sequential increment OutputLinkIterator++; //For_Condition if(OutputLinkIterator == ValidOutputLinksIdx.Length) { bAllChildNodesActivated = TRUE; `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); } } } /** This NodeSelectionBehavior activates Nodes one at time (non-blocking), dependent on weight/priority/random. */ function NodeSelectionBehaviorSelector(int stub) { `trace(); `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); //Activate A Child Node if(!bAllChildNodesActivated) { //set CurrentValidOutputLinkIdx CurrentValidOutputLinkIdx = ValidOutputLinksIdx[OutputLinkIterator]; //Set CurrentValidOutputLink CurrentOutputLink = OutputLinks[CurrentValidOutputLinkIdx]; //validate OutputLink; if OutputLink connects to child node, activate Child `log("Activate ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); ForceActivateOutput(CurrentValidOutputLinkIdx); //For_Next: sequential increment OutputLinkIterator++; //For_Condition if(OutputLinkIterator == ValidOutputLinksIdx.Length) { bAllChildNodesActivated = TRUE; `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); } } } /** This NodeSelectionBehavior activates all Nodes simulutaneously */ function NodeSelectionBehaviorParallel(int stub) { `trace(); //Activate All ChildNodes foreach ValidOutputLinksIdx(CurrentValidOutputLinkIdx) { CurrentOutputLink = OutputLinks[CurrentValidOutputLinkIdx]; //validate OutputLink; if OutputLink connects to child node, activate Child `log("Activate ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); ForceActivateOutput(CurrentValidOutputLinkIdx); } bAllChildNodesActivated = TRUE; `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); } /** This NodeSelectionBehavior activates First Node */ function NodeSelectionBehaviorDecorator(int stub) { `trace(); if(!bAllChildNodesActivated) { //set CurrentValidOutputLinkIdx CurrentValidOutputLinkIdx = ValidOutputLinksIdx[0]; //Set CurrentValidOutputLink CurrentOutputLink = OutputLinks[CurrentValidOutputLinkIdx]; //validate OutputLink; if OutputLink connects to child node, activate Child `log("Activate ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); ForceActivateOutput(CurrentValidOutputLinkIdx); bAllChildNodesActivated = TRUE; `log(Self.Name@"CurrentValidOutputLinkIdx="$CurrentValidOutputLinkIdx@"OutputLinkIterator=0 OutputLinkIterator="$OutputLinkIterator@"bAllChildNodesActivated="$bAllChildNodesActivated); } } /** NodeCheckBehaviors Delegate Functions */ function NodeCheckBehaviorNone(int stub) { `trace(); `log("Bail = SUCCESS"); Result = 1; } /** This NodeCheckBehavior checks Current Child Node Task Result (non-blocking), If result = 1 Pass, continue to select next; else Result 0 bails on Failure; */ function NodeCheckBehaviorSequence(int stub) { `trace(); `log("Checking ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); if(BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Task.Status == Finish) { `log("ChildNode["$CurrentValidOutputLinkIdx$"] Finished!"); if( BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Result == 1) { if(!bAllChildNodesActivated) { `log("ChildNode["$CurrentValidOutputLinkIdx$"] Result = SUCCESS, Proceed to Next ChildNode."); SelectNode(); } else { `log("Bail = SUCCESS"); Result = 1; } } else { `log("Bail = FAILURE"); Result = 0; } } } /** This NodeCheckBehavior checks Current Child Node Task Result (non-blocking), If result = 0 Fails, continue to select next; else Result 0 bails on Success; */ function NodeCheckBehaviorSelector(int stub) { `trace(); `log("Checking ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); if(BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Task.Status == Finish) { `log("ChildNode["$CurrentValidOutputLinkIdx$"] Finished!"); if( BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Result == 0) { if(!bAllChildNodesActivated) { `log("ChildNode["$CurrentValidOutputLinkIdx$"] Result = FAILURE, Proceed to Next ChildNode."); SelectNode(); } else { `log("Bail = SUCCESS"); Result = 1; } } else { `log("Bail = FAILURE"); Result = 0; } } } function NodeCheckBehaviorParallel(int stub) { local int TempResult; TempResult = 1; `trace(); foreach ValidOutputLinksIdx(CurrentValidOutputLinkIdx) { CurrentOutputLink = OutputLinks[CurrentValidOutputLinkIdx]; `log("Checking ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); if(BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Task.Status == Finish) { `log("ChildNode["$CurrentValidOutputLinkIdx$"] Finished"); /* 1*1=1, (1*0|0*1|0*0)=0, Bitwise And?*/ TempResult *= BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Result; } else /* if not all Finished, exit function*/ { `log("All ChildNodes are NOT is Finished. Exiting..."); return; } } `log("All ChildNodes Finished!"); `log("Bail = "$TempResult); Result = TempResult; } function NodeCheckBehaviorDecorator(int stub) { `trace(); `log("Checking ChildNode["$CurrentValidOutputLinkIdx$"]:"@CurrentOutputLink.Links[0].LinkedOp.Name); if(BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Task.Status == Finish) { if( BrainsPlannerSeqCond_CompositeNode(CurrentOutputLink.Links[0].LinkedOp).Result == 1) { //if decorator functionality complete then Result = 1 `log("Bail = SUCCESS"); Result = 1; } else { `log("Bail = FAILURE"); Result = 0; } } } defaultproperties { ObjCategory="B.R.A.I.N.S" ObjName="B.R.A.I.N.S Composite Node" ObjColor=(R=255,G=255,B=255,A=255) NodeType=0 bLatentExecution=TRUE bAutoActivateOutputLinks=FALSE OutputLinkIterator=0 bAllChildNodesActivated=FALSE InputLinks(0)=(LinkDesc="In") OutputLinks(0)=(LinkDesc="Out0") VariableLinks(0)=(ExpectedType=class'SeqVar_Object',LinkDesc="Com0",PropertyName=Component0,MinVars=1,MaxVars=1) Result=-1 }
Code:/** * Plugin: b.r.a.i.n.s - behavioral rules artificial intelligence nodes system * Version: 1.0 * Name: BrainsPlannerNodeTask * Authors: F.L.Taylor (techlord) 2014 * References: http://unrealdb.com/the-hidden-powers-of-kismet-pt-2-custom-nodes/ * Purpose: Composite Node State Task and Latent Function Management */ class BrainsPlannerNodeTask extends Actor; /** Composite Node XReference */ var BrainsPlannerSeqCond_CompositeNode CompositeNode; var bool bErrorTriggered; var int ErrorCode; var string ErrorDescription; /**latency testing*/ var int randomsleep; var int fakelatentfunction; var enum ProcessStates { Inactive, Start, Running, Halt, Finish, Deactivate } Status; /** Inactive */ auto state Inactive { Begin: `log(CompositeNode.Name@"Inactive State"); Status = Inactive; GotoState( 'Start' ); } /** Start Processing*/ state Start { Begin: `log(CompositeNode.Name@"Start State"); Status = Start; CompositeNode.SelectNode(); GotoState( 'Running' ); } /** Selecting or Waiting For Children to complete process*/ state Running { function Tick( float Delta ) { //`trace(); `log(CompositeNode.Name@"Checking"); super.Tick(Delta); CompositeNode.CheckNode(); if(CompositeNode.Result != -1) { `log(CompositeNode.Name@"Results are in. Change to Finish."); GotoState( 'Finish' ); } } Begin: `log(CompositeNode.Name@"Running State"); Status = Running; } /** Pause or Suspend Due To Error*/ state Halt { Begin: `log(CompositeNode.Name@"Halt State "$randomsleep$" seconds"); Status = Halt; //randomsleep = rand(0); /**test random latency call (sleep)*/ //Sleep(randomsleep); if(bErrorTriggered) { //error encountered GotoState('Finish'); } else { //suspended; return to previous. PopState(); } } /** Processing Completed, Calculate Children ReturnStatus Generate TRUE(Success)|FALSE(Fail)*/ state Finish { Begin: `log(CompositeNode.Name@"Finish State"); Status = Finish; } state Deactivate { Begin: `log(CompositeNode.Name@"Deactivate State"); Status = Deactivate; } defaultproperties { }
Object -> SequenceObject -> SequenceOp -> SequencEvent, SequenceAction, SequenceCondition
Object -> SequenceObject -> SequenceVariable
I narrow my focus on the InputLinks/OutputLinks/VariableLinks member declared in the SequenceOp Class. In the context of Tree Hierarchy: InputLinks are ParentNodes,
OutputLinks are ChildNodes.
to be continued....
PS: I'm open to suggestions on using custom shapes and sprites for Kismet Nodes (circles, squares, diamond, etc).
Comment
-
Originally posted by Demestotenes View PostThank your for your explanation.
I wanted to use Kismet classes in my project, however it turned out that activating them from unrealscript crashed the game.
Comment
-
[SHOT]https://arcadekomodo.com/home/wp-content/uploads/2014/03/brainsbehaviortreeinkismet1.png[/SHOT]
[SHOT]https://arcadekomodo.com/home/wp-content/uploads/2014/03/brainsbehaviortreeinkismet2.png[/SHOT]
[SHOT]https://arcadekomodo.com/home/wp-content/uploads/2014/03/brainsbehaviortreeinkismet3.png[/SHOT]
[SHOT]https://arcadekomodo.com/home/wp-content/uploads/2014/03/brainsbehaviortreeinkismet41.png[/SHOT]
Comment
Comment