From 8b5df6bf2c82e11a1fbf7025a00aa30fe9ff343a Mon Sep 17 00:00:00 2001 From: Oliver Brakmann Date: Sat, 28 Nov 2015 01:30:21 +0100 Subject: [PATCH] Improve ProximityCapturable The following changes are made: * Replaced the custom actor search with ProximityTriggers. * Added a `Sticky` flag, which causes the ownership to not revert back to the original owner when the captor leaves. * Added trait documentation. --- .../Traits/ProximityCapturable.cs | 197 +++++++++++------- 1 file changed, 121 insertions(+), 76 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/ProximityCapturable.cs b/OpenRA.Mods.Common/Traits/ProximityCapturable.cs index 7cfeac3ff5..6bebb8c485 100644 --- a/OpenRA.Mods.Common/Traits/ProximityCapturable.cs +++ b/OpenRA.Mods.Common/Traits/ProximityCapturable.cs @@ -9,8 +9,11 @@ #endregion using System.Collections.Generic; +using System.Drawing; using System.Linq; using OpenRA.Effects; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -18,15 +21,26 @@ namespace OpenRA.Mods.Common.Traits [Desc("Actor can be captured by units in a specified proximity.")] public class ProximityCapturableInfo : ITraitInfo { - public readonly bool Permanent = false; + [Desc("Maximum range at which a ProximityCaptor actor can initiate the capture.")] public readonly WDist Range = WDist.FromCells(5); - public readonly bool MustBeClear = false; + + [Desc("Allowed ProximityCaptor actors to capture this actor.")] public readonly HashSet CaptorTypes = new HashSet { "Vehicle", "Tank", "Infantry" }; + [Desc("If set, the capturing process stops immediately after another player comes into Range.")] + public readonly bool MustBeClear = false; + + [Desc("If set, the ownership will not revert back when the captor leaves the area.")] + public readonly bool Sticky = false; + + [Desc("If set, the actor can only be captured via this logic once.", + "This option implies the `Sticky` behaviour as well.")] + public readonly bool Permanent = false; + public object Create(ActorInitializer init) { return new ProximityCapturable(init.Self, this); } } - public class ProximityCapturable : ITick + public class ProximityCapturable : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged { public readonly Player OriginalOwner; public bool Captured { get { return Self.Owner != OriginalOwner; } } @@ -34,6 +48,11 @@ namespace OpenRA.Mods.Common.Traits public ProximityCapturableInfo Info; public Actor Self; + readonly List actorsInRange = new List(); + int proximityTrigger; + WPos prevPosition; + bool skipTriggerUpdate; + public ProximityCapturable(Actor self, ProximityCapturableInfo info) { Info = info; @@ -41,59 +60,123 @@ namespace OpenRA.Mods.Common.Traits OriginalOwner = self.Owner; } + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + if (skipTriggerUpdate) + return; + + // TODO: Eventually support CellTriggers as well + proximityTrigger = self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, ActorEntered, ActorLeft); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (skipTriggerUpdate) + return; + + self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); + actorsInRange.Clear(); + } + public void Tick(Actor self) { - if (Captured && Info.Permanent) return; // Permanent capture + if (!self.IsInWorld || self.CenterPosition == prevPosition) + return; - if (!Captured) + self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, self.CenterPosition, Info.Range); + prevPosition = self.CenterPosition; + } + + void ActorEntered(Actor other) + { + if (skipTriggerUpdate || !CanBeCapturedBy(other)) + return; + + actorsInRange.Add(other); + UpdateOwnership(); + } + + void ActorLeft(Actor other) + { + if (skipTriggerUpdate || !CanBeCapturedBy(other)) + return; + + actorsInRange.Remove(other); + UpdateOwnership(); + } + + bool CanBeCapturedBy(Actor a) + { + if (a == Self) + return false; + + var pc = a.Info.TraitInfoOrDefault(); + return pc != null && pc.Types.Overlaps(Info.CaptorTypes); + } + + bool IsClear(Actor self, Player captorOwner) + { + return actorsInRange + .All(a => a.Owner == captorOwner || WorldUtils.AreMutualAllies(a.Owner, captorOwner)); + } + + void UpdateOwnership() + { + if (Captured && Info.Permanent) { - var captor = GetInRange(self); - - if (captor != null) - if (!Info.MustBeClear || IsClear(self, captor.Owner, OriginalOwner)) - ChangeOwnership(self, captor); - + // This area has been captured and cannot ever be re-captured, so we get rid of the + // ProximityTrigger and ensure that it won't be recreated in AddedToWorld. + skipTriggerUpdate = true; + Self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); return; } - // if the area must be clear, and there is more than 1 player nearby => return ownership to default - if (Info.MustBeClear && !IsClear(self, self.Owner, OriginalOwner)) + // The actor that has been in the area the longest will be the captor. + // The previous implementation used the closest one, but that doesn't work with + // ProximityTriggers since they only generate events when actors enter or leave. + var captor = actorsInRange.FirstOrDefault(); + + // The last unit left the area + if (captor == null) { - // Revert Ownership - ChangeOwnership(self, OriginalOwner.PlayerActor); - return; + // Unless the Sticky option is set, we revert to the original owner. + if (Captured && !Info.Sticky) + ChangeOwnership(Self, OriginalOwner.PlayerActor); } - - // See if the 'temporary' owner still is in range - if (!IsStillInRange(self)) + else { - // no.. So find a new one - var captor = GetInRange(self); - - // got one - if (captor != null) + if (Info.MustBeClear) { - ChangeOwnership(self, captor); - return; - } + var isClear = IsClear(Self, captor.Owner); - // Revert Ownership otherwise - ChangeOwnership(self, OriginalOwner.PlayerActor); + // An enemy unit has wandered into the area, so we've lost control of it. + if (Captured && !isClear) + ChangeOwnership(Self, OriginalOwner.PlayerActor); + + // We don't own the area yet, but it is clear from enemy units, so we take possession of it. + else if (Self.Owner != captor.Owner && isClear) + ChangeOwnership(Self, captor); + } + else + { + // In all other cases, we just take over. + if (Self.Owner != captor.Owner) + ChangeOwnership(Self, captor); + } } } - static void ChangeOwnership(Actor self, Actor captor) + void ChangeOwnership(Actor self, Actor captor) { self.World.AddFrameEndTask(w => { - if (self.Disposed || captor.Disposed) return; + if (self.Disposed || captor.Disposed) + return; + // prevent (Added|Removed)FromWorld from firing during Actor.ChangeOwner + skipTriggerUpdate = true; var previousOwner = self.Owner; - - // momentarily remove from world so the ownership queries don't get confused - w.Remove(self); - self.Owner = captor.Owner; - w.Add(self); + self.ChangeOwner(captor.Owner); if (self.Owner == self.World.LocalPlayer) w.Add(new FlashTarget(self)); @@ -103,47 +186,9 @@ namespace OpenRA.Mods.Common.Traits }); } - bool CanBeCapturedBy(Actor a) + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { - var pc = a.Info.TraitInfoOrDefault(); - return pc != null && pc.Types.Overlaps(Info.CaptorTypes); - } - - IEnumerable UnitsInRange() - { - return Self.World.FindActorsInCircle(Self.CenterPosition, Info.Range) - .Where(a => a.IsInWorld && a != Self && !a.Disposed && !a.Owner.NonCombatant); - } - - bool IsClear(Actor self, Player currentOwner, Player originalOwner) - { - return UnitsInRange() - .All(a => a.Owner == originalOwner || a.Owner == currentOwner || - WorldUtils.AreMutualAllies(a.Owner, currentOwner) || !CanBeCapturedBy(a)); - } - - // TODO exclude other NeutralActor that aren't permanent - bool IsStillInRange(Actor self) - { - return UnitsInRange().Any(a => a.Owner == self.Owner && CanBeCapturedBy(a)); - } - - IEnumerable CaptorsInRange(Actor self) - { - return UnitsInRange() - .Where(a => a.Owner != OriginalOwner && CanBeCapturedBy(a)); - } - - // TODO exclude other NeutralActor that aren't permanent - Actor GetInRange(Actor self) - { - return CaptorsInRange(self).ClosestTo(self); - } - - int CountPlayersNear(Actor self, Player ignoreMe) - { - return CaptorsInRange(self).Select(a => a.Owner).Where(p => p != ignoreMe) - .Distinct().Count(); + skipTriggerUpdate = false; } } }