Tamable Creatures
Description
This project was to test the creation and usage of an advanced AI system. Hierarchical Task Network Planning (HTNP) was implemented for an animal like AI, along with other supporting systems such a day night cycle, path finding, and object interaction. Not only did the creature have behaviours for eating when hungry and sleeping when night fell, but it also would investigate new things it could see and remember what and where they are. It has no understanding of the world around it by default, requiring it to find essentials like food sources before interacting with them.
A basic player controller was also made to allow simple interaction with the creature in simple ways, such as laying food out for it.
This project was finished as a prototype, with the Hierarchical Task Network Planning implemented and a simple creature to test it’s use and how that be utilised for a creature. Further plans existed for the player to be able to tame the creature and to implement several different creatures that were left out when deemed excessive for only a prototype project.
Goals and Achievements
Programming
Hierarchical Task Network Planning (HTNP) uses a planning structure for it’s behaviours, and hierarchical rules for it’s planning. This effectively combines the foreplanning capabilities of an AI system like Goal Oriented Action Planning (GOAP), and the curated behaviour patterns of an AI system like Behaviour Trees.
It accomplishes this by choosing it’s behaviour from a domain, being a tree of options based on the current state of the world. The tree is a compound task, which comprises of different methods containing sub tasks. Each method will have conditions be able to run, and the first valid method will be run each time a task plan is needed. Methods can contain a combination of primitive tasks and compound tasks to accomplish a complicated goal. Primitive tasks are the simple behaviours a plan is made of, and string together in a plan to make complicated behaviours intended to accomplish the goals decided for it by the branching methods.
Here is the domain used for the creature in this project, using the builder pattern to easily curate the behaviour:
Root = new CompoundTaskBuilder()
.AddPrecondition<CreatureBlackboard>(x => x.hungry.value == true)
.AddSubTask(new CompoundTaskBuilder()
.AddPrecondition<CreatureBlackboard>(x => x.handSlot.value.HoldingItem)
.AddPrecondition<CreatureBlackboard>(x => x.handSlot.value.GetInteractable().Type == InteractableType.food)
//.AddSubTask(new DebugLogPrimitiveTask(owner, "Going to eat food"))
.AddSubTask(new EatPrimitiveTask(owner, aIEntity.HandSlot, aIEntity.Blackboard))
.AddNewMethod()
.AddPrecondition<CreatureBlackboard>(x => x.foodInteractables.value.Count > 0)
//.AddSubTask(new DebugLogPrimitiveTask(owner, "Going to scavange food"))
.AddSubTask(new MoveToInteractablePrimitiveTask(owner, aIEntity.NavMeshAgent, ((CreatureBlackboard)aIEntity.Blackboard).foodInteractables.value))
.AddSubTask(new HoldInteractablePrimitiveTask(owner, ((CreatureBlackboard)aIEntity.Blackboard).foodInteractables.value))
.AddNewMethod()
.AddPrecondition<CreatureBlackboard>(x => x.foodProvidingInteractables.value.Count > 0)
//.AddSubTask(new DebugLogPrimitiveTask(owner, "Going to pick food"))
.AddSubTask(new MoveToInteractablePrimitiveTask(owner, aIEntity.NavMeshAgent, ((CreatureBlackboard)aIEntity.Blackboard).foodProvidingInteractables.value))
.AddSubTask(new InteractPrimitiveTask(owner, ((CreatureBlackboard)aIEntity.Blackboard).foodProvidingInteractables.value, aIEntity.Blackboard))
.Build())
.AddNewMethod()
.AddPrecondition<CreatureBlackboard>(x => x.tired.value == true)
.AddSubTask(new MoveToTargetNavMeshPrimitiveTarget(owner, aIEntity.NavMeshAgent, aIEntity.SleepingCaveEnternace))
.AddSubTask(new MoveToTargetPrimitiveTask(owner, aIEntity.NavMeshAgent, aIEntity.SleepingCave))
.AddSubTask(new SleepPrimitiveTask(owner, aIEntity.SleepDuration))
.AddSubTask(new MoveToTargetPrimitiveTask(owner, aIEntity.NavMeshAgent, aIEntity.SleepingCaveEnternace))
.AddNewMethod()
.AddPrecondition<CreatureBlackboard>(x => x.newInteractables.value.Count > 0)
//.AddSubTask(new DebugLogPrimitiveTask(owner, "Going to investigate"))
.AddSubTask(new MoveToNewInteractablePrimitiveTask(owner, aIEntity.NavMeshAgent, ((CreatureBlackboard)aIEntity.Blackboard).newInteractables))
.AddSubTask(new InvestigateInteractablePrimitiveTask(owner, aIEntity.InvestigateDuration))
.AddNewMethod()
.AddSubTask(new MoveRandomPrimitiveTask(owner, aIEntity.NavMeshAgent))
.AddSubTask(new WalkingIdleWaitPrimitiveTask(owner))
.Build();
Once the methods have been explored to their ends a plan is formed, and this will run until it is either completed or unable to continue, at which point it will re-plan. It will also re-plan when the world state changes, and swap to that plan if it is found to be better than the current plan. The world state information relevant to it is stored in a blackboard, and is updated by sensors in the world.
For this project, the sensors used were vision, tiredness, and hunger. It also more directly modified it’s blackboard for it’s memory of objects investigated.
Project Details
Role: Programmer
Group Size: 1
Game Engine: Unity
Code Language: C#
Project Purpose: Academic