Skip to content

Latest commit

 

History

History
189 lines (166 loc) · 6.24 KB

custom-node.md

File metadata and controls

189 lines (166 loc) · 6.24 KB

HOWTO: Create a custom Behavior Tree node

Custom Behavior Tree Delay Node

Behavior Trees are very extensible meaning you can create own node types in your game project or use the ones from engine and plugins. Each node can define custom logic, contain properties and store runtime state (per-instance).

Warning

Please keep in mind that Behavior execution is done in async via Job System (Behavior.System).

Warning

Don't store any Behavior state in node properties but custom Node State data container (nodes are not instanced per behavior).

Custom delay node

Below is an example of a custom node that waits a specific amount of time before graph execution can continue to another node. It stores the time left value per-instance (data cached for each node, for each behavior using it).

using System.Runtime.InteropServices;
using FlaxEngine;

/// <summary>
/// Simple delay node.
/// </summary>
public class MyDelayNode : BehaviorTreeNode
{
    // Node state container (allocated per active node, see InitState/ReleaseState)
    [StructLayout(LayoutKind.Sequential)]
    private struct State
    {
        public float TimeLeft;
        public Actor ObjectRef;
    }

    /// <summary>
    /// Delay duration (in seconds). Used only if DurationSelector is unused.
    /// </summary>
    public float Duration = 5.0f;

    /// <summary>
    /// Delay duration selector. If set Duration is unused.
    /// </summary>
    public BehaviorKnowledgeSelector<float> DurationSelector;

    /// <inheritdoc />
    public override int GetStateSize()
    {
        // Return size of the per-instance node memory to allocate
        return GetStateSize<State>();
    }

    /// <inheritdoc />
    public override void InitState(BehaviorUpdateContext context)
    {
        // Initialize state and assign it
        var state = new State
        {
            TimeLeft = Duration,
            ObjectRef = context.Behavior.Actor, // State can contain object references
        };
        if (DurationSelector.TryGet(context.Knowledge, out var duration))
            state.TimeLeft = duration;
        NewState(context.Memory, state);
    }

    /// <inheritdoc />
    public override void ReleaseState(BehaviorUpdateContext context)
    {
        // Free node state (eg. object references)
        ref var state = ref GetState<State>(context.Memory);
        state.ObjectRef = null;
        FreeState(context.Memory);
    }

    /// <inheritdoc />
    public override BehaviorUpdateResult Update(BehaviorUpdateContext context)
    {
        // Decrement timer by the current update delta and keep running until it's above 0
        ref var state = ref GetState<State>(context.Memory);
        state.TimeLeft -= context.DeltaTime;
        return state.TimeLeft <= 0.0f ? BehaviorUpdateResult.Success : BehaviorUpdateResult.Running;
    }

#if FLAX_EDITOR
    /// <inheritdoc />
    public override string GetDebugInfo(BehaviorUpdateContext context)
    {
        // Editor-only debug text (shown under node title, can be multiline)
        if (context.Knowledge)
        {
            ref var state = ref GetState<State>(context.Memory);
            return string.Format("Time left: {0}s", state.TimeLeft);
        }

        // Context might not contain state at edit-time so display defaults
        if (!string.IsNullOrEmpty(DurationSelector.Path))
            return string.Format("Duration: {0}", DurationSelector);
        return string.Format("Duration: {0}s", Duration);
    }
#endif
}
#pragma once

#include "Engine/AI/BehaviorTreeNode.h"
#include "Engine/AI/BehaviorKnowledgeSelector.h"
#include "Engine/Scripting/ScriptingObjectReference.h"
#include "Engine/AI/Behavior.h"
#include "Engine/Level/Actor.h"

/// <summary>
/// Simple delay node.
/// </summary>
API_CLASS() class GAME_API MyDelayNode : public BehaviorTreeNode
{
    DECLARE_SCRIPTING_TYPE(MyDelayNode);
    API_AUTO_SERIALIZATION();

    /// <summary>
    /// Delay duration (in seconds). Used only if DurationSelector is unused.
    /// </summary>
    API_FIELD() float Duration = 5.0f;

    /// <summary>
    /// Delay duration selector. If set Duration is unused.
    /// </summary>
    API_FIELD() BehaviorKnowledgeSelector<float> DurationSelector;

public:
    // [BehaviorTreeNode]
    int32 GetStateSize() const override
    {
        // Return size of the per-instance node memory to allocate
        return sizeof(State);
    }
    void InitState(const BehaviorUpdateContext& context) override
    {
        // Initialize state and assign it
        auto state = GetState<State>(context.Memory);
        new(state)State();
        state->TimeLeft = Duration;
        state->ObjectRef = context.Behavior->GetActor(); // State can contain object references
        DurationSelector.TryGet(context.Knowledge, state->TimeLeft);
    }
    void ReleaseState(const BehaviorUpdateContext& context) override
    {
        // Free node state (eg. object references)
        auto state = GetState<State>(context.Memory);
        state->~State();
    }
    BehaviorUpdateResult Update(const BehaviorUpdateContext& context) override
    {
        // Decrement timer by the current update delta and keep running until it's above 0
        auto state = GetState<State>(context.Memory);
        state->TimeLeft -= context.DeltaTime;
        return state->TimeLeft <= 0.0f ? BehaviorUpdateResult::Success : BehaviorUpdateResult::Running;
    }
#if USE_EDITOR
    String GetDebugInfo(const BehaviorUpdateContext& context) const override
    {
        // Editor-only debug text (shown under node title, can be multiline)
        if (context.Knowledge)
        {
            auto state = GetState<State>(context.Memory);
            return String::Format(TEXT("Time left: {0}s"), state->TimeLeft);
        }
        // Context might not contain state at edit-time so display defaults
        if (DurationSelector.Path.HasChars())
            return String::Format(TEXT("Duration: {0}"), DurationSelector.ToString());
        return String::Format(TEXT("Duration: {0}s"), Duration);
    }
#endif

private:
    struct State
    {
        float TimeLeft;
        ScriptingObjectReference<Actor> ObjectRef;
    };
};

inline MyDelayNode::MyDelayNode(const SpawnParams& params)
    : BehaviorTreeNode(params)
{
}