1

I'm adding the notion of actions that are repeatable after a set time interval in my game.
I have a class that manages whether a given action can be performed.
Callers query whether they can perform the action by calling CanDoAction, then if so, perform the action and record that they've done the action with MarkActionDone.

if (WorldManager.CanDoAction(playerControlComponent.CreateBulletActionId))
{
    // Do the action

    WorldManager.MarkActionDone(playerControlComponent.CreateBulletActionId);
}

Obviously this could be error prone, as you could forget to call MarkActionDone, or possibly you could forget to call CanDoAction to check.

Ideally I want to keep a similar interface, not having to pass around Action's or anything like that as I'm running on the Xbox and would prefer to avoid passing actions around and invoking them. Particularly as there would have to be a lot of closures involved as the actions are typically dependent on surrounding code.

I was thinking of somehow (ab)using the IDisposeable interface, as that would ensure the MarkActionDone could be called at the end, however i don't think i can skip the using block if CanDoAction would be false.

Any ideas?

George Duckett
  • 31,770
  • 9
  • 95
  • 162

2 Answers2

4

My preferred approach would be to keep this logic as an implementation detail of WorldManager (since that defines the rules about whether an action can be performed), using a delegate pattern:

public class WorldManager
{
  public bool TryDoAction(ActionId actionId, Action action)
  {
    if (!this.CanDoAction(actionId)) return false;
    try
    {
      action();
      return true;
    }
    finally
    {
      this.MarkActionDone(actionId);
    }
  }

  private bool CanDoAction(ActionId actionId) { ... }
  private void MarkActionDone(ActionId actionId) { ... }
}

This seems to fit best with SOLID principals, since it avoids any other class having to 'know' about the 'CanDoAction', 'MarkActionDone' implementation detail of WorldManager.

Update

Using an AOP framework, such as PostSharp, may be a good choice to ensure this aspect is added to all necessary code blocks in a clean manner.

Rich O'Kelly
  • 41,274
  • 9
  • 83
  • 114
  • That was my first thought, however i'd like to avoid passing `Action`'s around if possible (see 2nd to last paragraph in my question) – George Duckett Jan 16 '12 at 11:21
  • @GeorgeDuckett The effort for the runtime to create a closure is minimal (the compiler has already created a suitable class), why do you not wish to pass an Action? – Rich O'Kelly Jan 16 '12 at 11:27
  • Mainly because of the garbage / object allocations it creates. On the xbox it's important to reduce the number of GCs (and therefore object allocations), ideally to zero. – George Duckett Jan 16 '12 at 11:45
  • i beleive, If Action is a class then it wont be copied by being passed around. Object's that are passed arent copied, the reference itself is copied on the stack, this does not induce GCs. – exnihilo1031 Jan 16 '12 at 13:42
  • @GeorgeDuckett hmm an interesting problem then. If method groups are passed rather than an anonymous method, no additional objects will be created - a debug only check can be done to enforce that the passed action is indeed a method group. Alternatively if you are able, PostSharp, or a tool of that ilk, would be a good fit to ensure that this aspect makes it into all relevant code blocks. – Rich O'Kelly Jan 16 '12 at 13:50
  • @exnihilo1031: I think that if an action lambda includes a closure then an object to capture it would be created (each time I believe). – George Duckett Jan 16 '12 at 13:59
  • @rich.okelly: The problem I've got with making a method that gets passed in is that actions require different local variables depending on the action, so I can't easily take my `// Do the action` code and put it in a separate method. Nice idea though. I think I'll probably end up having a debug check, or using something like PostSharp. If you want to edit that in, i'll accept. – George Duckett Jan 16 '12 at 14:07
2

If you want to minimize GC pressure, I would suggest using interfaces rather than delegates. If you use IDisposable, you can't avoid having Dispose called, but you could have the IDisposable implementation use a flag to indicate that the Dispose method shouldn't do anything. Beyond the fact that delegates have some built-in language support, there isn't really anything they can do that interfaces cannot, but interfaces offer two advantages over delegates:

  1. Using a delegate which is bound to some data will generally require creating a heap object for the data and a second for the delegate itself. Interfaces don't require that second heap instance.
  2. In circumstances where one can use generic types which are constrained to an interface, instead of using interface types directly, one may be able to avoid creating any heap instances, as explained below (since back-tick formatting doesn't work in list items). A struct that combines a delegate to a static method along with data to be consumed by that method can behave much like a delegate, without requiring a heap allocation.

One caveat with the second approach: Although avoiding GC pressure is a good thing, the second approach may end up creating a very large number of types at run-time. The number of types created will in most cases be bounded, but there are circumstances where it could increase without bound. I'm not sure if there would any convenient way to determine the full set of types that could be produced by a program in cases where static analysis would be sufficient (in the general case, where static analysis does not suffice, determining whether any particular run-time type would get produced would be equivalent to the Halting Problem, but just as many programs can in practice be statically determined to always halt or never halt, I would expect that in practice one could often identify a closed set of types that a program could produce at run-time).

Edit

The formatting in point #2 above was messed up. Here's the explanation, cleaned up.

Define a type ConditionalCleaner<T> : IDisposable, which holds an instance of T and an Action<T> (both supplied in the constructor--probably with the Action<T> as the first parameter). In the IDisposable.Dispose() method, if the Action<T> is non-null, invoke it on the T. In a SkipDispose() method, null out the Action<T>. For convenience, you may want to also define ConditionalCleaner<T,U>: IDisposable similarly (perhaps three- and four-argument versions as well), and you may want to define a static class ConditionalCleaner with generic Create<T>, Create<T,U>, etc. methods (so one could say e.g. using (var cc = ConditionalCleaner.Create(Console.WriteLine, "ABCDEF") {...} or ConditionalCleaner.Create((x) => {Console.WriteLine(x);}, "ABCDEF") to have the indicated action performed when the using block exits. The biggest requirement if one uses a Lambda expression is to ensure that the lambda expression doesn't close over any local variables or parameters from the calling function; anything the calling function wants to pass to the lambda expression must be an explicit parameter thereof. Otherwise the system will define a class object to hold any closed-over variables, as well as a new delegate pointing to it.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • That certainly sounds interesting, thanks for answering despite the question being marked as answered. You lost me a bit when discussing the types / method calls of the `ConditionalCleaner` though. If you could elaborate a bit, maybe an example usage that would be great. – George Duckett Jan 16 '12 at 17:46