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));
}