2011年10月8日星期六

How To Make a Simple iPhone Game with Flash CS5

How To Make a Simple iPhone Game with Flash CS5:

This is a tutorial by iOS Tutorial Team member Russell Savage, the founder of Dented Pixel, a creative development company. He recently released its first app – Princess Piano – and he is currently hard at work on several new projects.


Create a simple Air Hockey game for the iPhone in Flash!

Create a simple Air Hockey game for the iPhone in Flash!


I originally got into Objective C reluctantly. I had been holding out because I heard that the upcoming Flash CS5 would be able to compile to the iPhone, and because I was well versed with writing Actionscript code, I decided I could wait a couple of months and save myself the effort of having to learn a new language.


When Flash CS5 came out, it did in fact compile to an iPhone build but the performance was painfully slow, it was really unusable even for the simplest of games.


Luckily, things have changed – with Adobe’s latest release CS5.5 that slow performance is a thing of the past, and making an iPhone game with Flash is a viable option! Not only is developing in Flash quite quick and easy, but it also offers some additional benefits like being able to export to Android and of course to the web, with minimal or no changes to the code.


In this tutorial, you’ll get hands-on experience creating a simple iPhone game with Adobe Flash CS5.5. You’ll take a hockey paddle, and use it to flick a hockey puck around the board to hit targets, and gain points!


To go through this tutorial, you don’t need any prior experience with Adobe Flash or Actionscript, because I’ll walk you through step by step. In fact you don’t even need Flash itself, because you can download a free trial!


So let’s get started with a Flash!



Getting Started


First things first, you need a copy of Adobe Flash CS5.5.


If you don’t have a copy of Flash CS5.5, you can download a free trial of version of it here, that has no limitations besides the fact that it only lasts for a month.


Important note: Unfortunately, CS5.5 does not ship with the latest version of AIR which is Adobe’s runtime system for applications. I would highly recommend upgrading the version of AIR to the latest before you publish your game. You can find instructions from Adobe on how to do so here .


So once you have your copy of Flash CS5.5 ready to go, let’s move on!


Setting Things Up


Create a new document in Flash by selecting File->New. Set the type to “Air for iOS” and set the framerate to 60 frames per second (fps). Also set the document size to 640×960 – this way we can target the retina display and scale down for older phones.


Creating a new project with Flash CS5.5


Let’s start by creating a graphic for our air-hockey paddle. One advantage to Flash is that you can create graphics and animations right from inside the same app!


Click on Insert->New Symbol. You need to give the symbol a name, so name it “HockeyPaddle”.


Click on Advanced if it’s not already expanded to define what this symbol will be referred to as in our Actionscript code. Click on Export for Actionscript and the class name should auto-fill in as “HockeyPaddle”. Click on OK to create the symbol.


Creating a new symbol for the hockey paddle


Now we need to draw our object. Tap the “O” key to bring up the circle tool. I am going to create a medium sized circle about 80 pixels in diameter, and I will make another circle inside of it to make it extra purty.


Here’s an example of what it can look like (but be as creative as you want!)


A Hockey Paddle drawn in Flash


Now we are going to write our first code! First we will make the Document Class that will act as the central piece of code, so go to File\New and select “Actionscript 3.0 Class” (not plain old “ActionScript 3.0!”)


Name your class “HockeyMain”. Then save the class (the default should autofill to HockeyMain.as which is correct).


Before we can see our handywork we need to tell the Flash project to use this document class, so switch back to the Flash project and click on “Scene 1″ in the breadcrumb bar to switch back to the Scene. In the Document Properties on the side there is a Class property – set this to “HockeyMain”.


Setting the Document Class of a scene in Flash CS 5.5


Your code is almost ready to run, but you won’t see anything on if you publish it, so let’s add our hockey paddle first. Replace the contents of HockeyMain.as with the following:



package  {

import flash.display.Sprite;

public class HockeyMain extends Sprite {

var hockeyPaddle:Sprite;

public function HockeyMain() {
hockeyPaddle = new HockeyPaddle() as Sprite;
this.addChild(hockeyPaddle);
}
}
}


