This tutorial will take a look at porting a Flash/Flex game to the Corona SDK. Specifically, we will be porting from ActionScript to Lua, with the end goal of playing formerly Flash-only games on the iPhone. In addition to demonstrating language and API differences, this tutorial series will also account for hardware restrictions such as screen size and lack of physical buttons on the iPhone.
What is Corona?
Before we get into the actual coding, I would like to give a quick overview of the software we will
be using. Corona SDK is a product by Ansca Mobile for the creation of games for iPhone, iPod Touch, iPad, and Android devices. At the time of this writing, there is a free unlimited trial version of the kit available here. It includes all of the functionality of the paid version, except for publishing to respective app stores of the iPhone and Android devices. This tutorial will make a great introduction to the power and efficiency of Corona.
The Game
In this tutorial, we will be porting a flixel powered game by Andreas Zecher. This game was created with Andreas' tutorial on the open-source flixel library and the ActionScript language. The original code is available here (Andreas was kind enough to release it as open source). If you are interested in making flash games, be sure to check out the original tutorial. There is some really helpful information there!
Lua Language
Ansca Mobile's software development kit uses the lua programming language and extends their API. Lua is a very clean language. While catering towards beginners, I have found it to be very powerful with very little code. Lua is not an
object-oriented language, but it can be made to follow the object-oriented model with some fine-tuning.
With that being said, let's take a look at our first hurdle: Lua is syntactically different than ActionScript.
Variables in Lua are handled differently than in ActionScript. In ActionScript, variables are statically-typed. This means they declare their type and only store values of that type unless explicitly converted.
... private var _ship: Ship; // Variables declare their type. This variable is of type "Ship" private var _aliens: FlxGroup; private var _bullets: FlxGroup; private var _scoreText: FlxText; private var _gameOverText: FlxText; private var _spawnTimer: Number; private var _spawnInterval: Number = 2.5; ...
In lua, variables are dynamically typed. A Lua variable can contain any type at any time. Another
thing to notice is that variables in Lua are either local or global. A variable is local
(only accesible) to its .lua file or its function, etc. if it is prefixed with "local" at the
variable's declaration. Otherwise, it is considered "global" in scope. The best practice in Lua is to always use
local variables. This tutorial will adhere to that practice.
... local _ship -- Variables do not declare their type. This variable could be a "Ship" or it could be -- a string. Later, it could be used to store an integer or an array. local _aliens local _bullets local _scoreText local _gameOverText local _spawnTimer = nil -- This variable has an assignment and a declaration on the same line. local _spawnInterval = 2.5 -- So does this one. ...
As you probably have already noticed, comments are handled differently in Lua than in ActionScript.
// This is a comment in ActionScript /* This is a multiline comment in ActionScript */
-- This is a comment in lua --[[ This is a multiline comment in lua --]]
An important thing to know about the comments in Lua is that everything after "--" is a comment except
for "--[[". This allows for a neat trick. If you add an extra dash to the beginning of the multiline
comment "---[[", it comments the multiline part out. This happens because everything after the first
two dashes is a comment. Now look at the end comment "--]]". It is already a comment because it too
is after two dashes. This effectively becomes a switch to turn off and on bits of code!
--[[ This is a comment --]] ---[[ This is code and will be executed as such. In this case, an error would be thrown. --]]
Loops, brackets, and operators are defined by words in Lua:
// ActionScript if(_spawnTimer < 0) { spawnAlien(); resetSpawnTimer(); }
-- lua if(_spawnTimer < 0) then spawnAlien() resetSpawnTimer() end
Lua uses "if-then", "else", "elseif-then", "end", "and", "or", etc. Also notice that Lua doesn't use
semicolons to end each line. Without all of the brackets and semicolons, Lua code looks a lot more
like English language than other programming languages.
One more thing to note before we get started is Lua does not have support for assignment shortcuts.
// ActionScript _spawnInterval *= 0.95; // This is allowed in ActionScript
-- lua _spawnInterval *= 0.95 -- This will result in an error in lua _spawnInterval = _spawnInterval*0.95 -- This is proper Lua code
Beginning Porting
Now that we understand some of the fundamental differences between ActionScript and Lua, we can begin the
process of transferring the game between them. The source code for this tutorial and attached to this post includes the
original source code and the finished code for today's project.
Syntax
To make porting easier down the road, we will start by converting all of the syntax in our source
files, without concerning ourselves with actual application logic. This way, when we do start working on the logic, the
code will already be formatted properly. Let's begin the process with the Main.as file, which looks
something like this:
package { import org.flixel.*; import de.pixelate.flixelprimer.*; [SWF(width="640", height="480", backgroundColor="#ABCC7D")] [Frame(factoryClass="Preloader")] public class Main extends FlxGame { public function Main():void { super(640, 480, PlayState, 1); } } }
There is not much to do here. We don't need the "package" declaration, so we can remove the top and bottom lines of code.
import org.flixel.*; import de.pixelate.flixelprimer.*; [SWF(width="640", height="480", backgroundColor="#ABCC7D")] [Frame(factoryClass="Preloader")] public class Main extends FlxGame { public function Main():void { super(640, 480, PlayState, 1); } }
Since Lua does not have classes, we can remove the line "public class Main extends FlxGame" and it's
corresponding brackets.
import org.flixel.*; import de.pixelate.flixelprimer.*; [SWF(width="640", height="480", backgroundColor="#ABCC7D")] [Frame(factoryClass="Preloader")] public function Main():void { super(640, 480, PlayState, 1); }
Now we can work on the Main() function. First of all, notice the "public" declaration of the method.
Lua does not have separate function types, so we can remove that. Remember that in lua, functions
do not declare their return type. There is a keyword ":void" at the end of the function declaration.
This means that this function will not return a value, which isn't necessary in lua. So we can drop
it. You may want to just comment it out for reference later. Finally, remove the brackets around
Main(). In lua, we use words. Add "end" at the end of the function.
import org.flixel.*; import de.pixelate.flixelprimer.*; [SWF(width="640", height="480", backgroundColor="#ABCC7D")] [Frame(factoryClass="Preloader")] function Main() --:void super(640, 480, PlayState, 1); end
To wrap things up, remove all of the semicolons ";" and comment out all of the lines of code. This
way, we can begin porting logic without getting errors.
--import org.flixel.* --import de.pixelate.flixelprimer.* --[SWF(width="640", height="480", backgroundColor="#ABCC7D")] --[Frame(factoryClass="Preloader")] --function Main() --:void -- super(640, 480, PlayState, 1) --end
At this point you will want to create a project folder to hold your files. I called mine
"alien_shooter". This is the folder that you will pass to Corona when you are ready to run your
code. Inside of the folder, Corona looks for the file "main.lua". Corona is case-sensitive, so you
must not capitalize the first letter as in "Main.as". Save the above code as main.lua.
Let's move on to the next file. Today, we will only work with two of the source files. The second
file is located at "de/pixelate/flixelprimer/PlayState.as" in the attached source code. This file is where
all of the gameplay is handled. As such, it is the longest file. We will modify the syntax in small chunks.
Just like the main file, this code has a package and class declaration. Remove the lines that start
with "package" and "public class PlayState" and their brackets.
We now see a list of local variables.
... private var _ship: Ship; private var _aliens: FlxGroup; private var _bullets: FlxGroup; private var _scoreText: FlxText; private var _gameOverText: FlxText; private var _spawnTimer: Number; private var _spawnInterval: Number = 2.5; ...
Remember from earlier that variables in Lua are either local or global, and that they need not
declare their type. Modify the variables to look like this:
... local _ship; local _aliens; local _bullets; local _scoreText; local _spawnInterval = 2.5; ...
Now, move down the list of functions declarations, starting with create(). Remove all of the function type declarations, such as "public"
or "override public". Comment out or remove all of the return types for the functions. Almost all of these functions are ":void". Replace the function's brackets with "end". Create() should now look like this:
... function create() --:void FlxG.score = 0; bgColor = 0xFFABCC7D; _ship = new Ship(); add(_ship); _aliens = new FlxGroup(); add(_aliens); _bullets = new FlxGroup(); add(_bullets); _scoreText = new FlxText(10, 8, 200, "0"); _scoreText.setFormat(null, 32, 0xFF597137, "left"); add(_scoreText); resetSpawnTimer(); super.create(); end ...
When we arrive at functions like update(), we also have to deal with if-then statements. Just
replace the opening bracket "{" with "then", and the closing bracket "}" with "end". Any "&&" should
be replaced with "and". "||" should be replaced with "or".
... function update() --:void FlxU.overlap(_aliens, _bullets, overlapAlienBullet); FlxU.overlap(_aliens, _ship, overlapAlienShip); if(FlxG.keys.justPressed("SPACE") and _ship.dead == false) then spawnBullet(_ship.getBulletSpawnPosition()); end if(FlxG.keys.ENTER and _ship.dead) then FlxG.state = new PlayState(); end _spawnTimer -= FlxG.elapsed; if(_spawnTimer < 0) then spawnAlien(); resetSpawnTimer(); end super.update(); end ...
Some functions, such as overlapAlienBullet(), take arguments. In ActionScript, we have to declare the type of arguments that we will be passing. The function overlapAlienBullet() takes a variable "alien" (of type Alien), and a variable named "bullet" (of type Bullet). These type declarations should be removed. Also, overlapAlienBullet() has local variables. Remove the type declarations from these as well. Note: Local variable declarations need to have the keyword local before them.
Before:
... private function overlapAlienBullet(alien: Alien, bullet: Bullet):void { var emitter:FlxEmitter = createEmitter(); emitter.at(alien); alien.kill(); bullet.kill(); FlxG.play(SoundExplosionAlien); FlxG.score += 1; _scoreText.text = FlxG.score.toString(); } ...
After:
... function overlapAlienBullet(alien, bullet) --:void local emitter = createEmitter(); emitter.at(alien); alien.kill(); bullet.kill(); FlxG.play(SoundExplosionAlien); FlxG.score += 1; _scoreText.text = FlxG.score.toString(); end ...
The very last function, createEmitter(), has a for-do statement. We will not be using this function
in the final game, but we should take a look at this statement:
for(var i: int = 0; i < particles; i++) { var particle:FlxSprite = new FlxSprite(); particle.createGraphic(2, 2, 0xFF597137); particle.exists = false; emitter.add(particle); }
The top line of code creates a variable "i" with a value of 0. The code in the brackets then repeats
itself while "i" is less than the variable "particles". Every loop, the variable "i" gets
incremented by 1.
for i=0, particles-1 do ... end
The above Lua statement creates a variable "i" with a value of 0. The code in the brackets
repeats itself until "i" is equal to the variable "particles" minus 1 (the same as checking if "i"
is less than "particles"). The variable "i" also gets incremented by 1 every loop.
We can now wrap up dealing with just syntax. Remove all of the semicolons. Comment out all of the code as
with the main file. Add single line comments before individual lines of code.
--local _ship --local _aliens --local _bullets --local _scoreText --local _spawnInterval = 2.5
Add multiline comments around functions and chunks of code.
--[[ function create() --:void FlxG.score = 0 bgColor = 0xFFABCC7D _ship = new Ship() add(_ship) _aliens = new FlxGroup() add(_aliens) _bullets = new FlxGroup() add(_bullets) _scoreText = new FlxText(10, 8, 200, "0") _scoreText.setFormat(null, 32, 0xFF597137, "left") add(_scoreText) resetSpawnTimer() super.create() end --]]
Go ahead and save this file as PlayState.lua in your project folder. Do not put it in a subdirectory
like the source code. Corona has issues with modules in subdirectories.
Modules And Requiring
Now we are ready to begin porting the logic behind this game. Let's talk about the file logic first.
In our ActionScript source, the Main.as file is executed first. The other files, such as
PlayState.as, are called packages. These packages are imported into the Main file. In Lua, the
main.lua file gets executed first. The other files are known as modules and are required into the
main file. In ActionScript, packages have a package declaration. In Lua, modules must also have a
declaration, or the main file cannot see them. This means we need to modify our PlayState.lua file.
At the top of the file, or any other module file, add this line so that main.lua can work with the
module.
module(..., package.seeall)
Now go back to the main.lua file so that we can load our module. It should be looking like this:
--import org.flixel.* --import de.pixelate.flixelprimer.* --[SWF(width="640", height="480", backgroundColor="#ABCC7D")] --[Frame(factoryClass="Preloader")] --function Main() --:void -- super(640, 480, PlayState, 1) --end
We see that the original ActionScript code imported all of the packages in "org/flixel" and in
"de/pixelate/flixelprimer". Replace those lines to make this:
local PlayState = require("PlayState") --[SWF(width="640", height="480", backgroundColor="#ABCC7D")] --[Frame(factoryClass="Preloader")] --function Main() --:void -- super(640, 480, PlayState, 1) --end
This code creates a new variable (that is local to the main file) to hold the required module. We
can now access any of the code from the file PlayState.lua from the variable "PlayState".
OOP In Lua
Before we continue with main.lua, we need to make some changes to PlayState.lua. If we look at the
original PlayState.as file, we see that that all of the functionality is wrapped inside a class
called PlayState. We cannot create classes with lua, but we can organise our code in a way to make
it object-oriented. By wrapping all of the functionality with a constructor function, we can achieve
the same effect. Keeping all of your code commented out, add these lines around all of the code.
function PlayState() ... code goes here ... end
Now, all of the functions will be local to the function PlayState. These functions become private to
a sort of PlayState instance that gets created when PlayState() is called. If you wanted to mimic a
class method, you could put the declaration outside of the function. We will learn in later lessons
how to create instance methods here too.
Here is an example module demonstrating this functionality:
module(..., package.seeall) local numberOfInstances = 0 -- this function mimics a class method function getNumberOfInstances() return numberOfInstances end -- this function mimics a class function Instance() local Instance = {} numberOfInstances = numberOfInstances + 1 local instanceNumber = numberOfInstances -- this function mimics an instance method function Instance:getInstanceNumber() return instanceNumber end -- this function mimics a private method function create() return Instance end -- this calls the private method after all of the functions have been declared return create() end
If you want to play with the above code, here is a main.lua file demonstrating usage (assuming the
module is saved as test.lua):
local test = require("test") local instance1 = test.Instance() local instance2 = test.Instance() local instance3 = test.Instance() local instance4 = test.Instance() print(test.getNumberOfInstances()) print(instance2:getInstanceNumber())
Notice that the module's functions use dot syntax, while all other Lua functions (and even some of
their declarations as we just saw) use a colon (:) in place of the dot. This can be confusing, as
properties in Lua are accessed using dot syntax. Because of this, we left all of the function calls
alone while porting the syntax earlier. We will decide whether to use a colon or a dot for each case
we come across.
Now that we have all of the functionality in PlayState.lua inside the PlayState() function, all of
the functions are now mimicking private functions in ActionScript. Which is exactly
what we want. Add a local PlayState variable at the top of the function.
function PlayState() local PlayState = {} ... end
Now, go ahead and uncomment the create() function and move it to the bottom (just before
the final end). Add single line comments to all of the lines inside the function, so we can still
see the logic, but it doesn't throw any errors.
module(..., package.seeall) function PlayState() local PlayState = {} ... a whole bunch of commented code ... function create() --:void --FlxG.score = 0 --bgColor = 0xFFABCC7D --_ship = new Ship() --add(_ship) --_aliens = new FlxGroup() --add(_aliens) --_bullets = new FlxGroup() --add(_bullets) --_scoreText = new FlxText(10, 8, 200, "0") --_scoreText.setFormat(null, 32, 0xFF597137, "left") --add(_scoreText) --resetSpawnTimer() --super.create() end end
Moving To Corona's API
The original ActionScript game was built using the flixel library discussed at the beginning of this
lesson. Unfortunately, there is no Lua port of flixel as of this writing. So, we will be
implementing all of the flixel logic with Corona SDK's API.
The first things to note, is back in the main.lua file. In flixel, the Main() function automatically
gets called when the program is run. In Corona, the main.lua file is run from the top to the bottom.
So, to achieve the same effect as in the source, add Main() to the end of main.lua.
local PlayState = require("PlayState") --[SWF(width="640", height="480", backgroundColor="#ABCC7D")] --[Frame(factoryClass="Preloader")] function Main() --:void -- super(640, 480, PlayState, 1) end Main()
Also, in most flixel games, a preloader with the flixel logo is displayed while the program is
loading. We don't need that for our app, so we will not make a preloader.lua file. The lines adding
the preloader and setting the program size/background can be removed (we cannot change the app
size).
local PlayState = require("PlayState") function Main() --:void -- super(640, 480, PlayState, 1) end Main()
While we are here, create a new PlayState in Main().
local PlayState = require("PlayState") function Main() --:void PlayState.PlayState() end Main()
Returning to PlayState.lua, we notice that create() is not being called. In flixel, create() and
update() are automatically handled. We will need to take care of these ourselves in Corona. Add a
call to create() before the last end.
module(..., package.seeall) function PlayState() local PlayState = {} ... a whole bunch of commented code ... function create() --:void ... commented create() logic ... end create() end
Before we move on, let's take a look at what we have so far. We now have a main.lua file that
requires PlayState.lua and runs PlayState(). PlayState() makes a new PlayState instance and then
calls create(). Create() is where all of the setup code for the game goes. We now have a framework
to build our logic around.
Display Objects
We now need to start adding features to our create() function to set up our game. Let's start by
creating the game's green colored background. In our code, we have this line commented out in our
create() function.
--bgColor = 0xFFABCC7D;
This tells us the hexadecimal value for the original green color. An easy way to make a background
in Corona is to make a colored rectangle that is the same size as the screen. This can be done using
Corona's display objects. Similar to ActionScript, Corona gives us an API for displaying objects to
the screen. We can use the display module for a lot of different tasks. It can be used to create
new objects or groups of objects, or for finding the screen's width/height. Let's create a new
rectangle and save it as a property of our PlayState variable.
module(..., package.seeall) function PlayState() local PlayState = {} ... a whole bunch of commented code ... function create() --:void PlayState._background = display.newRect(0, 0, display.contentWidth, display.contentHeight) ... commented create() logic ... end create() end
Here we created a new rectangle from (0,0) to (the screen's width, the screen's height), and stored
it in the variable _background. I put the underscore there in order to mimic the original code. All
of the private variables in the ActionScript version of the game start with an underscore.
_background is private to each PlayState instance, so I formatted it like the other variables.
Notice the dot syntax. If you recall from earlier, this means that _background is now a property of
PlayState. Since we created the variable PlayState outside of the create() function, it is available
now to all of the functions. If we had created PlayState inside of create() like this:
module(..., package.seeall) function PlayState() function create() -- PlayState is now local to create() local PlayState = {} -- PlayState can still be used in create, but it is not available outside. PlayState._aNumber = 10 end function otherFunction() -- This will throw an error. otherFunction doesn't know what PlayState is. print(PlayState._aNumber) end create() otherFunction() end
We wouldn't have been able to use it in otherFunction(). Since we created PlayState at the beginning
of PlayState(), all of the functions can use it.
module(..., package.seeall) function PlayState() -- PlayState is now local to PlayState() local PlayState = {} function create() -- PlayState (and its properties) can be used in create and anywhere else. PlayState._aNumber = 10 end function otherFunction() PlayState._aNumber = PlayState._aNumber + 4 -- This will print 14 print(PlayState._aNumber) end create() otherFunction() end
If you run the code with the new rectangle, you will see a white background. We need to change its
fill color to green. To do this we need to convert the hex code (ABCC7D) to RGB, which is what
Corona uses to fill display objects. You can paste that code into any of the hex to RGB websites.
The result will be (171, 204, 125). Now use the setFillColor instance method (notice the colon) on
PlayState._background.
module(..., package.seeall) function PlayState() local PlayState = {} ... a whole bunch of commented code ... function create() --:void PlayState._background = display.newRect(0, 0, display.contentWidth, display.contentHeight) PlayState._background:setFillColor(171, 204, 125) ... commented create() logic ... end create() end
Now we have the beginning of a game. Let's make a few more changes before we wrap up for the day.
Let's add a variable to each instance of PlayState to tell whether the game is running or not.
module(..., package.seeall) function PlayState() local PlayState = {} ... a whole bunch of commented code ... function create() --:void PlayState._inGame = true PlayState._background = display.newRect(0, 0, display.contentWidth, display.contentHeight) PlayState._background:setFillColor(171, 204, 125) ... commented create() logic ... end create() end
That will become very useful later. Let's also get rid of the status bar at the top of the iPhone.
We only want this done once when the app is launched, so add this to main.lua.
local PlayState = require("PlayState") function Main() --:void display.setStatusBar( display.HiddenStatusBar ) PlayState.PlayState() end Main()
Another important step when making a Corona app, is to add a config file that will tell Corona how
to scale your app on other platforms. In this tutorial, we are only concerned with the iPhone's
screen size (although the app will run on Android too). Create a file called config.lua and fill it
with this.
application = { content = { width = 320, height = 480, scale = "letterbox" }, }
Now our app will run on retina displays too. Note: this file will work with any Corona app.
Conclusion
We now have a working framework to start building on. Our app doesn't look like much yet, but we
have covered a lot of the important steps in porting from one language to another. In the next
lesson in this series we will begin to make a playable game.
Comments