Significantly improve actor placement and removal speeds

This commit is contained in:
Gustas
2025-09-11 12:58:09 +03:00
committed by Paul Chote
parent 7d4a590240
commit b5b44c048d
5 changed files with 228 additions and 50 deletions

View File

@@ -572,6 +572,11 @@ namespace OpenRA
return i.ToString(NumberFormatInfo.InvariantInfo); return i.ToString(NumberFormatInfo.InvariantInfo);
} }
public static string ToStringInvariant(this uint i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this float f) public static string ToStringInvariant(this float f)
{ {
return f.ToString(NumberFormatInfo.InvariantInfo); return f.ToString(NumberFormatInfo.InvariantInfo);

View File

@@ -9,6 +9,7 @@
*/ */
#endregion #endregion
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -89,6 +90,31 @@ namespace OpenRA
return GetEnumerator(); return GetEnumerator();
} }
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
{
if (cells == null || cells.Count == 0)
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
var minX = int.MaxValue;
var minY = int.MaxValue;
var maxX = int.MinValue;
var maxY = int.MinValue;
foreach (var cell in cells)
{
if (minX > cell.X)
minX = cell.X;
if (maxX < cell.X)
maxX = cell.X;
if (minY > cell.Y)
minY = cell.Y;
if (maxY < cell.Y)
maxY = cell.Y;
}
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
}
public CPos TopLeft { get; } public CPos TopLeft { get; }
public CPos BottomRight { get; } public CPos BottomRight { get; }
} }

View File

@@ -12,8 +12,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits;
using OpenRA.Support;
namespace OpenRA.Mods.Common.EditorBrushes namespace OpenRA.Mods.Common.EditorBrushes
{ {
@@ -130,8 +132,7 @@ namespace OpenRA.Mods.Common.EditorBrushes
if (blitFilters.HasFlag(MapBlitFilters.Actors)) if (blitFilters.HasFlag(MapBlitFilters.Actors))
{ {
// Clear any existing actors in the paste cells. // Clear any existing actors in the paste cells.
var regionActors = editorActorLayer.PreviewsInCellRegion(blitRegion.CellCoords).ToList(); //
// revertBlitSource's mask may be a superset of the commitBlitSource's mask if // revertBlitSource's mask may be a superset of the commitBlitSource's mask if
// - Its a sparse blit; and // - Its a sparse blit; and
// - The revert actors removed by the commit are partially outside of the commit mask. // - The revert actors removed by the commit are partially outside of the commit mask.
@@ -146,9 +147,8 @@ namespace OpenRA.Mods.Common.EditorBrushes
// This means we use the commit mask, not the revert one. // This means we use the commit mask, not the revert one.
var commitBlitVec = blitPosition - commitBlitSource.CellRegion.TopLeft; var commitBlitVec = blitPosition - commitBlitSource.CellRegion.TopLeft;
var mask = GetBlitSourceMask(commitBlitSource, commitBlitVec); var mask = GetBlitSourceMask(commitBlitSource, commitBlitVec);
foreach (var regionActor in regionActors) using (new PerfTimer("RemoveActors", 1))
if (regionActor.Footprint.Any(kv => mask.Contains(kv.Key))) editorActorLayer.RemoveRegion(blitRegion.CellCoords, mask);
editorActorLayer.Remove(regionActor);
} }
foreach (var tileKeyValuePair in source.Tiles) foreach (var tileKeyValuePair in source.Tiles)
@@ -181,12 +181,13 @@ namespace OpenRA.Mods.Common.EditorBrushes
if (isRevert) if (isRevert)
{ {
// For reverts, just place the original actors back exactly how they were. // For reverts, just place the original actors back exactly how they were.
foreach (var actor in source.Actors.Values) using (new PerfTimer("AddActors", 1))
editorActorLayer.Add(actor); editorActorLayer.AddRange(source.Actors.Values.ToArray().AsSpan());
} }
else else
{ {
// Create copies of the original actors, update their locations, and place. // Create copies of the original actors, update their locations, and place.
var copies = new List<ActorReference>(source.Actors.Count);
foreach (var actorKeyValuePair in source.Actors) foreach (var actorKeyValuePair in source.Actors)
{ {
var copy = actorKeyValuePair.Value.Export(); var copy = actorKeyValuePair.Value.Export();
@@ -201,8 +202,11 @@ namespace OpenRA.Mods.Common.EditorBrushes
copy.Add(new LocationInit(actorPosition)); copy.Add(new LocationInit(actorPosition));
} }
editorActorLayer.Add(copy); copies.Add(copy);
} }
using (new PerfTimer("AddActors", 1))
editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies));
} }
} }
} }

