Skip to content

Instantly share code, notes, and snippets.

@Westenburg
Last active March 18, 2023 17:46
Show Gist options
  • Save Westenburg/6487497 to your computer and use it in GitHub Desktop.
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…
--# 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