Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Proper way to implement GOAP in ECS

Discussion in 'Entity Component System' started by ChromeCat, May 23, 2020.

  1. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    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.
     
  2. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    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
     
    Last edited: May 23, 2020
  3. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    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.
     
    ChromeCat likes this.
  4. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    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.
     
    Last edited: May 23, 2020
  5. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    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 :(
    https://forum.unity.com/threads/ai-in-ecs-what-approaches-do-we-have.672310/
     
    Last edited: May 23, 2020
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    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)
     
    Last edited: May 23, 2020
  7. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,907
    Last edited: May 24, 2020
    ChromeCat and TheGabelle like this.
  8. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    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!
     
  9. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
  10. quabug

    quabug

    Joined:
    Jul 18, 2015
    Posts:
    66
    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 `Parameter`s into a blob which is more extensible than union struct.
    Code (CSharp):
    1. struct GOAPStateBlob
    2. {
    3.     public BlobArray<int> Offsets;
    4.     public BlobArray<byte> Data;
    5.  
    6.     public unsafe T* GetData<T>(int index) where T : unmanaged => (T*)((byte*)Data.GetUnsafePtr() + Offsets[index]);
    7. }
    8.  
    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.
     
    Last edited: May 24, 2020
  11. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    Thanks for sharing code! Wow that code is really something I haven't seen. Isn't it blobAsset immutable data?
     
  12. quabug

    quabug

    Joined:
    Jul 18, 2015
    Posts:
    66
    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.
     
    ChromeCat likes this.
  13. ChromeCat

    ChromeCat

    Joined:
    Jan 27, 2016
    Posts:
    35
    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?
     
  14. quabug

    quabug

    Joined:
    Jul 18, 2015
    Posts:
    66
    I don't think there's any trouble in ECS by using `[StructLayout(LayoutKind.Explicit)]` on any struct.
     
  15. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    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.
     
  16. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    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.
     
    MNNoxMortem likes this.
  17. quabug

    quabug

    Joined:
    Jul 18, 2015
    Posts:
    66
    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?
     
  18. SubPixelPerfect

    SubPixelPerfect

    Joined:
    Oct 14, 2015
    Posts:
    224
  19. gabex

    gabex

    Joined:
    Sep 10, 2019
    Posts:
    1
    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:

    Code (CSharp):
    1. public NativeArray<Action> AllActions;
    2.  
    3. public NativeMultiHashMap<int, Condition> AllPreconditions;
    4.  
    5. 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:

    Code (CSharp):
    1. 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 :).
     
    Last edited: Jun 5, 2020
    Cachete, DotusX, elJoel and 1 other person like this.