Files
OpenRA/OpenRA.Mods.Common/Traits/World/ResourceClaimLayer.cs
RoosterDragon 058b725ca9 Reduce lag spikes from HarvesterBotModule.
The AI uses HarvesterBotModule to check for idle harvesters, and give them harvest orders. By default it scans every 50 ticks (2 seconds at normal speed), and for any idle harvesters locates an ore patch and issues a harvest order. This FindNextResource to scan for a suitable ore path is quite expensive. If the AI has to scan for ore patches for several harvesters, then this can produce a noticeable lag spike. Additionally, when there are no available ore patches, the scan will just keep repeating since the harvesters will always be idle - thus the lag spikes repeat every 50 ticks.

To reduce the impact, there already exists a randomization on the first scan interval so that multiple different AIs scan on different ticks. By ensuring the AI players scan at different times, we avoid a huge lag spike where they all operate on the same tick.

To reduce the impact even more, we make four additional changes:
- Scans continue to be done every 50 ticks to detect harvesters. But we spread out the searches for ore patches over multiple later ticks. We'll only perform one ore patch search per tick. This means instead of ordering e.g. 30 harvesters on a single tick and creating a spike, we order one on each tick over the next 30 ticks instead. This spreads out the performance impact.
- When a harvester fails to locate any suitable ore patch, we put it on a longer cooldown, by default 5x the regular cooldown. We don't need to scan as often for these harvesters, since it'll take time for new resources to appear.
- We change the path search in FindNextResource from FindPathToTargetCellByPredicate to FindPathToTargetCells. The format in an undirected path search that must flood fill from the start location. If ore is on the other side of the map, this entails searching the whole map which is very expensive. By maintaining a lookup of resource types per cell, we can instead give the target locations directly to the path search. This lookup requires a small overhead to maintain, but allows for a far more efficient path search to be carried out. The search can be directed towards the target locations, and the hierarchical path finder can be employed resulting in a path search that explores far fewer cells. A few tweaks are made to ResourceClaimLayer to avoid it creating empty list entries when this can be avoided.
- We adjust how the enemy avoidance cost is done. Previously, this search used world.FindActorsInCircle to check for nearby enemies, but this check was done for every cell that was searched, and is itself quite expensive. Now, we create a series of "bins" and cache the additional cost for that bin. This is a less fine grained approach but is sufficient for our intended goal of "avoid resource patches with too many enemies nearby". The customCost function is now less expensive so we can reuse the avoidance cost stored for each bin, rather than calculating fresh for every cell.
2024-08-07 19:17:00 +03:00

75 lines
2.3 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.World)]
[Desc("Allows harvesters to coordinate their operations. Attach this to the world actor.")]
public sealed class ResourceClaimLayerInfo : TraitInfo<ResourceClaimLayer> { }
public sealed class ResourceClaimLayer
{
readonly Dictionary<CPos, List<Actor>> claimByCell = new(32);
readonly Dictionary<Actor, CPos> claimByActor = new(32);
/// <summary>
/// Attempt to reserve the resource in a cell for the given actor.
/// </summary>
public bool TryClaimCell(Actor claimer, CPos cell)
{
if (claimByCell.TryGetValue(cell, out var claimers))
{
// Clean up any stale claims
claimers.RemoveAll(a => a.IsDead);
// Prevent harvesters from the player or their allies fighting over the same cell
if (claimers.Any(c => c != claimer && claimer.Owner.IsAlliedWith(c.Owner)))
return false;
}
// Remove the actor's last claim, if it has one
if (claimByActor.TryGetValue(claimer, out var lastClaim) &&
claimByCell.TryGetValue(lastClaim, out var lastClaimers))
lastClaimers.Remove(claimer);
if (claimers == null)
claimByCell.Add(cell, claimers = new List<Actor>());
claimByActor[claimer] = cell;
return true;
}
/// <summary>
/// Returns false if the cell is already reserved by an allied actor.
/// </summary>
public bool CanClaimCell(Actor claimer, CPos cell)
{
return !claimByCell.TryGetValue(cell, out var claimers) ||
!claimers.Any(c => c != claimer && !c.IsDead && claimer.Owner.IsAlliedWith(c.Owner));
}
/// <summary>
/// Release the last resource claim made by this actor.
/// </summary>
public void RemoveClaim(Actor claimer)
{
if (claimByActor.TryGetValue(claimer, out var lastClaim) &&
claimByCell.TryGetValue(lastClaim, out var lastClaimers))
lastClaimers.Remove(claimer);
claimByActor.Remove(claimer);
}
}
}