Files
2026-05-10 11:47:55 -04:00

202 lines
10 KiB
C#

#if GRAPH_DESIGNER
/// ---------------------------------------------
/// Behavior Designer
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.BehaviorDesigner.Runtime.Components
{
using Opsive.BehaviorDesigner.Runtime.Groups;
using Opsive.BehaviorDesigner.Runtime.Systems;
using Opsive.BehaviorDesigner.Runtime.Utility;
using Unity.Entities;
using UnityEngine;
/// <summary>
/// Indicates that the behavior tree was baked.
/// </summary>
public class BakedBehaviorTree : IComponentData
{
[Tooltip("The index of the connected start task.")]
public int StartEventConnectedIndex;
[Tooltip("Should the behavior tree be started after it has been initialized?")]
public bool StartWhenEnabled;
[Tooltip("Should the behavior tree be started after it has been baked?")]
public bool StartEvaluation;
[Tooltip("The indicies of the reevaluate task systems.")]
public string[] ReevaluateTaskSystems;
[Tooltip("The indicies of the interrupt task systems.")]
public string[] InterruptTaskSystems;
[Tooltip("The indicies of the traversal task systems.")]
public string[] TraversalTaskSystems;
[Tooltip("The hashes that correspond to the TaskComponent's ComponentType.")]
public ulong[] TagStableTypeHashes;
[Tooltip("The hashes that correspond to the ReevaluateTaskComponent's ComponentType.")]
public ulong[] ReevaluateFlagStableTypeHashes;
}
/// <summary>
/// Baked editor-only metadata used to link a baked entity back to its authoring BehaviorTree for runtime debugging.
/// Populated by BehaviorTreeBaker only in editor builds and stripped from entity scenes before they are packaged into a player build by StripEditorBehaviorTreeReferenceSystem.
/// </summary>
public class BakedEditorReference : IComponentData
{
[Tooltip("The GlobalObjectId string for the authoring BehaviorTree component.")]
public string AuthoringBehaviorTreeGlobalObjectId;
[Tooltip("The design-time graph unique ID.")]
public int DesignGraphUniqueID;
[Tooltip("Maps design-time logic node index to runtime task index.")]
public ushort[] LogicNodeRuntimeIndices;
}
/// <summary>
/// Stores the start data for a baked behavior tree that should be started manually.
/// </summary>
public struct DeferredBakedBehaviorTreeStart : IComponentData
{
[Tooltip("The index of the connected start task.")]
public ushort StartEventConnectedIndex;
[Tooltip("Should the behavior tree start evaluation after the branch has started?")]
public bool StartEvaluation;
}
/// <summary>
/// The behavior tree has been baked. Start the tree using the baked data.
/// </summary>
public partial struct StartBakedBehaviorTreeSystem : ISystem
{
/// <summary>
/// Restricts when the system should run.
/// </summary>
/// <param name="state">The current SystemState.</param>
private void OnCreate(ref SystemState state)
{
state.RequireForUpdate<BakedBehaviorTree>();
}
/// <summary>
/// Starts the baked behavior tree.
/// </summary>
/// <param name="state">The current SystemState.</param>
private void OnUpdate(ref SystemState state)
{
// The components are baked, but systems are not baked. Create the required systems within the current world.
var reevaluateTaskSystemGroup = state.World.GetOrCreateSystemManaged<ReevaluateTaskSystemGroup>();
var interruptTaskSystemGroup = state.World.GetOrCreateSystemManaged<InterruptTaskSystemGroup>();
var traversalTaskSystemGroup = state.World.GetOrCreateSystemManaged<TraversalTaskSystemGroup>();
// Add the necessary cleanup systems.
var behaviorTreeSystemGroup = state.World.GetOrCreateSystemManaged<BehaviorTreeSystemGroup>();
behaviorTreeSystemGroup.AddSystemToUpdateList(state.World.GetOrCreateSystem<EvaluationCleanupSystem>());
behaviorTreeSystemGroup.AddSystemToUpdateList(state.World.GetOrCreateSystem<InterruptedCleanupSystem>());
var canReevaluate = false;
var ecb = new EntityCommandBuffer(state.WorldUpdateAllocator);
#if UNITY_EDITOR
// Collected during the foreach and applied after ecb.Playback() because ECB.AddComponentObject only supports EntityQuery, not individual Entity.
var editorGraphReferences = new System.Collections.Generic.List<(Entity entity, EditorBehaviorTreeGraphReference reference)>();
#endif
foreach (var (bakedBehaviorTree, entity) in SystemAPI.Query<BakedBehaviorTree>().WithEntityAccess()) {
AddSystems(state.World, reevaluateTaskSystemGroup, bakedBehaviorTree.ReevaluateTaskSystems);
AddSystems(state.World, interruptTaskSystemGroup, bakedBehaviorTree.InterruptTaskSystems);
AddSystems(state.World, traversalTaskSystemGroup, bakedBehaviorTree.TraversalTaskSystems);
// ComponentTypes cannot be serialized. Convert the StableTypeHash to a ComponentType.
var taskComponents = state.World.EntityManager.GetBuffer<TaskComponent>(entity);
for (int i = 0; i < taskComponents.Length; ++i) {
var taskComponent = taskComponents[i];
taskComponent.FlagComponentType = ComponentType.FromTypeIndex(TypeManager.GetTypeIndexFromStableTypeHash(bakedBehaviorTree.TagStableTypeHashes[i]));
taskComponents[i] = taskComponent;
}
TraversalUtility.PopulateChildUpperIndices(ref taskComponents);
if (state.World.EntityManager.HasBuffer<ReevaluateTaskComponent>(entity)) {
var reevaluateComponents = state.World.EntityManager.GetBuffer<ReevaluateTaskComponent>(entity);
canReevaluate = true;
for (int i = 0; i < reevaluateComponents.Length; ++i) {
var reevaluateComponent = reevaluateComponents[i];
reevaluateComponent.ReevaluateFlagComponentType = ComponentType.FromTypeIndex(TypeManager.GetTypeIndexFromStableTypeHash(bakedBehaviorTree.ReevaluateFlagStableTypeHashes[i]));
reevaluateComponents[i] = reevaluateComponent;
}
}
// All of the systems have been added. Start the behavior tree or defer the start.
if (bakedBehaviorTree.StartWhenEnabled) {
BehaviorTree.StartBranch(state.World, entity, (ushort)bakedBehaviorTree.StartEventConnectedIndex, bakedBehaviorTree.StartEvaluation);
} else {
ecb.AddComponent(entity, new DeferredBakedBehaviorTreeStart
{
StartEventConnectedIndex = (ushort)bakedBehaviorTree.StartEventConnectedIndex,
StartEvaluation = bakedBehaviorTree.StartEvaluation
});
}
#if UNITY_EDITOR
// BakedEditorReference is stripped from player build entity scenes by StripEditorBehaviorTreeReferenceSystem before serialization, so it only ever reaches
// this system during editor play mode.
if (state.EntityManager.HasComponent<BakedEditorReference>(entity)) {
var editorRef = state.EntityManager.GetComponentObject<BakedEditorReference>(entity);
if (!string.IsNullOrEmpty(editorRef.AuthoringBehaviorTreeGlobalObjectId)) {
editorGraphReferences.Add((entity, new EditorBehaviorTreeGraphReference
{
AuthoringBehaviorTreeGlobalObjectId = editorRef.AuthoringBehaviorTreeGlobalObjectId,
DesignGraphUniqueID = editorRef.DesignGraphUniqueID,
LogicNodeRuntimeIndices = editorRef.LogicNodeRuntimeIndices,
}));
}
ecb.RemoveComponent<BakedEditorReference>(entity);
}
#endif
ecb.RemoveComponent<BakedBehaviorTree>(entity);
}
if (canReevaluate) {
state.World.GetOrCreateSystemManaged<BeforeTraversalSystemGroup>().AddSystemToUpdateList(state.World.GetOrCreateSystem(typeof(ReevaluateSystem)));
}
reevaluateTaskSystemGroup.SortSystems();
interruptTaskSystemGroup.SortSystems();
traversalTaskSystemGroup.SortSystems();
ecb.Playback(state.EntityManager);
ecb.Dispose();
#if UNITY_EDITOR
foreach (var (entity, reference) in editorGraphReferences) {
state.EntityManager.AddComponentObject(entity, reference);
}
#endif
}
/// <summary>
/// Adds the systems indicated by the SystemTypeIndex to the specified group.
/// </summary>
/// <param name="world">The current World.</param>
/// <param name="group">The group that the systems should be added to.</param>
/// <param name="systemTypes">The types of systems that should be added.</param>
private void AddSystems(World world, ComponentSystemGroup group, string[] systemTypes)
{
if (systemTypes == null) { return; }
for (int i = 0; i < systemTypes.Length; ++i) {
group.AddSystemToUpdateList(world.GetOrCreateSystem(Shared.Utility.TypeUtility.GetType(systemTypes[i])));
}
}
}
#if UNITY_EDITOR
/// <summary>
/// Strips BakedEditorReference from all entities during the entity scene optimization pass, which runs
/// after baking but before the .entities file is serialized to disk for player builds. This ensures the
/// type hash for BakedEditorReference never appears in a standalone build's entity scene, while the
/// component remains available during editor play mode for StartBakedBehaviorTreeSystem to consume.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.EntitySceneOptimizations)]
public partial class StripEditorBehaviorTreeReferenceSystem : SystemBase
{
protected override void OnUpdate()
{
var query = GetEntityQuery(ComponentType.ReadOnly<BakedEditorReference>());
EntityManager.RemoveComponent<BakedEditorReference>(query);
}
}
#endif
}
#endif