Last active
March 18, 2023 17:46
-
-
Save Westenburg/6487497 to your computer and use it in GitHub Desktop.
A step by step tutorial for Asteroids written in Codea aimed at beginners. I would recommend looking at the Noob Lander and Snake tutorial before this one if you haven't done any coding before. In fact, this tutorial builds on the first few steps of the Snake tutorial. Also there is more involved in each step and the comments are less line by li…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# Main | |
--Main | |
--This code manages which Code tab is run | |
--it remembers your last choice, and if you select a different one, it runs that instead | |
local tabs = {} | |
local fnames = {"setup","draw","touched","collide","orientationChanged","close","restart","keyboard","cleanup"} | |
local fns = {} | |
local tabDesc={} | |
function setup() | |
saveProjectInfo("Description", "Step by step tutorial for a simple asteroids game") | |
saveProjectInfo("Author", "Graeme West") | |
for k,v in ipairs(fnames) do --store addresses of key event functions | |
fns[v] = _G[v] | |
end | |
LastCode=readProjectData("Code") or 0 --load stored tab number | |
parameter.integer("Choose_a_tab",0,#tabs,LastCode,ShowList) --tab selector | |
parameter.action("Run selected tab", RunCode) | |
RunCode() | |
end | |
function ShowList() | |
output.clear() | |
for i=0,#tabs do | |
-- print(i,tabDesc[i]) | |
end | |
end | |
--these two functions do all the tab switching magic (thanks to Andrew_Stacey) | |
function localise(n,d) | |
if d then tabDesc[n]=d end | |
local t= {} | |
setmetatable(t,{__index = _G}) | |
tabs[n] = t | |
return t | |
end | |
--change tabs | |
function RunCode() | |
output.clear() | |
saveProjectData("Code",Choose_a_tab) | |
cleanup() | |
local t = tabs[Choose_a_tab] | |
for k,v in ipairs(fnames) do | |
if t[v] then _G[v] = t[v] else _G[v] = fns[v] end -- overwrite with the new code | |
end | |
setup() | |
end | |
--default empty function to avoid errors for tabs that don't have it | |
function cleanup() | |
end | |
--# About | |
-- Asteroids tutorial | |
-- by West | |
_ENV=localise(0,"Intro") | |
function setup() | |
displayMode(STANDARD) | |
supportedOrientations(LANDSCAPE_ANY) --locks the screen to use only landscape | |
end | |
function draw() | |
background(40, 40, 50) | |
fill(218, 188, 28, 255) | |
font("Baskerville-Bold") | |
fontSize(32) | |
text("Asteroids Tutorial by West",WIDTH/2,HEIGHT-180) | |
fontSize(18) | |
textWrapWidth(WIDTH-100) | |
introtext="This tutorial uses the first few steps of the Snake tutorial " | |
introtext = introtext.."to position the space ship on the screen. There are then 6 steps which build up to a simple game of asteroids. " | |
introtext = introtext.." These steps are:\n" | |
introtext = introtext.."\nStep 1. Get the ship rotating on the screen." | |
introtext = introtext.."\nStep 2. Add the firing of laser bullets." | |
introtext = introtext.."\nStep 3. Better management of bullets." | |
introtext = introtext.."\nStep 4. Add asteroids." | |
introtext = introtext.."\nStep 5. Add collision detection." | |
introtext = introtext.."\nStep 6. Add different game states, scoring and some polishing." | |
introtext = introtext.."\n\n Any questions, comments, suggestions post a message on the Codea forums.\n\nGraphics used are those supplied with Codea, particularly those of Kenney.nl and twolivesleft.com" | |
text(introtext,WIDTH/2,HEIGHT-400) | |
sprite("Cargo Bot:Play Solution Icon",70,HEIGHT-32,-20,20) | |
fill(255) | |
text("Tap this button to hide/show the sidebar",270,HEIGHT-32) | |
pushMatrix() | |
translate(WIDTH-100,HEIGHT-100) | |
rotate(ElapsedTime*25) | |
sprite("Space Art:Red Ship",0,0) | |
popMatrix() | |
end | |
--# Step1 | |
-- Asteroids tutorial | |
-- by West | |
--Use the first 5 steps of snake as the starter for this tutorial | |
--1. Control rotation through screen tap - turn the ship towards where the screen was tapped | |
_ENV=localise(1,"tab1") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) --locks the screen to use only landscape | |
x=512--put ship in centre of screen | |
y=HEIGHT/2 | |
angle=90 | |
end | |
function draw() | |
background(40, 40, 50) | |
--this simple example will use the CurrentTouch command. A more advanced (and better) way would be to handle touches separately. This may be explored later. | |
-- check to see if the screen has been touched | |
if CurrentTouch.state==BEGAN or CurrentTouch.state==MOVING then | |
--calculate the angle between the centre of the screen(where the ship is) and the touch point | |
angle = math.deg(math.atan2(CurrentTouch.y-y,CurrentTouch.x-x))-90 | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) --change the ship sprite and make smaller | |
popMatrix() | |
end | |
--# Step2 | |
-- Asteroids tutorial | |
-- by West | |
--2. Add bullets | |
_ENV=localise(2,"tab2") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
x=512 | |
y=HEIGHT/2 | |
angle=90 | |
bullet={} --a table for the bullet info | |
bulletspeed=5 | |
end | |
function draw() | |
background(40, 40, 50) | |
if CurrentTouch.state==BEGAN or CurrentTouch.state==MOVING then | |
angle = math.deg(math.atan2(CurrentTouch.y-y,CurrentTouch.x-x))-90 | |
table.insert(bullet,{x=x,y=y,a=angle})--add a new bullet to the table | |
end | |
--a special type of loop for iterating through the table bullet. | |
for i,b in pairs(bullet) do | |
--the same code as was used to move the snake sprite in the snake tutorial, except the snake parts are replaced with bullet info (position, angle and sprite) | |
pushMatrix() | |
translate(b.x,b.y) | |
rotate(b.a) | |
sprite("Space Art:Green Bullet",0,0) | |
popMatrix() | |
--same maths as was used to kove the snake in the snake tutorial | |
b.x = b.x + bulletspeed*math.sin(math.rad(-b.a)) | |
b.y = b.y + bulletspeed*math.cos(math.rad(-b.a)) | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) | |
popMatrix() | |
end | |
--# Step3 | |
-- Asteroids tutorial | |
-- by West | |
--3. Add a delay for the bullets and also remove them when they go off screen | |
_ENV=localise(3,"tab3") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
x=512 | |
y=HEIGHT/2 | |
angle=90 | |
bullet={} | |
bulletspeed=5 | |
bulletdelay=10 --a counter for cooling off period between shots | |
end | |
function draw() | |
background(40, 40, 50) | |
bulletdelay = bulletdelay -1 --decrease the bullet delay counter | |
if CurrentTouch.state==BEGAN or CurrentTouch.state==MOVING then | |
angle = math.deg(math.atan2(CurrentTouch.y-y,CurrentTouch.x-x))-90 | |
--only fire the bullet if the delay variable is negative | |
if bulletdelay<0 then | |
table.insert(bullet,{x=x,y=y,a=angle})--add a new bullet to the table | |
bulletdelay=10 --reset the bullet delay | |
end | |
end | |
for i,b in pairs(bullet) do | |
pushMatrix() | |
translate(b.x,b.y) | |
rotate(b.a) | |
sprite("Space Art:Green Bullet",0,0) | |
popMatrix() | |
b.x = b.x + bulletspeed*math.sin(math.rad(-b.a)) | |
b.y = b.y + bulletspeed*math.cos(math.rad(-b.a)) | |
--test to see if the bullet has left the screen if so then remove it from the table | |
if b.x>WIDTH or b.x<0 or b.y>HEIGHT or b.y<0 then | |
table.remove(bullet,i) | |
end | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) | |
popMatrix() | |
end | |
--# Step4 | |
-- Asteroids tutorial | |
-- by West | |
--4. Add in asteroids | |
_ENV=localise(4,"tab4") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
x=512 | |
y=HEIGHT/2 | |
angle=90 | |
bullet={} | |
bulletspeed=5 | |
bulletdelay=10 | |
asteroid={} --table to hold asteroid details | |
--set up initial asteroid locations | |
numAsteroids=2 --initial number of asteroids | |
for i=1,numAsteroids do | |
table.insert(asteroid,{x=math.random(WIDTH),y=math.random(HEIGHT),angle=math.random(360),size=3,rot=-3+math.random(6),dir=math.random(360),speed=1}) | |
end | |
end | |
function draw() | |
background(40, 40, 50) | |
bulletdelay = bulletdelay -1 | |
if CurrentTouch.state==BEGAN or CurrentTouch.state==MOVING then | |
angle = math.deg(math.atan2(CurrentTouch.y-y,CurrentTouch.x-x))-90 | |
if bulletdelay<0 then | |
table.insert(bullet,{x=x,y=y,a=angle}) | |
bulletdelay=10 | |
end | |
end | |
for i,b in pairs(bullet) do | |
pushMatrix() | |
translate(b.x,b.y) | |
rotate(b.a) | |
sprite("Space Art:Green Bullet",0,0) | |
popMatrix() | |
b.x = b.x + bulletspeed*math.sin(math.rad(-b.a)) | |
b.y = b.y + bulletspeed*math.cos(math.rad(-b.a)) | |
if b.x>WIDTH or b.x<0 or b.y>HEIGHT or b.y<0 then | |
table.remove(bullet,i) | |
end | |
end | |
for i,a in pairs(asteroid) do | |
--same approach as bullet | |
pushMatrix() | |
translate(a.x,a.y) | |
rotate(a.angle) | |
sprite("Space Art:Asteroid Large",0,0,100*a.size/3,100*a.size/3) | |
popMatrix() | |
--rotate and move asteroid | |
a.angle = a.angle + a.rot | |
a.x = a.x + a.speed*math.sin(math.rad(-a.dir)) | |
a.y = a.y + a.speed*math.cos(math.rad(-a.dir)) | |
--detect if the asteroid has left the screen, if it has then wrap it round to the other side | |
if a.x<0 then | |
a.x=WIDTH | |
elseif a.x>WIDTH then | |
a.x=0 | |
end | |
if a.y<0 then | |
a.y=HEIGHT | |
elseif a.y>HEIGHT then | |
a.y=0 | |
end | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) | |
popMatrix() | |
end | |
--# Step5 | |
-- Asteroids tutorial | |
-- by West | |
--5. Add collision detection | |
_ENV=localise(5,"tab5") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
x=512 | |
y=HEIGHT/2 | |
angle=90 | |
bullet={} | |
bulletspeed=5 | |
bulletdelay=10 | |
asteroid={} | |
numAsteroids=2 | |
for i=1,numAsteroids do | |
table.insert(asteroid,{x=math.random(WIDTH),y=math.random(HEIGHT),angle=math.random(360),size=3,rot=-3+math.random(6),dir=math.random(360),speed=1}) | |
end | |
end | |
function draw() | |
background(40, 40, 50) | |
bulletdelay = bulletdelay -1 | |
if CurrentTouch.state==BEGAN or CurrentTouch.state==MOVING then | |
angle = math.deg(math.atan2(CurrentTouch.y-y,CurrentTouch.x-x))-90 | |
if bulletdelay<0 then | |
table.insert(bullet,{x=x,y=y,a=angle}) | |
bulletdelay=10 | |
end | |
end | |
for i,b in pairs(bullet) do | |
pushMatrix() | |
translate(b.x,b.y) | |
rotate(b.a) | |
sprite("Space Art:Green Bullet",0,0) | |
popMatrix() | |
b.x = b.x + bulletspeed*math.sin(math.rad(-b.a)) | |
b.y = b.y + bulletspeed*math.cos(math.rad(-b.a)) | |
if b.x>WIDTH or b.x<0 or b.y>HEIGHT or b.y<0 then | |
table.remove(bullet,i) | |
end | |
end | |
for i,a in pairs(asteroid) do | |
pushMatrix() | |
translate(a.x,a.y) | |
rotate(a.angle) | |
sprite("Space Art:Asteroid Large",0,0,100*a.size/3,100*a.size/3) | |
popMatrix() | |
a.angle = a.angle + a.rot | |
a.x = a.x + a.speed*math.sin(math.rad(-a.dir)) | |
a.y = a.y + a.speed*math.cos(math.rad(-a.dir)) | |
if a.x<0 then | |
a.x=WIDTH | |
elseif a.x>WIDTH then | |
a.x=0 | |
end | |
if a.y<0 then | |
a.y=HEIGHT | |
elseif a.y>HEIGHT then | |
a.y=0 | |
end | |
--for each asteroid check all the bullets to see if there is a collision | |
for j,b in pairs(bullet) do | |
if math.abs(a.x-b.x)<50*a.size/3 and math.abs(a.y-b.y)<50*a.size/3 then | |
--if there is a collision then remove the bullet | |
table.remove(bullet,j) | |
--if the asteroid is bigger than the smallest one the split into two smaller asteroids travelling in different directions | |
if a.size>1 then | |
table.insert(asteroid,{x=a.x,y=a.y,angle=a.angle+90,size=a.size-1,rot=-3+math.random(6),dir=a.dir+90,speed=a.speed+1}) | |
table.insert(asteroid,{x=a.x,y=a.y,angle=a.angle-90,size=a.size-1,rot=-3+math.random(6),dir=a.dir-90,speed=a.speed+1}) | |
end | |
table.remove(asteroid,i) | |
end | |
end | |
--now check to see if the asteroid has hit the ship | |
if math.abs(a.x-x)<50*a.size/3 and math.abs(a.y-y)<50*a.size/3 then | |
--flash | |
background(math.random(255),math.random(255),math.random(255)) | |
table.remove(asteroid,i) | |
end | |
end | |
--Do a check to see if any asteroids are left, if not print a winning message | |
if #asteroid<1 then | |
fontSize(32) | |
text("You Win!",WIDTH/2,HEIGHT*2/3) | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) | |
popMatrix() | |
end | |
--# Step6 | |
-- Asteroids tutorial | |
-- by West | |
--6. Add finite state machine,scoring and general polishing | |
_ENV=localise(6,"tab6") | |
function setup() | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
--Define the possible game states | |
READY=1 | |
PLAYING=2 | |
LEVELCOMPLETE=3 | |
LOSE=4 | |
gamestate=READY | |
--sounds - there is a small delay when a sound is played for the first time. Play them in the set up with zero volume to avoid a jitter during the game. I preview using the sound command here to get the id, then set this as a variable | |
--sound(SOUND_SHOOT, 23109) | |
shootsound=23109 | |
--now play with zero volume | |
sound(SOUND_SHOOT,shootsound,0) | |
--repeat for the explosions, but store in a table. the position in the table will match the size of the asteroid | |
expsound={36012,36018,36002} | |
-- sound(SOUND_EXPLODE, 36018) | |
for i=1,#expsound do | |
sound(SOUND_EXPLODE, expsound[i],0) | |
end | |
--and gameover sound | |
deadexp=47415 | |
--and level complete sound | |
sound(SOUND_EXPLODE,deadexp,0) | |
levelup=47367 | |
sound(SOUND_RANDOM,levelup,0) | |
x=512 | |
y=HEIGHT/2 | |
angle=90 | |
bullet={} | |
bulletspeed=10 --make the bullets faster... | |
bulletdelay=35 --..but less often | |
asteroid={} | |
numAsteroids=2 | |
score=0 | |
touches={} | |
--add in some variables to monitor a delay between screens | |
counter=200 | |
delay=100 | |
--add in spacedust | |
spacedust={} | |
end | |
function initialiseAsteroids() | |
for i=1,numAsteroids do | |
local ax=math.random(WIDTH) | |
local ay=math.random(HEIGHT) | |
--check to see if this puts the asteroid on the ship which would mean instant death. if this is the case then move it by 300 to the side | |
if math.abs(ax-WIDTH/2)<100 and math.abs(ay-HEIGHT/2)<100 then | |
ax = ax + 300 | |
end | |
table.insert(asteroid,{x=ax,y=ay,angle=math.random(360),size=3,rot=-3+math.random(6),dir=math.random(360),speed=1}) | |
end | |
counter=0 | |
end | |
function draw() | |
background(0) --the space dust fades to black so the background should reflect this | |
--Add in the main menu screen | |
if gamestate==READY then | |
counter = counter + 1 | |
font("ArialRoundedMTBold") | |
fontSize(64) | |
--Only show the title at the start | |
if numAsteroids==2 then | |
text("Asteroid Field",WIDTH/2,HEIGHT-200) | |
fontSize(32) | |
text("by West",WIDTH/2,HEIGHT-250) | |
fontSize(64) | |
end | |
text("Level "..(numAsteroids-1),WIDTH/2,HEIGHT/2) | |
fontSize(18) | |
text("Remember, the odds of successfully navigating an asteroid field are approximately 3,720 to 1",WIDTH/2,150) | |
if counter>delay then | |
fontSize(64) | |
text("Ready?",WIDTH/2,HEIGHT/2-100) | |
end | |
--loop through the touch table - if any touches appear move to the next game state, provided the counter is past the time limit | |
for k,touch in pairs(touches) do | |
--any touch will do | |
if counter>delay then | |
initialiseAsteroids() | |
gamestate=PLAYING | |
end | |
end | |
--the main game routine | |
elseif gamestate==PLAYING then | |
bulletdelay = bulletdelay -1 | |
for k,touch in pairs(touches) do | |
angle = math.deg(math.atan2(touch.y-y,touch.x-x))-90 | |
if bulletdelay<0 then | |
table.insert(bullet,{x=x,y=y,a=angle}) | |
bulletdelay=10 | |
sound(SOUND_SHOOT,shootsound) | |
end | |
end | |
for i,b in pairs(bullet) do | |
pushMatrix() | |
translate(b.x,b.y) | |
rotate(b.a) | |
sprite("Space Art:Green Bullet",0,0) | |
popMatrix() | |
b.x = b.x + bulletspeed*math.sin(math.rad(-b.a)) | |
b.y = b.y + bulletspeed*math.cos(math.rad(-b.a)) | |
if b.x>WIDTH or b.x<0 or b.y>HEIGHT or b.y<0 then | |
table.remove(bullet,i) | |
end | |
end | |
for i,a in pairs(asteroid) do | |
pushMatrix() | |
translate(a.x,a.y) | |
rotate(a.angle) | |
sprite("Space Art:Asteroid Large",0,0,100*a.size/3,100*a.size/3) | |
popMatrix() | |
a.angle = a.angle + a.rot | |
a.x = a.x + a.speed*math.sin(math.rad(-a.dir)) | |
a.y = a.y + a.speed*math.cos(math.rad(-a.dir)) | |
if a.x<0 then | |
a.x=WIDTH | |
elseif a.x>WIDTH then | |
a.x=0 | |
end | |
if a.y<0 then | |
a.y=HEIGHT | |
elseif a.y>HEIGHT then | |
a.y=0 | |
end | |
for j,b in pairs(bullet) do | |
if math.abs(a.x-b.x)<50*a.size/3 and math.abs(a.y-b.y)<50*a.size/3 then | |
table.remove(bullet,j) | |
--increase the score 10 points for big asteroid, 15 for a medium and 30 for a small | |
score=score+30/a.size | |
--play explosion sound | |
sound(SOUND_EXPLODE, expsound[a.size]) | |
if a.size>1 then | |
table.insert(asteroid,{x=a.x,y=a.y,angle=a.angle+90,size=a.size-1,rot=-3+math.random(6),dir=a.dir+90,speed=a.speed+1}) | |
table.insert(asteroid,{x=a.x,y=a.y,angle=a.angle-90,size=a.size-1,rot=-3+math.random(6),dir=a.dir-90,speed=a.speed+1}) | |
end | |
table.remove(asteroid,i) | |
--add spacedust, variable s will be the angle | |
for s=0,360,12 do | |
table.insert(spacedust,{x=a.x,y=a.y,dir=s,fade=175+math.random(50)+3*a.size,size=2*a.size+math.random(5),speed=5+math.random(5)}) | |
end | |
end | |
end | |
if math.abs(a.x-x)<50*a.size/3 and math.abs(a.y-y)<50*a.size/3 then | |
background(math.random(255),math.random(255),math.random(255)) | |
table.remove(asteroid,i) | |
gamestate=LOSE | |
counter=0 | |
end | |
end | |
--if there are no asteroids left and that the last one wasn't destroyed by hitting the ship then the level is complete | |
if #asteroid<1 and gamestate~=LOSE then | |
gamestate=LEVELCOMPLETE | |
end | |
pushMatrix() | |
translate(x,y) | |
rotate(angle) | |
sprite("Space Art:Red Ship",0,0,50,40) | |
popMatrix() | |
--draw the spacedust - this is similar to the asteroid routine | |
for s,d in pairs(spacedust) do | |
--add transparency to the space dust so it fadees away | |
tint(d.fade) | |
pushMatrix() | |
translate(d.x,d.y) | |
sprite("Cargo Bot:Star Filled",0,0,d.size,d.size) | |
popMatrix() | |
d.x = d.x + d.speed*math.sin(math.rad(-d.dir)) | |
d.y = d.y + d.speed*math.cos(math.rad(-d.dir)) | |
d.fade = d.fade -5 | |
if d.fade<0 then | |
table.remove(spacedust,s) | |
end | |
end | |
tint(255) | |
fontSize(18) | |
text("Score: "..score,100,HEIGHT-50) | |
--level complete | |
elseif gamestate==LEVELCOMPLETE then | |
if counter==3 then | |
sound(SOUND_RANDOM,levelup) | |
end | |
counter = counter + 1 | |
font("ArialRoundedMTBold") | |
fontSize(64) | |
text("Level "..(numAsteroids-1).." complete",WIDTH/2,HEIGHT/2) | |
if counter>delay then | |
text("Next level?",WIDTH/2,HEIGHT/2-100) | |
end | |
for k,touch in pairs(touches) do | |
--any touch will do | |
if counter>delay then | |
--increase the number of asteroids (the level is always 1 less than the number of asteroids) | |
numAsteroids = numAsteroids + 1 | |
counter=0 | |
gamestate=READY | |
end | |
end | |
--Game over, man. Game over. | |
elseif gamestate==LOSE then | |
if counter==3 then | |
sound(SOUND_EXPLODE,deadexp) | |
end | |
counter = counter + 1 | |
font("ArialRoundedMTBold") | |
fontSize(32) | |
text("Game over man, Game over. Your final score is "..score,WIDTH/2,HEIGHT/2) | |
if counter>delay then | |
text("Try again?",WIDTH/2,HEIGHT/2-100) | |
end | |
for k,touch in pairs(touches) do | |
--any touch will do | |
if counter>delay then | |
--reset the level/number of asteroids | |
numAsteroids = 2 | |
counter=0 | |
--reset the score | |
score=0 | |
gamestate=READY | |
end | |
end | |
end | |
end | |
function touched(touch) | |
if touch.state == ENDED then | |
touches[touch.id] = nil | |
else | |
touches[touch.id] = touch | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment