From fb55f2824e35aced082875e274b7aa7eb230cb14 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Tue, 19 Jul 2022 19:22:06 +0800 Subject: [PATCH] UnitBuilderBotModule and BaseBuilderBotModule fix on muti-queue performance: 1. Only allow new item being queued when cash above a certain number 2. Only tick one kind of queues at one tick, reduce the pressure on the actived tick 3. 'BaseBuilderBotModule' will check all buildings in producing, avoid queue mutiple same buildings. --- .../Traits/BotModules/BaseBuilderBotModule.cs | 64 ++++++++++++++++--- .../BotModuleLogic/BaseBuilderQueueManager.cs | 25 ++++++-- .../Traits/BotModules/UnitBuilderBotModule.cs | 26 ++++++-- 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index fa137dc4dc..c72e3e343a 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -132,6 +132,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("When should the AI start building specific buildings.")] public readonly Dictionary BuildingDelays = null; + [Desc("Only queue construction of a new structure when above this requirement.")] + public readonly int ProductionMinCashRequirement = 500; + public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); } } @@ -149,6 +152,9 @@ namespace OpenRA.Mods.Common.Traits public CPos DefenseCenter { get; private set; } + // Actor, ActorCount. + public Dictionary BuildingsBeingProduced = new(); + readonly World world; readonly Player player; PowerManager playerPower; @@ -156,13 +162,16 @@ namespace OpenRA.Mods.Common.Traits IResourceLayer resourceLayer; IBotPositionsUpdated[] positionsUpdatedModules; CPos initialBaseCenter; - readonly List builders = new(); + + readonly BaseBuilderQueueManager[] builders; + int currentBuilderIndex = 0; public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info) : base(info) { world = self.World; player = self.Owner; + builders = new BaseBuilderQueueManager[info.BuildingQueues.Count + info.DefenseQueues.Count]; } protected override void Created(Actor self) @@ -171,14 +180,14 @@ namespace OpenRA.Mods.Common.Traits playerResources = self.Owner.PlayerActor.Trait(); resourceLayer = self.World.WorldActor.TraitOrDefault(); positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing().ToArray(); - } - protected override void TraitEnabled(Actor self) - { + var i = 0; + foreach (var building in Info.BuildingQueues) - builders.Add(new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer)); + builders[i++] = new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer); + foreach (var defense in Info.DefenseQueues) - builders.Add(new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer)); + builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer); } void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) @@ -195,10 +204,49 @@ namespace OpenRA.Mods.Common.Traits void IBotTick.BotTick(IBot bot) { + // TODO: this causes pathfinding lag when AI's gets blocked in SetRallyPointsForNewProductionBuildings(bot); - foreach (var b in builders) - b.Tick(bot); + BuildingsBeingProduced.Clear(); + + // PERF: We tick only one type of valid queue at a time + // if AI gets enough cash, it can fill all of its queues with enough ticks + var findQueue = false; + for (int i = 0, builderIndex = currentBuilderIndex; i < builders.Length; i++) + { + if (++builderIndex >= builders.Length) + builderIndex = 0; + + --builders[builderIndex].WaitTicks; + + var queues = AIUtils.FindQueues(player, builders[builderIndex].Category).ToArray(); + if (queues.Length != 0) + { + if (!findQueue) + { + currentBuilderIndex = builderIndex; + findQueue = true; + } + + // Refresh "BuildingsBeingProduced" only when AI can produce + if (playerResources.GetCashAndResources() >= Info.ProductionMinCashRequirement) + { + foreach (var queue in queues) + { + var producing = queue.AllQueued().FirstOrDefault(); + if (producing == null) + continue; + + if (BuildingsBeingProduced.TryGetValue(producing.Item, out var number)) + BuildingsBeingProduced[producing.Item] = number + 1; + else + BuildingsBeingProduced.Add(producing.Item, 1); + } + } + } + } + + builders[currentBuilderIndex].Tick(bot); } void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 4a7354b71c..4285432359 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -18,7 +18,8 @@ namespace OpenRA.Mods.Common.Traits { sealed class BaseBuilderQueueManager { - readonly string category; + public readonly string Category; + public int WaitTicks; readonly BaseBuilderBotModule baseBuilder; readonly World world; @@ -27,7 +28,6 @@ namespace OpenRA.Mods.Common.Traits readonly PlayerResources playerResources; readonly IResourceLayer resourceLayer; - int waitTicks; Actor[] playerBuildings; int failCount; int failRetryTicks; @@ -36,6 +36,8 @@ namespace OpenRA.Mods.Common.Traits int cachedBuildings; int minimumExcessPower; + bool itemQueuedThisTick = false; + WaterCheck waterState = WaterCheck.NotChecked; public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm, @@ -47,7 +49,7 @@ namespace OpenRA.Mods.Common.Traits playerPower = pm; playerResources = pr; resourceLayer = rl; - this.category = category; + Category = category; failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; minimumExcessPower = baseBuilder.Info.MinimumExcessPower; if (baseBuilder.Info.NavalProductionTypes.Count == 0) @@ -94,23 +96,27 @@ namespace OpenRA.Mods.Common.Traits } // Only update once per second or so - if (--waitTicks > 0) + if (WaitTicks > 0) return; playerBuildings = world.ActorsHavingTrait().Where(a => a.Owner == player).ToArray(); var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Length / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue)); minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower); + // PERF: Queue only one actor at a time per category + itemQueuedThisTick = false; var active = false; - foreach (var queue in AIUtils.FindQueues(player, category)) + foreach (var queue in AIUtils.FindQueues(player, Category)) + { if (TickQueue(bot, queue)) active = true; + } // Add a random factor so not every AI produces at the same tick early in the game. // Minimum should not be negative as delays in HackyAI could be zero. var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay); - waitTicks = active ? baseBuilder.Info.StructureProductionActiveDelay + randomFactor + WaitTicks = active ? baseBuilder.Info.StructureProductionActiveDelay + randomFactor : baseBuilder.Info.StructureProductionInactiveDelay + randomFactor; } @@ -121,11 +127,16 @@ namespace OpenRA.Mods.Common.Traits // Waiting to build something if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts) { + // PERF: We shouldn't be queueing new units when we're low on cash + if (playerResources.GetCashAndResources() < baseBuilder.Info.ProductionMinCashRequirement || itemQueuedThisTick) + return false; + var item = ChooseBuildingToBuild(queue); if (item == null) return false; bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1)); + itemQueuedThisTick = true; } else if (currentBuilding != null && currentBuilding.Done) { @@ -335,7 +346,7 @@ namespace OpenRA.Mods.Common.Traits var buildingVariantInfo = actorInfo.TraitInfoOrDefault(); var variants = buildingVariantInfo?.Actors ?? Array.Empty(); - var count = playerBuildings.Count(a => a.Info.Name == name || variants.Contains(a.Info.Name)); + var count = playerBuildings.Count(a => a.Info.Name == name || variants.Contains(a.Info.Name)) + (baseBuilder.BuildingsBeingProduced.TryGetValue(name, out var num) ? num : 0); // Do we want to build this structure? if (count * 100 > frac.Value * playerBuildings.Length) diff --git a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs index b388387d63..9e34d0bc61 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/UnitBuilderBotModule.cs @@ -26,7 +26,7 @@ namespace OpenRA.Mods.Common.Traits public readonly int IdleBaseUnitsMaximum = 12; [Desc("Production queues AI uses for producing units.")] - public readonly HashSet UnitQueues = new() { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; + public readonly string[] UnitQueues = { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" }; [Desc("What units to the AI should build.", "What relative share of the total army must be this type of unit.")] public readonly Dictionary UnitsToBuild = null; @@ -37,6 +37,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("When should the AI start train specific units.")] public readonly Dictionary UnitDelays = null; + [Desc("Only queue construction of a new unit when above this requirement.")] + public readonly int ProductionMinCashRequirement = 500; + public override object Create(ActorInitializer init) { return new UnitBuilderBotModule(init.Self, this); } } @@ -51,6 +54,8 @@ namespace OpenRA.Mods.Common.Traits IBotRequestPauseUnitProduction[] requestPause; int idleUnitCount; + int currentQueueIndex = 0; + PlayerResources playerResources; int ticks; @@ -64,6 +69,7 @@ namespace OpenRA.Mods.Common.Traits protected override void Created(Actor self) { requestPause = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + playerResources = self.Owner.PlayerActor.Trait(); } void IBotNotifyIdleBaseUnits.UpdatedIdleBaseUnits(List idleUnits) @@ -73,7 +79,8 @@ namespace OpenRA.Mods.Common.Traits void IBotTick.BotTick(IBot bot) { - if (requestPause.Any(rp => rp.PauseUnitProduction)) + // PERF: We shouldn't be queueing new units when we're low on cash + if (playerResources.GetCashAndResources() < Info.ProductionMinCashRequirement || requestPause.Any(rp => rp.PauseUnitProduction)) return; ticks++; @@ -87,8 +94,19 @@ namespace OpenRA.Mods.Common.Traits queuedBuildRequests.Remove(buildRequest); } - foreach (var q in Info.UnitQueues) - BuildUnit(bot, q, idleUnitCount < Info.IdleBaseUnitsMaximum); + for (var i = 0; i < Info.UnitQueues.Length; i++) + { + if (++currentQueueIndex >= Info.UnitQueues.Length) + currentQueueIndex = 0; + + if (AIUtils.FindQueues(player, Info.UnitQueues[currentQueueIndex]).Any()) + { + // PERF: We tick only one type of valid queue at a time + // if AI gets enough cash, it can fill all of its queues with enough ticks + BuildUnit(bot, Info.UnitQueues[currentQueueIndex], idleUnitCount < Info.IdleBaseUnitsMaximum); + break; + } + } } }