View File

@@ -11,11 +11,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Runtime.InteropServices;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.EditorBrushes; using OpenRA.Mods.Common.EditorBrushes;
using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits;
using OpenRA.Support;
using OpenRA.Widgets; using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets namespace OpenRA.Mods.Common.Widgets
@@ -384,8 +385,8 @@ namespace OpenRA.Mods.Common.Widgets
if (blitFilters.HasFlag(MapBlitFilters.Actors)) if (blitFilters.HasFlag(MapBlitFilters.Actors))
{ {
// Clear any existing actors in the paste cells. // Clear any existing actors in the paste cells.
foreach (var regionActor in editorActorLayer.PreviewsInCellRegion(area.CellCoords).ToList()) using (new PerfTimer("RemoveActors", 1))
editorActorLayer.Remove(regionActor); editorActorLayer.RemoveRegion(area.CellCoords);
} }
foreach (var tileKeyValuePair in editorBlitSource.Tiles) foreach (var tileKeyValuePair in editorBlitSource.Tiles)
@@ -432,6 +433,7 @@ namespace OpenRA.Mods.Common.Widgets
if (blitFilters.HasFlag(MapBlitFilters.Actors)) if (blitFilters.HasFlag(MapBlitFilters.Actors))
{ {
// Create copies of the original actors, update their locations, and place. // Create copies of the original actors, update their locations, and place.
var copies = new List<ActorReference>(editorBlitSource.Actors.Count);
foreach (var actorKeyValuePair in editorBlitSource.Actors) foreach (var actorKeyValuePair in editorBlitSource.Actors)
{ {
var copy = actorKeyValuePair.Value.Export(); var copy = actorKeyValuePair.Value.Export();
@@ -445,8 +447,10 @@ namespace OpenRA.Mods.Common.Widgets
copy.Add(new LocationInit(locationInit.Value)); copy.Add(new LocationInit(locationInit.Value));
} }
editorActorLayer.Add(copy); copies.Add(copy);
} }
editorActorLayer.AddRange(CollectionsMarshal.AsSpan(copies));
} }
} }
} }

View File

