diff --git a/Barotrauma/BarotraumaShared/SharedCode.projitems b/Barotrauma/BarotraumaShared/SharedCode.projitems index f1aa81dab..f15f9dbd1 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.projitems +++ b/Barotrauma/BarotraumaShared/SharedCode.projitems @@ -45,6 +45,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index dca3baa58..b8f58acd0 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -122,6 +122,7 @@ namespace Barotrauma private readonly float memoryFadeTime = 0.5f; public LatchOntoAI LatchOntoAI { get; private set; } + public SwarmBehavior SwarmBehavior { get; private set; } public bool AttackHumans { @@ -215,6 +216,10 @@ namespace Barotrauma case "latchonto": LatchOntoAI = new LatchOntoAI(subElement, this); break; + case "swarm": + case "swarmbehavior": + SwarmBehavior = new SwarmBehavior(subElement, this); + break; case "targetpriority": targetingPriorities.Add(subElement.GetAttributeString("tag", "").ToLowerInvariant(), new TargetingPriority(subElement)); break; @@ -364,12 +369,8 @@ namespace Barotrauma default: throw new NotImplementedException(); } - - // Just some debug code that makes the characters to follow the mouse cursor - //run = true; - //Vector2 mousePos = ConvertUnits.ToSimUnits(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); - //steeringManager.SteeringSeek(mousePos, Character.AnimController.GetCurrentSpeed(run)); - + + SwarmBehavior?.Update(deltaTime); steeringManager.Update(Character.AnimController.GetCurrentSpeed(run)); } @@ -790,6 +791,7 @@ namespace Barotrauma { UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance); } + return false; } private bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs new file mode 100644 index 000000000..1a68cd2d8 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs @@ -0,0 +1,95 @@ +using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + class SwarmBehavior + { + private float minDistFromClosest; + private float maxDistFromCenter; + private float cohesion; + + private List members = new List(); + + private AIController ai; + + public SwarmBehavior(XElement element, AIController ai) + { + this.ai = ai; + minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); + maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); + cohesion = element.GetAttributeFloat("cohesion", 0.1f); + } + + public static void CreateSwarm(IEnumerable swarm) + { + foreach (AICharacter character in swarm) + { + if (character.AIController is EnemyAIController enemyAI && enemyAI.SwarmBehavior != null) + { + enemyAI.SwarmBehavior.members = swarm.ToList(); + } + } + } + + public void Update(float deltaTime) + { + members.RemoveAll(m => m.IsDead || m.Removed); + if (members.Count < 2) { return; } + + //calculate the "center of mass" of the swarm and the distance to the closest character in the swarm + float closestDistSqr = float.MaxValue; + Vector2 center = Vector2.Zero; + AICharacter closest = null; + foreach (AICharacter member in members) + { + center += member.SimPosition; + if (member == ai.Character) { continue; } + float distSqr = Vector2.DistanceSquared(member.SimPosition, ai.Character.SimPosition); + if (distSqr < closestDistSqr) + { + closestDistSqr = distSqr; + closest = member; + } + } + center /= members.Count; + + if (closest == null) { return; } + + //steer away from the closest if too close + float closestDist = (float)Math.Sqrt(closestDistSqr); + if (closestDist < minDistFromClosest) + { + Vector2 diff = closest.SimPosition - ai.SimPosition; + if (diff.LengthSquared() < 0.0001f) + { + diff = Vector2.UnitX; + } + ai.SteeringManager.SteeringManual(deltaTime, -diff); + } + //steer closer to the center of mass if too far + else if (Vector2.DistanceSquared(center, ai.SimPosition) > maxDistFromCenter * maxDistFromCenter) + { + float distFromCenter = Vector2.Distance(center, ai.SimPosition); + ai.SteeringManager.SteeringSeek(center, distFromCenter - maxDistFromCenter); + } + + //keep the characters moving in roughly the same direction + if (cohesion > 0.0f) + { + Vector2 avgVel = Vector2.Zero; + foreach (AICharacter member in members) + { + avgVel += member.AnimController.TargetMovement; + } + avgVel /= members.Count; + ai.SteeringManager.SteeringManual(deltaTime, avgVel * cohesion); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/Source/Events/MonsterEvent.cs index c5db6fa09..6c65cee87 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/MonsterEvent.cs @@ -227,17 +227,17 @@ namespace Barotrauma monsters = new List(); float offsetAmount = spawnPosType == Level.PositionType.MainPath ? 1000 : 100; for (int i = 0; i < amount; i++) - { + { CoroutineManager.InvokeAfter(() => { - bool isClient = false; -#if CLIENT - isClient = GameMain.Client != null; -#endif + bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; monsters.Add(Character.Create(characterFile, spawnPos + Rand.Vector(offsetAmount, Rand.RandSync.Server), i.ToString(), null, isClient, true, true)); if (monsters.Count == amount) { spawnReady = true; + //this will do nothing if the monsters have no swarm behavior defined, + //otherwise it'll make the spawned characters act as a swarm + SwarmBehavior.CreateSwarm(monsters.Cast()); } }, Rand.Range(0f, amount / 2, Rand.RandSync.Server)); }