Flap and Shoot

Hi guys, here is a simple 2-player game I made. Tap to flap and shoot, don’t get hit by a bullet. WARNING: bullets wrap around the screen (if they go off on left they will come back on right) and gravity switches every now and then. Feedback?

-- Flap and shoot
ellipseMode(RADIUS)
supportedOrientations(LANDSCAPE_ANY)
displayMode(FULLSCREEN)

function setup()
    size=50
    shotspeed=5
    shotspeedincrease=5
    shotsize=20
    shotrate=0.5
    switchtime=5
    shotrepeats=2
    reset()
end

function reset()
    y1=HEIGHT/2
    y2=HEIGHT/2
    fall1=0
    fall2=0
    lastfire1=ElapsedTime-shotrate
    lastfire2=ElapsedTime-shotrate
    fall=0.2
    jump=-5
    shots1={}
    shots2={}
    startleft=false
    startright=false
    winner=0
    switches = 1
    wintime=ElapsedTime
    starttime=ElapsedTime
end

function draw()
    pushStyle()
    pushMatrix()
    handleShots()
    background(255, 255, 255, 255)
    fill(0)
    stroke(0)
    fontSize(50)
    if startleft then
        rect(0,0,WIDTH/2,HEIGHT)
    else
        text("Tap to flap and shoot",WIDTH/4,HEIGHT/2+200)
    end
    if startright then
        rect(WIDTH/2,0,WIDTH/2,HEIGHT)
    else
        text("Tap to flap and shoot",3*WIDTH/4,HEIGHT/2+200)
    end
    fill(255)
    stroke(255)
    strokeWidth(5)
    line(WIDTH/2,0,WIDTH/2,HEIGHT)
    drawChar(100,y1,size,color(0,0,255,255))
    drawShot(shots1)
    translate(WIDTH,0)
    scale(-1,1)
    drawChar(100,y2,size,color(255,0,0,255))
    drawShot(shots2)
    if startleft and startright then
        y1 = y1 - fall1
        y2 = y2 - fall2
        fall1 = fall1 + fall
        fall2 = fall2 + fall
        if y1<size then y1 = size end
        if y1>HEIGHT-size then y1=HEIGHT-size;fall1=0 end
        if y2<size then y2 = size end
        if y2>HEIGHT-size then y2=HEIGHT-size;fall2=0 end
    end
    popMatrix()
    popStyle()
    if winner ~= 0 then
        drawwin(winner)
    end
    if math.abs(ElapsedTime-(switchtime*switches+starttime))<DeltaTime then
        fall=-fall
        jump=-jump
        fall1=0
        fall2=0
        switches=switches+1
    end
end

function touched(t)
        if winner ~= 0 and ElapsedTime> wintime+2 then
            reset()
        end
    if t.x < WIDTH/2 and t.state == BEGAN then
        if startleft and startright then
            fall1 = jump
            if ElapsedTime>lastfire1+shotrate then
                table.insert(shots1,{y1,2*size+shotsize+100,shotspeed,0})
                lastfire1=ElapsedTime
            end
        end
        startleft=true
    end
    if t.x > WIDTH/2 and t.state == BEGAN then
        if startleft and startright then
            fall2 = jump
            if ElapsedTime>lastfire2+shotrate then
                table.insert(shots2,{y2,2*size+shotsize+100,shotspeed,0})
                lastfire2=ElapsedTime
            end
        end
        startright=true
    end
end

function drawChar(x,y,r,c)
    pushStyle()
    fill(c)
    ellipse(x+r,y,2*r)
    popStyle()
end

function drawShot(shots)
    for k,shot in pairs(shots) do
        pushStyle()
        fill(255)
        ellipse(shot[2],shot[1],shotsize)
        shot[2]=shot[2]+shot[3]
        popStyle()
    end
end

function handleShots()
    for i,shot1 in pairs(shots1) do
        for j,shot2 in pairs(shots2) do
            if vec2(shot1[2]+shot2[2]-WIDTH,shot1[1]-shot2[1]):len()<shotsize then
                table.remove(shots1,i)
                table.remove(shots2,j)
            end
        end
    end
    for i,shot1 in pairs(shots1) do
        if vec2(shot1[2]-size-100,shot1[1]-y1):len()<size+shotsize/2 and winner==0then
            winner = 2
            wintime = ElapsedTime
            table.remove(shots1,i)
        end
        if vec2(shot1[2]+size+100-WIDTH,shot1[1]-y2):len()<size+shotsize/2 and winner==0 then
            winner = 1
            wintime = ElapsedTime
            table.remove(shots1,i)
        end
        if shot1[2]>WIDTH then
            if shot1[4]<shotrepeats then
                shot1[2]=0
                shot1[3]=shot1[3]+shotspeedincrease
                shot1[4]=shot1[4]+1
            else
                table.remove(shots1,i)
            end
        end
    end
    for j,shot2 in pairs(shots2) do
        if vec2(shot2[2]-size-100,shot2[1]-y2):len()<size+shotsize/2 and winner==0 then
            winner = 1
            wintime = ElapsedTime
            table.remove(shots2,j)
        end
        if vec2(shot2[2]+size+100-WIDTH,shot2[1]-y1):len()<size+shotsize/2 and winner==0 then
            winner = 2
            wintime = ElapsedTime
            table.remove(shots2,j)
        end
        if shot2[2]>WIDTH then
            if shot2[4]<shotrepeats then
                shot2[2]=0
                shot2[3]=shot2[3]+shotspeedincrease
                shot2[4]=shot2[4]+1
            else
                table.remove(shots2,j)
            end
        end
    end
end

function drawwin(i)
    pushStyle()
    fontSize(100)
    local c
    local message
    if i==1 then
        c=color(0,0,255,255)
        message = "BLUE WINS"
    end
    if i==2 then
        c=color(255,0,0,255)
        message = "RED WINS"
    end
    fill(c)
    text(message,WIDTH/2,HEIGHT/2)
    if ElapsedTime>wintime+2 then
        text("Tap to reset",WIDTH/2,HEIGHT/2-200)
    end
end

Just a few little things, not criticisms, mainly things you might want to adjust when you write bigger apps.

When you reset() in the touched function, you probably want to put return straight after that so the rest of the touched function doesn’t run. (Doesn’t matter much in this case, though, because the screen will reset 1/60 of a second later anyway).

In drawwin(), there’s no need for pushStyle() because you don’t need to reset the style during the function. The “push” functions are only used to store current settings so you can restore them quickly later before drawing something else.

Within drawshot, I would put the pushStyle and popStyle and fill statements outside the do loop because they are the same for every shot, ie they only need to be set once.

It doesn’t matter in such a small app, but if you start drawing more complex shapes, it may be faster to create an image for the bullets (and players) in setup, and sprite them or use a mesh to draw them. The same applies with backgrounds, it’s best to create/load them in an image at the beginning.

Using a semi colon to separate statements works, but the Lua convention is to use one or more spaces between statements.

The handleShots function has a big chunk of repeated code to deal with both players. In a bigger app, you’d probably use a class here. That would also remove the need to have two of all the variables, eg y1, y2, fall1, fall2, etc.

Otherwise, I think you’re doing fine!

@Ignatz Thanks! Yeah, the return works well. There was meant to be a popStyle at the end but clearly it was unnecessary, I got paranoid about continued styles after I had to go through my last project putting fills everywhere. How much does using sprites affect the speed? I’ll use classes for my next project. Btw I meant feedback for the gameplay, any tips for that? Thanks.

Using sprites won’t make a difference until your drawing gets a lot more complicated, or if you’re drawing many images. That’s also when you should look at using meshes.

Wrt gameplay, I wouldn’t wrap the shots around the screen so they come back again, I found that confusing. Also, winning after one hit is a bit quick, maybe use a health bar and make it 5 or 10 hits to win.

It also wasn’t clear quite how finger touches move the circles, perhaps this will be clearer if the game is longer.

It might be more interesting if the bullets have something to bounce off in the middle, so you get shots that go in several directions, not just left and right.

@Ignatz, thanks for your advice. When I have some free time I will start on v2 with classes, main menu et cetera. I can get the bounces if the object in the middle is a circle, but how do circles bounce off fixed triangles?

This might be the time to begin using physics objects, which will handle the collisions for you.

I explain those in my posts and ebooks, and there are dozens of posts on the forum.

I’m confused about my code.

-- Flap and Shoot 2
ellipseMode(CENTER)
displayMode(FULLSCREEN)

function setup()
    states = {{drawMenu,touchMenu},{drawGame1,touchGame1}}
    state = 1
    size = 50
    fall = 0.2
    jump = -5
    pos = vec2(size+100,HEIGHT/2)
    player1 = Character(pos,size,color(0,0,255))
    player2 = Character(pos,size,color(255,0,0))
end

function draw()
    states[state][1]()
end

function touched(t)
    states[state][2](t)
end