This imports the Sprite class we need, marks HockeyMain as extending Sprite, then creates a HockeyPaddle and adds it to the screen.


The :Sprite is added to the variable so the compiler knows it will be of type Sprite, also the “as Sprite” on the end to type-caste it as such. This typecasting of the variables is added so that we can squeeze some extra efficiency out of the compiler, but we could have easily left it in this format:



hockeyPaddle = new HockeyPaddle(); // no type-casting
this.addChild(hockeyPaddle);


Now you are ready to run your project! Save your Flash project as Hockey.fla in the same folder as HockeyMain.as, then hit Command & Enter (or CTRL Enter on a PC) to see your creation!


The hockey paddle added to the game


OK not that thrilling yet, but you should be able to see your paddle in the top-left corner.


As you can see from the diagram above, Flash has the 0,0 coordinate in the top-left corner and the Y value increases as it goes downwards. This is a little different than how some platforms setup their coordinate system (such as OpenGL or Cocos2D) so keep this in mind if you are new to Flash.


Now add the following code to make the paddle follow the users touch (or mouse if they’re running it on the web):



package  {
import flash.display.Sprite;
import flash.events.MouseEvent;

public class HockeyMain extends Sprite {
var hockeyPaddle:Sprite;

public function HockeyMain() {
hockeyPaddle = new HockeyPaddle() as Sprite;
this.addChild(hockeyPaddle);
hockeyPaddle.mouseEnabled = false;

stage.addEventListener(MouseEvent.MOUSE_DOWN, handleMouseDown, false, 0, true);
}

private function handleMouseDown(e:MouseEvent){
hockeyPaddle.x = e.target.mouseX;
hockeyPaddle.y = e.target.mouseY;
stage.addEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove, false, 0, true);
stage.addEventListener(MouseEvent.MOUSE_UP, handleMouseUp, false, 0, true);
}

private function handleMouseMove(e:MouseEvent){
hockeyPaddle.x = e.target.mouseX;
hockeyPaddle.y = e.target.mouseY;
}

private function handleMouseUp(e:MouseEvent){
stage.removeEventListener(MouseEvent.MOUSE_MOVE, handleMouseMove);
stage.removeEventListener(MouseEvent.MOUSE_UP, handleMouseUp);
}
}
}


We moved the hockeyPaddle variable outside of the scope of just the constructor so that it can be accessed by the other functions. Also we set it to not be mouseEnabled so that it would not get in the way when we are capturing other touch events on the screen.


In the constructor we added a listener that calls the handleMouseDown function when the user pushes on the screen. The additional variables in the addEventListener make sure that the Actionscript Virtual Machine (AVM) doesn’t keep this object in memory in case we forget to remove this listener ourselves (it counts the listener as a reference to the object and therefore thinks it’s in use otherwise).


This is an important distinction from Objective C. Even though this Flash project is compiled into native bytecode, the AVM is also compiled into the bytecode. This provides some advantages such as not having to worry about the memory management ourselves, although there are some practices like the one above that you need to keep in mind so that the AVM does not keep objects in memory because of a reference to it.


In the handleMouseDown function we set the target of the Mouse Down event to the x,y coordinates of our paddle. We are also doing the same in the handleMouseMove function.


Lastly in the handleMouseUp function we are removing both the listeners for MOUSE_MOVE and MOUSE_UP events. Now if you run your project, you can see that the paddle follows the mouse cursor on click and release!


Hockey Paddle following mouse


Adding the Hockey Puck


Well a hockey paddle isn’t much fun without a hockey puck, so let’s make that next. Create the puck in a similar way that we created the paddle earlier:



  • Switch back to the Flash project, and select Insert->New Symbol.

  • Name the symbol HockeyPuck.

  • Click on Advanced, Click on Export for Actionscript and the class name should auto-fill in as “HockeyPuck”.
  • Click on OK to create the symbol.

  • Tap the O key to bring up the circle tool, and draw a black hockey puck.


