It turns out that Skye still has a partner in crime on this
project. I tend to keep to my hermitage, but from time to time, I feel it
necessary to let people know I'm still here. Lately I've been focusing on
building the code instead of the artwork, which is why there hasn't been much
to show. However, I thought it might be interesting to talk about some of the
design.
One of the most constructive bits of feedback we received
from the Salt Lake gaming convention was that our movement system was not
immediately intuitive. Because of that we decided to overhaul it and move to a
point-and-click waypoint system similar to other tactical games. There are a
few working parts to this, including the path finding algorithm, the grid
system, the waypoints, and the actual movement. However, I'd like to focus more
on the design of the waypoint system.
For those familiar with the fundamentals of programming, there
is the concept of a stack. For those that know a bit more, there's another
concept called the state pattern. A stack is essentially what it sounds like.
You add things to the top and when you need something, you take it off the top,
making it a "last in, first out" structure. The state pattern is a
way to design the flow of a program. Essentially, there is one object that
everything references and it delegates to state objects that behave
differently. In order to implement a path (with waypoints) that can be undone,
I employed a combination of these two concepts.
Here come
the technical details, so if that makes you uneasy, feel free to stop here or skip
to the last paragraph. Anyway, to start, I added an interface for what I refer
to as an “action”. This action is essentially one of the state objects and any
state in the state machine will need to have the same interface to its actions
as the others in order for it to be reused. As I am using C#, the code looks
something like this:
The
1 2 3 4 5 6 | public interface IAction { void Act(Vector2 move); void Act(bool confirmation); .... ActionType GetActionType(); } |
Act()
function accepts a number of different types of arguments. Each one will be
implemented differently based on the state.
An example
of one of the states might be something like this:
As you can
see, the state is in charge of initializing stuff for itself, storing any
relative data, and then moving to the next state if appropriate. It also can
return the type of state it is for bookkeeping if necessary. Then the next
state (
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class MovementSelection : IAction { private Vector2 location; public MovementSelection() { // Initiation stuff for this state } public void Act(Vector2 move) { // Check for stuff that might need to validated before the move actually happens // Now we have a move, so we need to store it and move to the next state location = move; ActionHandler.Instance.SetState(new MovementWaypoint(move)); } .... public ActionType GetActionType() { return ActionType.MovementSelection; } } |
MovementWaypoint
) handles itself in a like manner. And of course
different states will keep track of different stuff and behave differently.
Now you
may be wondering where all of this is initiated from and where the main handler
object comes in. One of the keys to the state pattern is that the states are
essentially hidden from the rest of the application. The state handler and
other states are the only ones that should know anything about states.
Ultimately, the state handler becomes the interface to the rest of the
application for this.
There’s
kind of a lot going on here, so explain it a bit. The first part makes this
into a singleton, which ultimately means that only one instance of this object
will ever exist. I won’t go into too much detail on this concept, but this is
important because we only want one occurrence of the state machine. The next
part is the stack that I mentioned previously. This is used to keep track of a
history of states, or in our case, moves. The
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class ActionHandler : IAction { // The following makes this object a singleton private static ActionHandler instance; public static ActionHandler Instance { get { if (instance == null) instance = new ActionHandler(); return instance; } } private ActionHandler() { /* any necessary initialization here */ } private Stack<IAction> stateStack; public void SetState(IAction newState) { // sets the current state or, for us, adds it to the stack of states that are used to keep track of state history } public void RevertState() { // Pop off the stack to have the previous state be the current } public ActionType GetActionType() { // get the type of the current state } public void Act(Vector2 move) { // calls Act on the current state } public void Act(bool confirmation) { // calls Act on the current state } .... } |
MovementSelection
state handles
the initial selection and the MovementWaypoint
state handles subsequent
selections and can also close out the path. Since all of these states are kept
track of in the handler, it can handle all of it and delegate calls to the
states themselves. Then if we need to undo, we can just revert the state and
have the previous state as our current.
The
benefit of using a stack is relatively obvious as it provides a history of actions, however, the state pattern is another
powerful tool. Instead of having to edit a lot of code that could potentially
be in a giant if statement, we can just add a state and update who goes to that
state and what that state’s behavior is. Additionally, the state pattern can be
expanded to more than just the movement (hint: I used this for the entire
battle flow). Anyway, I hope this has given some useful information to those
curious about the process.
No comments:
Post a Comment