function drawMenu()
    background(0)
    fontSize(100)
    fill(255)
    text("FLAP TO SHOOT 2",WIDTH/2,HEIGHT-200)
    ellipse(WIDTH/2,HEIGHT/2,200)
    fill(0)
    fontSize(50)
    text("PLAY",WIDTH/2,HEIGHT/2)
end

function touchMenu(t)
    if vec2(t.x-WIDTH/2,t.y-HEIGHT/2):len()<=100 and t.state == BEGAN then
        state = 2
    end
end

function drawGame1()
    pushMatrix()
    background(0)
    player1:draw()
    translate(WIDTH,0)
    scale(-1,1)
    player2:draw()
    popMatrix()
end

function touchGame1(t)
    if t.x<WIDTH/2 and t.state == BEGAN then
        player1.falling = jump
        print("1")
    end
    if t.x>WIDTH/2 and t.state == BEGAN then
        player2.falling = jump
        print("2")
    end
end

Character = class()

function Character:init(pos,radius,c)
    self.pos = pos
    self.radius = radius
    self.colour = c
    self.falling = 0
end

function Character:draw()
    strokeWidth(5)
    stroke(255)
    fill(self.colour)
    ellipse(self.pos.x,self.pos.y,2*self.radius)
    self.pos.y = self.pos.y - self.falling
    self.falling = self.falling + fall
    if self.pos.y<self.radius then
        self.pos.y = self.radius
        self.falling = 0
    end
end

The two characters jump together whenever the device is tapped. However, when I comment out the player2:draw() the program works fine and the left player does not jump when the right side is tapped.
EDIT: In addition, the program works fine when there is no transformation and the 2nd player’s x is WIDTH-size-100.

@MattthewLXXIII - It’s quite a subtle bug. You need to replace the first line in your Character:init function with this

self.pos = vec2(pos.x,pos.y)

The problem is that you are passing pos to the class by reference, not by value, ie a pointer. Since you pass the same pos vector for both characters, they both refer to the same vector position, mirroring each other.

The alteration above is to create a new vector so that they will not be entangled any more.

I think this is one of the trickiest things with Lua, that objects are automatically reference types, and because vectors are part of the Codea API, they have to be Lua userdata objects. If you’re performing an operation that returns a “new” vector (even if it’s the same vector, eg vec2(5,7) * 1 ), then they behave as value types (because you’re getting a new vector back), but if you just pass the vector directly they behave as reference types. The thing is, a vector in itself doesn’t have an identity, so you almost always want it to be passed as a value type.

I’ve been caught out by this a number of times. If the spaceship fires a bullet, and then the spaceship zooms off as if its attached to that bullet, then it’s probably because you’ve passed the bullet’s position as a reference to the ships position by mistake.

Thanks guys. I wrote this before reading your advice and I don’t get why the shots are not linked to the characters, even though they were defined by the character’s position. Here is my second attempt, but there are still some bugs that I cannot figure out:
• Shots sometimes disappear off the screen when they get close to the edge.
• When 2 shots collide, sometimes one does not get removed.
Any idea why this happens/how I can fix it?

-- Flap and Shoot 2
ellipseMode(CENTER)
displayMode(FULLSCREEN_NO_BUTTONS)

function setup()
    states={{drawMenu,touchMenu},{drawGame1,touchGame1},{drawEnd,touchEnd},{drawSettings,touchSettings}}
    state=1
    lives=5
    ts=ENDED
    hit=0
end

function reset()
    winner=0
    startleft=false
    startright=false
    shots={}
    size=50
    shotsize=10
    shotspeed=5
    fall=-0.2
    jump=5
    blocksize=100
    player1=Character(vec2(size+100,HEIGHT/2),size,lives,color(0,0,255))
    player2=Character(vec2(WIDTH-size-100,HEIGHT/2),size,lives,color(255,0,0))
end

function draw()
    states[state][1]()
end

function touched(t)
    states[state][2](t)
end

function drawMenu()
    background(0)
    fontSize(100)
    fill(255)
    text("FLAP TO SHOOT 2",WIDTH/2,HEIGHT-200)
    if hit==1 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/2,HEIGHT/2,200)
    if hit==2 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/4,HEIGHT/2,200)
    if hit==3 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(3*WIDTH/4,HEIGHT/2,200)
    fill(0)
    fontSize(35)
    text("PLAY",WIDTH/2,HEIGHT/2)
    text("QUIT",WIDTH/4,HEIGHT/2)
    text("SETTINGS",3*WIDTH/4,HEIGHT/2)
