Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
squiddingme opened this issue Dec 13, 2023 · 3 comments
Labels
enhancement New feature or request

Comments

@squiddingme
Copy link

squiddingme commented Dec 13, 2023

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 limbonaut added the enhancement New feature or request label Dec 13, 2023
@limbonaut
Copy link
Owner

limbonaut commented Dec 13, 2023

Some thoughts about this proposal:

  • Blackboard can already set/get a dictionary as a data source, which can be used for simple scenarios.
    • Blackboard architecture involves a scope chain, and parent scopes are not aware of other scopes down the tree.
    • Currently, BTNewScope and BTSubtree are creating new scopes, and each instance of LimboState creates a scope which has a non-empty blackboard_data property set.
    • Full serialization requires finding/caching each blackboard scope in the state-machine-behavior-tree structure.
    • Q: What to do with object references on the blackboard (storing target is a common pattern)? What about the non-Node variety, like extended Object classes and Ref<Resource>?
  • Can't use ResourceSaver for behavior tree serialization:
    • Q: Is loading a resource from an external file still vulnerable to embedded code execution?
  • Can be a JSON serializer.
  • The serializer should collect each property with the PROPERTY_USAGE_STORAGE flag set. This way, the user has full control over which property should be serialized.
  • Should avoid per-task specialized serialize(), deserialize(), because it's a lot of unnecessary work.
  • Deserializing HSM: The state machine should set proper current state at each level respectively.

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
Copy link
Author

squiddingme commented Dec 13, 2023

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).

limbonaut added a commit that referenced this issue Dec 29, 2023
limbonaut added a commit that referenced this issue Apr 8, 2024
@onze
Copy link
Contributor

onze commented Jun 1, 2024

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)
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants