
16 changed files with 685 additions and 10 deletions
@ -0,0 +1,34 @@ |
|||
|
|||
Behavior Trees |
|||
==================== |
|||
- Leaf: Cannot have children |
|||
- Conditional: returns success or failure immediately |
|||
- Action: can return running |
|||
- Composite: Has one or more children |
|||
- Parallel: runs multiple children at once |
|||
- Sequence: runs children in order, aborts on child failure |
|||
- Selector: runs children in order, continues on child failure |
|||
- Random: runs one of its children at random |
|||
- Decorator: Has exactly one child |
|||
- Inverter |
|||
- Succeeder: always return success |
|||
- Failer: always return failure |
|||
- Repeat X times |
|||
- Repeat until failure |
|||
- Repeat until success |
|||
|
|||
|
|||
Behavior Tree |
|||
ReceiveStatuses(Node, (TreeInstances, Statuses)) |
|||
|
|||
RunChildren(TreeInstances, ChildNodes) |
|||
|
|||
Update |
|||
loopCounter |
|||
do |
|||
update trees which have nodes which aren't running |
|||
|
|||
|
|||
while there are trees which aren't running and not at max number loops |
|||
|
|||
tick running nodes |
@ -0,0 +1,82 @@ |
|||
Tasks |
|||
Something to be done which can be broken down into a method to accomplish (task sort of equals goal) |
|||
|
|||
Methods |
|||
If the agent's state matches a set of preconditions, return a list of methods and primitives which complete the task |
|||
|
|||
Primitives/Operators |
|||
If the agent's state matches a set of preconditions, return that the primitive can be performed |
|||
Perform the actual change in the world |
|||
Change the world state (this can be both during planning and after actually performing the primitive task) (should |
|||
these be separated?) |
|||
|
|||
agent = entityID |
|||
agentState = struct |
|||
{ |
|||
isUndead = false |
|||
hasAmmo = false |
|||
target = entityID (should this in agent state or parameters?) |
|||
} |
|||
|
|||
worldState = struct |
|||
{ |
|||
towns = {townA: [x, y, z, properties], townB: [x, y, z, properties]} |
|||
canUndeadDoShit = true |
|||
} |
|||
|
|||
parameters = vector of param |
|||
param = struct |
|||
{ |
|||
enum type |
|||
union |
|||
{ |
|||
entityID entity |
|||
coordinate coord |
|||
|
|||
scalar val |
|||
vector vec |
|||
float floatVal |
|||
} |
|||
} |
|||
|
|||
MethodA = 0 |
|||
MethodB = 1 |
|||
|
|||
PrimitiveA = 0 |
|||
PrimitiveB = 1 |
|||
|
|||
MethodA = struct |
|||
{ |
|||
preconditionsSatisfied(agent, agentState, worldState, parameters) |
|||
{ |
|||
return worldState.useMethodA |
|||
} |
|||
newTasks = [(1, method, [param1, param2]), (0, primitive, [])]; |
|||
} |
|||
|
|||
PrimitiveA = module |
|||
{ |
|||
arePreconditionsSatisfied(agent, agentState, worldState, parameters) |
|||
{ |
|||
return worldState.useMethodA |
|||
} |
|||
|
|||
applyEffects(agent, agentState, worldState, parameters) |
|||
{ |
|||
worldState.hasDonePrimitiveA = true |
|||
} |
|||
|
|||
execute(agent, agentState, worldState, parameters) |
|||
{ |
|||
if preconditionsSatisfied(agent, agentState, worldState, parameters) |
|||
primitiveAComponentManager.subscribeTaskAgent(agent, this) // pass self in for callback (instead: event listener?) |
|||
return true |
|||
return false |
|||
} |
|||
|
|||
PrimitiveADoneCallback(agent, bool succeeded) |
|||
{ |
|||
// plan follow code will call applyEffects ? |
|||
notifyPlanFollowCode(agent, succeeded) |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
SubDir . src ai ; |
|||
|
|||
Library libGalaAi : htn/HTNTasks.cpp ; |
|||
#Library libGalaAi : htn/HTNTasks.cpp htn/HTNPlanner.cpp ; |
|||
|
|||
MakeLocate libGalaAi.a : lib ; |
@ -0,0 +1,307 @@ |
|||
#include "HTNTypes.hpp" |
|||
#include "HTNTasks.hpp" |
|||
|
|||
namespace Htn |
|||
{ |
|||
bool DecomposeGoalTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments) |
|||
{ |
|||
GoalTask* goalTask = static_cast<GoalTask*>(task); |
|||
return false; // TODO
|
|||
} |
|||
|
|||
bool DecomposeCompoundTask(TaskList& taskList, Task* task, const TaskArguments& taskArguments) |
|||
{ |
|||
CompoundTask* compoundTask = static_cast<CompoundTask*>(task); |
|||
|
|||
if (compoundTask->StateMeetsPreconditions(taskArguments)) |
|||
// Decomposition is determined by the task itself
|
|||
return compoundTask->Decompose(taskList, taskArguments); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
struct DecomposeTaskState |
|||
{ |
|||
Task* DecomposedTask; |
|||
ParameterList Parameters; |
|||
}; |
|||
|
|||
typedef std::vector<DecomposeTaskState> DecomposedTaskList; |
|||
|
|||
struct TaskDecomposition |
|||
{ |
|||
Task* DecomposedTask; |
|||
unsigned int DecompositionIndex; |
|||
DecomposedTaskList* Decomposition; |
|||
}; |
|||
|
|||
typedef std::vector<TaskDecomposition> TaskDecompositionStack; |
|||
|
|||
enum class PlanStepStatus |
|||
{ |
|||
Failed = 0, // generic failure
|
|||
|
|||
FailedDecomposition, |
|||
SuccessfulDecomposition, |
|||
|
|||
FailedToMeetPrecondtions, |
|||
StateMeetsPreconditions, |
|||
|
|||
NoTasks, |
|||
|
|||
PlanComplete |
|||
}; |
|||
|
|||
/*
|
|||
Goal Task // Set Agent StateA and StateB = true
|
|||
Method1 = Compound1(AgentState and Params from initial call) |
|||
Method2 = Compound2 |
|||
Method3 = Compound3 |
|||
|
|||
Compound1 |
|||
StateMeetsPrecondtion(AgentState, Params from Goal) then |
|||
return {primitiveA(paramA, paramB), primitiveB(ParamA, ParamB, ParamC)} |
|||
|
|||
primitiveA |
|||
StateMeetsPrecondition(AgentState, paramA, paramB) |
|||
then AgentState.StateA = true |
|||
|
|||
primitiveB |
|||
StateMeetsPrecondition(AgentState, paramA, paramB, paramC) |
|||
then AgentState.StateB = true |
|||
|
|||
PlanStep(stack, taskList) |
|||
if not stack |
|||
// startup
|
|||
stack.push(decompose(taskList[0])) |
|||
|
|||
else |
|||
decomposition = stack.pop() |
|||
allPrimitives = true |
|||
for task in decomposition |
|||
if task == primitive |
|||
task.StateMeetsPrecondition then |
|||
modify currentState |
|||
// don't break here
|
|||
else |
|||
goto fail |
|||
|
|||
if task == compound |
|||
allPrimitives = false |
|||
task.StateMeetsPrecondition then |
|||
stack.push(decompose(task)) |
|||
break |
|||
else |
|||
goto fail |
|||
|
|||
if task == goal |
|||
allPrimitives = false |
|||
methodIndex = 0 |
|||
decomposedTasks = task.GetMethodAtIndex(0), currentState |
|||
stack.push(decomposition) |
|||
break |
|||
|
|||
fail: |
|||
|
|||
|
|||
*/ |
|||
|
|||
PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList, |
|||
const TaskArguments& taskArguments) |
|||
{ |
|||
if (!stack.empty()) |
|||
{ |
|||
// Check to see if the current stack item is all primitive tasks with met preconditions. If
|
|||
// so, we can add them to the tasklist and walk all the way back up
|
|||
// TODO: naming
|
|||
TaskDecomposition currentDecomposition = stack.end(); |
|||
bool onlyPrimitiveTasksRemain = true; |
|||
bool failedToDecompose = false; |
|||
for (DecomposeTaskState* currentTaskState : currentDecomposition.Decomposition) |
|||
{ |
|||
if (!currentTaskState || !currentTaskState->DecomposeTask) |
|||
continue; |
|||
Task* currentTask = currentTaskState->DecomposeTask; |
|||
TaskType currentTaskType = currentTask->GetType(); |
|||
TaskArguments& currentTaskArguments = currentDecomposition->Arguments; |
|||
|
|||
if (currentTaskType == TaskType::Primitive) |
|||
{ |
|||
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(currentTask); |
|||
|
|||
if (!primitiveTask->StateMeetsPreconditions(currentTaskArguments)) |
|||
{ |
|||
failedToDecompose = true; |
|||
break; |
|||
} |
|||
|
|||
// Modify state
|
|||
|
|||
} |
|||
else |
|||
{ |
|||
onlyPrimitiveTasksRemain = false; |
|||
// TODO
|
|||
//failedToDecompose = DecomposeTask()
|
|||
} |
|||
} |
|||
|
|||
// TODO: Goal tasks trying new decompositions
|
|||
if (failedToDecompose) |
|||
{ |
|||
stack.pop_back(); |
|||
delete currentDecomposition; |
|||
return PlanStepStatus::FailedDecomposition; |
|||
} |
|||
} |
|||
else if (!taskList.empty()) |
|||
{ |
|||
// No stack; we must be just starting
|
|||
Task* currentTask = *taskList.data(); |
|||
|
|||
if (!currentTask) |
|||
return PlanStepStatus::Failed; |
|||
|
|||
TaskType currentTaskType = currentTask->GetType(); |
|||
|
|||
// What if taskList just has primitives? Multiple goals?
|
|||
if (currentTaskType == TaskType::Goal) |
|||
{ |
|||
GoalTask* goalTask = static_cast<GoalTask*>(currentTask); |
|||
int numMethods = goalTask->GetNumMethods(); |
|||
TaskDecomposition newDecomposition; |
|||
newDecomposition.DecomposedTask = currentTask; |
|||
newDecomposition.DecompositionIndex = 0; |
|||
newDecomposition.Decomposition = nullptr; |
|||
|
|||
// Goal tasks are decomposed via methods. Methods are Tasks which can be used to achieve
|
|||
// the goal. Goal tasks do not have conditonals - they just blindly pick methods in
|
|||
// order until they find one which works
|
|||
for (int methodIndex = newDecomposition.DecompositionIndex; |
|||
!newDecomposition.Decomposition && methodIndex < numMethods; methodIndex++) |
|||
{ |
|||
Task* method = goalTask->GetMethodAtIndex(newDecomposition.DecompositionIndex); |
|||
if (!method) |
|||
continue; |
|||
TaskType methodType = method->GetType(); |
|||
|
|||
if (methodType == TaskType::GoalTask) |
|||
{ |
|||
newDecomposition.newDecomposition = new TaskList(1); |
|||
newDecomposition.Decomposition->push_back(method); |
|||
newDecomposition.DecompositionIndex = methodIndex; |
|||
} |
|||
else if (methodType == TaskType::Primitive) |
|||
{ |
|||
PrimitiveTask* primitiveTask = static_cast<PrimitiveTask*>(method); |
|||
if (primitiveTask->StateMeetsPreconditions(taskArguments)) |
|||
{ |
|||
newDecomposition.newDecomposition = new TaskList(1); |
|||
newDecomposition.Decomposition->push_back(method); |
|||
newDecomposition.DecompositionIndex = methodIndex; |
|||
} |
|||
} |
|||
else if (methodType == TaskType::CompoundTask) |
|||
{ |
|||
CompoundTask* compoundTask = static_cast<CompoundTask*>(method); |
|||
if (compoundTask->StateMeetsPreconditions(taskArguments)) |
|||
{ |
|||
newDecomposition.newDecomposition = new TaskList(); |
|||
compoundTask->Decompose(&newDecomposition.newDecomposition, taskArguments); |
|||
newDecomposition.DecompositionIndex = methodIndex; |
|||
} |
|||
} |
|||
else |
|||
// We got something we don't know how to handle
|
|||
return PlanStepStatus::Failed; |
|||
} |
|||
|
|||
stack.push_back(newDecomposition); |
|||
return PlanStepStatus::SuccessfulDecomposition; |
|||
} |
|||
} |
|||
else |
|||
return PlanStepStatus::NoTasks; |
|||
} |
|||
|
|||
/*PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList,
|
|||
const TaskArguments& taskArguments) |
|||
{ |
|||
bool planComplete = true; |
|||
|
|||
// Check to see if there are tasks which need to be decomposed
|
|||
for (Task* currentTask : taskList) |
|||
{ |
|||
if (!currentTask) |
|||
continue; |
|||
|
|||
TaskType currentTaskType = currentTask->GetType(); |
|||
|
|||
// The plan is complete once all Goal and Compound Tasks have been decomposed into Primitive
|
|||
// Tasks, which we can actually execute
|
|||
if (currentTaskType != TaskType::Primitive) |
|||
{ |
|||
planComplete = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!planComplete) |
|||
{ |
|||
Task* currentTask = *taskList.data(); |
|||
|
|||
if (!currentTask) |
|||
return PlanStepStatus::Failed; |
|||
|
|||
TaskType currentTaskType = currentTask->GetType(); |
|||
|
|||
if (currentTaskType == TaskType::Goal) |
|||
{ |
|||
GoalTask* goalTask = static_cast<GoalTask*>(currentTask); |
|||
|
|||
} |
|||
} |
|||
else |
|||
return PlanStepStatus::PlanComplete; |
|||
}*/ |
|||
|
|||
/*// Take the first task in the list and decompose it or make sure it's a valid part of the plan
|
|||
PlanStepStatus PlanStep(TaskDecompositionStack& stack, TaskList& taskList, |
|||
const TaskArguments& taskArguments) |
|||
{ |
|||
if (taskList.size()) |
|||
{ |
|||
Task* currentTask = *taskList.data(); |
|||
TaskType taskType = currentTask->GetType(); |
|||
|
|||
// If it's a goal, we want to find a compound task that will accomplish it
|
|||
if (taskType == TaskType::Goal) |
|||
{ |
|||
if (DecomposeGoalTask(taskList, currentTask, taskArguments)) |
|||
return PlanStepStatus::SuccessfulDecomposition; |
|||
else |
|||
return PlanStepStatus::FailedDecomposition; |
|||
} |
|||
|
|||
// If it's a compound task, we want to decompose it
|
|||
if (taskType == TaskType::Compound) |
|||
{ |
|||
if (DecomposeCompoundTask(taskList, currentTask, taskArguments)) |
|||
return PlanStepStatus::SuccessfulDecomposition; |
|||
else |
|||
return PlanStepStatus::FailedDecomposition; |
|||
} |
|||
|
|||
// If it's a primitive task, we want to make sure we can actually execute it with our given
|
|||
// state
|
|||
if (taskType == TaskType::Primitive) |
|||
{ |
|||
// TODO
|
|||
} |
|||
|
|||
return PlanStepStatus::Failed; |
|||
} |
|||
|
|||
return PlanStepStatus::NoTasks; |
|||
}*/ |
|||
} |
@ -0,0 +1,42 @@ |
|||
#include "HTNTasks.hpp" |
|||
|
|||
namespace Htn |
|||
{ |
|||
Task::Task(void) |
|||
{ |
|||
Type = TaskType::None; |
|||
} |
|||
|
|||
TaskType Task::GetType(void) |
|||
{ |
|||
return Type; |
|||
} |
|||
|
|||
int GoalTask::GetNumMethods(void) |
|||
{ |
|||
return Methods ? Methods->size() : 0; |
|||
} |
|||
|
|||
Task* GoalTask::GetMethodAtIndex(int index) |
|||
{ |
|||
if (index < 0 || index >= GetNumMethods() || !Methods) |
|||
return nullptr; |
|||
|
|||
return (*Methods)[index]; |
|||
} |
|||
|
|||
void GoalTask::SetMethods(TaskList* newMethods) |
|||
{ |
|||
Methods = newMethods; |
|||
} |
|||
|
|||
CompoundTask::CompoundTask(void) |
|||
{ |
|||
Type = TaskType::Compound; |
|||
} |
|||
|
|||
PrimitiveTask::PrimitiveTask(void) |
|||
{ |
|||
Type = TaskType::Primitive; |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
#pragma once |
|||
|
|||
#include "HTNTypes.hpp" |
|||
|
|||
namespace Htn |
|||
{ |
|||
enum class TaskType |
|||
{ |
|||
None = 0, |
|||
Goal, |
|||
Compound, |
|||
Primitive |
|||
}; |
|||
|
|||
// Task only exists so that we can store all our Task types in a flat list
|
|||
class Task |
|||
{ |
|||
public: |
|||
Task(void); |
|||
|
|||
// Type is used to know what to cast this Task to
|
|||
TaskType GetType(void); |
|||
|
|||
protected: |
|||
// This is set by the constructors of each different Task
|
|||
TaskType Type; |
|||
}; |
|||
|
|||
typedef std::vector<Task*> TaskList; |
|||
|
|||
//
|
|||
// Different types of tasks
|
|||
//
|
|||
|
|||
class GoalTask : public Task |
|||
{ |
|||
private: |
|||
TaskList* Methods; |
|||
|
|||
public: |
|||
GoalTask(void); |
|||
|
|||
int GetNumMethods(void); |
|||
Task* GetMethodAtIndex(int index); |
|||
|
|||
void SetMethods(TaskList* newMethods); |
|||
}; |
|||
|
|||
class CompoundTask : public Task |
|||
{ |
|||
public: |
|||
CompoundTask(void); |
|||
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0; |
|||
virtual bool Decompose(TaskList& taskList, const TaskArguments& arguments) = 0; |
|||
}; |
|||
|
|||
class PrimitiveTask : public Task |
|||
{ |
|||
public: |
|||
PrimitiveTask(void); |
|||
virtual bool StateMeetsPreconditions(const TaskArguments& arguments) = 0; |
|||
// Returns whether or not starting the task was successful (NOT whether the task completed)
|
|||
virtual bool Execute(const TaskArguments& arguments) = 0; |
|||
}; |
|||
} |
@ -0,0 +1,49 @@ |
|||
#pragma once |
|||
|
|||
#include "../entityComponentSystem/EntityTypes.hpp" |
|||
|
|||
namespace Htn |
|||
{ |
|||
// Htn::Parameter is used as a way to send arbitrary arguments between different types of Tasks
|
|||
// TODO: Add getters/setters so this is safe to use
|
|||
struct Parameter |
|||
{ |
|||
enum class ParamType |
|||
{ |
|||
None = 0, |
|||
|
|||
// Primitives
|
|||
Int, |
|||
Float, |
|||
Bool, |
|||
|
|||
// Game-specific
|
|||
Entity |
|||
}; |
|||
|
|||
ParamType Type; |
|||
|
|||
union |
|||
{ |
|||
int IntValue; |
|||
float FloatValue; |
|||
bool BoolValue; |
|||
|
|||
Entity EntityValue; |
|||
}; |
|||
}; |
|||
|
|||
typedef std::vector<Parameter> ParameterList; |
|||
typedef std::vector<Parameter>::iterator ParameterListIterator; |
|||
typedef std::vector<Parameter>::const_iterator ParameterListConstIterator; |
|||
typedef std::vector<Parameter>::reverse_iterator ParameterListReverseIterator; |
|||
|
|||
// The arguments passed to most all Task functions
|
|||
struct TaskArguments |
|||
{ |
|||
Entity Agent; |
|||
// AgentState* AgentState;
|
|||
// WorldState* WorldState;
|
|||
ParameterList Parameters; |
|||
}; |
|||
} |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,70 @@ |
|||
#include <iostream> |
|||
|
|||
#include "../ai/htn/HTNTypes.hpp" |
|||
#include "../ai/htn/HTNTasks.hpp" |
|||
|
|||
class TestCompoundTaskA : public Htn::CompoundTask |
|||
{ |
|||
public: |
|||
TestCompoundTaskA(void) |
|||
{ |
|||
//Type = Htn::TaskType::Primitive;
|
|||
} |
|||
|
|||
virtual ~TestCompoundTaskA(void) |
|||
{ |
|||
} |
|||
|
|||
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
virtual bool Decompose(Htn::TaskList& taskList, const Htn::TaskArguments& args) |
|||
{ |
|||
std::cout << "Decompose TestCompoundTaskA: " << args.Parameters[0].IntValue << "\n"; |
|||
return true; |
|||
} |
|||
}; |
|||
|
|||
class TestPrimitiveTask : public Htn::PrimitiveTask |
|||
{ |
|||
public: |
|||
TestPrimitiveTask(void) |
|||
{ |
|||
//Type = Htn::TaskType::Primitive;
|
|||
} |
|||
|
|||
virtual ~TestPrimitiveTask(void) |
|||
{ |
|||
} |
|||
|
|||
virtual bool StateMeetsPreconditions(const Htn::TaskArguments& args) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
virtual bool Execute(const Htn::TaskArguments& args) |
|||
{ |
|||
std::cout << "execute TestPrimitiveTask: " << args.Parameters[0].IntValue << "\n"; |
|||
return true; |
|||
} |
|||
}; |
|||
|
|||
int main() |
|||
{ |
|||
// Test parameters
|
|||
Htn::Parameter testParam = {Htn::Parameter::ParamType::Int, 123}; |
|||
std::cout << testParam.FloatValue << "\n"; |
|||
|
|||
// Test Task types
|
|||
TestPrimitiveTask testPrimitive; |
|||
std::cout << (int)testPrimitive.GetType() << "\n"; |
|||
|
|||
// Goal task setup
|
|||
TestCompoundTaskA testCompoundTaskAA; |
|||
TestCompoundTaskA testCompoundTaskAB; |
|||
const Htn::TaskList goalTaskMethods = {&testCompoundTaskAA, &testCompoundTaskAB}; |
|||
|
|||
return 0; |
|||
} |
Loading…
Reference in new issue