State-aware programming in C#, part II
Having defined the approach for the stateful framework, let's tackle the design goals:
- Serializable state-aware objects
- Different logic paths per state on state aware methods
- State specific methods should be inherited
- Default and state specific methods
- Type-safe states
Ideally we would like our code so look something like this:
public class Bot : AStateObject
{
public virtual void Touch()
{
//default implementation
}
virtual state(BotStates.Idle)
{
public virtual void Touch()
{
// Idle specific implementation
}
}
}
public class GuardBot : Bot
{
public override void Touch()
{
// override of default implementation
}
virtual state(BotStates.Guarding)
{
public virtual void Touch()
{
// Guarding specific implementation
}
}
}
Since that's not possible without altering the language, let's see what we can do to approximate that behavior.
Serializable state-aware objects
I'm just going to get this out of the way first. For the initial version, i'm handling the serializable portion by simple making the abstract base class ISerializable
and putting the burden on the subclass to be serializable. .NET already has great facilities for serializing and it won't be hard to retrofit the base class with the methods for automating the saving of state later, as long as we just insist on ISerializable
.
Different logic paths per state on state aware methods
Since we can't just create new keywords in the language, what can we do? Well, to start, a large part of the motivation behind stateful programming in UnrealScript can be alleviated by using events. I.e. a common example is a Touch()
function. I.e. the object responds to an external event. Well, instead of creating Touch()
in different parts of the code, you could just have a Touched
event in the class and subscribe and unsubscribe different handlers depending on the state.
While events will play a large part of any complex game logic, the approach for this framework doesn't use events. Instead we use the language feature that makes events possible, i.e. Delegates. Sure we could have a case statement in each stateful Method that branches on the state, but that makes for error-prone and hard to maintain code. Instead each stateful method could simply call a state handler, which is initialized with the appropriate delegate on state change. Now we have a single set of plumbing for setting up all code paths for each state.
But that still leaves a lot of code at state change to attach the right delegates to each method per state change. Instead of doing this manually, we can use Attributes to identify the appropriate handlers and Reflection to discover state methods and the matching state specific handlers.
The resulting code looks like this:
public class Bot : AStateObject
{
// Start: Definition of the Stateful method
protected delegate void TouchDelegate();
protected TouchDelegate touchDelegate;
[StateMethod("touchDelegate")]
public void Touch()
{
touchDelegate();
}
// End: Definition of the Stateful method
// Tagged as default handler
[StateMethodHandler("Touch")]
protected virtual void Default_Touch()
{
//default implementation
}
// Tagged as Idle state handler
[StateMethodHandler("Touch", (byte)BotStates.Idle)]
protected virtual void Idle_Touch()
{
// Idle specific implementation
}
}
public class GuardBot : Bot
{
// normal override mechanism works in our state code as well
protected override void Default_Touch()
{
// override of default implementation
}
// Tagged as Idle state handler
[StateMethodHandler("Touch", (byte)BotStates.Guarding)]
protected virtual void Guarding_Touch()
{
// Guarding specific implementation
}
}
This covers pretty much all the bases, but let me just point out the ones specific to state handling.
First we have to separate the state method from its implemenation and then tag it as such. That's how we get:
protected delegate void TouchDelegate();
protected TouchDelegate touchDelegate;
[StateMethod("touchDelegate")]
public void Touch()
{
touchDelegate();
}
Clearly a lot of busy work for a simple method. But it's busy work a code generator could easily move to a partial
class. The StateMethod
attribute both tags the method and provides information to our framework which delegate needs to be wired up.
Now we're free to define our state specific implemenation:
[StateMethodHandler("Touch", (byte)BotStates.Idle)] protected virtual void Idle_Touch() { // Idle specific implementation }
The name Idle_Touch
is convention and does not affect execution. Its purpose serves both to make sure we get a unique method name and that its easily recognizable by someone trying to subclass the class. It's the StateMethodHandler
attribute that tells the framework that the method is the Idle handler for Touch()
. Why are we casting our state enum
to byte
? I'll cover that when I talk about type-safe states.
State specific methods should be inherited
Since our state specific methods are really just normal methods that have been tagged as handlers, inheritance proceeds in the regular fashion. Marked as virtual
, any subclass can override any state handler. Since Intellisense won't tell you which method is tagged to what state, the naming convention of
Default and state specific methods
In addition to the StateMethodHandler(string methodName, byte state)
constructor, there also exists StateMethodHandler(string methodName)
. Methods tagged as such are the default handlers:
Type-safe states
This last goal is a bit tricky and the solution is only somewhat satisfying. My desire is to have states that are type-safe and discoverable enumerations of states. Naturally enums come to mind. But there are a number of disadvantages to enums:
- If we define the enum as part of the framework, all classes that implement the framework have to share a fixed set of states. This is clearly not useful.
- If we let a subclass define the enumeration and the framework just stores it as an Id, it solves the first problem. However any further subclassing can only add states by changing the original enum, which is only possible if you are the author of the assembly containing that enumeration.
- Even if a subclass decides to throw out the parent's state enum and just create its own, any State property will not permit this, since we can't change the property's signature in the override.
The first we address by hiding state in the bowels of the framework simply as a byte and requiring the subclasses to define their own state enum
and casting them. Hence the byte
cast in the StateMethodHandler
attribute. (If you have more than 256 states, you probably have something a bit more complex than should be handled on simple single valued state changes).`` ```
The second is a trade-off for being a compile-time checked framework. I'd rather have my enums to be machine-discoverable and fixed than some dynamic value that only becomes meaningful at runtime. I'll go out on a limb and say that if you are subclassing an existing member of your game's simulation, it should only contain the existing states because the simulation is unlikely to ever set your new states. If you do need more states, maybe your class isn't a subclass at all. You probably have two game objects that share common code, but different states, so they instead have a common ancestor (which doesn't define states). Then your two classes become siblings, each defining their own states.
If you really must extend the states of your base class, you can still handle the last case by simply marking your override of the State accessor as new
and taking on the responsibility of doing all the casting.
So that covers the syntax of the implementation for a simple stateful framework. Next I'll cover the guts that implements that syntax in State-aware programming in C#, part III.