In my current project, which is not a game, I wanted to use a pattern similar to GKStateMachine for various UI and model states, but decided to implement a simple framework that addressed these issues rather than use GKStateMachine.
The basic pattern is provided by a protocol that *could* be implemented by any class, but the interface strongly suggests that a state will be an enum : Int type:
protocol MPState {
var rawEnumValue : Int { get }
var validNextStates : [Int] { get }
func didEnter(from prevState: MPState?, with machine: MPStateMachine)
func willExit(to nextState: MPState?, with machine: MPStateMachine)
}
Obviously the rawEnumValue will be very easy to implement as a wrapper on rawValue if the state is implemented as an enum : Int rather than some other type of struct or class. Also, the required validNextStates computed property is expected to return values that can be compared with rawEnumValue for inclusion/exclusion, and this logic is made explicit in an extension to the protocol (which also implements didEnter and willExit as no-ops to make them optional):
extension MPState {
func isValidNextState(_ nextState : MPState,
with machine: MPStateMachine) -> Bool {
return self.validNextStates.contains(nextState.rawEnumValue)
}
func didEnter(from prevState: MPState?, with machine: MPStateMachine) {
// no-op, override to handle
}
func willExit(to nextState: MPState?, with machine: MPStateMachine) {
// no-op, override to handle
}
}
One thing this pattern precludes: the state object itself can't retain any persistent reference to either the state machine or any model object, since enumerations in swift can't have stored properties. This is handled by simply passing the machine object into the three relevant methods; the machine is a full-fledged class which also has an optional model property that can bring in more object context.
The state-machine class provides an interface like that of GKStateMachine, but instead of passing a class to the enter() method you can just pass your enumerated state value. It has a similar currentState read-only computed property (returning a private machineState value which can only be updated by successful enter). Here's the entire implementation:
class MPStateMachine : NSObject {
var model: Any? = nil
var currentState : MPState? {
get {
return self.machineState
}
}
func canEnterState(_ nextState : MPState) -> Bool {
if nil != self.machineState {
return self.machineState!.isValidNextState(nextState, with: self)
}
return true // assume we can enter any state from nil
}
func enter(_ state: MPState) -> Bool {
var setState : Bool = false
if self.machineState == nil {
self.machineState = state
state.didEnter(from: nil, with: self)
setState = true
} else if self.machineState!.isValidNextState(state, with: self) {
let oldState = self.machineState!
self.machineState?.willExit(to: state, with: self)
self.machineState = state
state.didEnter(from: oldState, with: self)
setState = true
}
return setState
}
fileprivate var machineState : MPState? = nil
}
The model property in the state machine is up to the client code to define; it will typically be the model object whose state is being managed, i.e. the domain-specific owner of the state machine instance.
As a sample implementation I created an MPApplicationState which tracks the app states of starting, running, terminating, owned by my AppDelegate class. This demonstrates the pattern of how valid next-states can be handled by a simple switch statement for most state collections, but you could always add helper methods for various state-transition contexts for more complex cases that need to inspect the model:
enum MPApplicationState : Int, MPState {
case starting
case running
case terminating
var rawEnumValue: Int {
get {
return self.rawValue
}
}
var validNextStates: [Int] {
get {
var nextStates : [Int] = []
switch self {
case .starting:
nextStates.append(MPApplicationState.running.rawValue)
nextStates.append(MPApplicationState.terminating.rawValue)
case .running:
nextStates.append(MPApplicationState.terminating.rawValue)
case .terminating:
nextStates.removeAll()
}
return nextStates
}
}
func didEnter(from prevState: MPState?, with machine: MPStateMachine) {
print ("*** MPApplicationState *** did enter state: \(String(describing:self)) from \(String(describing:prevState))")
}
func willExit(to nextState: MPState, with machine: MPStateMachine) {
print ("*** MPApplicationState *** will exit state: \(String(describing:self)) to state: \(String(describing:nextState))")
}
}
Just to show the functionality (not really because it's useful) I created an MPStateMachine property on my AppDelegate (allocated at the declaration), and transition states in the following method overrides:
func applicationWillFinishLaunching(_ notification: Notification) {
_ = self.stateMachine.enter(MPApplicationState.starting)
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
_ = self.stateMachine.enter(MPApplicationState.running)
// etc...
}
func applicationWillTerminate(_ aNotification: Notification) {
_ = self.stateMachine.enter(MPApplicationState.terminating)
}
I'm not bothering to check the output values for these simple cases but the idea is that more involved state transitions could do so, and/or pre-check the ability of the state machine to transition with a call a like:
self.stateMachine.canEnterState(MPApplicationState.running)
And now checking the current application state looks a bit nicer than it would with GKStateMachine:
if appDelegate.stateMachine.currentState != MPApplicationState.terminating {
// do something...
That's it!
No comments:
Post a Comment