feat: add actor behavior runtime
This commit is contained in:
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user