[The Best
Rod Serling impression that I can muster]
There is a fifth dimension beyond those known to developers. It is a dimension vast as space and timeless as infinity. It is the middle ground between fun and productivity, between the pit of his fears and the summit of his knowledge. This is the dimension of imagination. It is an area called the WWF Fun Zone
[End Rod Serling]
So I was standing on the platform waiting for the train the other day when it hit me - I can use WWF and WPF to provide the framework for tech/research trees in RTS games! I realize that some people might be hit with more interesting tidbits than this, but...you make the best out of what you've got. In case you aren't familiar with the genre, an RTS game is a Real-Time Strategy game. Typically, you are in control over a collection of units (be they an army, a horde of aliens, or a group of wizards and warriors). You tell these units what to do and they happily (most of the time) obey. They'll chop down wood, harvest resources from a mine, attack your enemy, build fortifications, or improve the landscape to make it more friendly to your army. What most of these games have in common is the notion of
research. You can typically dedicate some resources (time, money, equipment, facilities, or even people/units) to research. There is often what is referred to as a 'tech tree': a hierarchical structure representing the technology that your people can acquire, and the order in which they can acquire it. For example, in a space-based RTS, one might research laser technology to gain access to improved ship-board weapons. Once laser technology has been acquired, you might then be able to branch off and either research more advanced ship-board lasers or invest time in developing land-based laser defense systems.
Let me describe it another way: you (the player) decide to invest resources. The result is you learn a new technology. Having learned that new technology, you now have a different list of available technologies that you can acquire. You repeat the process over and over again - until you have decided to stop learning, or have learned everything you can. This process absolutely
screams workflow. There is an underlying workflow that is not in the foreground controlling all of your actions, but it does limit the actions you can take based on your current
state within the workflow. This is an ideal candidate for a
state machine workflow.
Typically, in a state machine workflow, each state receives a list of events. Each event can then transition the workflow from that state to another state. The difference here is that in an RTS "tech tree", the player can decide on any number of potential paths through the tech tree. We still want the state machine workflow to do the guiding, so what we can do is have
every state respond to the same event (OnResearchCompleted). When a state with multiple potential states receives this event, a little
Rule-Based Condition can be used to determine which state to progress into. For example, if the player has told the game that they are interested in progressing toward ship-based weapons, when the research cycle goes through an iteration, the workflow can move forward into the ship-based weapons state even though there are a number of potential states. I'm not sure if there is a term for this, but I'm going to call it
selective state transition. By that, I mean that the state machine workflow can transition from state A to states B, C, or D based on a variable such as a user preference or affinity for a particular state.
For this particular blog entry, I am going to show you the workflow, the code that supports the workflow, and the Console application code that verifies that the workflow works properly. In my subsequent blog entries, I will develop the game a little further by supplying a WPF demo that pokes and prods the workflow in various ways.
First, the
state machine workflow:
As you can see, there is an initial state where the workflow is waiting for research to begin. Every time the application / game completely finishes a research cycle, the workflow transitions from the current state to the next state. The next state is indicated either by a single possibility (a few of the states can only transition to 1 other state) or by a preference for the next state indicated by a custom event-args class passed in the
OnResearchCompleted event called
StrategyDataEventArgs using a property called
NextResearchState.
In order to fire events into a state machine workflow, you need an
External Data Exchange Service. Here is the interface I'm using (you must use an interface or the WWF designer will not be able to rig up event handlers) and the class implementation:
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
namespace Sample.StrategyGame.Workflow
{
[System.Workflow.Activities.ExternalDataExchange]
internal interface IStrategyExternalDataExchange
{
event EventHandler<StrategyDataEventArgs> OnResearchCompleted;
}
}
And here's the class implementation of IStrategyExternalDataExchange:
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Runtime;
namespace Sample.StrategyGame.Workflow
{
[Serializable]
public class StrategyExternalDataExchangeService : IStrategyExternalDataExchange
{
#region IStrategyExternalDataExchange Members
public event EventHandler<StrategyDataEventArgs> OnResearchCompleted;
#endregion
public void CompleteResearch(Guid instanceId, string nextState)
{
if (OnResearchCompleted != null)
{
OnResearchCompleted(this, new StrategyDataEventArgs(instanceId, nextState));
}
}
}
}
The important things to keep in mind here are:
- The Workflow runtime will subscribe to any events indicated by your workflow when the exchange service is associated with the runtime.
- Your data exchange service, and all data types exposed by it, must be marked as [Serializable]
With the workflow defined, and the exchange service rigged up, the only thing left to do is use a simple Console application to verify that the workflow does as it should. Next blog entry will have a far more appealing UI than the console app, but, the task of manually exercising a workflow through stub code is very valuable as it gives you the chance to debug your code, debug your workflow, and see if the results are what you intended.
Here is the code to the Console application harness:
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Workflow.Activities;
using Sample.StrategyGame.Workflow;
#endregion
namespace ConsoleHarness
{
class Program
{
static void Main(string[] args)
{
using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};
workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine(e.Exception.Message);
waitHandle.Set();
};
workflowRuntime.Started += delegate(object sender, WorkflowRuntimeEventArgs e)
{
Console.WriteLine("Workflow is started: " + e.IsStarted.ToString());
};
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Sample.StrategyGame.Workflow.ResearchWorkflow));
ExternalDataExchangeService eds = new ExternalDataExchangeService();
workflowRuntime.AddService(eds);
StrategyExternalDataExchangeService stratService = new StrategyExternalDataExchangeService();
eds.AddService(stratService);
StateMachineWorkflowInstance smwi = new StateMachineWorkflowInstance(workflowRuntime, instance.InstanceId);
instance.Start();
// sample progression : AwaitingResearch -> BasicWeaponry -> SideArms -> AssaultRifles
smwi.SetState("AwaitingResearch");
Thread.Sleep(300);
Console.WriteLine("Research Progress: " + smwi.CurrentStateName);
Console.WriteLine("Researching Weapons... Press enter to move on.");
stratService.CompleteResearch(smwi.InstanceId, "BasicWeaponry");
Console.ReadLine();
Console.WriteLine("Research Progress: " + smwi.CurrentStateName);
Console.WriteLine("Researching SideArms... Press enter to move on.");
stratService.CompleteResearch(smwi.InstanceId, "SideArms");
Console.ReadLine();
Console.WriteLine("Research Progress: " + smwi.CurrentStateName);
Console.WriteLine("Researching forward with no preference.... Press enter to move on.");
stratService.CompleteResearch(smwi.InstanceId, string.Empty);
Console.ReadLine();
Console.WriteLine("Research Progress: " + smwi.CurrentStateName);
Console.WriteLine("Press enter to finish the research workflow.");
Console.ReadLine();
smwi.SetState("ResearchComplete");
waitHandle.WaitOne();
}
Console.WriteLine("all done... press enter.");
Console.ReadLine();
}
}
}
If you want, you can draw out your own state machine workflow and give this a whirl. Otherwise, once I put a somewhat useful GUI on the top of this workflow, I will probably make it available as a downloadable file. Stay tuned, because the next blog entry on this topic will turn this console harness into a WPF application as I move it in the ultimate direction of being something playable.
Another caveat: When testing your workflow - make sure you don't do so quickly. By that I mean it can be
quite easy to signal an event to the workflow while it is still in transition from one state to another. Rather than waiting for the workflow to finish its current state transition, it attempts to handle the event immediately. This has the end result of firing an event that is
not appropriate or not handled by the current state - and you get all kinds of weird error messages about message queues being disabled. The console harness uses
Console.ReadLine() to allow the states to transition smoothly in the background so that when you send the event to the workflow, its being sent in a consistent state.
tags: vista gaming winfx fun statemachine netfx3 wf wpf
links: digg this del.icio.us technorati reddit