Statecharts and Angular.js

June 24, 2014

Motivation: Sidescroller game

Example from: Game Programming Patterns: State

Sidescroller Game


function Hero() {
}

Hero.prototype = {
  handleInput: function(event) {
    if (event.type == BUTTON_B) {
      this.jump();
    }
  },

  jump: function() {
    // animate jump...
  }
};
          

Jump Bugfix


Hero.prototype = {
  handleInput: function(event) {
    var self = this;

    if (event.type == BUTTON_B) {
      if (!this._isJumping) {
        this._isJumping = true;
        this.jump(function() {
          self._isJumping = false;
        });
      }
    }
  },

  jump: function(callback) {
    // animate jump, then invoke callback.
  }
};
          

Duck


Hero.prototype = {
  handleInput: function(event) {
    var self = this;

    if (event.type == BUTTON_B) {
      if (!this._isJumping) {
        this._isJumping = true;
        this.jump(function() {
          self._isJumping = false;
        });
      }
    }
    else if (event.type == PRESS_DOWN) {
      if (!this._isJumping) {
        this.duck();
      }
    }
    else if (event.type == RELEASE_DOWN) {
      this.stand();
    }
  },

  jump: function(callback) {},
  duck: function() {},
  stand: function() {}
};
          

Duck Bugfix


Hero.prototype = {
  handleInput: function(event) {
    var self = this;

    if (event.type == BUTTON_B) {
      if (!this._isJumping && !this._isDucking) {
        this._isJumping = true;
        this.jump(function() {
          self._isJumping = false;
        });
      }
    }
    else if (event.type == PRESS_DOWN) {
      if (!this._isJumping) {
        this._isDucking = true;
        this.duck();
      }
    }
    else if (event.type == RELEASE_DOWN) {
      if (this._isDucking) {
        this._isDucking = false;
        this.stand();
      }
    }
  },

  jump: function(callback) {},
  duck: function() {},
  stand: function() {}
};
          

Dive Attack


Hero.prototype = {
  handleInput: function(event) {
    var self = this;

    if (event.type == BUTTON_B) {
      if (!this._isJumping && !this._isDucking) {
        this._isJumping = true;
        this.jump(function() {
          self._isJumping = false;
        });
      }
    }
    else if (event.type == PRESS_DOWN) {
      if (!this._isJumping) {
        this._isDucking = true;
        this.duck();
      }
      else {
        this._isJumping = false;
        this.dive();
      }
    }
    else if (event.type == RELEASE_DOWN) {
      if (this._isDucking) {
        this._isDucking = false;
        this.stand();
      }
    }
  },

  jump: function(callback) {},
  duck: function() {},
  stand: function() {},
  dive: function() {}
};
          

Finite State Machine

Finite State Machine


function Hero() {
  var self = this;

  this._stateMachine = State.define(function() {
    this.state('standing', function() {
      this.enter(function() { self.stand(); });
      this.event('pressB', function() { this.goto('jumping'); });
    });

    this.state('ducking', function() {
      this.enter(function() { self.duck(); });
      this.event('releaseDown', function() { this.goto('standing'); });
    });

    this.state('jumping', function() {
      this.enter(function() { self.jump(); });
      this.event('pressDown', function() { this.goto('diving'); });
      this.event('jumpFinished', function() { this.goto('standing'); });
    });

    this.state('diving', function() {
      this.enter(function() { self.dive(); });
      this.event('diveFinished', function() { this.goto('standing'); });
    });
  });
}

Hero.prototype = {
  jump: function() {
    // do jump
    this._stateMachine.send('jumpFinished');
  },
  duck: function() {},
  stand: function() {},
  dive: function() {
    // do dive
    this._stateMachine.send('diveFinished');
  }
};
          

FSM Car Example

FSM Car Example

FSM Car Example

FSM Car Example

Solution: Statecharts!

What is a statechart?

  • A visual diagram for describing the behavior of a system.
  • An extension to a traditional state machine that provides the following features:
    • hierarchical states
    • orthogonal states
    • history states

Hierarchical states

  • A parent state is the XOR of its child states.
  • To be in state P, one must be in either state C1 or state C2, but not both.
  • P is an abstraction of C1 and C2 which allows you to denote common properties of both in an economical way.
  • Important: A statechart's current state must always be leaf state.

Hierarchical states


State.define(function() {
  this.state('P', function() {
    this.state('C1');
    this.state('C2');
  });
});
          

Orthogonal (Concurrent) states

  • AND decomposition: being in a state - the system must be in all of its components.
  • At any given point in time a statechart will be in a vector of states whose length is not fixed.
  • (A ⊕ B) ∧ (C ⊕ D) ∧ (E ⊕ F)
  • Eg: P1.A, P2.D, P3.E
  • Syncronicity - a single event causes multiple simultaneous happenings.

Orthogonal (Concurrent) states


State.define({concurrent: true}, function() {
  this.state('P1', function() {
    this.state('A');
    this.state('B');
  });

  this.state('P2', function() {
    this.state('C');
    this.state('D');
  });

  this.state('P3', function() {
    this.state('E');
    this.state('F');
  });
});
          

History states

  • When entering a parent state, transition to the most recently exited child state.
  • If this is the first time entering the parent state, transition to the default child state.

this.state('radio', {H: true}, function() {
  this.state('off');
  this.state('on');
});
          

Events

  • Usually triggered by the user (e.g. clicking a button, focusing a field, etc.)
  • When an event is sent to the statechart, each current state is given the opportunity to handle it.
  • Events bubble up the parent state chain.

Transitions

  • When a state handles an event, it typically triggers a state transition.
  • Transitions start from the current state (or states) and traverse up to the pivot state, triggering exit handlers along the way.
  • From the pivot state, child states are traversed, triggering enter handlers until the final substate is reached.

C1.2 -> C2.1

C1.2 -> C2.1

C1.2 -> C2.1

(W.P1.B, W.P2.D, W.P3.E) -> W.X.Z

(W.P1.B, W.P2.D, W.P3.E) -> W.X.Z

(W.P1.B, W.P2.D, W.P3.E) -> W.X.Z

W.X.Z -> (W.P1.B, W.P2.D, W.P3.E)

Statecharts in Angular.js

  • A $state service is shared between statechart and views.
  • States set $state properties upon entry.
  • States clean up $state properties upon exit.
  • Views configure themselves according to $state properties.
  • Views convert raw events to semantic events and forward to statechart.
  • Example 1
  • Example 2

Routing

  • States are mapped to route patterns.
  • When the location changes to match a route pattern, a transition is triggered to the state that is mapped to the matching pattern.
  • When a state with a defined route is entered, a path is generated from its route pattern and set to the current location.
  • Query params are used to handle concurrent states.

Alternatives

References