Corona SDK: Build a Space Shooter - Adding Interactivity
This is the second installment in our Corona SDK space shooter tutorial. In today's tutorial, we'll add to our interface and start coding the game interaction. Read on!
Where We Left Off. . .
Please be sure to read part 1 of this series to fully understand and prepare for this tutorial.
Step 1: Load Sounds
Sound effects used in the game will be loaded at start, this will make them ready for reproduction.
1 |
|
2 |
local shot = audio.loadSound('shot.mp3') |
3 |
local explo = audio.loadSound('explo.mp3') |
4 |
local bossSound = audio.loadSound('boss.mp3') |
Step 2: Variables
These are the variables we'll use, read the comments in the code to know more about them. Some of their names are self explaining so there will be no comment there.
1 |
|
2 |
local timerSource |
3 |
local lives = display.newGroup() |
4 |
local bullets = display.newGroup() |
5 |
local enemies = display.newGroup() |
6 |
local scoreN = 0 |
7 |
local bossHealth = 20 |
Step 3: Declare Functions
Declare all functions as local at the start.
1 |
|
2 |
local Main = {} |
3 |
local addTitleView = {} |
4 |
local showCredits = {} |
5 |
local removeCredits = {} |
6 |
local removeTitleView = {} |
7 |
local addShip = {} |
8 |
local addScore = {} |
9 |
local addLives = {} |
10 |
local listeners = {} |
11 |
local moveShip = {} |
12 |
local shoot = {} |
13 |
local addEnemy = {} |
14 |
local alert = {} |
15 |
local update = {} |
16 |
local collisionHandler = {} |
17 |
local restart = {} |
Step 4: Constructor
Next we'll create the function that will initialize all the game logic:
1 |
|
2 |
function Main() |
3 |
addTitleView() |
4 |
end
|
Step 5: Add a Title View
Now we place the background and TitleView in the stage.
1 |
|
2 |
function addTitleView() |
3 |
title = display.newImage('title.png') |
4 |
playBtn = display.newImage('playBtn.png') |
5 |
playBtn.x = display.contentCenterX |
6 |
playBtn.y = display.contentCenterY + 10 |
7 |
playBtn:addEventListener('tap', removeTitleView) |
8 |
|
9 |
creditsBtn = display.newImage('creditsBtn.png') |
10 |
creditsBtn.x = display.contentCenterX |
11 |
creditsBtn.y = display.contentCenterY + 60 |
12 |
creditsBtn:addEventListener('tap', showCredits) |
13 |
|
14 |
titleView = display.newGroup(title, playBtn, creditsBtn) |
15 |
end
|
Step 6: Remove the Title View
The title view is removed from memory and the addShip
function is called after.
1 |
|
2 |
function removeTitleView:tap(e) |
3 |
transition.to(titleView, {time = 300, y = -display.contentHeight, onComplete = function() display.remove(titleView) titleView = null addShip() end}) |
4 |
end
|
Step 7: Show Credits
The credits screen is shown when the user taps the credits button, a tap listener is added to the credits view to remove it.
1 |
|
2 |
function showCredits:tap(e) |
3 |
creditsBtn.isVisible = false |
4 |
creditsView = display.newImage('creditsView.png') |
5 |
creditsView:setReferencePoint(display.TopLeftReferencePoint) |
6 |
transition.from(creditsView, {time = 300, x = display.contentWidth}) |
7 |
creditsView:addEventListener('tap', removeCredits) |
8 |
end
|
Step 8: Hide Credits
When the credits screen is tapped, it'll be tweened out of the stage and removed.
1 |
|
2 |
function removeCredits:tap(e) |
3 |
creditsBtn.isVisible = true |
4 |
transition.to(creditsView, {time = 300, x = display.contentWidth, onComplete = function() display.remove(creditsView) creditsView = null end}) |
5 |
end
|
Step 9: Add Ship
When the Start button is pressed, the title view is tweened and removed revealing the game view, the ship movieclip will be added first by the next lines:
1 |
|
2 |
function addShip() |
3 |
ship = movieclip.newAnim({'shipA.png', 'shipB.png'}) |
4 |
ship.x = display.contentWidth * 0.5 |
5 |
ship.y = display.contentHeight - ship.height |
6 |
ship.name = 'ship' |
7 |
ship:play() |
8 |
physics.addBody(ship) |
9 |
|
10 |
addScore() |
11 |
end
|
Step 10: Add Score
The following function creates and places the score text in the stage.
1 |
|
2 |
function addScore() |
3 |
score = display.newText('Score: ', 1, 0, native.systemFontBold, 14) |
4 |
score.y = display.contentHeight - score.height * 0.5 |
5 |
score.text = score.text .. tostring(scoreN) |
6 |
score:setReferencePoint(display.TopLeftReferencePoint) |
7 |
score.x = 1 |
8 |
|
9 |
addLives() |
10 |
end
|
Step 11: Add Lives
The lives graphics are added by the next code, it also uses a table to store the lives number. This will help us later to detect when the player is out of lives.
1 |
|
2 |
function addLives() |
3 |
for i = 1, 3 do |
4 |
live = display.newImage('live.png') |
5 |
live.x = (display.contentWidth - live.width * 0.7) - (5 * i+1) - live.width * i + 20 |
6 |
live.y = display.contentHeight - live.height * 0.7 |
7 |
|
8 |
lives.insert(lives, live) |
9 |
end
|
10 |
listeners('add') |
11 |
end
|
Step 12: Listeners
In this function we add the necesary listeners to the interactive objects. We also start the timer that will add the enemies. A parameter is used to determine if the listeners should be added or removed.
1 |
|
2 |
function listeners(action) |
3 |
if(action == 'add') then |
4 |
bg:addEventListener('touch', moveShip) |
5 |
bg:addEventListener('tap', shoot) |
6 |
Runtime:addEventListener('enterFrame', update) |
7 |
timerSource = timer.performWithDelay(800, addEnemy, 0) |
8 |
else
|
9 |
bg:removeEventListener('touch', moveShip) |
10 |
bg:removeEventListener('tap', shoot) |
11 |
Runtime:removeEventListener('enterFrame', update) |
12 |
timer.cancel(timerSource) |
13 |
end
|
14 |
end
|
Step 13: Move Ship
The ship will be controlled moving the finger horizontally across the screen. This code handles that behavior:
1 |
|
2 |
function moveShip:touch(e) |
3 |
if(e.phase == 'began') then |
4 |
lastX = e.x - ship.x |
5 |
elseif(e.phase == 'moved') then |
6 |
ship.x = e.x - lastX |
7 |
end
|
8 |
end
|
Step 14: Shoot
Tapping anywhere in the screen will make the ship shoot a bullet, this bullet is added as a physics object to detect its collision later.
1 |
|
2 |
function shoot:tap(e) |
3 |
local bullet = display.newImage('bullet.png') |
4 |
bullet.x = ship.x |
5 |
bullet.y = ship.y - ship.height |
6 |
bullet.name = 'bullet' |
7 |
physics.addBody(bullet) |
8 |
|
9 |
audio.play(shot) |
10 |
|
11 |
bullets.insert(bullets, bullet) |
12 |
end
|
Step 15: Add Enemy
The next function is executed by a timer every 800 milliseconds. It will add an enemy on the top of the screen. The enemy will be later moved by the update function.
1 |
|
2 |
function addEnemy(e) |
3 |
local enemy = movieclip.newAnim({'enemyA.png', 'enemyA.png','enemyA.png','enemyA.png','enemyA.png','enemyA.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png'}) |
4 |
enemy.x = math.floor(math.random() * (display.contentWidth - enemy.width)) |
5 |
enemy.y = -enemy.height |
6 |
enemy.name = 'enemy' |
7 |
physics.addBody(enemy) |
8 |
enemy.bodyType = 'static' |
9 |
enemies.insert(enemies, enemy) |
10 |
enemy:play() |
11 |
enemy:addEventListener('collision', collisionHandler) |
12 |
end
|
Step 16: Alert
The Alert View will be shown when the user reaches a game state (i.e. win or lose), a parameter is used to determine which screen to display.
1 |
|
2 |
function alert(e) |
3 |
listeners('remove') |
4 |
local alertView |
5 |
|
6 |
if(e == 'win') then |
7 |
alertView = display.newImage('youWon.png') |
8 |
alertView.x = display.contentWidth * 0.5 |
9 |
alertView.y = display.contentHeight * 0.5 |
10 |
else
|
11 |
alertView = display.newImage('gameOver.png') |
12 |
alertView.x = display.contentWidth * 0.5 |
13 |
alertView.y = display.contentHeight * 0.5 |
14 |
end
|
15 |
|
16 |
alertView:addEventListener('tap', restart) |
17 |
end
|
Step 17: Code Review
Here is the full code written in this tutorial alongside with comments to help you identify each part:
1 |
|
2 |
-- Space Shooter Game |
3 |
-- Developed by Carlos Yanez |
4 |
|
5 |
-- Hide Status Bar |
6 |
|
7 |
display.setStatusBar(display.HiddenStatusBar) |
8 |
|
9 |
-- Import MovieClip Library |
10 |
|
11 |
local movieclip = require('movieclip') |
12 |
|
13 |
-- Import Physics |
14 |
|
15 |
local physics = require('physics') |
16 |
physics.start() |
17 |
physics.setGravity(0, 0) |
18 |
|
19 |
-- Graphics |
20 |
|
21 |
-- Background |
22 |
|
23 |
local bg = display.newImage('bg.png') |
24 |
|
25 |
-- [Title View] |
26 |
|
27 |
local title |
28 |
local playBtn |
29 |
local creditsBtn |
30 |
local titleView |
31 |
|
32 |
-- [Credits] |
33 |
|
34 |
local creditsView |
35 |
|
36 |
-- [Ship] |
37 |
|
38 |
local ship |
39 |
|
40 |
-- [Boss] |
41 |
|
42 |
local boss |
43 |
|
44 |
-- [Score] |
45 |
|
46 |
local score |
47 |
|
48 |
-- [Lives] |
49 |
|
50 |
local lives |
51 |
|
52 |
-- Load Sounds |
53 |
|
54 |
local shot = audio.loadSound('shot.mp3') |
55 |
local explo = audio.loadSound('explo.mp3') |
56 |
local bossSound = audio.loadSound('boss.mp3') |
57 |
|
58 |
-- Variables |
59 |
|
60 |
local timerSource |
61 |
local lives = display.newGroup() |
62 |
local bullets = display.newGroup() |
63 |
local enemies = display.newGroup() |
64 |
local scoreN = 0 |
65 |
local bossHealth = 20 |
66 |
|
67 |
-- Functions |
68 |
|
69 |
local Main = {} |
70 |
local addTitleView = {} |
71 |
local showCredits = {} |
72 |
local removeCredits = {} |
73 |
local removeTitleView = {} |
74 |
local addShip = {} |
75 |
local addScore = {} |
76 |
local addLives = {} |
77 |
local listeners = {} |
78 |
local moveShip = {} |
79 |
local shoot = {} |
80 |
local addEnemy = {} |
81 |
local alert = {} |
82 |
local update = {} |
83 |
local collisionHandler = {} |
84 |
local restart = {} |
85 |
|
86 |
-- Main Function |
87 |
|
88 |
function Main() |
89 |
addTitleView() |
90 |
end
|
91 |
|
92 |
function addTitleView() |
93 |
title = display.newImage('title.png') |
94 |
playBtn = display.newImage('playBtn.png') |
95 |
playBtn.x = display.contentCenterX |
96 |
playBtn.y = display.contentCenterY + 10 |
97 |
playBtn:addEventListener('tap', removeTitleView) |
98 |
|
99 |
creditsBtn = display.newImage('creditsBtn.png') |
100 |
creditsBtn.x = display.contentCenterX |
101 |
creditsBtn.y = display.contentCenterY + 60 |
102 |
creditsBtn:addEventListener('tap', showCredits) |
103 |
|
104 |
titleView = display.newGroup(title, playBtn, creditsBtn) |
105 |
end
|
106 |
|
107 |
function removeTitleView:tap(e) |
108 |
transition.to(titleView, {time = 300, y = -display.contentHeight, onComplete = function() display.remove(titleView) titleView = null addShip() end}) |
109 |
end
|
110 |
|
111 |
function showCredits:tap(e) |
112 |
creditsBtn.isVisible = false |
113 |
creditsView = display.newImage('creditsView.png') |
114 |
creditsView:setReferencePoint(display.TopLeftReferencePoint) |
115 |
transition.from(creditsView, {time = 300, x = display.contentWidth}) |
116 |
creditsView:addEventListener('tap', removeCredits) |
117 |
end
|
118 |
|
119 |
function removeCredits:tap(e) |
120 |
creditsBtn.isVisible = true |
121 |
transition.to(creditsView, {time = 300, x = display.contentWidth, onComplete = function() display.remove(creditsView) creditsView = null end}) |
122 |
end
|
123 |
|
124 |
function addShip() |
125 |
ship = movieclip.newAnim({'shipA.png', 'shipB.png'}) |
126 |
ship.x = display.contentWidth * 0.5 |
127 |
ship.y = display.contentHeight - ship.height |
128 |
ship.name = 'ship' |
129 |
ship:play() |
130 |
physics.addBody(ship) |
131 |
|
132 |
addScore() |
133 |
end
|
134 |
|
135 |
function addScore() |
136 |
score = display.newText('Score: ', 1, 0, native.systemFontBold, 14) |
137 |
score.y = display.contentHeight - score.height * 0.5 |
138 |
score.text = score.text .. tostring(scoreN) |
139 |
score:setReferencePoint(display.TopLeftReferencePoint) |
140 |
score.x = 1 |
141 |
|
142 |
addLives() |
143 |
end
|
144 |
|
145 |
function addLives() |
146 |
for i = 1, 3 do |
147 |
live = display.newImage('live.png') |
148 |
live.x = (display.contentWidth - live.width * 0.7) - (5 * i+1) - live.width * i + 20 |
149 |
live.y = display.contentHeight - live.height * 0.7 |
150 |
|
151 |
lives.insert(lives, live) |
152 |
end
|
153 |
listeners('add') |
154 |
end
|
155 |
|
156 |
function listeners(action) |
157 |
if(action == 'add') then |
158 |
bg:addEventListener('touch', moveShip) |
159 |
bg:addEventListener('tap', shoot) |
160 |
Runtime:addEventListener('enterFrame', update) |
161 |
timerSource = timer.performWithDelay(800, addEnemy, 0) |
162 |
else
|
163 |
bg:removeEventListener('touch', moveShip) |
164 |
bg:removeEventListener('tap', shoot) |
165 |
Runtime:removeEventListener('enterFrame', update) |
166 |
timer.cancel(timerSource) |
167 |
--timerSource = nil |
168 |
end
|
169 |
end
|
170 |
|
171 |
function moveShip:touch(e) |
172 |
if(e.phase == 'began') then |
173 |
lastX = e.x - ship.x |
174 |
elseif(e.phase == 'moved') then |
175 |
ship.x = e.x - lastX |
176 |
end
|
177 |
end
|
178 |
|
179 |
function shoot:tap(e) |
180 |
local bullet = display.newImage('bullet.png') |
181 |
bullet.x = ship.x |
182 |
bullet.y = ship.y - ship.height |
183 |
bullet.name = 'bullet' |
184 |
physics.addBody(bullet) |
185 |
|
186 |
audio.play(shot) |
187 |
|
188 |
bullets.insert(bullets, bullet) |
189 |
end
|
190 |
|
191 |
function addEnemy(e) |
192 |
local enemy = movieclip.newAnim({'enemyA.png', 'enemyA.png','enemyA.png','enemyA.png','enemyA.png','enemyA.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png','enemyB.png'}) |
193 |
enemy.x = math.floor(math.random() * (display.contentWidth - enemy.width)) |
194 |
enemy.y = -enemy.height |
195 |
enemy.name = 'enemy' |
196 |
physics.addBody(enemy) |
197 |
enemy.bodyType = 'static' |
198 |
enemies.insert(enemies, enemy) |
199 |
enemy:play() |
200 |
enemy:addEventListener('collision', collisionHandler) |
201 |
end
|
202 |
|
203 |
function alert(e) |
204 |
listeners('remove') |
205 |
local alertView |
206 |
|
207 |
if(e == 'win') then |
208 |
alertView = display.newImage('youWon.png') |
209 |
alertView.x = display.contentWidth * 0.5 |
210 |
alertView.y = display.contentHeight * 0.5 |
211 |
else
|
212 |
alertView = display.newImage('gameOver.png') |
213 |
alertView.x = display.contentWidth * 0.5 |
214 |
alertView.y = display.contentHeight * 0.5 |
215 |
end
|
216 |
|
217 |
alertView:addEventListener('tap', restart) |
218 |
end
|
Next Time...
In the next and final part of the series, we'll handle the enter frame behavior, collisions, and the final steps to take prior to release, like app testing, creating a start screen, adding an icon and, building the app. Stay tuned for the final part!