feat: add actor behavior runtime

This commit is contained in:
ud18010
2026-07-30 13:12:27 +08:00
parent 7ef8427e01
commit 1517d46fc0
2 changed files with 387 additions and 0 deletions
@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
namespace XWorld.Framework.ActorBehavior
{
public sealed class BehaviorWorld
{
private readonly BehaviorWorldOptions _options;
private readonly Dictionary<string, BehaviorConfigCatalog> _catalogs = new Dictionary<string, BehaviorConfigCatalog>(StringComparer.Ordinal);
private readonly Dictionary<int, BehaviorActorState> _actors = new Dictionary<int, BehaviorActorState>();
private readonly Dictionary<int, ActiveBehavior> _activeBehaviors = new Dictionary<int, ActiveBehavior>();
private readonly List<BehaviorEvent> _events = new List<BehaviorEvent>();
public BehaviorWorld(BehaviorWorldOptions options)
{
_options = options;
}
public int ActiveProjectileCount => 0;
public void RegisterCatalog(BehaviorConfigCatalog catalog)
{
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
_catalogs[catalog.Type] = catalog;
}
public void AddActor(BehaviorActorState actor)
{
if (actor == null) throw new ArgumentNullException(nameof(actor));
_actors[actor.ActorId] = actor;
}
public bool SwitchActorCatalog(int actorId, string catalogType)
{
if (!_actors.TryGetValue(actorId, out BehaviorActorState actor)) return false;
if (!_catalogs.ContainsKey(catalogType)) return false;
actor.CatalogType = catalogType;
return true;
}
public BehaviorStartResult TryStartBehavior(int actorId, string behaviorId, BehaviorInput input)
{
if (!_actors.TryGetValue(actorId, out BehaviorActorState actor))
return BehaviorStartResult.Fail("actor not found");
if (!_catalogs.TryGetValue(actor.CatalogType, out BehaviorConfigCatalog catalog))
return BehaviorStartResult.Fail("catalog not found");
if (!catalog.Behaviors.TryGetValue(behaviorId, out BehaviorSpec spec))
return BehaviorStartResult.Fail("behavior not found");
if (_activeBehaviors.ContainsKey(actorId))
return BehaviorStartResult.Fail("actor already has active behavior");
var active = new ActiveBehavior
{
ActorId = actorId,
Catalog = catalog,
Spec = spec,
Input = input,
Elapsed = 0f
};
_activeBehaviors[actorId] = active;
_events.Add(new BehaviorEvent { Kind = BehaviorEventKind.BehaviorStarted, ActorId = actorId, BehaviorId = spec.Id });
FireTimeline(active, -0.00001f, 0f);
return BehaviorStartResult.Ok();
}
public bool StopBehavior(int actorId, BehaviorStopReason reason)
{
if (!_activeBehaviors.TryGetValue(actorId, out ActiveBehavior active)) return false;
if (!active.Spec.CanBeInterrupted && reason != BehaviorStopReason.Death && reason != BehaviorStopReason.Completed) return false;
_activeBehaviors.Remove(actorId);
_events.Add(new BehaviorEvent
{
Kind = BehaviorEventKind.BehaviorStopped,
ActorId = actorId,
BehaviorId = active.Spec.Id,
StopReason = reason
});
return true;
}
public void Tick(float deltaTime)
{
if (deltaTime <= 0f) return;
var completed = new List<int>();
foreach (ActiveBehavior active in new List<ActiveBehavior>(_activeBehaviors.Values))
{
float previous = active.Elapsed;
active.Elapsed += deltaTime;
FireTimeline(active, previous, active.Elapsed);
if (active.Elapsed >= active.Spec.Duration)
{
completed.Add(active.ActorId);
}
}
for (int i = 0; i < completed.Count; i++)
{
StopBehavior(completed[i], BehaviorStopReason.Completed);
}
}
public IReadOnlyList<BehaviorEvent> DrainEvents()
{
var drained = _events.ToArray();
_events.Clear();
return drained;
}
public bool HasActiveBehavior(int actorId)
{
return _activeBehaviors.ContainsKey(actorId);
}
public int ActiveBuffCount(int actorId)
{
return 0;
}
public bool ApplyBuff(int targetActorId, string catalogType, string buffId, int sourceActorId)
{
return false;
}
private void FireTimeline(ActiveBehavior active, float previousElapsed, float currentElapsed)
{
List<TimelineEntrySpec> timeline = active.Spec.Timeline;
for (int i = 0; i < timeline.Count; i++)
{
if (active.FiredTimeline.Contains(i)) continue;
TimelineEntrySpec entry = timeline[i];
if (entry.Trigger != TimelineTriggerKind.Time) continue;
if (entry.Time > currentElapsed || entry.Time <= previousElapsed) continue;
active.FiredTimeline.Add(i);
FireTimelineEntry(active, entry);
}
}
private void FireTimelineEntry(ActiveBehavior active, TimelineEntrySpec entry)
{
if (entry.Kind == TimelineEntryKind.Animation)
{
if (_options.Mode == BehaviorRunMode.ClientPresentation || _options.Mode == BehaviorRunMode.ClientPredict)
{
_events.Add(new BehaviorEvent
{
Kind = BehaviorEventKind.TimelineAnimation,
ActorId = active.ActorId,
BehaviorId = active.Spec.Id,
Animation = entry.Animation
});
}
return;
}
if (entry.Kind == TimelineEntryKind.Effect)
{
if (_options.Mode == BehaviorRunMode.ClientPresentation || _options.Mode == BehaviorRunMode.ClientPredict)
{
_events.Add(new BehaviorEvent
{
Kind = BehaviorEventKind.TimelineEffect,
ActorId = active.ActorId,
BehaviorId = active.Spec.Id,
Prefab = entry.Prefab
});
}
return;
}
if (entry.Kind == TimelineEntryKind.LogicEffect && _options.Mode == BehaviorRunMode.ServerLogic)
{
LogicEffectSpec effect = FindLogicEffect(active.Spec.LogicEffects, entry.EffectId);
if (effect != null) ApplyLogicEffect(active.ActorId, active.Catalog, effect, active.Input, 0);
}
}
private void ApplyLogicEffect(int sourceActorId, BehaviorConfigCatalog catalog, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
{
foreach (int targetActorId in SelectTargets(sourceActorId, effect, input, hitActorId))
{
_events.Add(new BehaviorEvent
{
Kind = BehaviorEventKind.LogicEffectApplied,
ActorId = sourceActorId,
SourceActorId = sourceActorId,
TargetActorId = targetActorId,
LogicEffectId = effect.Id
});
for (int i = 0; i < effect.Effects.Count; i++)
{
LogicEffectOpSpec op = effect.Effects[i];
if (op.Type == LogicEffectOpKind.Attribute)
{
_events.Add(new BehaviorEvent
{
Kind = BehaviorEventKind.AttributeEffectRequested,
ActorId = sourceActorId,
SourceActorId = sourceActorId,
TargetActorId = targetActorId,
LogicEffectId = effect.Id,
Attribute = op.Attribute,
Value = op.Value
});
}
}
}
}
private IEnumerable<int> SelectTargets(int sourceActorId, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
{
if (!_actors.TryGetValue(sourceActorId, out BehaviorActorState source))
yield break;
if (effect.Target == LogicTargetKind.HitActor)
{
int targetId = hitActorId != 0 ? hitActorId : input.TargetActorId;
if (targetId != 0 && _actors.ContainsKey(targetId)) yield return targetId;
yield break;
}
foreach (BehaviorActorState actor in _actors.Values)
{
if (!actor.Alive) continue;
if (!PassesTargetFilter(source, actor, effect.Target)) continue;
if (IsInsideShape(source, actor, effect))
yield return actor.ActorId;
}
}
private static bool PassesTargetFilter(BehaviorActorState source, BehaviorActorState target, LogicTargetKind filter)
{
if (filter == LogicTargetKind.Self || filter == LogicTargetKind.Owner)
return target.ActorId == source.ActorId;
if (filter == LogicTargetKind.Ally)
return target.ActorId != source.ActorId && target.TeamId == source.TeamId;
if (filter == LogicTargetKind.Enemy)
return target.TeamId != source.TeamId;
return true;
}
private static bool IsInsideShape(BehaviorActorState source, BehaviorActorState target, LogicEffectSpec effect)
{
float distance = BehaviorVector3.DistanceXZ(source.Position, target.Position);
if (effect.Shape == LogicShapeKind.Sphere)
return distance <= effect.Radius + target.Radius;
if (effect.Shape == LogicShapeKind.Ring)
return distance >= effect.MinRadius && distance <= effect.Radius + target.Radius;
if (effect.Shape == LogicShapeKind.Sector)
{
if (distance > effect.Radius + target.Radius) return false;
BehaviorVector3 toTarget = (target.Position - source.Position).NormalizedXZ();
BehaviorVector3 forward = source.Forward.NormalizedXZ();
float dot = Clamp(BehaviorVector3.DotXZ(forward, toTarget), -1f, 1f);
float angle = (float)(Math.Acos(dot) * 180.0 / Math.PI);
return angle <= effect.Angle * 0.5f;
}
if (effect.Shape == LogicShapeKind.Line)
{
float width = effect.Width > 0f ? effect.Width : target.Radius;
float dist = DistancePointToSegmentXZ(target.Position, source.Position, source.Position + source.Forward.NormalizedXZ() * effect.Radius);
return dist <= width + target.Radius;
}
return target.ActorId == source.ActorId;
}
private static float DistancePointToSegmentXZ(BehaviorVector3 point, BehaviorVector3 start, BehaviorVector3 end)
{
BehaviorVector3 ab = end - start;
BehaviorVector3 ap = point - start;
float abLenSq = ab.X * ab.X + ab.Z * ab.Z;
if (abLenSq <= 0.00001f) return BehaviorVector3.DistanceXZ(point, start);
float t = Clamp((ap.X * ab.X + ap.Z * ab.Z) / abLenSq, 0f, 1f);
BehaviorVector3 nearest = start + ab * t;
return BehaviorVector3.DistanceXZ(point, nearest);
}
private static float Clamp(float value, float min, float max)
{
if (value < min) return min;
return value > max ? max : value;
}
private static LogicEffectSpec FindLogicEffect(List<LogicEffectSpec> effects, string effectId)
{
for (int i = 0; i < effects.Count; i++)
if (effects[i].Id == effectId) return effects[i];
return null;
}
private sealed class ActiveBehavior
{
public int ActorId;
public BehaviorConfigCatalog Catalog;
public BehaviorSpec Spec;
public BehaviorInput Input;
public float Elapsed;
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
}
}
}