Have you ever been amazed at the variety of attacks in fighting games like Mortal Kombat, Super Smash Bros, Soul Calibur and others? Now you can learn how to create an engine to detect key combinations and build your own fighting game as well!
Final Result Preview
Let's take a look at the final result we will be working towards:
The combos in this demo are: ASDF
, AAA
, and SSS
. Type them!
Step 1: Introduction
Ever wanted to build a fighting game (or any other genre) with lots of combos? In this tutorial we will create a simple class to detect key combos and tell us when a combo has been done by the user. We will also create a very simple graphical interface to let us test our class.
Step 2: Starting a New Project
For this tutorial, we will use FlashDevelop's pure AS3 project with preloader. We will create a project with a preloader only to make it easy for you if you want to keep working on the final result towards a game. Let's start by opening FlashDevelop and selecting our project:
With that, we can begin working on our classes.
In order to use the graphics we'll create in Flash Pro within our AS3 project, we need to export our images from the .fla file to a .swc format. More information about this format can be found in Option 2 of this guide to FlashDevelop. Create a new AS3 FLA in Flash Professional, then change the settings on our .fla file to export its content to a .swc format: go to File > Publish Settings (or press Ctrl+Shift+F12) and check the "Export SWC" box under the "Flash" tab.
If you don't have Flash Professional, don't worry. I've included the final SWC file in the download package for this tutorial. Download it, then skip to Step 6.
Step 3: The Basic Shape of the Button
We will first create all the graphical part and worry only with the code later. Since we will be dealing with key combos, let's create a button with a letter in it to represent a key. Our button will be very simple: three circles with different colors and some filters in it. That's how I built mine: a big gray circle with a white circle on top of it, and a red circle on top of the white one. After that, I applied a glow and two drop shadow filters on the red circle in order to get the final result, which is included in the source files.
For more details on how the button was built, grab the source files for this tutorial!
Step 4: Up and Down Images
We now have to give our button "up" and "down" images. Before doing that, we need to turn it into a symbol. Let's convert it to a symbol, give it a name of KeyButtonImage
and export it as "SWCAssets.KeyButtonImage". We are adding the SWCAssets
package in the class name for organization purposes when we start coding. This will be more clear later.
On our KeyButtonImage
symbol, let's create another keyframe with the same image of the first one, and then reverse the angle of the filters that we use on the red circle. We will also need to label our frames in order to identify them in our code, so label the first frame as "Up" and the second frame as "Down". The down image should look like this:
Step 5: Generating the .swc File
Now that we have our button image ready, it's time to generate our .swc file and add it to our FlashDevelop project. In order to publish, press Alt+Shit+F12. You will notice that a .swc file has been created in the same directory of the flash file. Copy this file and put it in the "lib" folder of our FlashDevelop project. Right click it and select "Add to library" to make FlashDevelop recognize the file. The name of the file will turn blue once it's added to the library.
FlashDevelop is now ready to start using our button image!
Step 6: Our KeyButton Class
Our image is ready, so we need to create a class to hold the image and add functionalities to it. In FlashDevelop, add a new class in the src folder, name it KeyButton
and put flash.display.Sprite as the Base class.
Step 7: Adding the Image
Since our KeyButton
class inherits from the Sprite class, it can add images to its child display list. In this step we will add the image to our class and put the letter text inside it. Let's jump to the code:
package { import flash.display.Sprite; import flash.text.TextField; import flash.text.TextFormat; import flash.text.TextFormatAlign; import SWCAssets.KeyButtonImage; public class KeyButton extends Sprite { private var _image:KeyButtonImage; private var _text:TextField; public function KeyButton(letter:String) { _image = new KeyButtonImage(); _image.stop(); addChild(_image); _text = new TextField(); _text.defaultTextFormat = new TextFormat("Verdana", 28, 0xFFFFFF, true, null, null, null, null, TextFormatAlign.CENTER); _text.text = letter; _text.height = 38; _text.x = -(_text.width / 2); _text.y = -(_text.height / 2); _text.mouseEnabled = false; addChild(_text); } } }
In lines 11 and 13, we declare variables that will hold the image of our button and the text of the letter, respectively. In the constructor of our class (line 15) we ask for a string, which will be the letter that our button will represent.
Since our KeyButtonImage
has two frames, in line 18 we call the stop()
method to stop it from looping through them. We will define specific moments for the image to switch frames later. Line 20 adds the image to the button's child list.
From line 22 to line 30, we create our text field that will contain a letter, position it and disable it from mouse events (not necessary, but good to do if your text field is supposed to do nothing but show the text). Line 32 adds the text on the button's child list.
Step 8: Creating the Image Objects
Our KeyButton
class can already show an image and represent a letter, so in this step we will add a few buttons on the screen. Since we are building only an example to test our class, we will not add all letters in the example. Instead, we will work only with 4 letters (but our class will be able to detect combos with any keys!): A, S, D and F. We will add them to our screen now:
private var keyButtons:Array = []; private var keys:Array = ["A", "S", "D", "F"]; private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } }
Line 1 creates an array to contain all the buttons in our screen. This will be very useful later, because it will let us loop through the array instead of checking button by button. Line 2 only defines which keys we will be working with (as said, A, S, D and F). Lines 12-16 are inside a loop that will create the 4 buttons and position them on the screen.
You can now compile the project and see the buttons in the screen:
Step 9: Creating the ComboHandler Class
We are ready now to begin working on the detection of combos. For that, we will create a ComboHandler
class. Just follow the same steps you did to create the KeyCombo
class, but this time our ComboHandler
class will not have a base class.
What should be the first part of the ComboHandler
class? Well, we will first need to detect when a key has been pressed. In order to do that, we need to add an event listener to the stage (remember: as suggested by the ActionScript 3 reference, in order to listen globally for KeyboardEvent
event listeners, they should be added to the stage!
package { import flash.display.Stage; import flash.events.KeyboardEvent; public class ComboHandler { public static function initialize(stageReference:Stage):void { stageReference.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } private static function onKeyDown(e:KeyboardEvent):void { } } }
This code only builds the basic structure of the ComboHandler
class. A lot more will be added later! Notice that we only used static methods. That is because we will only have one ComboHandler
class in our example. Some suggestions on improvements on this class are available in the conclusion step.
Our ComboHandler
class needs to be initialized through the initialize()
method in order to have the listener added to the stage. In our Main
class, we should initialize the class before working with it. Let's head to Main.as and do that:
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } ComboHandler.initialize(stage); }
Step 10: Registering Combos
We have the basic structure of the ComboHandler
class built, so now we need to add things to it. The first thing is to register combos in the class, so it can detect a combo.
Now, how will we store the combos in this class? You may have heard of the Dictionary class. This class can hold any value based on a key. In our class, the keys are going to be the combo names, and the value will be an array, with each index being a key from the combo. The code for it (explained below):
private static var combos:Dictionary; public static function initialize(stageReference:Stage):void { combos = new Dictionary(); stageReference.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } public static function registerCombo(comboName:String, comboKeys:Array):Boolean { if (combos[comboName]) { return false; } combos[comboName] = comboKeys; return true; }
In line 1, we create the dictionary that we talked about. Line 5 initializes it, and the function registerCombo()
registers a combo in the dictionary. This function will return true
if the combo was successfully registered, or false
if there was already a combo with that name in the class (in which case you may need to remove the old combo - see Step 19 for that!).
Step 11: Registering a Pressed Key and Setting up Interval Time
Another thing that our class should have is a maximum interval time between each key press. In some games that have combos, you probably noticed that when you press the key A, for instance, wait a second and press the key B (assuming there is the "AB" combo), no combo will be detected, because you waited too much to press the B key. That happens because there is a maximum interval time between each key press. That's exactly what we will do in our class. So, in the ComboHandler
class:
private static var pressedKeys:Array; private static const MAX_INTERVAL:int = 250; // Milliseconds private static var interval:int; public static function initialize(stageReference:Stage):void { combos = new Dictionary(); interval = 0; stageReference.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } private static function onKeyDown(e:KeyboardEvent):void { if (getTimer() - interval > MAX_INTERVAL) { pressedKeys = []; } interval = getTimer(); pressedKeys.push(e.keyCode); }
In line 1, we create an array called pressedKeys
. This array will contain all the keys pressed by the user, with the interval time between two keys being less than the maximum interval.
Lines 3 and 4 define a MAX_INTERVAL
constant, which will be our maximum interval, and the interval
variable, which will help us calculate what was the interval time between two key presses.
The onKeyDown()
function is almost finished now: in it, we first detect if the interval time between the current key press and the latest key press is higher than the maximum interval. If it is, then we reset our pressedKeys
array, to delete all keys that were in it. After that, we update the interval variable to the current time (see the documentation of getTimer() for more details on how we calculate the interval) and push()
the current key to the pressedKeys
array.
Step 12: Verifying Whether a Combo has Been Done or Not
One last thing is missing in our ComboHandler
's onKeyDown()
function: the ability to check whether, after a key has been pressed, a combo was performed by the user. This is what we will do now:
private static function onKeyDown(e:KeyboardEvent):void { if (getTimer() - interval > MAX_INTERVAL) { pressedKeys = []; } interval = getTimer(); pressedKeys.push(e.keyCode); checkForCombo(); } private static function checkForCombo():void { var i:int; var comboFound:String = ""; for (var comboName:String in combos) { if (pressedKeys.join(" ").indexOf((combos[comboName] as Array).join(" ")) > -1) { comboFound = comboName; break; } } // Combo Found if (comboFound != "") { pressedKeys = []; } }
Line 12 is the only change that we made in our onKeyDown()
function: call the checkForCombo()
function. This will check whether a combo has been performed or not.
The way we check if a combo has been performed is very interesting: we work with strings. Working with strings, in this case, allows us to detect things that would have been harder without working with them. For instance, imagine if we had a combo with the keys ASDF, but the pressedKeys
array has the following sequence of keys: ASFDASDF. Even though the user has pressed the first four keys ("ASFD", which don't correspond to a combo) within the time limit, this shouldn't change the fact that the user has performed a combo, as indicated by the last 4 keys ("ASDF"). Without strings, our work could have been much longer.
The idea of working with strings is this: we put all the keys in pressedKeys
inside a string, separating each index by a space (thus the pressedKeys.join(" ")
function call), then check if there is a particular substring inside it. This particular substring is a string formed by the keys of a combo, also with each key separated by a space. If this substring is found, then that means that a combo has been performed.
You can test this yourself with some code like this:
pressedKeys = ["A", "S", "F", "D", "A", "S", "D", "F"]; checkForCombo();
...though you'll also want to add a temporary trace(comboFound)
call in checkForCombo()
to see the result.
Notice, however, that this method will not work on all cases. It will not work if, instead of having an array of Strings, we had an Array of Objects, for example. If we had an array of Object, the default toString() function that is called when join() is called would print "[object Object]", and thus all our objects in the array would be the "same" in the string created. If you still would like to do that, simply override the default toString() function and put a custom text there. In Step 14 we will do that on the ComboEvent class - take a look at it for a reference!
Now, we will stop focusing on the ComboHandler
class and work again on the buttons that we created. (Remove the test code you just added.)
Step 13: Making our Buttons Act on a Key Event
Currently, our buttons are on the screen, but they show us nothing. You may remember that we created two button images: one for when the button is not pressed and other for when it is pressed. In this step, we will make our buttons behave when a key has been pressed. Let's head to our KeyButton
class:
public function onUp():void { _image.gotoAndStop("Up"); } public function onDown():void { _image.gotoAndStop("Down"); }
Notice that this code only changes the frames of the button's image. We still have to call them when the corresponding key has been pressed. We'll do that in Main.as:
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } ComboHandler.initialize(stage); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); } private function onKeyDown(e:KeyboardEvent):void { var i:int; for (i = 0; i < 4; i++) { if (String.fromCharCode(e.keyCode) == keys[i]) { KeyButton(keyButtons[i]).onDown(); break; } } } private function onKeyUp(e:KeyboardEvent):void { var i:int; for (i = 0; i < 4; i++) { if (String.fromCharCode(e.keyCode) == keys[i]) { KeyButton(keyButtons[i]).onUp(); break; } } }
Lines 18 and 19 are the only additions to the init()
function. They add more event listeners on the stage to detect when a key has been pressed and when a key has been released. We'll use these two listeners to tell our buttons whether they should be up or down.
The only thing that you may have never seen in the onKeyDown()
and onKeyUp()
functions is the String.fromCharCode()
function. This function returns a string with the char codes that you pass as arguments. Since we pass only one char code, that function will return a string with only one character, and if it matches the strings that we have in the keys array, that means we should tell the correspondent button to act.
You can now test the buttons going up and down!
Step 14: Creating a Custom Combo Event
Right now, our ComboHandler
class detects key presses, handles interval times, verifies when combos have been performed, but it still doesn't act when a combo has been performed. How can it tell other things that a combo has been done? That's what we will begin here. Since Flash has a very strong event system, we will dispatch an event for when a combo has been detected, and let other objects receive these events.
For more information about custom events, please visit this link on 8bitrocket. Create the ComboEvent
class with this code in it:
package { import flash.events.Event; public class ComboEvent extends Event { public static const COMBO_FINISHED:String = "comboFinished"; public var params:Object; public function ComboEvent(type:String, params:Object, bubbles:Boolean = false, cancelable:Boolean = false) { super(type, bubbles, cancelable); this.params = params; } public override function clone():Event { return new ComboEvent(type, this.params, bubbles, cancelable); } public override function toString():String { return formatToString("ComboEvent", "params", "type", "bubbles", "cancelable"); } } }
In line 9 we declare the params
variable. It will contain informations about the combo that has been performed (in this example we will only put the name, but you can put whatever you want). Notice the clone()
function in the class. It is created to allow redispatched events to contain the same information of the original event. For more information about it, visit this blog post on Bit 101
Step 15: Dispatching an Event Whenever a Combo has Been Done
Now, in our ComboHandler
class, it's time to act when a combo has been performed. In order to do that, we need to dispatch a ComboEvent
. Events can only be dispatched by objects that inherit from EventDispatcher
, but ComboHandler
doesn't inherit from EventDispatcher
. Instead of making it inherit from EventDispatcher
(that would force us to have an instance of ComboHandler
, which we don't want for the purposes of this tutorial), we will create an EventDispatcher
object in the class, and make this object dispatch events. Additionally, other classes will listen to this object for events. In our ComboHandler.as file:
Import this:
import flash.events.EventDispatcher;
And add this code:
public static var dispatcher:EventDispatcher; public static function initialize(stageReference:Stage):void { combos = new Dictionary(); interval = 0; dispatcher = new EventDispatcher(); stageReference.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } private static function checkForCombo():void { var i:int; var comboFound:String = ""; for (var comboName:String in combos) { if (pressedKeys.join(" ").indexOf((combos[comboName] as Array).join(" ")) > -1) { comboFound = comboName; break; } } // Combo Found if (comboFound != "") { pressedKeys = []; dispatcher.dispatchEvent(new ComboEvent(ComboEvent.COMBO_FINISHED, {comboName: comboFound} )); } }
In line 1, we declare our dispatcher object. In line 9, we initialize it. In line 34, we dispatch a ComboEvent
. And that's it for the ComboHandler
class. Now we need to listen for the event that is being dispatched.
Step 16: Checking For an Event in Main
Checking for custom events is done in the same way that you deal with any other event in AS3: add a listener to the dispatcher and create a function to do something when the listener receives an event. Our Main
class should be the one to receive the event, because it will show something on the screen when a combo is detected.
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } ComboHandler.initialize(stage); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); ComboHandler.dispatcher.addEventListener(ComboEvent.COMBO_FINISHED, onComboComplete); } private function onComboComplete(e:ComboEvent):void { }
The highlighted line is where we add the listener to ComboHandler
's dispatcher
.
Step 17: Do Something When a Combo has Been Done
In this tutorial, we will just display text on the screen with the name of the combo that was performed. We will need a textfield on the screen and we will need to get the name of the combo that was performed.
private var textField:TextField; private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } ComboHandler.initialize(stage); textField = new TextField(); textField.defaultTextFormat = new TextFormat("Verdana", 20, 0x000000, true); textField.x = 100; textField.y = 200; textField.width = 350; textField.text = "No combo"; addChild(textField); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); ComboHandler.dispatcher.addEventListener(ComboEvent.COMBO_FINISHED, onComboComplete); } private function onComboComplete(e:ComboEvent):void { textField.text = e.params.comboName; }
Line 1 contains the declaration of the text field that we will be using. Lines 20-27 contain the initialization of the text field. In our onComboComplete()
function, we only put the combo name in the text of the text field. That's it! Now the only thing left to do is to register some combos and test the class!
Step 18: Registering Combos and Testing
In the Main.as file, let's register some combos: I will register the "AAA Combo", "SSS Combo" and "ASDF Combo". You can register as many combos as you want!
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); var i:int; for (i = 0; i < 4; i++) { keyButtons[i] = new KeyButton(keys[i]); KeyButton(keyButtons[i]).x = 100 + (100 * i); KeyButton(keyButtons[i]).y = 50; addChild(KeyButton(keyButtons[i])); } ComboHandler.initialize(stage); ComboHandler.registerCombo("AAA Combo", [65, 65, 65]); ComboHandler.registerCombo("SSS Combo", [83, 83, 83]); ComboHandler.registerCombo("ASDF Combo", [65, 83, 68, 70]); textField = new TextField(); textField.defaultTextFormat = new TextFormat("Verdana", 20, 0x000000, true); textField.x = 100; textField.y = 200; textField.width = 350; textField.text = "No combo"; addChild(textField); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); ComboHandler.dispatcher.addEventListener(ComboEvent.COMBO_FINISHED, onComboComplete); }
Compile the project and that's what you get:
Step 19: Removing a Combo From the Combos List
What if we wanted to remove a combo that has been registered? The following function can be added in the ComboHandler.as file to reset the combos dictionary's position to the default. It will return true if the combo was removed, and false if it wasn't (there wasn't a combo registered with that name).
public static function removeCombo(comboName:String):Boolean { if (combos[comboName]) { combos[comboName] = undefined; return true; } return false; }
Conclusion
Well, congratulations for finishing this tutorial! You are ready to implement a basic combo handler for your games!
However, a basic combo handler doesn't fit all games. What if you wanted to use a game that had different characters, and each character had its combos and you needed more control over the handlers? You can easily do that if you allow instantiation of the ComboHandler
class, which will remain as a challenge for you.
You could also turn that class into a Singleton, instead of having only static functions. There are many other creative uses for it, including registering words as a combo and checking whether someone typed a word or not.
Did you have a creative use for this class? Share it with us in the comments section!
Comments