limbonaut / limboai

LimboAI - Behavior Trees and State Machines for Godot 4
MIT License
775 stars 24 forks source link

Allow serialising the execution state and blackboard of the behavior tree (and HSMs) #2

Open squiddingme opened 6 months ago

squiddingme commented 6 months ago

This should include all blackboard variables and the current running indexes of any composite nodes, as well as the current running state of any HSMs. This would allow developers to dump the state of the entire behavior tree to a save file, which is useful for allowing players to save and load at any time (including mid-behavior tree execution, so an AI agent can remember what it was doing).

limbonaut commented 6 months ago

Some thoughts about this proposal:

Example of a blackboard chain:

LimboHSM -- new blackboard scope
-- LimboState -- inherits scope (but can also define new blackboard)
-- BTState -- new blackboard scope
---- Sequence -- inherits scope
------ Action -- inherits scope
------ BTSubtree -- new blackboard scope
-------- SubtreeRootTask -- inherits scope

In the example, SubtreeRootTask has the following scope chain: BTSubtree's Blackboard -> BTState's Blackboard -> LimboHSM's Blackboard.

squiddingme commented 6 months ago

In my own behavior tree implementation (which isn't very good, and LimboAI is looking much better), I've just been serialising node references to node paths and back. It... works for what I need, but may not necessarily make sense for a generic serialiser.

Godot's built-in serialisers (JSON.parse_string and var_to_bytes) will just use a basic string representation for types it doesn't support, which doesn't correctly serialise and deserialise back (for example, bizarrely, the JSON serialiser does not support Vector types, but you can use var_to_string and string_to_var to get around this). I wouldn't say full serialisation is necessary -- though warnings when a type can't be serialised would help developers design the way they use LimboAI around their serialisation requirements (and would already be a step up from the zero feedback Godot's serialisers give you).

onze commented 3 weeks ago

This issue was mentioned in a discussion in the discord channel.

One key outcome of the discussion was about serializing object references. @limbonaut suggested using a strategy pattern with a Serializer class, taking an ObjectSerializer instance, which by default would ignore object references, and not deserialize any objects.

API draft:

// de/serialize tasks and their attributes - can be subclassed to add support for user-defined types
class ObjectSerializer : public RefCounted {
    // serialize fields
    Variant serialize_object_reference(Ref<Object> object, ObjectID obj_id);
    Ref<Object> deserialize_object_reference(data: Variant, ObjectID obj_id);

    // serialize tasks
    Variant serialize_task(Ref<BTTask> task);
    Ref<BTTask> deserialize_task(Variant task_data);
};

// walk the tree and aggregates the ObjectSerializer's outputs
class Serializer : public RefCounted {
    // serialize (json) a task and its children
    String serialize(Ref<BTTask> task, Ref<ObjectSerializer> object_serializer)
    Ref<BTtask> deserialize(String tree_payload)
};