Next we want to add some code to create the hockey puck and position it randomly along the x-axis. We also want to add some code to initially position the hockey paddle.


Switch back to HockeyMain.as, and add all of this to the constructor, below the hockeyPaddle initialization code:



hockeyPaddle.x = stage.stageWidth * 0.5;
hockeyPaddle.y = stage.stageHeight * 0.5;

hockeyPuck = new HockeyPuck() as Sprite;
this.addChild(hockeyPuck);
hockeyPuck.mouseEnabled = false;
hockeyPuck.x = Math.random()*stage.stageWidth*0.6 + stage.stageWidth*0.2;
hockeyPuck.y = stage.stageHeight*0.3;


Notice all of the code is based on the stageWidth and stageHeight values, this will make it easy for porting to the iPad which has a different pixel resolution than the iPhone (1024×768). It will also help for Android devices which usually have a different aspect ratio all together.


Also you need to declare a variable for the hockey puck in your class like this:



var hockeyPuck:Sprite;


Run your game, and you should now see the hockey puck in the game!


Paddle with puck


Hit Detection


So we have our hockey puck and paddle, now let’s make them react to one another. For advanced hit detection I would recommend a physics engine like Box2D, but since this game is simple enough, we can write our own hit detection.


First we will need some variables to record what the last x,y coordinates of the paddle were so we can use this to calculate the direction it is moving in. Add these amongst the other class variables.



var paddleLastX:Number;
var paddleLastY:Number;


Then we need some variables to record the direction that the puck is being hit in. It is resting at the moment, so initialize them to zero:



var puckMomentumX:Number = 0;
var puckMomentumY:Number = 0;


Now we are going to add a function that fires on every enter frame to check for hit detection and do any other game logic.



this.addEventListener(Event.ENTER_FRAME, update, false, 0, true); // add this to your constructor!


The enter frame event is the point before Flash has drawn anything to the screen for that frame. This is equivalent to the update method in Cocos2D.


So if your game is set to 60 frames per second (fps), you can expect that this function will be fired 60 times a second. Of course if too much is going on and your game slows down, then it may fire slower than this, so you can not rely upon a constant 60fps.


Next implement the update method as follows:



private function update(e:Event){
if(hockeyPaddle.hitTestObject(hockeyPuck)){
var diffX:Number = hockeyPaddle.x - paddleLastX;
var diffY:Number = hockeyPaddle.y - paddleLastY;
puckMomentumX = diffX;
puckMomentumY = diffY;
}
hockeyPuck.x += puckMomentumX;
hockeyPuck.y += puckMomentumY;

paddleLastX = hockeyPaddle.x;
paddleLastY = hockeyPaddle.y;
}


This simply detects when the hockey paddle hits the paddle, via a bounding box test, and sets the puck’s velocity to the instantaneous velocity of the paddle.


For this to work, you also have to import the Event class at the top of the file:



import flash.events.Event;


Compile and run, and you should now be able to whack the puck with your paddle!


Whacking the puck with the paddle


Better Collision Detection and Boundaries


Currently, we use the hitTestObject function checks whether the paddle has hit the puck. This test is done using the object’s bounding box, so it does not take into account the circular shape of both objects.


For a more advanced hit checking, we can use some math to check the distance of the paddle from the center of the puck. We can get the distance of the puck from the paddle by using Pythagoream Theorem:



var distanceFromPuck:Number = Math.sqrt( Math.pow(hockeyPaddle.x-hockeyPuck.x, 2) + Math.pow(hockeyPaddle.y-hockeyPuck.y, 2) );


Then if this distance is less than the combined radius’s of the puck and paddle we know that they have collided.


While we’re adding the code for this, let’s also add some walls for the puck to bounce off of. Modify your update method as follows:



