From d05b07a5b025f24bf209ee47030f631df228a95c Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Sat, 6 Jul 2024 12:10:55 +0100 Subject: [PATCH] AI uses better rally point placement - AI places rally points at pathable locations. A pathable location isn't strictly required, but avoids the AI setting rally points at seemingly dumb locations. This is an addtional check on top of the existing buildability check. - AI now evaluates rally points every AssignRallyPointsInterval (default 100 ticks). Invalid rally points aren't that harmful, so no need to check them every tick. Additionally we do a rolling update so rally points for multiple locations are spread across multiple ticks to reduce any potential lag spikes. --- .../Traits/BotModules/BaseBuilderBotModule.cs | 94 +++++++++++++++---- OpenRA.Mods.Common/Traits/World/PathFinder.cs | 19 ++++ OpenRA.Mods.Common/TraitsInterfaces.cs | 14 +++ 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index e785faabd4..56c8bd0c0c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.Collections.Generic; using System.Linq; using OpenRA.Traits; @@ -135,6 +136,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Only queue construction of a new structure when above this requirement.")] public readonly int ProductionMinCashRequirement = 500; + [Desc("Delay (in ticks) between reassigning rally points.")] + public readonly int AssignRallyPointsInterval = 100; + public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); } } @@ -159,9 +163,13 @@ namespace OpenRA.Mods.Common.Traits PowerManager playerPower; PlayerResources playerResources; IResourceLayer resourceLayer; + IPathFinder pathFinder; IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; + readonly Stack> rallyPoints = new(); + int assignRallyPointsTicks; + readonly BaseBuilderQueueManager[] builders; int currentBuilderIndex = 0; @@ -187,6 +195,7 @@ namespace OpenRA.Mods.Common.Traits playerPower = self.Owner.PlayerActor.TraitOrDefault(); playerResources = self.Owner.PlayerActor.Trait(); resourceLayer = self.World.WorldActor.TraitOrDefault(); + pathFinder = self.World.WorldActor.TraitOrDefault(); positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing().ToArray(); var i = 0; @@ -198,6 +207,12 @@ namespace OpenRA.Mods.Common.Traits builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer); } + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + assignRallyPointsTicks = world.LocalRandom.Next(0, Info.AssignRallyPointsInterval); + } + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) { initialBaseCenter = newLocation; @@ -212,8 +227,23 @@ namespace OpenRA.Mods.Common.Traits void IBotTick.BotTick(IBot bot) { - // TODO: this causes pathfinding lag when AI's gets blocked in - SetRallyPointsForNewProductionBuildings(bot); + if (--assignRallyPointsTicks <= 0) + { + assignRallyPointsTicks = Math.Max(2, Info.AssignRallyPointsInterval); + foreach (var rp in world.ActorsWithTrait().Where(rp => rp.Actor.Owner == player)) + rallyPoints.Push(rp); + } + else + { + // PERF: Spread out rally point assignments updates across multiple ticks. + var updateCount = Exts.IntegerDivisionRoundingAwayFromZero(rallyPoints.Count, assignRallyPointsTicks); + for (var i = 0; i < updateCount; i++) + { + var rp = rallyPoints.Pop(); + if (rp.Actor.Owner == player) + SetRallyPoint(bot, rp); + } + } BuildingsBeingProduced.Clear(); @@ -275,28 +305,31 @@ namespace OpenRA.Mods.Common.Traits n.UpdatedDefenseCenter(e.Attacker.Location); } - void SetRallyPointsForNewProductionBuildings(IBot bot) + void SetRallyPoint(IBot bot, TraitPair rp) { - foreach (var rp in world.ActorsWithTrait()) - { - if (rp.Actor.Owner != player) - continue; + var needsRallyPoint = rp.Trait.Path.Count == 0; - if (rp.Trait.Path.Count == 0 || !IsRallyPointValid(rp.Trait.Path[0], rp.Actor.Info.TraitInfoOrDefault())) + if (!needsRallyPoint) + { + var locomotors = LocomotorsForProducibles(rp.Actor); + needsRallyPoint = !IsRallyPointValid(rp.Actor.Location, rp.Trait.Path[0], locomotors, rp.Actor.Info.TraitInfoOrDefault()); + } + + if (needsRallyPoint) + { + bot.QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(world, ChooseRallyLocationNear(rp.Actor)), false) { - bot.QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(world, ChooseRallyLocationNear(rp.Actor)), false) - { - SuppressVisualFeedback = true - }); - } + SuppressVisualFeedback = true + }); } } // Won't work for shipyards... CPos ChooseRallyLocationNear(Actor producer) { + var locomotors = LocomotorsForProducibles(producer); var possibleRallyPoints = world.Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius) - .Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault())) + .Where(c => IsRallyPointValid(producer.Location, c, locomotors, producer.Info.TraitInfoOrDefault())) .ToList(); if (possibleRallyPoints.Count == 0) @@ -308,9 +341,38 @@ namespace OpenRA.Mods.Common.Traits return possibleRallyPoints.Random(world.LocalRandom); } - bool IsRallyPointValid(CPos x, BuildingInfo info) + Locomotor[] LocomotorsForProducibles(Actor producer) { - return info != null && world.IsCellBuildable(x, null, info); + var buildingInfo = producer.Info.TraitInfoOrDefault(); + var productionInfo = producer.Info.TraitInfoOrDefault(); + var locomotors = Array.Empty(); + + if (productionInfo != null && productionInfo.Produces.Length > 0) + { + var productionQueues = producer.Owner.PlayerActor.TraitsImplementing() + .Where(pq => productionInfo.Produces.Contains(pq.Info.Type)); + var producibles = productionQueues.SelectMany(pq => pq.BuildableItems()); + var locomotorNames = producibles + .Select(p => p.TraitInfoOrDefault()) + .Where(mi => mi != null) + .Select(mi => mi.Locomotor) + .ToHashSet(); + locomotors = world.WorldActor.TraitsImplementing() + .Where(l => locomotorNames.Contains(l.Info.Name)) + .ToArray(); + } + + return locomotors; + } + + bool IsRallyPointValid(CPos producerLocation, CPos rallyPointLocation, Locomotor[] locomotors, BuildingInfo buildingInfo) + { + return + (pathFinder == null || + locomotors.All(l => pathFinder.PathMightExistForLocomotorBlockedByImmovable(l, producerLocation, rallyPointLocation))) + && + (buildingInfo == null || + world.IsCellBuildable(rallyPointLocation, null, buildingInfo)); } // Require at least one refinery, unless we can't build it. diff --git a/OpenRA.Mods.Common/Traits/World/PathFinder.cs b/OpenRA.Mods.Common/Traits/World/PathFinder.cs index ffa4a51866..8b02271720 100644 --- a/OpenRA.Mods.Common/Traits/World/PathFinder.cs +++ b/OpenRA.Mods.Common/Traits/World/PathFinder.cs @@ -258,6 +258,25 @@ namespace OpenRA.Mods.Common.Traits return hierarchicalPathFindersBlockedByNoneByLocomotor[locomotor].PathExists(source, target); } + /// + /// Determines if a path exists between source and target. + /// Terrain and a *subset* of immovable actors are taken into account, + /// i.e. as if a subset of was given. + /// This would apply for any actor using the given . + /// + /// + /// It is allowed for an actor to occupy an inaccessible space and move out of it if another adjacent cell is + /// accessible, but it is not allowed to move into an inaccessible target space. Therefore it is vitally + /// important to not mix up the source and target locations. A path can exist from an inaccessible source space + /// to an accessible target space, but if those parameters as swapped then no path can exist. + /// As only a subset of immovable actors are taken into account, + /// this method can return false positives, indicating a path might exist where none is possible. + /// + public bool PathMightExistForLocomotorBlockedByImmovable(Locomotor locomotor, CPos source, CPos target) + { + return hierarchicalPathFindersBlockedByImmovableByLocomotor[locomotor].PathExists(source, target); + } + static Locomotor GetActorLocomotor(Actor self) { // PERF: This PathFinder trait requires the use of Mobile, so we can be sure that is in use. diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index c1bb12da2e..f85cd7de60 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -943,5 +943,19 @@ namespace OpenRA.Mods.Common.Traits /// Path searches are not guaranteed to by symmetric, /// the source and target locations cannot be swapped. bool PathExistsForLocomotor(Locomotor locomotor, CPos source, CPos target); + + /// + /// Determines if a path exists between source and target. + /// Terrain and immovable actors are taken into account, + /// i.e. as if was given. + /// Implementations are permitted to only account for a subset of actors, for performance. + /// This would apply for any actor using the given . + /// + /// Path searches are not guaranteed to by symmetric, + /// the source and target locations cannot be swapped. + /// If this method returns false, there is guaranteed to be no path. + /// If it returns true, there *might* be a path. + /// + bool PathMightExistForLocomotorBlockedByImmovable(Locomotor locomotor, CPos source, CPos target); } }