Fortunately, one of the original design patterns that GoF described is the State pattern. Closely resembling the Strategy pattern, the State pattern is used when an application's behavior depends on changing states at runtime or has complex conditional statements that branch depending on a current state (see Figure 3). When the internal states change, an object alters its behavior when designed using the State pattern.
Figure 3. State design structure
Using the SDP, all of the behaviors (methods) for a single state are placed into single objects (concrete states), and all transition behaviors for the application (state machine) are placed into a single interface. Each state object implements the interface in a fashion appropriate for the state. Because of this structure, no conditional statements are required to branch differentially depending on the current state. Rather than writing complex conditional statements, the individual state objects define how the methods are to behave for that state.
For example, with a two-state machine (Play and Stop) the following pseudo code could direct the state behavior to start playing the video depending on the state machine's current state:
function doPlay():void{
if(state == Play)
{
trace("You're already playing.");
}
else if (state == Stop)
{
trace("Go to the Play state.");
}
}
Note: By the way, an important but small difference between ActionScript 2.0 and ActionScript 3.0 is that all void special types are in lowercase. In ActionScript 2.0 the first letter was in caps: Void. Watch out for that!
With a couple of states, that's not too difficult. As you add states, however, things get more complicated and you're swimming in a sea of conditional statements that all have to work in sync.
The alternative is to set up "contextual" behavior using a State pattern. For example, the following code has two different objects with different implementations of behaviors from an interface:
//Interface
interface State
{
function startPlay():void;
function stopPlay():void;
}
//Play State object
class PlayState implements State
{
public function startPlay():void
{
trace("You're already playing");
}
public function stopPlay():void
{
trace("Go to the Stop state.");
}
}
//Stop State object
class StopState implements State
{
public function startPlay():void
{
trace("Go to the Play state.");
}
public function stopPlay():void
{
trace("You're already stopped");
}
}
As you can see, the behaviors (methods) have different implementations in the different states. When you add more states, all you need to do is add their transitional behaviors to the interface and create a new concrete state (class) that implements them. Each new behavior needs to be added to the existing state classes.
To manage the states and their transitions, you need some kind of management object—something to keep track of everything in the state machine. In Figure 3 the Context box is the abstraction of the state engine. The context manages the different states that make up the state machine and contain the different states. Figure 4 shows a more concrete representation of what needs to be transformed.
Figure 4. Video player state diagram
In looking at the example of creating a simple video player, we need a context that will serve to get and set the different states. So the next phase will be to look at a class (object) that does just that. Let's first take a look at the class, and then you'll see what's going on:
01 package
02 {
03 //Context class
04 class VideoWorks
05 {
06 var playState:State;
07 var stopState:State;
08 var state:State;
09 public function VideoWorks()
10 {
11 trace("Video Player is On");
12 playState = new PlayState(this);
13 stopState = new StopState(this);
14 state=stopState;
15 }
16 public function startPlay():void
17 {
18 state.startPlay();
19 }
20 public function stopPlay():void
21 {
22 state.stopPlay();
23 }
24 public function setState(state:State):void
25 {
26 trace("A new state is set");
27 this.state=state;
28 }
29 public function getState():State
30 {
31 return state;
32 }
33 public function getPlayState():State
34 {
35 return this.playState;
36 }
37 public function getStopState():State
38 {
39 return this.stopState;
40 }
41 }
42 }Initially, in lines 6–14, the script instantiates three State objects—one of each of the two you designed (PlayState and StopState), and one (state) that acts as a variable to hold the current state. Because the state machine begins in the Stop state, the state variable is assigned the Stop state. (This works just like your car in the morning before you change it from the Off state to the On state.)
Next, the two behaviors from the State interface are specified in terms of the current state's context (lines 16–23). Although you're going to have to add some code to the two state classes for it to work with the context class, for now think of what will happen in the two different states when those behaviors are executed. For example, in the Play state the startPlay() method doesn't do anything but in the Stop state it switches to the Play state.
Finally, add the getter and setter methods (lines 24–40). You need a total of six methods—a set and get function for each of the three state instances. The setters return nothing and the getters return a State object.
To get everything working, you need to revise the state classes to include the reference to the context: VideoWorks. Also, because you're working with ActionScript 3.0, all of the classes and interface need to be in a package container.
Note: All files are grouped into a single unreferenced folder for purposes of simplicity. However, typically you would be using multiple folders for organizing your files. Each package would import the appropriate related files. But by using a single folder, this application reduces a layer of complexity.
Save the following code as State.as:
package
{
//State Machine Interface
interface State
{
function startPlay():void;
function stopPlay():void;
}
}
Save the following code as PlayState.as:
package
{
//Play State
class PlayState implements State
{
var videoWorks:VideoWorks;
public function PlayState(videoWorks:VideoWorks)
{
trace("--Play State--");
this.videoWorks=videoWorks;
}
public function startPlay():void
{
trace("You're already playing");
}
public function stopPlay():void
{
trace("Stop playing.");
videoWorks.setState(videoWorks.getStopState());
}
}Save the following code as StopState.as:
package
{
//Stop State;
class StopState implements State
{
var videoWorks:VideoWorks;
public function StopState(videoWorks:VideoWorks)
{
trace("--Stop State--");
this.videoWorks=videoWorks;
}
public function startPlay():void
{
trace("Begin playing");
videoWorks.setState(videoWorks.getPlayState());
}
public function stopPlay():void
{
trace("You're already stopped");
}
}
}Save the following code as VideoWorks.as:
package
{
//Context class
class VideoWorks
{
var playState:State;
var stopState:State;
var state:State;
public function VideoWorks()
{
trace("Video Player is On");
playState = new PlayState(this);
stopState = new StopState(this);
state=stopState;
}
public function startPlay():void
{
state.startPlay();
}
public function stopPlay():void
{
state.stopPlay();
}
public function setState(state:State):void
{
trace("A new state is set");
this.state=state;
}
public function getState():State
{
return state;
}
public function getPlayState():State
{
return this.playState;
}
public function getStopState():State
{
return this.stopState;
}
}
}To test the state engine completely, you need to test each state. By calling each state from a different state as well as from within itself, you can see the contextual nature of the state machine. The following steps show you how:
In order to use the Stage and execute a test of the program in ActionScript 3.0, you need to use the Display class. In general you can use either a sprite or a movie clip, but because you're not using a Timeline here, use a sprite. Open a new ActionScript file and enter the following code as TestState.as:
package
{
//Test states
import flash.display.Sprite;
public class TestState extends Sprite
{
public function TestState():void
{
var test:VideoWorks = new VideoWorks();
test.startPlay();
test.startPlay();
test.stopPlay();
test.stopPlay();
}
}
}TestState in the Document Class text window.Test the movie by pressing Control+Enter (Command+Return) just as you would for any application test. In the Output window, you should see the following:
Video Player is On --Play State-- --Stop State-- Begin playing A new state is set You're already playing Stop playing. A new state is set You're already stopped
Both the VideoWorks class and the PlayState and StopState classes include a trace() statement to indicate their instantiation, and appear as soon as you test the script. Because the initial state is Stop, when the script calls the first startPlay() method, you will see "Begin playing" in the output window, along with the message, "A new state is set."
However, note that when the same state is called a second time, the state responds with the message, "You're already playing," and no state transition occurs. However, as soon as the script calls the first stopPlay() method, it notes both that the playing has stopped and a new state is set. The second call of stopPlay(), though, responds with "You're already stopped."
The advantage of making each state self-aware becomes clear when you consider the alternative. For example, if I knock together an application that I want to use for playing FLV files, what happens if I click the Play button when it's already playing? It starts playing the video all over again because it has no idea what state it's in. However, if I click Play and have a state machine design, the application knows it's in a Play state and will require the user to transition first to the Stop state before starting the Play over again. Of course, you can design the state machine to restart playing if that's what you want to happen on all Play commands.
The point is that you can design the states to do exactly what you want—not what they will do automatically if left unconsidered.
All you've seen so far has been the output of trace() statements to help understand how a state design pattern and state machine works. To add something useful, you need to include a reference to both a NetStream() object and a string for referencing a FLV file. However, you need only a string reference for playing the video because you can stop it simply by closing the NetStream() instance.
Below is an update to the VideoWorks.as file. Importantly, I've added the NetConnection and NetStream classes from the flash.net package because both classes have been included in the user package created. While the new script has set up the state machine to actually play and stop a video, all of the trace() statements have been left in place:
package
{
import flash.net.NetStream;
interface State
{
function startPlay(ns:NetStream,flv:String):void;
function stopPlay(ns:NetStream):void;
}
}Save the following code as PlayState.as:
package
{
import flash.net.NetStream;
//Play State
class PlayState implements State
{
var videoWorks:VideoWorks;
public function PlayState(videoWorks:VideoWorks)
{
trace("--Play State--");
this.videoWorks=videoWorks;
}
public function startPlay(ns:NetStream,flv:String):void
{
trace("You're already playing");
}
public function stopPlay(ns:NetStream):void
{
ns.close();
trace("Stop playing.");
videoWorks.setState(videoWorks.getStopState());
}
}
}Save the following code as StopState.as:
package {
import flash.net.NetStream;
class StopState implements State
{
var videoWorks:VideoWorks;
public function StopState(videoWorks:VideoWorks)
{
trace("--Stop State--");
this.videoWorks = videoWorks;
}
public function startPlay(ns:NetStream,flv:String):void
{
ns.play(flv);
trace("Begin playing");
videoWorks.setState(videoWorks.getPlayState());
}
public function stopPlay(ns:NetStream):void
{
trace("You're already stopped");
}
}
}Save the following code as VideoWorks.as:
package
{
import flash.net.NetStream;
class VideoWorks
{
var playState:State;
var stopState:State;
var state:State;
public function VideoWorks()
{
trace("Video Player is on");
playState = new PlayState(this);
stopState = new StopState(this);
state=stopState;
}
public function startPlay(ns,flv):void
{
state.startPlay(ns,flv);
}
public function stopPlay(ns):void
{
state.stopPlay(ns);
}
public function setState(state:State):void
{
trace("A new state is set");
this.state=state;
}
public function getState():State
{
return state;
}
public function getPlayState():State
{
return this.playState;
}
public function getStopState():State
{
return this.stopState;
}
}
}The FLA that tests the state engine uses the new ActionScript 3.0 SimpleButton class to create buttons for starting and stopping the video play. Like most classes, this one is set up for reuse whenever an application requires a button labeled with text. A second class file, BtnState.as, provides the necessary styling context. Used together, the two classes can easily create and style different buttons. Thus, before writing the test code in the Flash authoring environment, create the following classes in an ActionScript file and save as BtnState.as and NetBtn.as, respectively, in the same folder as the VideoWorks.as file:
package
{
//Create a button that will display states #5
import flash.display.Sprite;
import flash.display.Shape;
import flash.text.TextFormat;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
class BtnState extends Sprite
{
private var btnLabel:TextField;
private var btnWidth:Number;
private var bkground:Shape
public function BtnState (color:uint,color2:uint,btnLabelText:String)
{
btnLabel=new TextField ;
btnLabel.text=btnLabelText;
btnLabel.x=5;
btnLabel.autoSize=TextFieldAutoSize.LEFT;
var format:TextFormat=new TextFormat("Verdana");
format.size=12;
btnLabel.setTextFormat (format);
btnWidth=btnLabel.textWidth + 10;
bkground=new Shape;
bkground.graphics.beginFill (color);
bkground.graphics.lineStyle (2,color2);
bkground.graphics.drawRect (0,0,btnWidth,18);
addChild (bkground);
addChild (btnLabel);
}
}
}
package
{
//Button for transition triggers #6
import flash.display.SimpleButton;
public class NetBtn extends SimpleButton
{
public function NetBtn (txt:String)
{
upState = new BtnState(0xfab383, 0x9e0039,txt);
downState = new BtnState(0xffffff,0x9e0039, txt);
overState= new BtnState (0x9e0039,0xfab383,txt);
hitTestState=upState;
}
}
}Keep BtnState.as and NetBtn.as handy; they will be used to provide buttons in all of the developing examples.
The final step is to create a script for bringing everything to the Stage. The first step is to build a testing class as you did with the abstract state machine. Open up a new ActionScript file and add the following code (saving it as TestVid.as):
package
{
//Test State Machine #7
import flash.display.Sprite;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.media.Video;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
public class TestVid extends Sprite
{
private var nc:NetConnection=new NetConnection();
private var ns:NetStream;
private var vid:Video=new Video(320,240);
private var vidTest:VideoWorks;
private var playBtn:NetBtn;
private var stopBtn:NetBtn;
private var flv:String;
private var flv_txt:TextField;
private var dummy:Object;
public function TestVid ()
{
nc.connect (null);
ns=new NetStream(nc);
addChild (vid);
vid.x=(stage.stageWidth/2)-(vid.width/2);
vid.y=(stage.stageHeight/2)-(vid.height/2);
//Instantiate State Machine
vidTest=new VideoWorks();
//Play and Stop Buttons
playBtn=new NetBtn("Play");
addChild (playBtn);
playBtn.x=(stage.stageWidth/2)-50;
playBtn.y=350;
stopBtn=new NetBtn("Stop");
addChild (stopBtn);
stopBtn.x=(stage.stageWidth/2)+50;
stopBtn.y=350;
//Add Event Listeners
playBtn.addEventListener (MouseEvent.CLICK,doPlay);
stopBtn.addEventListener (MouseEvent.CLICK,doStop);
//Add the text field
flv_txt= new TextField();
flv_txt.border=true;
flv_txt.borderColor=0x9e0039;
flv_txt.background=true;
flv_txt.backgroundColor=0xfab383;
flv_txt.type=TextFieldType.INPUT;
flv_txt.x=(stage.stageWidth/2)-45;
flv_txt.y=10;
flv_txt.width=90;
flv_txt.height=16;
addChild (flv_txt);
//This prevents a MetaData error being thrown
dummy=new Object();
ns.client=dummy;
dummy.onMetaData=getMeta;
//NetStream
ns.addEventListener (NetStatusEvent.NET_STATUS, flvCheck);
}
//MetaData
private function getMeta (mdata:Object):void
{
trace (mdata.duration);
}
//Handle flv
private function flvCheck (event:NetStatusEvent):void
{
switch (event.info.code)
{
case "NetStream.Play.Stop" :
vidTest.stopPlay (ns);
vid.clear ();
break;
case "NetStream.Play.StreamNotFound" :
vidTest.stopPlay (ns);
flv_txt.text="File not found";
break;
}
}
//Start play
private function doPlay (e:MouseEvent):void
{
if (flv_txt.text != "" && flv_txt.text != "Provide file name")
{
flv_txt.textColor=0x000000;
flv=flv_txt.text + ".flv";
vidTest.startPlay (ns,flv);
vid.attachNetStream (ns);
}
else
{
flv_txt.textColor=0xcc0000;
flv_txt.text="Provide file name";
}
}
//Stop play
private function doStop (e:MouseEvent):void
{
vidTest.stopPlay (ns);
vid.clear ();
}
}
}As you can see, the code includes importing several different packages. In using ActionScript 3.0, you will find that it is crucial to import the right packages for your applications. By avoiding the use of the wildcard character (*), the application imports only exactly what you need and no more, thereby keeping the overhead down.
Open a new Flash document and save it in the same folder as all of the other files for this application. In the Document Class window of the Property inspector, type TestVid and save the file.
To test actual the application, you will need an FLV file. You can convert an existing video file (AVI or MOV format) or use any FLV file on hand. Place the file in the same folder as the application. Figure 5 shows what your initial state machine looks like when completed.
Figure 5. Initial state of the video player
The UI is simple and relates to the transitions: Stop and Start (playing video). Furthermore, you can see the relationship between the video playing and the trace() statement showing what happens when you click the button. For example, if you click Start and the video is already playing, nothing new occurs because the startPlay() function in the Play state does nothing other than offering a trace() statement to the effect that you're already playing. As an added bonus, you get to see the length of your FLV file from the metadata function.
A fundamental feature of virtually all design patterns is their ability to expand and accept change. The kind of change you're expecting in an application to some extent determines the type of design pattern you select. In this particular application, you are adding states.
The first state to add to the state machine is a Pause state. This state exists only in the Play state; you cannot get there directly from the Stop state. Once in the Play state, the user can turn the Pause state on and off. A hierarchical state diagram depicts this new state accurately (see Figure 6).
Figure 6. Hierarchical statechart including the Pause state
The hierarchy is a simple one. The first level is the Play and Stop states. Within the Play state is the Pause and No Pause states.
Because the Pause function is a toggle between the Play and Pause states, the No Pause state is exactly the same as the Play state. So rather than creating "pause start" and "pause stop" functions, you can establish a Do Pause behavior that acts differently in either state. In the Pause state, the Do Pause behavior returns to the default Play state; in the Play state, it goes to the Pause state.
ActionScript 3.0 has two different options for creating a Pause state. First, you can create controls around NetStream.pause() and NetStream.resume() that stand as two different NetStream methods. In previous versions of ActionScript, only the pause() method was available and it worked as a toggle. Second, you can use the new method NetStream.togglePause(). This new method is actually just the old method with a new name. Sticking with the statechart depicted in Figure 6, this application uses the togglePause() method.