Space Shooter Issues

Hey guys! So I’ve been working on a simple fake 3D asteroid shooting game, but there are a few bugs I’ve run into. First of all is performance. Maybe I just did it wrong, but deleting objects from tables didn’t speed it up. I also tried reusing the asteroids and lasers that were no longer active, but that kinda slowed it down more. I commented out the tests for right now.

Another problem is that when it loops through the asteroid table, it draws the newer ones last, so the small asteroids are in front of the bigger ones. Is there a way to loop through the table backwards? Thanks for the help!

--# Main
-- Space Shooter

displayMode(FULLSCREEN)
supportedOrientations(LANDSCAPE_ANY)

function setup()
    lasers = {}
    asteroids = {}
    explosions = {}
    rock1 = readImage("Space Art:Asteroid Large",1400,1160)
    rock2 = readImage("Space Art:Asteroid Small",1000,1000)
    bang = readImage("Space Art:Red Explosion",232,232)
    points = 0
    
    space = image(WIDTH*2,HEIGHT*2)
    setContext(space)
    
    for i = 1, math.random(1000,2000) do
        local colr = math.random(180,220)
        fill(colr,colr+20,colr+35,math.random(255))
        ellipse(math.random(WIDTH*2),math.random(HEIGHT*2),math.random(3,8))
    end
    setContext()
    
    parameter.watch("1/DeltaTime")
    left = {}
    right = {}
    left.angle = -30
    right.angle = 30
    left.size = 400
    right.size = 400
    left.y = 0
    right.y = 0
    cannon = readImage("Space Art:Part Gray Wing 2",260,840)
    hull = readImage("Space Art:Part Gray Hull 4",WIDTH-200,300)
    
    counter = 1
    
    hit = {}
    hit.alpha = 0
end

function draw()
    background(20,20,30)
    
    if counter%10 == 0 then
        --[[
        local test = false
        for i,a in pairs(asteroids) do
            if a.dist == 101 then
                test = true
                a:init()
            end
        end
        if test == false then table.insert(asteroids,Asteroid()) end
          ]] -- Test for reusing asteroids
        table.insert(asteroids,Asteroid())
    end
    counter = counter + 1
    
    pushMatrix()
    translate(WIDTH/2,HEIGHT/2)
    translate(math.random(-1,1),math.random(-1,1))
    rotate(ElapsedTime*5)
    sprite(space,0,0)
    popMatrix()
    
    for i,l in pairs(lasers) do if l.dist ~= 0 and l ~= nil then l:draw() end end
    for i,a in pairs(asteroids) do if a.dist < 100 and a ~= nil then a:draw()
    elseif a.dist == 100 then a:draw();a.dist = 101 end end
    for i,e in pairs(explosions) do if e.size ~= 0 then e:draw() end end
    
    translate(0,5*math.sin(ElapsedTime))
    pushMatrix()
    rotate(left.angle+5)
    sprite(cannon,0,left.y,70,left.size)
    popMatrix()
    pushMatrix()
    translate(WIDTH,0)
    rotate(right.angle-5)
    scale(-1,1)
    sprite(cannon,0,right.y,70,right.size)
    popMatrix()
    sprite(hull,WIDTH/2,-50)
    
    fill(255, 255, 255, 255)
    textMode(CENTER)
    font("Futura-Medium")
    fontSize(40)
    text(points,WIDTH/2,24)
    
    fill(255,0,0,hit.alpha)
    rect(0,0,WIDTH,HEIGHT)
end

function touched(touch)
    if touch.state == BEGAN and touch.x < WIDTH/2 then
        left.angle = vec2(0,1):angleBetween(vec2(touch.x,touch.y))
        left.angle = math.deg(left.angle)
        left.size = vec2(0,0):dist(vec2(touch.x,touch.y))
        left.y = -50
        tween(0.1,left,{y=0})
        table.insert(lasers,Laser(touch.x,touch.y,"left",left.angle))
        
        --[[
        local test = false
        for i,l in pairs(lasers) do
            if l.dist == 0 and l.side == "left" then
                test = true
                l:init(touch.x,touch.y,"left",left.angle)
            end
        end
        if test == false then table.insert(lasers,Laser(touch.x,touch.y,"left",left.angle)) end
          ]] -- Test for reusing lasers
        
    elseif touch.state == BEGAN and touch.x >= WIDTH/2 then
        right.angle = vec2(0,1):angleBetween(vec2(WIDTH-touch.x,touch.y))
        right.angle = math.deg(-right.angle)
        right.size = vec2(WIDTH,0):dist(vec2(touch.x,touch.y))
        right.y = -50
        tween(0.1,right,{y=0})
        table.insert(lasers,Laser(touch.x,touch.y,"right",right.angle))
        
        --[[
        local test = false
        for i,l in pairs(lasers) do
            if l.dist == 0 and l.side == "right" then
                test = true
                l:init(touch.x,touch.y,"right",right.angle)
            end
        end
        if test == false then table.insert(lasers,Laser(touch.x,touch.y,"right",right.angle)) end
          ]] -- Test for reusing lasers
        
    end
    if touch.state == BEGAN then
        sound(SOUND_SHOOT, 46566)
    end
end


--# Laser
Laser = class()

function Laser:init(touchX,touchY,side,angle)
    self.side = side
    self.dist = 100
    self.angle = angle
    self.beam = readImage("Space Art:Red Bullet",self.dist/1.5,self.dist*3)
    
    if self.side == "left" then
        self.x = 0
        self.y = 0
        self.move = tween(0.7,self,{x=touchX*1.5,y=touchY*1.5,dist=0},tween.easing.quartOut,self:destroy())
    elseif self.side == "right" then
        self.x = WIDTH
        self.y = 0
        self.move = tween(0.7,self,{x=touchX*1.5-WIDTH/2,y=touchY*1.5,dist=0},tween.easing.quartOut,self:destroy())
    end
end

function Laser:draw()
    pushMatrix()
    translate(self.x,self.y)
    rotate(self.angle)
    sprite(self.beam,0,0,self.dist/1.5+5,self.dist*3)
    popMatrix()
    
    for i,a in pairs(asteroids) do
        if self.dist > 0 and a.dist < 100 then
            local distance = vec2(self.x,self.y):dist(vec2(a.x,a.y))
            if distance < a.dist * 2 and math.abs(self.dist-a.dist) < 20 then
                if a.rock == rock2 then
                    self.points = (100-self.dist)*2
                    self.points = math.ceil(self.points)
                else
                    self.points = 100-self.dist
                    self.points = math.ceil(self.points)
                end
                points = points + self.points
                table.insert(explosions,Explosion(self.x,self.y,self.dist,self.points))
                lasers[self] = nil
                tween.stop(self.move)
                self.dist = 0
                asteroids[a] = nil
                tween.stop(a.move)
                a.dist = 100
                -- a = nil
                table.remove(asteroids,i)
                sound(SOUND_EXPLODE, 16856)
            end
        end
    end
    
end

function Laser:destroy()
    lasers[self] = nil
    self = nil
end

--# Asteroid
Asteroid = class()

function Asteroid:init(x,y)
    self.x = math.random(200,WIDTH-200)
    self.y = math.random(300,HEIGHT-200)
    self.dist = 0
    self.pos = vec2(WIDTH/2,HEIGHT/2)+vec2(self.x-WIDTH/2,self.y-HEIGHT/2)*5
    self.move = tween(5,self,{x=self.pos.x,y=self.pos.y,dist=100},tween.easing.quintIn,self:testHit())
    self.random = math.random(1,2)
    if self.random == 1 then
        self.rock = rock1
    else
        self.rock = rock2
    end
    self.spin = math.random(-50,50)/10
    self.angle = 0
end

function Asteroid:draw()
    pushMatrix()
    translate(self.x,self.y)
    rotate(self.angle)
    sprite(self.rock,0,0,self.dist*5)
    popMatrix()
    self.angle = self.angle + self.spin
    self:testHit()
end

function Asteroid:testHit()
    if self.x > 0 and self.x < WIDTH and self.y > 0 and self.y < HEIGHT and self.dist == 100 then
        hit.alpha = 255
        tween(0.7,hit,{alpha=0})
        sound(SOUND_EXPLODE, 16856)
    end
end


--# Explosion
Explosion = class()

function Explosion:init(x,y,size,points)
    self.x = x
    self.y = y
    self.size = size * 3
    self.bang = tween(0.5,self,{size=0},tween.easing.backIn)
    self.textAlpha = 255
    self.fade = tween(0.5,self,{textAlpha=0},tween.easing.quadIn)
    self.points = points
end

function Explosion:draw()
    sprite(bang,self.x,self.y,self.size)
    
    fill(255, 255, 255, self.textAlpha)
    textMode(CENTER)
    font("Futura-Medium")
    fontSize(40)
    text("+"..self.points,self.x,self.y+50)
end

@Dwins Here’s a routine to do your asteroids in reverse order.

    for z=#asteroids,1,-1 do
        if asteroids[z].dist<100 and asteroids[z]~=nil then 
            asteroids[z]:draw()
        elseif asteroids[z].dist==100 then
            asteroids[z]:draw()
            asteroids[z].dist=101
        end
    end

@Dwins As for speed, it runs pretty fast on my iPad Air.

Good work! Performance seems good on an an Air. Sprite isn’t the fastest way to draw though. For all of the objects using the same image (eg all the asteroids), try drawing them as rects on a single mesh (so you have one mesh for asteroids, one for the stars, one for the lasers etc). So asteroid:draw() wouldn’t exactly draw per se, just position the asteroid using setRect. Then, at the end of the loop, you have a single draw call for the entire mesh. If you do a forum search for “fastest draw” or something like that you’ll people have done a number of profiling tests to find the quickest way to draw. Batching your objects onto as few meshes as you can, thus making a small number of draw calls is by far the fastest way.

It runs at 57 FPS on my iPad3, which isn’t slow.

I suggest doing some more testing if you have performance issues. Try to measure the effect of anything that could be having an effect. I know you’ve been doing this, but I would try things like different numbers of asteroids, not rotating or translating them (when you’re testing, don’t worry about making a mess on the screen), etc.