end

function touchMenu(t)
    ts=t.state
    if vec2(t.x-WIDTH/2,t.y-HEIGHT/2):len()<=100 then
        hit=1
        if ts==ENDED then
            reset()
            state=2
        end
    elseif vec2(t.x-WIDTH/4,t.y-HEIGHT/2):len()<=100 then
        hit=2
        if ts==ENDED then
            close()
        end
    elseif vec2(t.x-3*WIDTH/4,t.y-HEIGHT/2):len()<=100 then
        hit=3
        if ts==ENDED then
            state=4
        end
    else
        hit=0
    end
end

function drawGame1()
    fontSize(100)
    if startleft and startright then
        background(0)
        fill(255)
        ellipse(WIDTH/2,HEIGHT/2,2*blocksize)
        rect(0,0,200,200)
        rect(0,HEIGHT-200,200,200)
        rect(WIDTH-200,0,200,200)
        rect(WIDTH-200,HEIGHT-200,200,200)
        fill(0)
        noStroke()
        ellipse(200,200,400)
        ellipse(200,HEIGHT-200,400)
        ellipse(WIDTH-200,200,400)
        ellipse(WIDTH-200,HEIGHT-200,400)
        fill(0,255,0)
        text(player1.lives,WIDTH/4,HEIGHT-100)
        text(player2.lives,3*WIDTH/4,HEIGHT-100)
        player1:draw()
        player2:draw()
        for i,v in pairs(shots) do
            v:draw()
            for j,w in pairs(shots) do
                if i~=j and (v.pos-w.pos):len()<2*shotsize then
                    table.remove(shots,i)
                    table.remove(shots,j)
                end
            end
            if (v.pos-player1.pos):len()<size+shotsize then
                table.remove(shots,i)
                player1.lives=player1.lives-1
            end
            if (v.pos-player2.pos):len()<size+shotsize then
                table.remove(shots,i)
                player2.lives=player2.lives-1
            end
        end
        if winner~=0 then
            state=3
        end
        if player1.lives==0 then
            winner=2
        end
        if player2.lives==0 then
            winner=1
        end
    else
        background(255)
        fill(0)
        fontSize(20)
        text("Tap to flap and shoot",WIDTH/4,HEIGHT/2+100)
        text("Tap to flap and shoot",3*WIDTH/4,HEIGHT/2+100)
        strokeWidth(5)
        stroke(0)
        if startleft then
            rect(0,0,WIDTH/2,HEIGHT)
            stroke(255)
        end
        fill(player1.colour)
        ellipse(player1.pos.x,player1.pos.y,2*player1.radius)
        stroke(0)
        if startright then
            fill(0)
            rect(WIDTH/2,0,WIDTH,HEIGHT)
            stroke(255)
        end
        fill(player2.colour)
        ellipse(player2.pos.x,player2.pos.y,2*player2.radius)
    end
end

function touchGame1(t)
    if t.x<WIDTH/2 and t.state==BEGAN then
        if startleft and startright then
            player1.falling=jump
            table.insert(shots,Shot(vec2(2*size+shotsize+101,player1.pos.y),vec2(shotspeed,0)))
        else
            startleft=true
        end
    end
    if t.x>WIDTH/2 and t.state==BEGAN then
        if startright and startleft then
            player2.falling=jump
            table.insert(shots,Shot(vec2(WIDTH-2*size-shotsize-101,player2.pos.y),vec2(-shotspeed,0)))
        else
            startright=true
        end
    end
end

function drawEnd()
    background(0)
    fontSize(100)
    if winner==1 then
        fill(player1.colour)
        text("BLUE WINS",WIDTH/2,HEIGHT-200)
    end
    if winner==2 then
        fill(player2.colour)
        text("RED WINS",WIDTH/2,HEIGHT-200)
    end
    fontSize(35)
    fill(255)
    if hit==1 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/3,HEIGHT/2,200)
    if hit==2 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(2*WIDTH/3,HEIGHT/2,200)
    fill(0)
    text("MENU",WIDTH/3,HEIGHT/2)
    text("AGAIN",2*WIDTH/3,HEIGHT/2)
end

function touchEnd(t)
    ts=t.state
    if vec2(t.x-WIDTH/3,t.y-HEIGHT/2):len()<=100 then
        hit=1
        if ts==ENDED then
            state=1
        end
    elseif vec2(t.x-2*WIDTH/3,t.y-HEIGHT/2):len()<=100 then
        hit=2
        if ts==ENDED then
            reset()
            state=2
        end
    else
        hit=0
    end
