feat: add actor behavior projectiles
This commit is contained in:
@@ -9,14 +9,16 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
private readonly Dictionary<string, BehaviorConfigCatalog> _catalogs = new Dictionary<string, BehaviorConfigCatalog>(StringComparer.Ordinal);
|
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, BehaviorActorState> _actors = new Dictionary<int, BehaviorActorState>();
|
||||||
private readonly Dictionary<int, ActiveBehavior> _activeBehaviors = new Dictionary<int, ActiveBehavior>();
|
private readonly Dictionary<int, ActiveBehavior> _activeBehaviors = new Dictionary<int, ActiveBehavior>();
|
||||||
|
private readonly List<ActiveProjectile> _projectiles = new List<ActiveProjectile>();
|
||||||
private readonly List<BehaviorEvent> _events = new List<BehaviorEvent>();
|
private readonly List<BehaviorEvent> _events = new List<BehaviorEvent>();
|
||||||
|
private int _nextProjectileId = 1;
|
||||||
|
|
||||||
public BehaviorWorld(BehaviorWorldOptions options)
|
public BehaviorWorld(BehaviorWorldOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ActiveProjectileCount => 0;
|
public int ActiveProjectileCount => _projectiles.Count;
|
||||||
|
|
||||||
public void RegisterCatalog(BehaviorConfigCatalog catalog)
|
public void RegisterCatalog(BehaviorConfigCatalog catalog)
|
||||||
{
|
{
|
||||||
@@ -81,6 +83,8 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
public void Tick(float deltaTime)
|
public void Tick(float deltaTime)
|
||||||
{
|
{
|
||||||
if (deltaTime <= 0f) return;
|
if (deltaTime <= 0f) return;
|
||||||
|
TickProjectiles(deltaTime);
|
||||||
|
|
||||||
var completed = new List<int>();
|
var completed = new List<int>();
|
||||||
foreach (ActiveBehavior active in new List<ActiveBehavior>(_activeBehaviors.Values))
|
foreach (ActiveBehavior active in new List<ActiveBehavior>(_activeBehaviors.Values))
|
||||||
{
|
{
|
||||||
@@ -171,6 +175,12 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
{
|
{
|
||||||
LogicEffectSpec effect = FindLogicEffect(active.Spec.LogicEffects, entry.EffectId);
|
LogicEffectSpec effect = FindLogicEffect(active.Spec.LogicEffects, entry.EffectId);
|
||||||
if (effect != null) ApplyLogicEffect(active.ActorId, active.Catalog, effect, active.Input, 0);
|
if (effect != null) ApplyLogicEffect(active.ActorId, active.Catalog, effect, active.Input, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Kind == TimelineEntryKind.Projectile)
|
||||||
|
{
|
||||||
|
SpawnProjectile(active.ActorId, active.Catalog, entry.FlyObjectId, active.Input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,10 +213,150 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
Value = op.Value
|
Value = op.Value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (op.Type == LogicEffectOpKind.Projectile)
|
||||||
|
{
|
||||||
|
SpawnProjectile(sourceActorId, catalog, op.FlyObjectId, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SpawnProjectile(int sourceActorId, BehaviorConfigCatalog catalog, string flyObjectId, BehaviorInput input)
|
||||||
|
{
|
||||||
|
if (!_actors.TryGetValue(sourceActorId, out BehaviorActorState source)) return;
|
||||||
|
if (!catalog.FlyObjects.TryGetValue(flyObjectId, out FlyObjectSpec spec)) return;
|
||||||
|
|
||||||
|
int targetActorId = input.Kind == BehaviorInputKind.TargetActor ? input.TargetActorId : 0;
|
||||||
|
BehaviorVector3 direction = ResolveProjectileDirection(source, input, targetActorId);
|
||||||
|
var projectile = new ActiveProjectile
|
||||||
|
{
|
||||||
|
RuntimeId = _nextProjectileId++,
|
||||||
|
SourceActorId = sourceActorId,
|
||||||
|
Catalog = catalog,
|
||||||
|
Spec = spec,
|
||||||
|
Position = source.Position,
|
||||||
|
Direction = direction,
|
||||||
|
TargetActorId = targetActorId,
|
||||||
|
Age = 0f,
|
||||||
|
VerticalVelocity = spec.Movement.Speed * 0.5f
|
||||||
|
};
|
||||||
|
_projectiles.Add(projectile);
|
||||||
|
_events.Add(new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.ProjectileSpawned,
|
||||||
|
ActorId = sourceActorId,
|
||||||
|
SourceActorId = sourceActorId,
|
||||||
|
FlyObjectId = spec.Id,
|
||||||
|
Position = projectile.Position
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private BehaviorVector3 ResolveProjectileDirection(BehaviorActorState source, BehaviorInput input, int targetActorId)
|
||||||
|
{
|
||||||
|
if (input.Kind == BehaviorInputKind.Direction)
|
||||||
|
return input.Direction.NormalizedXZ();
|
||||||
|
if (input.Kind == BehaviorInputKind.TargetPosition)
|
||||||
|
return (input.TargetPosition - source.Position).NormalizedXZ();
|
||||||
|
if (targetActorId != 0 && _actors.TryGetValue(targetActorId, out BehaviorActorState target))
|
||||||
|
return (target.Position - source.Position).NormalizedXZ();
|
||||||
|
return source.Forward.NormalizedXZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickProjectiles(float deltaTime)
|
||||||
|
{
|
||||||
|
var expired = new HashSet<ActiveProjectile>();
|
||||||
|
foreach (ActiveProjectile projectile in _projectiles)
|
||||||
|
{
|
||||||
|
projectile.Age += deltaTime;
|
||||||
|
MoveProjectile(projectile, deltaTime);
|
||||||
|
_events.Add(new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.ProjectileMoved,
|
||||||
|
ActorId = projectile.SourceActorId,
|
||||||
|
SourceActorId = projectile.SourceActorId,
|
||||||
|
FlyObjectId = projectile.Spec.Id,
|
||||||
|
Position = projectile.Position
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_options.Mode == BehaviorRunMode.ServerLogic)
|
||||||
|
CheckProjectileCollision(projectile, expired);
|
||||||
|
|
||||||
|
if (projectile.Age >= projectile.Spec.Duration)
|
||||||
|
expired.Add(projectile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expired.Count == 0) return;
|
||||||
|
_projectiles.RemoveAll(p => expired.Contains(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveProjectile(ActiveProjectile projectile, float deltaTime)
|
||||||
|
{
|
||||||
|
FlyMovementSpec movement = projectile.Spec.Movement;
|
||||||
|
BehaviorVector3 direction = projectile.Direction.NormalizedXZ();
|
||||||
|
if (movement.Kind == FlyMovementKind.Homing
|
||||||
|
&& projectile.TargetActorId != 0
|
||||||
|
&& _actors.TryGetValue(projectile.TargetActorId, out BehaviorActorState target))
|
||||||
|
{
|
||||||
|
direction = (target.Position - projectile.Position).NormalizedXZ();
|
||||||
|
projectile.Direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectile.Position += direction * (movement.Speed * deltaTime);
|
||||||
|
if (movement.Kind == FlyMovementKind.Parabola)
|
||||||
|
{
|
||||||
|
projectile.Position = new BehaviorVector3(
|
||||||
|
projectile.Position.X,
|
||||||
|
projectile.Position.Y + projectile.VerticalVelocity * deltaTime - 0.5f * movement.Gravity * deltaTime * deltaTime,
|
||||||
|
projectile.Position.Z);
|
||||||
|
projectile.VerticalVelocity -= movement.Gravity * deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckProjectileCollision(ActiveProjectile projectile, HashSet<ActiveProjectile> expired)
|
||||||
|
{
|
||||||
|
if (!_actors.TryGetValue(projectile.SourceActorId, out BehaviorActorState source)) return;
|
||||||
|
foreach (BehaviorActorState actor in _actors.Values)
|
||||||
|
{
|
||||||
|
if (!actor.Alive) continue;
|
||||||
|
if (actor.ActorId == projectile.SourceActorId) continue;
|
||||||
|
if (actor.TeamId == source.TeamId) continue;
|
||||||
|
float distance = BehaviorVector3.DistanceXZ(projectile.Position, actor.Position);
|
||||||
|
if (distance > projectile.Spec.Collision.Radius + actor.Radius) continue;
|
||||||
|
|
||||||
|
projectile.HitCount++;
|
||||||
|
_events.Add(new BehaviorEvent
|
||||||
|
{
|
||||||
|
Kind = BehaviorEventKind.ProjectileHit,
|
||||||
|
ActorId = projectile.SourceActorId,
|
||||||
|
SourceActorId = projectile.SourceActorId,
|
||||||
|
TargetActorId = actor.ActorId,
|
||||||
|
FlyObjectId = projectile.Spec.Id,
|
||||||
|
Position = projectile.Position
|
||||||
|
});
|
||||||
|
FireProjectileCollisionLogic(projectile, actor.ActorId);
|
||||||
|
|
||||||
|
if (projectile.Spec.Collision.HitLimit > 0 && projectile.HitCount >= projectile.Spec.Collision.HitLimit)
|
||||||
|
{
|
||||||
|
expired.Add(projectile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FireProjectileCollisionLogic(ActiveProjectile projectile, int hitActorId)
|
||||||
|
{
|
||||||
|
List<TimelineEntrySpec> timeline = projectile.Spec.Timeline;
|
||||||
|
for (int i = 0; i < timeline.Count; i++)
|
||||||
|
{
|
||||||
|
TimelineEntrySpec entry = timeline[i];
|
||||||
|
if (entry.Kind != TimelineEntryKind.LogicEffect || entry.Trigger != TimelineTriggerKind.OnCollision)
|
||||||
|
continue;
|
||||||
|
LogicEffectSpec effect = FindLogicEffect(projectile.Spec.LogicEffects, entry.EffectId);
|
||||||
|
if (effect != null)
|
||||||
|
ApplyLogicEffect(projectile.SourceActorId, projectile.Catalog, effect, BehaviorInput.FromTargetActor(hitActorId), hitActorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<int> SelectTargets(int sourceActorId, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
|
private IEnumerable<int> SelectTargets(int sourceActorId, LogicEffectSpec effect, BehaviorInput input, int hitActorId)
|
||||||
{
|
{
|
||||||
if (!_actors.TryGetValue(sourceActorId, out BehaviorActorState source))
|
if (!_actors.TryGetValue(sourceActorId, out BehaviorActorState source))
|
||||||
@@ -297,5 +447,19 @@ namespace XWorld.Framework.ActorBehavior
|
|||||||
public float Elapsed;
|
public float Elapsed;
|
||||||
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
|
public readonly HashSet<int> FiredTimeline = new HashSet<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ActiveProjectile
|
||||||
|
{
|
||||||
|
public int RuntimeId;
|
||||||
|
public int SourceActorId;
|
||||||
|
public BehaviorConfigCatalog Catalog;
|
||||||
|
public FlyObjectSpec Spec;
|
||||||
|
public BehaviorVector3 Position;
|
||||||
|
public BehaviorVector3 Direction;
|
||||||
|
public int TargetActorId;
|
||||||
|
public float Age;
|
||||||
|
public float VerticalVelocity;
|
||||||
|
public int HitCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,45 @@ namespace XWorld.Framework.Tests.ActorBehavior
|
|||||||
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineEffect);
|
Assert.DoesNotContain(events, e => e.Kind == BehaviorEventKind.TimelineEffect);
|
||||||
Assert.Contains(events, e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2 && e.Attribute == "hp");
|
Assert.Contains(events, e => e.Kind == BehaviorEventKind.AttributeEffectRequested && e.TargetActorId == 2 && e.Attribute == "hp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Projectile_SpawnsMovesHitsAndExpiresAtHitLimit()
|
||||||
|
{
|
||||||
|
BehaviorWorld world = TestWorld.CreateServerWorld();
|
||||||
|
|
||||||
|
world.TryStartBehavior(1, "slash", BehaviorInput.FromDirection(1, 0));
|
||||||
|
world.Tick(0.33f);
|
||||||
|
Assert.True(world.ActiveProjectileCount > 0);
|
||||||
|
|
||||||
|
world.Tick(0.1f);
|
||||||
|
var events = world.DrainEvents();
|
||||||
|
|
||||||
|
Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileSpawned);
|
||||||
|
Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileMoved);
|
||||||
|
Assert.Contains(events, e => e.Kind == BehaviorEventKind.ProjectileHit && e.TargetActorId == 2);
|
||||||
|
Assert.Equal(0, world.ActiveProjectileCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Line")]
|
||||||
|
[InlineData("Parabola")]
|
||||||
|
[InlineData("Homing")]
|
||||||
|
public void ProjectileMovementModes_UpdatePosition(string movementKind)
|
||||||
|
{
|
||||||
|
string fly = TestBehaviorJson.FlyObject.Replace(@"""kind"": ""Line""", @"""kind"": """ + movementKind + @"""");
|
||||||
|
BehaviorConfigCatalog catalog = BehaviorConfigCatalog.LoadFromJson("fighter", TestBehaviorJson.Skill, fly, TestBehaviorJson.Buff);
|
||||||
|
var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = BehaviorRunMode.ClientPresentation });
|
||||||
|
world.RegisterCatalog(catalog);
|
||||||
|
world.AddActor(new BehaviorActorState { ActorId = 1, CatalogType = "fighter", TeamId = 1, Position = new BehaviorVector3(0, 0, 0), Forward = new BehaviorVector3(1, 0, 0), Radius = 0.3f, Alive = true });
|
||||||
|
world.AddActor(new BehaviorActorState { ActorId = 2, CatalogType = "fighter", TeamId = 2, Position = new BehaviorVector3(4, 0, 0), Forward = new BehaviorVector3(-1, 0, 0), Radius = 0.3f, Alive = true });
|
||||||
|
|
||||||
|
world.TryStartBehavior(1, "slash", BehaviorInput.FromTargetActor(2));
|
||||||
|
world.Tick(0.33f);
|
||||||
|
world.DrainEvents();
|
||||||
|
world.Tick(0.1f);
|
||||||
|
|
||||||
|
Assert.Contains(world.DrainEvents(), e => e.Kind == BehaviorEventKind.ProjectileMoved && e.Position.X > 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class TestWorld
|
internal static class TestWorld
|
||||||
|
|||||||
Reference in New Issue
Block a user