Proper way to implement GOAP in ECS

One of my recent biggest challenge is implementing AI in ECS.

So I’ve tried to implement GOAP in ECS. But there were some problems that I cannot solve.

Which is GOAP represent a world state in key/value dictionary.

Entity’s own knowledge to an world state is a Memory, and it also holds partial world states info as Dictionary.

But I don’t know how to implement dictionary type inside component.

I’ll lay down my implementation so far.

===========================

Components
-GOAPActorComponent:IComponentData
bool shouldPlan
int runningActionIdx
-GOAPActionComponent:IBufferElementData
GOAPActionType type
GOAPState preconditions
GOAPState effects
-GOAPGoalComponent:IBufferElementData
GOAPState goals
-GOAPQueuedActionComponent:IBufferElementData
GOAPActionType type
GOAPState settings
-GOAPMemoryComponent:IComponentData
GOAPState worldState

Systems
-GOAPPlanningSystem
=>Plan actions with GoalComponent and ActionComponent inside buffer and put all actions in QueuedActionComponent in ordered
-GOAPPlanExecutionSystem
=>Pull one action and attach corresponding action component to entity.
ex)If QueuedActionComponent.type = GOAPActionType.MoveToPos then attach AIMoveToPos Component
-AIMoveToPosSystem
=>Process AI which AIMoveToPos is attached

GOAPActionType(enum)
-Idle
-MoveToPos
-SaySomething

GOAPState(struct) <=Problem(1) kinda dictionary thingy struct.
-Parameter0
-Parameter1
-Parameter2
-Parameter3
.
.
-Parameter20?
-this[int index] property (to use it as a array)

Parameter(struct) <=Problem(2)
-int key
-int intVal
-bool boolVal
-float floatVal
-float3 float3Val

Actually this implementation looks very not a good way to ECS I know, but it works what i’ve expected.

But there is a limitation,

which GOAPState has fixed fake array type of component, if Memory has some big amount of world state, then this will exceed the size of the struct,

and also other Entity that holds Memory component that hold no states, will have an empty big fixed GOAPState, which is no good.

So far I tried to find other solutions but now I can’t think of any.

Is there a way to avoid this kinda situation?

And if there ain’t, then is there a good way to implement AI in ECS?

I wan’t to know how other people deals AI in ECS.

Sounds like a case where having a NativeHashMap<Entity, NativeHashMap<A, B>> would help, but I’m not sure if that’s allowed. I’d be interested to know if DynamicDictionnary component types could become a thing (a dictionnary equivalent of DynamicBuffer)

An alternative would be to have just one global NativeHashMap which contains the key/value pairs of all the things that all your entities could know, but your entities hold a DynamicBuffer of just the keys of that hashmap that they know

4 Likes

I know Entities can have class implementations of IComponentData, but it can’t be passed directly into a job. I thought about passing native collections inside the class-based component to a job directly, but I’m unsure how to write back to the class.

1 Like