private function update(e:Event){
var diffFromPuckX:Number = hockeyPaddle.x-hockeyPuck.x;
var diffFromPuckY:Number = hockeyPaddle.y-hockeyPuck.y;
var distanceFromPuck:Number = Math.sqrt( Math.pow(diffFromPuckX, 2) + Math.pow(diffFromPuckY, 2) );
if(distanceFromPuck <= (hockeyPaddle.width/2 + hockeyPuck.width/2)){
var diffX:Number = hockeyPaddle.x - paddleLastX;
var diffY:Number = hockeyPaddle.y - paddleLastY;
var angleFromPuck:Number = Math.atan2(diffFromPuckY, diffFromPuckX);
puckMomentumX = - Math.cos(angleFromPuck) * 20;
puckMomentumY = - Math.sin(angleFromPuck) * 20;
}
hockeyPuck.x += puckMomentumX;
hockeyPuck.y += puckMomentumY;

var edgePadding:Number = stage.stageWidth/40;
// check if too far left
if(hockeyPuck.x - hockeyPuck.width/2 - edgePadding < 0){
puckMomentumX = -puckMomentumX;
hockeyPuck.x = edgePadding + hockeyPuck.width/2; // sets the puck to the edge of the screen
}
// check if too far right
if(hockeyPuck.x + hockeyPuck.width/2 + edgePadding > stage.stageWidth){
puckMomentumX = -puckMomentumX;
hockeyPuck.x = stage.stageWidth - edgePadding - hockeyPuck.width/2;
}
// check if too far up
if(hockeyPuck.y - hockeyPuck.height/2 - edgePadding < 0){
puckMomentumY = -puckMomentumY;
hockeyPuck.y = edgePadding + hockeyPuck.height/2;
}
// check if too low
if(hockeyPuck.y + hockeyPuck.height/2 + edgePadding > stage.stageHeight){
puckMomentumY = -puckMomentumY;
hockeyPuck.y = stage.stageHeight - edgePadding - hockeyPuck.height/2;
}

// Apply Friction, a low amount
puckMomentumX *= 0.995;
puckMomentumY *= 0.995;

paddleLastX = hockeyPaddle.x;
paddleLastY = hockeyPaddle.y;
}


In the code above, if the puck hits the walls we simply reverse puck’s X or Y direction depending on which wall it has hit.


You can also see below that the code uses a more advanced method to determine the pucks ricocheting momentum. It is now based on the angle of the puck from the paddle. It finds the x and y coordinates of this angle and then reverses it in the other direction causing it to rebound in the opposite angle.


The code also multiplies the puck’s momentum by a fraction that is very close to 1 (0.995) in order to have it eventually slow down. This in effect adds friction to our puck, but not too much friction because this is an air hockey table after all!


A bouncing puck


Gratuitous Sound Effects


Awesome, we have some great puck hitting mechanics now, but you know what would really spice things? Gratuitous sound effects of course!


Download the resources for this project, which includes a ricochet effect we’ll use (puckRicochet.wav). Now import the effect by selecting your Flash project and going to File->Import->Import to Library.


You will see the wav file in your library pane:


A sound file in the flash library


But to make it available to your code you need to give it a ActionScript Linkage name. A quick way to do this is to double click the area next to the asset and name it there. Give it the name “PuckRicochet”.


Naming a sound in the Flash library


Now in your hit detection routine, add the following code to play the sound effect:



var puckRicochet:Sound = new PuckRicochet() as Sound;
puckRicochet.play();


And import the class you need to do this at the top of the file:



import flash.media.Sound;


Well that sound effect is fun but you know what would really spice this up? Some 8-bit polka music (courtesy of Kevin MacLeod)!


In the resources for this project, you’ll find an MP3 file. Import the file in the same way and name it PixelPolka.


Then add the following code in your constructor to make it play and loop:



var pixelPolka:Sound = new PixelPolka() as Sound;
pixelPolka.play(0, int.MAX_VALUE);


You can’t make it loop indefinitely, but by passing the max value for an integer as the second parameter we can make sure the user will get their fill of polka. Unless they love polka so much they listen to it more than 2147483647 times! :]


Optimization


Optimizing your game is quite important in any language but it is particularly important in Air for iOS because it will not run as smoothly out of the box as it would if you had programmed it in Objective C.