@@ -11,7 +11,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits.Render; using OpenRA.Mods.Common.Traits.Render;
using OpenRA.Network; using OpenRA.Network;
@@ -46,6 +48,7 @@ namespace OpenRA.Mods.Common.Traits
public readonly EditorActorLayerInfo Info; public readonly EditorActorLayerInfo Info;
readonly List<EditorActorPreview> previews = []; readonly List<EditorActorPreview> previews = [];
readonly HashSet<uint> previewIds = [];
int2 cellOffset; int2 cellOffset;
SpatiallyPartitioned<EditorActorPreview> cellMap; SpatiallyPartitioned<EditorActorPreview> cellMap;
@@ -86,12 +89,17 @@ namespace OpenRA.Mods.Common.Traits
var height = world.Map.MapSize.Height * ts.Height; var height = world.Map.MapSize.Height * ts.Height;
screenMap = new SpatiallyPartitioned<EditorActorPreview>(width, height, Info.BinSize); screenMap = new SpatiallyPartitioned<EditorActorPreview>(width, height, Info.BinSize);
foreach (var kv in world.Map.ActorDefinitions) var names = new string[world.Map.ActorDefinitions.Count];
Add(kv.Key, new ActorReference(kv.Value.Value, kv.Value.ToDictionary()), true); var references = new List<ActorReference>(world.Map.ActorDefinitions.Count);
// Update neighbours in one pass for (var i = 0; i < world.Map.ActorDefinitions.Count; i++)
foreach (var p in previews) {
UpdateNeighbours(p.Footprint); var kv = world.Map.ActorDefinitions.ElementAt(i);
names[i] = kv.Key;
references.Add(new ActorReference(kv.Value.Value, kv.Value.ToDictionary()));
}
AddRange(CollectionsMarshal.AsSpan(references), names);
} }
void ITickRender.TickRender(WorldRenderer wr, Actor self) void ITickRender.TickRender(WorldRenderer wr, Actor self)
@@ -121,9 +129,15 @@ namespace OpenRA.Mods.Common.Traits
bool IRenderAnnotations.SpatiallyPartitionable => false; bool IRenderAnnotations.SpatiallyPartitionable => false;
public EditorActorPreview Add(ActorReference reference) { return Add(NextActorName(), reference); } IEnumerable<CPos> OccupiedCells(EditorActorPreview preview)
{
// Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint
if (preview.Footprint.Count == 0)
return [worldRenderer.World.Map.CellContaining(preview.CenterPosition)];
return preview.Footprint.Keys;
}
public EditorActorPreview Add(string id, ActorReference reference, bool initialSetup = false) PlayerReference GetOrAddOwner(ActorReference reference)
{ {
// If an actor's doesn't have a valid owner transfer ownership to neutral // If an actor's doesn't have a valid owner transfer ownership to neutral
var ownerInit = reference.Get<OwnerInit>(); var ownerInit = reference.Get<OwnerInit>();
@@ -134,43 +148,97 @@ namespace OpenRA.Mods.Common.Traits
reference.Add(new OwnerInit(worldOwner.Name)); reference.Add(new OwnerInit(worldOwner.Name));
} }
var preview = new EditorActorPreview(worldRenderer, id, reference, owner); return owner;
Add(preview, initialSetup); }
public EditorActorPreview Add(ActorReference reference)
{
var owner = GetOrAddOwner(reference);
var preview = new EditorActorPreview(worldRenderer, NextActorName(), reference, owner);
Add(preview);
return preview; return preview;
} }
public void Add(EditorActorPreview preview, bool initialSetup = false) public void AddRange(ReadOnlySpan<ActorReference> references, ReadOnlySpan<string> names)
{
if (names.Length != references.Length)
throw new ArgumentException("Member name count must match reference count.");
var newPreviews = new EditorActorPreview[names.Length];
using (new PerfTimer("CreatePreviews"))
{
for (var i = 0; i < names.Length; i++)
{
var id = names[i];
var reference = references[i];
var owner = GetOrAddOwner(reference);
newPreviews[i] = new EditorActorPreview(worldRenderer, id, reference, owner);
}
}
AddRange(newPreviews);
}
public void AddRange(ReadOnlySpan<ActorReference> references)
{
AddRange(references, NextActorNames(references.Length));
}
public void Add(EditorActorPreview preview)
{ {
previews.Add(preview); previews.Add(preview);
if (TryGetActorId(preview.ID, out var id))
previewIds.Add(id);
if (!preview.Bounds.IsEmpty) if (!preview.Bounds.IsEmpty)
screenMap.Add(preview, preview.Bounds); screenMap.Add(preview, preview.Bounds);
var cellFootprintBounds = Footprint(preview).Select( var cellFootprintBounds = OccupiedCells(preview).Select(
cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union(); cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union();
cellMap.Add(preview, cellFootprintBounds); cellMap.Add(preview, cellFootprintBounds);
preview.AddedToEditor(); preview.AddedToEditor();
UpdateNeighbours(preview.Footprint);
if (!initialSetup) if (preview.Type == PlayerSpawnName)
{ SyncMultiplayerCount();
UpdateNeighbours(preview.Footprint);
if (preview.Type == PlayerSpawnName)
SyncMultiplayerCount();
}
} }
IEnumerable<CPos> Footprint(EditorActorPreview preview) public void AddRange(ReadOnlySpan<EditorActorPreview> newPreviews)
{ {
// Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint previews.AddRange(newPreviews);
if (preview.Footprint.Count == 0) previewIds.EnsureCapacity(previews.Count * 2);
return [worldRenderer.World.Map.CellContaining(preview.CenterPosition)];
return preview.Footprint.Keys; foreach (var preview in newPreviews)
{
if (TryGetActorId(preview.ID, out var id))
previewIds.Add(id);
if (!preview.Bounds.IsEmpty)
screenMap.Add(preview, preview.Bounds);
var cellFootprintBounds = OccupiedCells(preview)
.Select(cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union();
cellMap.Add(preview, cellFootprintBounds);
preview.AddedToEditor();
}
using (new PerfTimer("UpdateNeighbours"))
UpdateNeighbours(newPreviews);
SyncMultiplayerCount();
} }
public void Remove(EditorActorPreview preview) public void Remove(EditorActorPreview preview)
{ {
previews.Remove(preview); previews.Remove(preview);
if (TryGetActorId(preview.ID, out var id))
previewIds.Remove(id);
screenMap.Remove(preview); screenMap.Remove(preview);
cellMap.Remove(preview); cellMap.Remove(preview);
@@ -182,6 +250,38 @@ namespace OpenRA.Mods.Common.Traits
SyncMultiplayerCount(); SyncMultiplayerCount();
} }
public void RemoveRange(ReadOnlySpan<EditorActorPreview> removePreviews)
{
foreach (var preview in removePreviews)
{
previews.Remove(preview);
if (TryGetActorId(preview.ID, out var id))
previewIds.Remove(id);
screenMap.Remove(preview);
cellMap.Remove(preview);
}
using (new PerfTimer("RemovedFromEditor", 1))
foreach (var preview in removePreviews)
preview.RemovedFromEditor();
using (new PerfTimer("UpdateNeighbours", 1))
UpdateNeighbours(removePreviews);
SyncMultiplayerCount();
}
public void RemoveRegion(CellCoordsRegion region)
{
RemoveRange(PreviewsInCellRegion(region).ToArray().AsSpan());
}
public void RemoveRegion(CellCoordsRegion region, HashSet<CPos> mask)
{
RemoveRange(PreviewsInCellRegion(region).Where(p => mask.Overlaps(p.Footprint.Keys)).ToArray().AsSpan());
}
public void MoveActor(EditorActorPreview preview, CPos location) public void MoveActor(EditorActorPreview preview, CPos location)
{ {
Remove(preview); Remove(preview);
@@ -203,9 +303,12 @@ namespace OpenRA.Mods.Common.Traits
void SyncMultiplayerCount() void SyncMultiplayerCount()
{ {
var newCount = previews.Count(p => p.Info.Name == PlayerSpawnName); var newCount = previews.Count(p => p.Info.Name == PlayerSpawnName);
var mp = Players.Players.Where(p => p.Key.StartsWith("Multi", StringComparison.Ordinal)).ToList(); var playersChanged = false;
foreach (var kv in mp) foreach (var kv in Players.Players)
{ {
if (!kv.Key.StartsWith("Multi", StringComparison.Ordinal))
continue;
var name = kv.Key; var name = kv.Key;
var index = Exts.ParseInt32Invariant(name[5..]); var index = Exts.ParseInt32Invariant(name[5..]);
@@ -213,6 +316,7 @@ namespace OpenRA.Mods.Common.Traits
{ {
Players.Players.Remove(name); Players.Players.Remove(name);
OnPlayerRemoved(); OnPlayerRemoved();
playersChanged = true;
} }
} }
@@ -231,13 +335,34 @@ namespace OpenRA.Mods.Common.Traits
Players.Players.Add(pr.Name, pr); Players.Players.Add(pr.Name, pr);
worldRenderer.UpdatePalettesForPlayer(pr.Name, pr.Color, true); worldRenderer.UpdatePalettesForPlayer(pr.Name, pr.Color, true);
playersChanged = true;
} }
if (!playersChanged)
return;
var creeps = Players.Players.Keys.FirstOrDefault(p => p == "Creeps"); var creeps = Players.Players.Keys.FirstOrDefault(p => p == "Creeps");
if (!string.IsNullOrEmpty(creeps)) if (!string.IsNullOrEmpty(creeps))
Players.Players[creeps].Enemies = Players.Players.Keys.Where(p => !Players.Players[p].NonCombatant).ToArray(); Players.Players[creeps].Enemies = Players.Players.Keys.Where(p => !Players.Players[p].NonCombatant).ToArray();
} }
void UpdateNeighbours(ReadOnlySpan<EditorActorPreview> previews)
{
var cells = new HashSet<CPos>(previews.Length * 6);
foreach (var preview in previews)
cells.UnionWith(Util.ExpandFootprint(preview.Footprint.Keys, true));
if (cells.Count == 0)
return;
var bounds = CellCoordsRegion.BoundingRegion(cells);
var touchedPreviews = PreviewsInCellRegion(bounds)
.Where(p => cells.Overlaps(p.Footprint.Keys));
foreach (var p in touchedPreviews)
p.ReplaceInit(new RuntimeNeighbourInit(NeighbouringPreviews(p.Footprint)));
}
void UpdateNeighbours(IReadOnlyDictionary<CPos, SubCell> footprint) void UpdateNeighbours(IReadOnlyDictionary<CPos, SubCell> footprint)
{ {
// Include actors inside the footprint too // Include actors inside the footprint too
@@ -269,20 +394,20 @@ namespace OpenRA.Mods.Common.Traits
region.TopLeft.Y - cellOffset.Y, region.TopLeft.Y - cellOffset.Y,
region.BottomRight.X - cellOffset.X + 1, region.BottomRight.X - cellOffset.X + 1,
region.BottomRight.Y - cellOffset.Y + 1)) region.BottomRight.Y - cellOffset.Y + 1))
.Where(p => Footprint(p).Any(region.Contains)); .Where(p => OccupiedCells(p).Any(region.Contains));
} }
public IEnumerable<EditorActorPreview> PreviewsAtCell(CPos cell) public IEnumerable<EditorActorPreview> PreviewsAtCell(CPos cell)
{ {
return cellMap.At(new int2(cell.X - cellOffset.X, cell.Y - cellOffset.Y)) return cellMap.At(new int2(cell.X - cellOffset.X, cell.Y - cellOffset.Y))
.Where(p => Footprint(p).Any(fp => fp == cell)); .Where(p => OccupiedCells(p).Any(fp => fp == cell));
} }
public SubCell FreeSubCellAt(CPos cell) public SubCell FreeSubCellAt(CPos cell)
{ {
var map = worldRenderer.World.Map; var map = worldRenderer.World.Map;
var previews = PreviewsAtCell(cell).ToList(); var previews = PreviewsAtCell(cell).ToArray();
if (previews.Count == 0) if (previews.Length == 0)
return map.Grid.DefaultSubCell; return map.Grid.DefaultSubCell;
for (var i = (byte)SubCell.First; i < map.Grid.SubCellOffsets.Length; i++) for (var i = (byte)SubCell.First; i < map.Grid.SubCellOffsets.Length; i++)
@@ -303,18 +428,32 @@ namespace OpenRA.Mods.Common.Traits
public Action OnPlayerRemoved = () => { }; public Action OnPlayerRemoved = () => { };
static bool TryGetActorId(string name, out uint id)
{
id = 0;
return name.StartsWith(ActorPrefix, StringComparison.Ordinal)
&& uint.TryParse(name.AsSpan(5), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out id);
}
string NextActorName() string NextActorName()
{ {
var id = previews.Count; var currentId = 0u;
var possibleName = ActorPrefix + id.ToStringInvariant(); while (previewIds.Contains(currentId))
currentId++;
while (previews.Any(p => p.ID == possibleName)) return ActorPrefix + currentId.ToStringInvariant();
{ }
id++;
possibleName = ActorPrefix + id.ToStringInvariant();
}
return possibleName; ReadOnlySpan<string> NextActorNames(int count)
{
var newNamesCount = 0u;
var newNames = new string[count];
for (var currentId = 0u; newNamesCount < count; currentId++)
if (!previewIds.Contains(currentId))
newNames[newNamesCount++] = ActorPrefix + currentId.ToStringInvariant();
return newNames;
} }
public List<MiniYamlNode> Save() public List<MiniYamlNode> Save()
@@ -329,7 +468,7 @@ namespace OpenRA.Mods.Common.Traits
public void PopulateRadarSignatureCells(Actor self, List<(CPos Cell, Color Color)> destinationBuffer) public void PopulateRadarSignatureCells(Actor self, List<(CPos Cell, Color Color)> destinationBuffer)
{ {
foreach (var preview in cellMap.Keys) foreach (var preview in cellMap.Keys)
foreach (var cell in Footprint(preview)) foreach (var cell in OccupiedCells(preview))
destinationBuffer.Add((cell, preview.RadarColor)); destinationBuffer.Add((cell, preview.RadarColor));
} }