Generally, it’s fastest to use meshes for large numbers of images. If you’re rotating each asteroid individually, I think its impractical to put them all on a single mesh, because rotating all their vertices individually each frame would be very slow. However, I tried this, and it wasn’t any faster.

I might try making the asteroids smaller and a bit less frequent, and I think that will get your speed back to 60.

Generally, it’s fastest to use meshes for large numbers of images. If you’re rotating each asteroid individually, I think its impractical to put them all on a single mesh, because rotating all their vertices individually each frame would be very slow.

?? The last argument of setRect is angle. No need to rotate individual vertices.

Lol, I forgot that, I don’t use addRect in 3D

I found if you don’t shoot any lasers it runs at 60 fps but when you’ve been shooting for a while, it starts to slow down at around 10,000 points. I think just testing all the collisions slows it down. I did a bit more testing, and it doesn’t delete the objects from the tables. I used print(#asteroids) and the number didn’t go down at all. Sadly, I don’t have an Air, just my iPad mini

@Ignatz perhaps you’re still in shock from the cricket? :stuck_out_tongue:
For 2D, setRect is the fastest. The downside is that you don’t have much control over draw-ordering. Takes a bit of creative thinking to get round that.

@yojimbo2000 - luckily I don’t follow cricket any more.

I think you could control the draw order, since you are redefining all the rect values at every frame, so any rect can be used for any asteroid, and if you start redefining from the newest to the oldest, they should get drawn in that order.

@Dwins - I’m not sure why the asteroids aren’t being removed, but you might try a=nil instead of table.remove(asteroids,i)

In Asteroid:testHit , I don’t see any code to remove the asteroid, just to fade out the alpha. Unless I’ve missed something. I usually have a kill flag (or maybe it should be called a dead flag). Eg add a callback to the fade out tween function() self.dead=true end then in the main loop:

for i=#asteroids, 1, -1 do
  local v = asteroids[i]
  if v.dead then
    table.remove(asteroids, i)
  else
    v:draw()
  end 
end

The removal code is in Laser:TestHit

@Yojimbo2000 I added a kill flag, but it only destroys them when you shoot them with lasers, and putting the kill flag anywhere else didn’t work

Set the kill flag to true wherever you want to get rid of an asteroid, eg hit by a laser, gone behind the player etc.

I’ve tried that, but for some reason it still won’t work. I’m thinking it would be a good idea to go back and clean up my code a bit

And learn how to work with meshes…

Both of those are good ideas. I have written a lot of code in Codea, and I still have to clean up my code frequently, and I am still learning new stuff. (And I still get stuck regularly!).

Thanks @Ignatz

@Yojimbo2000 you suggested using setRect to draw the Asteroids. I haven’t done a whole lot with meshes, so would I change the position individually in the Asteroid class, or loop through all of them from main? Also, would I need two separate meshes for the two types of asteroids?

You need two meshes if you have different image textures, unless you combine the images in a single image, in which case you need to set texture coordinates.

For now I suggest using separate meshes.

Here’s how I handle high frequency objects:

In setup, or preferably in an Asteroid.assets() function that you call once only from setup, so that setup doesn’t get too cluttered:

function Asteroid.assets() --dot notation rather than colon to indicate that this is not a method of the class
Asteroid.mesh = {mesh(), mesh()} --use the Asteroid class variable to hold variables
Asteroid.mesh[1].texture = "Space Art:Asteroid Large"
Asteroid.mesh[2].texture = "Space Art:Asteroid Large"

Personally, I would then add a load of empty rects to the meshes. Say 100, or whatever you think the max number of asteroids could be. I would also implement a rect recycle array, which holds the rect numbers of dead rects available for reuse. I.e. You don’t want the number of rects to spiral out of control. Put local rectRecycle ={} at the top of the Asteroid tab, outside of any function, so that it’s local just to that tab. Asteroid methods can access it, and nothing else. Then, in Asteroid.assets again:


for i=1,#Asteroid.mesh do
  for a=1,maxAsteroids do
    rectRecycle[i][Asteroid.mesh[i]:addRect(0,0,0,0)
  end
end

In Asteroid:init()

self.rock = math.random[#Asteroid.mesh)
self.m = Asteroid.mesh[self.rock] --create a pointer to one of the meshes
self.rect = rectRecycle[self.rock][1] --get the rect number for this asteroid
table.remove(rectRecycle[self.rock], 1) --remove this rect number from pool of available rocks

Then Asteroid:draw would be

function Asteroid:draw()
  self.m:setRect(self.rect, self.x, self.y, self.w, self.h, math.rad(self.angle))
  self.angle = self.angle + self.spin
  self:testHit()
end

I would implement an Asteroid:destroy() method too, to handle cleanup:

function Asteroid:destroy()
  self.kill = true --deferred removal.
  self.m:setRect(self.rect, 0,0,0,0) --remove rect from screen
  table.insert(rectRecycle[self.rock], self.rect) --add this rect number to recycle pool
end

Finally, at the end of the draw loop:

Asteroid.mesh[1]:draw()
Asteroid.mesh[2]:draw()

It’s faster because there’s way fewer draw calls.

That should get you started.