end

function drawSettings()
    background(0)
    fill(255)
    if hit==1 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/2,150,200)
    if hit==2 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/4,HEIGHT/2,200)
    if hit==3 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(WIDTH/2,HEIGHT/2,200)
    if hit==4 and ts~=ENDED then fill(0,255,0) else fill(255) end
    ellipse(3*WIDTH/4,HEIGHT/2,200)
    fontSize(100)
    fill(255)
    text("LIVES:",WIDTH/2,HEIGHT-150)
    fontSize(35)
    fill(0)
    text("BACK",WIDTH/2,150)
    text("5",WIDTH/4,HEIGHT/2)
    text("10",WIDTH/2,HEIGHT/2)
    text("15",3*WIDTH/4,HEIGHT/2)
end

function touchSettings(t)
    ts=t.state
    if vec2(t.x-WIDTH/2,t.y-200):len()<=100 then
        hit=1
        if ts==ENDED then
            state=1
        end
    elseif vec2(t.x-WIDTH/4,t.y-HEIGHT/2):len()<=100 then
        hit=2
        if ts==ENDED then
            lives=5
            state=1
        end
    elseif vec2(t.x-WIDTH/2,t.y-HEIGHT/2):len()<=100 then
        hit=3
        if ts==ENDED then
            lives=10
            state=1
        end
    elseif vec2(t.x-3*WIDTH/4,t.y-HEIGHT/2):len()<=100 then
        hit=4
        if ts==ENDED then
            lives=15
            state=1
        end
    else
        hit=0
    end
end

Shot = class()

function Shot:init(pos,move)
    self.pos=pos
    self.move=move
end

function Shot:draw()
    fill(255)
    ellipse(self.pos.x,self.pos.y,2*shotsize)
    self.pos=self.pos+self.move
    if self.pos.x<shotsize or self.pos.x>WIDTH-shotsize then
        self.move.x=-self.move.x
    end
    if self.pos.y<shotsize or self.pos.y>HEIGHT-shotsize then
        self.move.y=-self.move.y
    end
    local d=self.pos-vec2(WIDTH/2,HEIGHT/2)
    if d:len()<blocksize+shotsize then
        self.move=-reflect(self.move,d)
    end
    local d=self.pos-vec2(200,200)
    if d:len()>200-shotsize and d.y<=0 and d.x<=0 then
        self.move=-reflect(self.move,d)
    end
    local d=self.pos-vec2(200,HEIGHT-200)
    if d:len()>200-shotsize and d.y>=0 and d.x<=0 then
        self.move=-reflect(self.move,d)
    end
    local d=self.pos-vec2(WIDTH-200,200)
    if d:len()>200-shotsize and d.y<=0 and d.x>=0 then
        self.move=-reflect(self.move,d)
    end
    local d=self.pos-vec2(WIDTH-200,HEIGHT-200)
    if d:len()>200-shotsize and d.y>=0 and d.x>=0 then
        self.move=-reflect(self.move,d)
    end
end

function reflect(a,b)
    return (2*a:dot(b)/b:lenSqr())*b-a
end

Character = class()

function Character:init(pos,radius,lives,c)
    self.pos=pos
    self.radius=radius
    self.lives=lives
    self.colour=c
    self.falling=0
end

function Character:draw()
    strokeWidth(5)
    stroke(255)
    fill(self.colour)
    ellipse(self.pos.x,self.pos.y,2*self.radius)
    self.pos.y=self.pos.y+self.falling
    self.falling=self.falling+fall
    if self.pos.y<self.radius+5 then
        self.pos.y=self.radius+5
        self.falling=jump
    end
    if self.pos.y>HEIGHT-self.radius-5 then
        self.pos.y=HEIGHT-self.radius-5
        self.falling=0
    end
end

Be careful with how you remove shots during a for loop, as you will end up skipping an element. Generally, you should use ipairs for an array and pairs for a key:value dictionary. TBH, I’m not sure what effect usingpairs on an array has. But, if you are removing items from an array with table.remove, you should use a backwards for loop for i = #shots, 1, -1 do, otherwise the element after the one you’ve removed will be skipped (because table remove shuffles all the elements up one place to fill the hole, but the pairs iterator doesn’t know this and still iterates up one slot).

@yojimbo2000 thanks, that clears up one of the problems, I set it to remove the bigger one first. Still:
• Even if you comment out the table.remove functions the shots still sometimes disappear
• When shots collide sometimes the other shots flicker
Can you think of the problem/a solution to those?