One of the easiest wins for better performance is to set the stage quality to low. This setting is mostly used for rendering vectors in flash and because most of your objects should be converted into a Bitmap anyways for best performance it really won’t effect the visual quality of your app too much. To do this add this line in the constructor:



stage.quality = "low";


Low quality images


As you can see in the image above setting of the stage quality to low effects the quality of the images produced (Notice the jagginess around the edges of the paddle and puck). These defects do not translate to the device though, the GPU will produce the same crisp images, but the performance will be better.


Now as I mentioned before it is important for your objects to be rendered as bitmap objects. If the object is a bitmap Air for iOS is smart enough to use the iPhone’s GPU to store this object in memory causing redraws of this object to be substantially faster than if it had to be drawn to the screen every time. Add these lines at the end of the constructor:



hockeyPaddle.cacheAsBitmap = true;
hockeyPuck.cacheAsBitmap = true;


Also it is recommended to add cacheAsBitmapMatrix property as well, this gives similar GPU performance increases but while cacheAsBitmap only helps for x, y translations of the object, cacheAsBitmapMatrix will work for any transformation of the object including rotation, alpha, and more complicated Matrix transformations like skewing or flipping.



hockeyPaddle.cacheAsBitmapMatrix = new Matrix();
hockeyPuck.cacheAsBitmapMatrix = new Matrix();


For this to work, you also have to add an extra import:



import flash.geom.Matrix;


I would warn developers to not use cacheAsBitmapMatrix too often. While in most literature it is recommended as a performance boost, in some cases I have seen it to have the exact opposite effect of slowing down performance. So this may take some trial and error to see if it will help performance in your game. But a general rule would be only to use it on a handful couple of objects.


In order to really test the performance of your game you need to run it on a device. The performance on the computer is not a good indication of how it will run on the device, it could be either better or worse, but most of the time it is worse (it is a mobile device after all). Take a look at my publish settings for the app:


Flash publish settings for iOS


It’s important to set the resolution to high so that it can support the retina display. It’s also worthwhile to make it a universal binary (supporting the iPhone and iPad) as it is relatively easy to resize the flash assets particularly if you are just scaling down.


To make sure the assets look ok on the older 3GS (non-retina display phone) let’s half the size of the assets if the resolution is below a certain threshold (it’s worth noting that Air for iOS does not support the older 3g and 1g phones).



if(stage.stageWidth<=320){
hockeyPaddle.scaleX = 0.5;
hockeyPaddle.scaleY = 0.5;
hockeyPuck.scaleX = 0.5;
hockeyPuck.scaleY = 0.5;
}


Deploying to a Device


Unfortunately one of the most painful steps is still left to get your app to run on the phone. Setting up the certificates in the deployment tab can be a bit of a challenge particularly if you have not dealt with Apple’s system of provisioning profiles before.


This process has been covered in great depth on other blogs so instead of duplicating the effort, I will provide you with a list of helpful resources to installing your app:



Furthering the Game


While we have a good start, right now there isn’t any point to hitting the puck around!


So let’s make the goal how many gold coins you can pick up within 30 seconds.


First we will add the count-down timer. Add this to your constructor:



// Add count-down timer textfield
timeLeft = new TextField();
timeLeft.text = "30";
timeLeft.setTextFormat( new TextFormat("Arial", stage.stageWidth * 0.15, 0x777777, "bold") );
timeLeft.x = stage.stageWidth * 0.8;
timeLeft.y = 0;
this.addChild( timeLeft );

currentScore = new TextField();
currentScore.text = "0";
currentScore.setTextFormat( new TextFormat("Arial", stage.stageWidth * 0.1, 0x000000, "bold") );
currentScore.x = stage.stageWidth * 0.8;
currentScore.y = stage.stageHeight * 0.1;
this.addChild( currentScore );

