Merge pull request #12461 from obrakmann/activities-pt2
Activities re-write, part 2 - Child and composite activities
This commit is contained in:
@@ -9,42 +9,254 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Activities
|
||||
{
|
||||
public enum ActivityState { Queued, Active, Done, Canceled }
|
||||
|
||||
/*
|
||||
* Activities are actions carried out by actors during each tick.
|
||||
*
|
||||
* Activities exist in a graph data structure built up amongst themselves. Each activity has a parent activity,
|
||||
* optionally child activities, and usually a next activity. An actor's CurrentActivity is a pointer into that graph
|
||||
* and moves through it as activities run.
|
||||
*
|
||||
* There are two kinds of activities, the base activity and composite activities. They differ in the way their children
|
||||
* are run: while a base activity is responsible for running its children itself, a composite activity relies on the actor's
|
||||
* activity-running code. Therefore, the actor's CurrentActivity stays on the base activity while it runs its children. With
|
||||
* composite activities however, the CurrentActivity moves through the list of children as they run.
|
||||
*
|
||||
*
|
||||
* Things to be aware of when writing activities:
|
||||
*
|
||||
* - Use "return NextActivity" at least once somewhere in the tick method.
|
||||
* - Do not use "return new SomeActivity()" as that will break the graph. Queue the new activity and use "return NextActivity" instead.
|
||||
* - Do not "reuse" (with "SequenceActivities", for example) activity objects that have already finished running.
|
||||
* Queue a new instance instead.
|
||||
* - Avoid calling actor.CancelActivity(). It is almost always a bug. Call activity.Cancel() instead.
|
||||
* - A composite activity will run at least twice. The first time when it returns its children,
|
||||
* the second time when its last child returns its Parent.
|
||||
* - Do not return the Parent explicitly unless you have an extremly good reason. "return NextActivity"
|
||||
* will do the right thing in all circumstances.
|
||||
* - You do not need to care about the ChildActivity pointer advancing through the list of children,
|
||||
* the activity code already takes care of that.
|
||||
* - If you want to check whether there are any follow-up activities queued, check against "NextInQueue"
|
||||
* in favour of "NextActivity" to avoid checking against the Parent activity.
|
||||
*
|
||||
*
|
||||
* Guide when to use which kind of activity:
|
||||
*
|
||||
* - The activity does not have any children -> base activity
|
||||
* - The activity needs to run preparatory steps during each tick before its children can be run -> base activity
|
||||
* - The activity or the actor is left in a bogus state when one of the child activities is canceled -> base activity
|
||||
* - The activity's children are self-contained and can run independently of the parent -> composite activity
|
||||
* - The activity does not have any or little logic of its own, but is just composed of sub-steps -> composite activity
|
||||
*/
|
||||
public abstract class Activity
|
||||
{
|
||||
public Activity NextActivity { get; set; }
|
||||
public ActivityState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the top-most activity *from the point of view of the calling activity*. Note that the root activity
|
||||
/// can and likely will have next activities of its own, which would in turn be the root for their children.
|
||||
/// </summary>
|
||||
public Activity RootActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
var p = this;
|
||||
while (p.ParentActivity != null)
|
||||
p = p.ParentActivity;
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
Activity parentActivity;
|
||||
public Activity ParentActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
return parentActivity;
|
||||
}
|
||||
|
||||
protected set
|
||||
{
|
||||
parentActivity = value;
|
||||
|
||||
var next = NextInQueue;
|
||||
if (next != null)
|
||||
next.ParentActivity = parentActivity;
|
||||
}
|
||||
}
|
||||
|
||||
Activity childActivity;
|
||||
protected Activity ChildActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
return childActivity != null && childActivity.State < ActivityState.Done ? childActivity : null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value == this || value == ParentActivity || value == NextInQueue)
|
||||
childActivity = null;
|
||||
else
|
||||
{
|
||||
childActivity = value;
|
||||
|
||||
if (childActivity != null)
|
||||
childActivity.ParentActivity = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Activity nextActivity;
|
||||
|
||||
/// <summary>
|
||||
/// The getter will return either the next activity or, if there is none, the parent one.
|
||||
/// </summary>
|
||||
public virtual Activity NextActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
return nextActivity != null ? nextActivity : ParentActivity;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value == this || value == ParentActivity || (value != null && value.ParentActivity == this))
|
||||
nextActivity = null;
|
||||
else
|
||||
{
|
||||
nextActivity = value;
|
||||
|
||||
if (nextActivity != null)
|
||||
nextActivity.ParentActivity = ParentActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The getter will return the next activity on the same level _only_, in contrast to NextActivity.
|
||||
/// Use this to check whether there are any follow-up activities queued.
|
||||
/// </summary>
|
||||
public Activity NextInQueue
|
||||
{
|
||||
get { return nextActivity; }
|
||||
set { NextActivity = value; }
|
||||
}
|
||||
|
||||
public bool IsInterruptible { get; protected set; }
|
||||
protected bool IsCanceled { get; private set; }
|
||||
public bool IsCanceled { get { return State == ActivityState.Canceled; } }
|
||||
|
||||
public Activity()
|
||||
{
|
||||
IsInterruptible = true;
|
||||
}
|
||||
|
||||
public Activity TickOuter(Actor self)
|
||||
{
|
||||
if (State == ActivityState.Done && Game.Settings.Debug.StrictActivityChecking)
|
||||
throw new InvalidOperationException("Actor {0} attempted to tick activity {1} after it had already completed.".F(self, this.GetType()));
|
||||
|
||||
if (State == ActivityState.Queued)
|
||||
{
|
||||
OnFirstRun(self);
|
||||
State = ActivityState.Active;
|
||||
}
|
||||
|
||||
var ret = Tick(self);
|
||||
if (ret == null || (ret != this && ret.ParentActivity != this))
|
||||
{
|
||||
// Make sure that the Parent's ChildActivity pointer is moved forwards as the child queue advances.
|
||||
// The Child's ParentActivity will be set automatically during assignment.
|
||||
if (ParentActivity != null && ParentActivity != ret)
|
||||
ParentActivity.ChildActivity = ret;
|
||||
|
||||
if (State != ActivityState.Canceled)
|
||||
State = ActivityState.Done;
|
||||
|
||||
OnLastRun(self);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public abstract Activity Tick(Actor self);
|
||||
|
||||
/// <summary>
|
||||
/// Runs once immediately before the first Tick() execution.
|
||||
/// </summary>
|
||||
protected virtual void OnFirstRun(Actor self) { }
|
||||
|
||||
/// <summary>
|
||||
/// Runs once immediately after the last Tick() execution.
|
||||
/// </summary>
|
||||
protected virtual void OnLastRun(Actor self) { }
|
||||
|
||||
public virtual bool Cancel(Actor self)
|
||||
{
|
||||
if (!IsInterruptible)
|
||||
return false;
|
||||
|
||||
IsCanceled = true;
|
||||
if (ChildActivity != null && !ChildActivity.Cancel(self))
|
||||
return false;
|
||||
|
||||
State = ActivityState.Canceled;
|
||||
NextActivity = null;
|
||||
ChildActivity = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void Queue(Activity activity)
|
||||
{
|
||||
if (NextActivity != null)
|
||||
NextActivity.Queue(activity);
|
||||
if (NextInQueue != null)
|
||||
NextInQueue.Queue(activity);
|
||||
else
|
||||
NextActivity = activity;
|
||||
NextInQueue = activity;
|
||||
}
|
||||
|
||||
public virtual void QueueChild(Activity activity)
|
||||
{
|
||||
if (ChildActivity != null)
|
||||
ChildActivity.Queue(activity);
|
||||
else
|
||||
ChildActivity = activity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the activity tree, starting from the root or optionally from a given origin.
|
||||
///
|
||||
/// Call this method from any place that's called during a tick, such as the Tick() method itself or
|
||||
/// the Before(First|Last)Run() methods. The origin activity will be marked in the output.
|
||||
/// </summary>
|
||||
/// <param name="origin">Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the root.</param>
|
||||
/// <param name="level">Initial level of indentation.</param>
|
||||
protected void PrintActivityTree(Activity origin = null, int level = 0)
|
||||
{
|
||||
if (origin == null)
|
||||
RootActivity.PrintActivityTree(this);
|
||||
else
|
||||
{
|
||||
Console.Write(new string(' ', level * 2));
|
||||
if (origin == this)
|
||||
Console.Write("*");
|
||||
|
||||
Console.WriteLine(this.GetType().ToString().Split('.').Last());
|
||||
|
||||
if (ChildActivity != null)
|
||||
ChildActivity.PrintActivityTree(origin, level + 1);
|
||||
|
||||
if (NextInQueue != null)
|
||||
NextInQueue.PrintActivityTree(origin, level);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IEnumerable<Target> GetTargets(Actor self)
|
||||
@@ -53,6 +265,29 @@ namespace OpenRA.Activities
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In contrast to the base activity class, which is responsible for running its children itself,
|
||||
/// composite activities rely on the actor's activity-running logic for their children.
|
||||
/// </summary>
|
||||
public abstract class CompositeActivity : Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// The getter will return the first non-null value of either child, next or parent activity, in that order, or ultimately null.
|
||||
/// </summary>
|
||||
public override Activity NextActivity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ChildActivity != null)
|
||||
return ChildActivity;
|
||||
else if (NextInQueue != null)
|
||||
return NextInQueue;
|
||||
else
|
||||
return ParentActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ActivityExts
|
||||
{
|
||||
public static IEnumerable<Target> GetTargetQueue(this Actor self)
|
||||
|
||||
@@ -203,13 +203,13 @@ namespace OpenRA
|
||||
if (CurrentActivity == null)
|
||||
CurrentActivity = nextActivity;
|
||||
else
|
||||
CurrentActivity.Queue(nextActivity);
|
||||
CurrentActivity.RootActivity.Queue(nextActivity);
|
||||
}
|
||||
|
||||
public bool CancelActivity()
|
||||
{
|
||||
if (CurrentActivity != null)
|
||||
return CurrentActivity.Cancel(this);
|
||||
return CurrentActivity.RootActivity.Cancel(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace OpenRA
|
||||
public bool SanityCheckUnsyncedCode = false;
|
||||
public int Samples = 25;
|
||||
public bool IgnoreVersionMismatch = false;
|
||||
public bool StrictActivityChecking = false;
|
||||
public bool SendSystemInformation = true;
|
||||
public int SystemInformationVersionPrompt = 0;
|
||||
public string UUID = System.Guid.NewGuid().ToString();
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace OpenRA.Traits
|
||||
while (act != null)
|
||||
{
|
||||
var prev = act;
|
||||
act = act.Tick(self);
|
||||
act = act.TickOuter(self);
|
||||
var current = Stopwatch.GetTimestamp();
|
||||
if (current - start > longTickThresholdInStopwatchTicks)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ namespace OpenRA.Traits
|
||||
else
|
||||
start = current;
|
||||
|
||||
if (prev == act)
|
||||
if (act == prev || act == prev.ParentActivity)
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace OpenRA.Mods.Common.Activities
|
||||
readonly AttackPlane attackPlane;
|
||||
readonly AmmoPool[] ammoPools;
|
||||
|
||||
Activity inner;
|
||||
int ticksUntilTurn;
|
||||
|
||||
public FlyAttack(Actor self, Target target)
|
||||
@@ -47,34 +46,25 @@ namespace OpenRA.Mods.Common.Activities
|
||||
if (attackPlane != null)
|
||||
attackPlane.DoAttack(self, target);
|
||||
|
||||
if (inner == null)
|
||||
if (ChildActivity == null)
|
||||
{
|
||||
if (IsCanceled)
|
||||
return NextActivity;
|
||||
|
||||
// TODO: This should fire each weapon at its maximum range
|
||||
if (attackPlane != null && target.IsInRange(self.CenterPosition, attackPlane.Armaments.Select(a => a.Weapon.MinRange).Min()))
|
||||
inner = ActivityUtils.SequenceActivities(new FlyTimed(ticksUntilTurn, self), new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
|
||||
ChildActivity = ActivityUtils.SequenceActivities(new FlyTimed(ticksUntilTurn, self), new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
|
||||
else
|
||||
inner = ActivityUtils.SequenceActivities(new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
|
||||
ChildActivity = ActivityUtils.SequenceActivities(new Fly(self, target), new FlyTimed(ticksUntilTurn, self));
|
||||
|
||||
// HACK: This needs to be done in this round-about way because TakeOff doesn't behave as expected when it doesn't have a NextActivity.
|
||||
if (self.World.Map.DistanceAboveTerrain(self.CenterPosition).Length < aircraft.Info.MinAirborneAltitude)
|
||||
inner = ActivityUtils.SequenceActivities(new TakeOff(self), inner);
|
||||
ChildActivity = ActivityUtils.SequenceActivities(new TakeOff(self), ChildActivity);
|
||||
}
|
||||
|
||||
inner = ActivityUtils.RunActivity(self, inner);
|
||||
ActivityUtils.RunActivity(self, ChildActivity);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public override bool Cancel(Actor self)
|
||||
{
|
||||
if (!IsCanceled && inner != null && !inner.Cancel(self))
|
||||
return false;
|
||||
|
||||
// NextActivity must always be set to null:
|
||||
return base.Cancel(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,59 +16,40 @@ using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Activities
|
||||
{
|
||||
public class ResupplyAircraft : Activity
|
||||
public class ResupplyAircraft : CompositeActivity
|
||||
{
|
||||
readonly Aircraft aircraft;
|
||||
Activity inner;
|
||||
public ResupplyAircraft(Actor self) { }
|
||||
|
||||
public ResupplyAircraft(Actor self)
|
||||
protected override void OnFirstRun(Actor self)
|
||||
{
|
||||
aircraft = self.Trait<Aircraft>();
|
||||
var aircraft = self.Trait<Aircraft>();
|
||||
var host = aircraft.GetActorBelow();
|
||||
|
||||
if (host == null)
|
||||
return;
|
||||
|
||||
if (aircraft.IsPlane)
|
||||
{
|
||||
ChildActivity = ActivityUtils.SequenceActivities(
|
||||
aircraft.GetResupplyActivities(host)
|
||||
.Append(new AllowYieldingReservation(self))
|
||||
.Append(new WaitFor(() => NextInQueue != null || aircraft.ReservedActor == null))
|
||||
.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Helicopters should take off from their helipad immediately after resupplying.
|
||||
// HACK: Append NextInQueue to TakeOff to avoid moving to the Rallypoint (if NextInQueue is non-null).
|
||||
ChildActivity = ActivityUtils.SequenceActivities(
|
||||
aircraft.GetResupplyActivities(host)
|
||||
.Append(new AllowYieldingReservation(self))
|
||||
.Append(new TakeOff(self)).Append(NextInQueue).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public override Activity Tick(Actor self)
|
||||
{
|
||||
if (IsCanceled)
|
||||
return NextActivity;
|
||||
|
||||
if (inner == null)
|
||||
{
|
||||
var host = aircraft.GetActorBelow();
|
||||
|
||||
if (host == null)
|
||||
return NextActivity;
|
||||
|
||||
if (aircraft.IsPlane)
|
||||
{
|
||||
inner = ActivityUtils.SequenceActivities(
|
||||
aircraft.GetResupplyActivities(host)
|
||||
.Append(new AllowYieldingReservation(self))
|
||||
.Append(new WaitFor(() => NextActivity != null || aircraft.ReservedActor == null))
|
||||
.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Helicopters should take off from their helipad immediately after resupplying.
|
||||
// HACK: Append NextActivity to TakeOff to avoid moving to the Rallypoint (if NextActivity is non-null).
|
||||
inner = ActivityUtils.SequenceActivities(
|
||||
aircraft.GetResupplyActivities(host)
|
||||
.Append(new AllowYieldingReservation(self))
|
||||
.Append(new TakeOff(self)).Append(NextActivity).ToArray());
|
||||
}
|
||||
}
|
||||
else
|
||||
inner = ActivityUtils.RunActivity(self, inner);
|
||||
|
||||
// The inner == NextActivity check is needed here because of the TakeOff issue mentioned in the comment above.
|
||||
return inner == null || inner == NextActivity ? NextActivity : this;
|
||||
}
|
||||
|
||||
public override bool Cancel(Actor self)
|
||||
{
|
||||
if (!IsCanceled && inner != null && !inner.Cancel(self))
|
||||
return false;
|
||||
|
||||
return base.Cancel(self);
|
||||
return NextActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var destination = rp != null ? rp.Location :
|
||||
(hasHost ? self.World.Map.CellContaining(host.CenterPosition) : self.Location);
|
||||
|
||||
if (NextActivity == null)
|
||||
if (NextInQueue == null)
|
||||
return new AttackMoveActivity(self, move.MoveTo(destination, 1));
|
||||
else
|
||||
return NextActivity;
|
||||
return NextInQueue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
public override Activity Tick(Actor self)
|
||||
{
|
||||
if (NextActivity != null)
|
||||
return NextActivity;
|
||||
if (NextInQueue != null)
|
||||
return NextInQueue;
|
||||
|
||||
// Find the nearest best refinery if not explicitly ordered to a specific refinery:
|
||||
if (harv.OwnerLinkedProc == null || !harv.OwnerLinkedProc.IsInWorld)
|
||||
|
||||
@@ -51,9 +51,12 @@ namespace OpenRA.Mods.Common.Activities
|
||||
|
||||
public override Activity Tick(Actor self)
|
||||
{
|
||||
if (IsCanceled || NextActivity != null)
|
||||
if (IsCanceled)
|
||||
return NextActivity;
|
||||
|
||||
if (NextInQueue != null)
|
||||
return NextInQueue;
|
||||
|
||||
var deliver = new DeliverResources(self);
|
||||
|
||||
if (harv.IsFull)
|
||||
@@ -81,8 +84,8 @@ namespace OpenRA.Mods.Common.Activities
|
||||
var randFrames = self.World.SharedRandom.Next(100, 175);
|
||||
|
||||
// Avoid creating an activity cycle
|
||||
var next = NextActivity;
|
||||
NextActivity = null;
|
||||
var next = NextInQueue;
|
||||
NextInQueue = null;
|
||||
return ActivityUtils.SequenceActivities(next, new Wait(randFrames), this);
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user