Yeah DynamicDictionary would help too. But I don’t know they could implement because of the chunk size limitation:( The MemoryComponent could get really bigger if there is lots of info.

Me too. But I’ve been implementing this about a month, and I couldn’t figure out to solve this within a struct based archetecture. If there is no good solution, I might just pass out Job and just iterate with ComponentSystem in main thread with class based.
I haven’t fully tried but, in my perspective, just only ECS’s DOD(Data Oriented Design) could make my game really emergent.

Mine game is just plane realtime 2d RPG and Enemy AI implementing is really a big part of the game. But so far I haven’t found a good solution for AI yet.

I’ve done FSM, Behaviour Tree, GOAP in OOP before.
FSM can be done in ecs easily but scaling & expanding AI is horrible.
Other two AI (BT,GOAP) I’ve been used was good for development, but
porting to ECS, in my case it’s way too hard to do it.

GOAP implementing in OOP took like 4 days, but porting this GOAP into ECS took like 1 month, in imperfect way.
And in a month, most of effort took was learning ECS fundamentally, even looking source code of ECS, try to find a proper way to implement GOAP because of all those restriction for Job system.

Still monitoring this thread for others AI solution but right now, no good news for me :frowning:
https://discussions.unity.com/t/741984

There’s nothing stopping you from coding OOP-style in the ECS, though. Using ComponentDataFromEntity usually solves all of those cases where you feel like something would be much easier to implement in OOP. I think it’s just the one-dictionary-per-entity thing that’s a limitation, but there are alternatives

I think a lot of people, when they start learning DOTS, get a feeling that any usage of ComponentDataFromEntity is some sort of failure. But that really shouldn’t be the case. There are many cases where that is the way to go. If your problem needs non-linear data access, it needs non-linear data access. And remember that things being coded in an “imperfect” way is the default way of doing things in OOP. DoD just gives you more opportunities to do things better for problems that could take advantage of linear data access (which is not every problem)

4 Likes

@SubPixelPerfect tried to implement a GOAP in DOD style, summoning him, maybe he has some insights what happened 1.5 years ago at the end… :smile:

BTW, found this the other day, but no example code.
https://pdfs.semanticscholar.org/77be/9609f890eb5942e8b02c3dc71b501727e705.pdf

2 Likes

Yeah, I really thought non-linear access is against the rules because every Unity video refered that non-linear access is a bad thing. Maybe its time to think different like you said. Thanks!

Thanks!

As @PhilSA said, there’s nothing wrong by using ComponentDataFromEntity directly, unless it has significant performance issue that you have to optimize.

Anyway, I think GOAPState is better to be an IBufferElementData and Parameter should be an union struct.
Or you could write Parameters into a blob which is more extensible than union struct.

struct GOAPStateBlob
{
    public BlobArray<int> Offsets;
    public BlobArray<byte> Data;

    public unsafe T* GetData<T>(int index) where T : unmanaged => (T*)((byte*)Data.GetUnsafePtr() + Offsets[index]);
}

Unity AI Planner are also working on pure ECS, it would be an interesting example that how to achieve planning AI in ECS once its done.

Thanks for sharing code! Wow that code is really something I haven’t seen. Isn’t it blobAsset immutable data?

No, you are able to write any data into a blob asset, but it is hard (not possible?) to expand once blob has been allocated.

1 Like

Thanks for the fast reply! I guess i might try blobAsset for use.

And sorry for another question, is there a chance that Union struct cause any trouble in ECS?

I don’t think there’s any trouble in ECS by using [StructLayout(LayoutKind.Explicit)] on any struct.

Being able to efficiently update values and collections in a blob asset without breaking references will be a real game changer. Not being able to do this is why I avoid blobs most of the time.

And that is a good thing. Blob Assets are meant to be immutable. That is what makes it safe to read from any job. If you want to mutate a blob asset. NativeContainers & DynamicBuffer are meant for mutable data.

1 Like

It ought to be very efficient to allocate blob on temp/tempjob?
And in this case GOAP system should update all the reference of GOAPState after blob was allocated.
But I have no idea of the detail of GOAP system, so it maybe hard to update though?

it is on hold, mostly because of AI Planner - v0.2-preview released

Have you considered storing the world state, actions with their preconditions / effects and the goal states in a Native container outside of ECS. I have taken that route in my basic GOAP implementation. GOAP is an essential part of what I am trying to achieve because whatever the AI can do the Player can, too so basically every Player action is a Goal. Therefore I had to get something usable done pretty quickly.

I have these three native containers in an ActionComposer singleton class:

public NativeArray<Action> AllActions;

public NativeMultiHashMap<int, Condition> AllPreconditions;

public NativeMultiHashMap<int, Condition> AllEffects;

Action struct has the action cost and all the blittable info an action requires along with a unique identifier, which is the ActionType enum. Each of these native NativeMultiHashMaps use the ActionType enum as key and a Condition struct as value. I use the ActionType to connect these containers together during planning. Condition structs are used to describe preconditions, effects and goal states. It consists of a key (enum), an integer value and an operator, which can be ==, <, >, etc…. It is used to compare world / goal states with the effects and preconditions. Based on your post I assume you know how GOAP works so I won’t get into details here.

I also have a GoalComposer class, that has the following native container:

public NativeMultiHashMap<int, Condition> AllGoalStates;

It is used to store the Goals and their states. The key is an enum called GoalType, which is used as a unique identifier for the goals, the value is the Condition struct I described above.

These native containers are all allocated persistently (Allocator.Persistent) and you can populate them from a DB or serialised data from local storage (which is my preference with a simple editor plugin providing the CMS) when the app starts.

The World State can be another native container, like a NativeArray with a struct that contains the Condition struct and some target data or a NativeMultiHashMap where your state type is the key. In my case I need to store quite a lot of different info here so I won’t go through it but what is important is that lots of other systems (such as F.E.A.R. style sensors) write into a TempJob allocated NativeArray that gets merged into this WorldState array at the end of the simulation group or even beginning of the Initialisation group (deferred writing, just like what ECBs do).

And now the ECS part:

I have a SupportedActions DynamicBuffer attached to all unit entities, which contains the list of ActionType enum values that the unit supports. I also have a SupportedGoals DynamicBuffer, which I add to entities that the Units can do actions on, like GoalType.Refuel on a filling station entity or even a GoalType.ReachDestination on the HexGrid. The SupportedGoals is used to check if the unit can do anything with the target by comparing the supported actions with the states of the SupportedGoals of the target.

When a unit is ordered to achieve a goal, I add a simple GoalPlanningParams component to the unit with a few fields, like the target entity the unit needs to achieve a goal, destination tile position, etc… This component triggers the GoalPlanningSystem, which gets the AllActions, AllPreconditions, AllEffects, WorldState and GoalState native containers as dependencies, optimises some of them (like strips out the non relevant world states) and performs the A* search inside a parallel job. The result is an ActiveActions component that contains the list of actions and a CurrentActionIndex added to the unit entity that can be picked up by Action Ordering Systems, like Move, Survey, Repair, etc…

This setup works pretty well for me so far, but it is nowhere near complete. One important missing part is the procedural (or context) preconditions, which I am planning to plug in when I need it.

I have done a few very basic, nonscientific performance tests, where 1100 units were ordered to do a few actions, such as: move → land → survey all in one frame to achieve a goal. The job system performed that calculation in 0.7ms on 10 worker threads and completed all of them way before the BuildPhysicsSystem completed its job on 350 entities on the main thread. These tests are obviously non representative but they helped me do some obvious optimisation on the data the A* searches through and gave me some confidence that the route I took is not leading me to a dead end (for now).

I hope this will give you some ideas and that it might anger a few experienced ECS purists and Burst Job experts, who will be nice enough to lead us to a better solution :).

5 Likes

I have one question. I know this reply is so far from your post.
But now, on DOTs 1.2.
When we use NativeArr for running job Parrallel. It will break amount of work to batches.
If i have huge amount of batchs. The cost of copid NativeArr could be harmful to performance ? Because it’ll take more RAM usage, right ?
And our system do that every frame Update().