levelTimer = new Timer(1000, 30); // repeat every 1000 milliseconds (1 second), 30 times (30 seconds)
levelTimer.addEventListener(TimerEvent.TIMER_COMPLETE, levelComplete, false, 0, true);
levelTimer.addEventListener(TimerEvent.TIMER, updateLevelTimer, false, 0, true);
levelTimer.start(); // timers have to be told to start, they don't start automatically


Also add the variables for these:



var timeLeft:TextField;
var currentScore:TextField;
var levelTimer:Timer;
var levelScore:Number = 0;


And the imports:



import flash.text.TextField;
import flash.utils.Timer;
import flash.text.TextFormat;
import flash.events.TimerEvent;


These functions are going to be controlled by the timer. They will keep track of when the level is over and how much time you have left.



private function updateLevelTimer(e:Event){
timeLeft.text = String(Number(timeLeft.text) - 1); // subtract one from current time
timeLeft.setTextFormat( new TextFormat("Arial", stage.stageWidth * 0.15, 0x777777, "bold") );
}

private function levelComplete(e:Event){
levelTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, levelComplete);
levelTimer.removeEventListener(TimerEvent.TIMER, updateLevelTimer);

var completeMessage:TextField = new TextField();
completeMessage.width = stage.stageWidth;
completeMessage.multiline = true;
completeMessage.text = "Level Complete!\nYour Score:"+levelScore;
var textFormat:TextFormat = new TextFormat("Arial", stage.stageWidth * 0.1, 0x00CCFF, "bold");
textFormat.align = "center"; // this center aligns the text
completeMessage.setTextFormat( textFormat );
completeMessage.y = stage.stageHeight*0.5 - completeMessage.textHeight/2;// to center the textfield I am subtracting half of the height of the text field's content
this.addChild( completeMessage );

this.removeEventListener(Event.ENTER_FRAME, update);
}


Notice we are removing the listener for the ENTER_FRAME to suspend gameplay once the level is complete, we are also removing the Timer listeners.


Hit command enter and you can see this code in action! You should see a countdown to 0, and then a final score message.


Adding a countdown timer


Pickups


Now let’s add some gold coins for you to pickup. First we have to create the gold coin, using the same technique we used to create the puck and paddle, I am going to name it “PickupGood”.


Creating a good pickup


Now let’s create the code that randomly distributes this item.



var pickupGood:Sprite;

private function addPickupGood(){
var edgePadding:Number = stage.stageWidth/40;
pickupGood = new PickupGood() as Sprite;
pickupGood.x = Math.random() * (stage.stageWidth-edgePadding) + edgePadding;
pickupGood.y = Math.random() * (stage.stageHeight-edgePadding) + edgePadding;
this.addChild( pickupGood );
}


Then add this line to the constructor to add the pickup the first time:



addPickupGood();


Now we need to add the hit detection code for the pickups. Add this code in the update function:



// Hit detection for pickups
if( hockeyPuck.hitTestObject( pickupGood ) ){
// increase the score
levelScore++;
currentScore.text = String(levelScore);
currentScore.setTextFormat( new TextFormat("Arial", stage.stageWidth * 0.1, 0x000000, "bold") );

// remove this pickup from the screen
this.removeChild( pickupGood );
pickupGood = null;

// add a new pickup
addPickupGood();
}


This code checks if the puck has hit the pickup. If it does it increases the score, as well as spawning a new pickup.


Guess what – you’re done! Hit Command+Enter to see the final game result:


The final simple iPhone game made in Flash CS5.5!


Where To Go From Here?


Here is the final example project with all of the code from the above tutorial.


Congratulations, you now know the basics of developing a simple iPhone game with Flash CS5.5!


Here’s some great resources to learn more about Flash from here:



Hope you enjoy playing around with Flash, and I hope to see some Flash-developed games from you guys in the App Store! In the meantime, if you have any questions about Flash CS 5 for iPhone development or comments or questions on this tutorial, please join the forum discussion below!


This is a tutorial by iOS Tutorial Team member Russell Savage, the founder of Dented Pixel, a creative development company. He recently released its first app – Princess Piano – and he is currently hard at work on several new projects.

没有评论:

发表评论