Plane Crazy

This is my work-in-progress game, Plane Crazy.

http://www.youtube.com/watch?v=xmhhAqIDgno

I hesitate to call it a flight sim, since the world view is all top down and very simple. Behind the scenes, the math is not too bad – incorporating drag, lift, etc. So the behavior of the plane is moderately realistic (though not based on any particular plane). Game play is designed more around the old Dreamcast game, Crazy Taxi, in which you try to reach targets before the time runs out. In Plane Crazy, the timer represents fuel, so you race from one target to the next trying to keep your engine humming along.

It can lead to some amusing behavior, such as running out of gas only to glide to the next target and hear the engine kick in again. You can even throttle down to nothing in mid-flight, saving fuel for times when you want to run up your altitude.

There’s still clearly a lot of work to be done. The instruments all look pretty good, but the world is nothing but some squares and a few very regular “forests.” Since meshes have proven to draw very, very quickly, I think I can toss in a few thousand more trees and maybe some other items before the performance really takes a hit.

I’ll have the code up soon.

AWSOME! Did you start this for my competition? This is amazing. You should change the subject to competition to enter it into the competition :smiley:

Where are da bombs? - Oh, this IS da bombs!

Great work!!!

Sort of. I was fiddling with the gauge and chart classes I made earlier and came up with the artificial horizon display. That got me started on the other controls – the clipped numbers for the altimeter, the speedometer dial. And of course, once I had instruments, I had to have a plane.

I think it would be quite possible to do a decent job on a POV 3D flight sim, but I didn’t think I could get it together by the 18th.

Good. You put it in the competition category. By the way, it’s almost as cool as the Google Earth flight simulator>

Great work! Are you using sprites for the instruments or drawing from primitives? They look really good.

Yeah the instruments are great. I love the analogue numerical dial.

Also like the fields scrolling past. Gives a real sense of scale.

Thanks. The instruments are drawn from primitives. As has happened so many times with Codea, I started off with “gee, I’ll do it this way until the performance sucks” then never had to change,

And the code, in a few easy pieces. You’ll quickly see that 1) there’s a lot of duplicated code among gauges that really should be in one piece, 2) there are a lot of numbers in there meaning that as-is the code WILL NOT work on another screen size, 3) I’m still fighting to keep things straight with multiple uses of rotation and translation. I’ll keep knocking those things around. In the meantime, here’s the first draft.

Deleted – see new version below

Ok, here we go with version 0.2.

  1. This one should work much better with devices of different resolution.
  2. The terrain is more varied and colorful, with 2000 bushes, 3500 trees, “sandy” areas, rocks, water, etc.
  3. Gameplay balance has been tuned up, so that planning your next target, adjusting the throttle, and careful attention to altitude and speed pay off in prolonged play. (I hit 900… Beat that.)
  4. That crazy acceleration control mode is in there for people who have a parking lot handy and don’t mind people seeing them run around with an iPad held out at chest level can give it a go.

And… I think that’s about it. If someone can test performance on an iPad 1 that would be great. I kept cranking up the number of ground features until the iPad 2 started to stutter, then backed off a few thousand items. I can’t tell you if an iPad 1 would be silk or a slide show.

I deleted the code above because there was some rearranging. If no one spots anything serious in this version, I’m counting this “official” and working on updates of orb-bits and Sprite Invaders between now and Saturday. Thanks.

Altimeter = class()

function Altimeter:init(x, y, r)
    -- you can accept and set parameters here
    self.x = x
    self.y = y
    self.r = r
    self.val = 0
end

function Altimeter:draw()
    local x, y, r, one, ten, hundred, thousand
    x = self.x
    y = self.y
    r = self.r
    if self.val > 9999 then self.val = 9999 end
    font("ArialMT")
    pushMatrix()
    pushStyle()
    translate(x, y)
    fill(61, 61, 61, 255)
    stroke(84, 84, 84, 255)
    rect(-r/2-5, -r/2-5, r+10, r+10)
    fill(175, 129, 77, 255)
    ellipse(-r/2+5, -r/2+5, 10)
    ellipse(-r/2+5, r/2-5, 10)
    ellipse(r/2-5, -r/2+5, 10)
    ellipse(r/2-5, r/2-5, 10)
    strokeWidth(3)
    fill(176, 176, 176, 255)
    stroke(219, 191, 149, 255)
    ellipse(0, 0, r)
    stroke(141, 139, 139, 255)
    ellipse(0,0,r-10)
    strokeWidth(1)
    w = r / 170
    scale(w)
    fill(76, 76, 76, 255)
    rect(- 65, - 25, 130, 50)
    fontSize(48)
    fill(255, 255, 255, 255)
    rect(30,-20,30,40)
    fill(0, 0, 0, 255)
    text("0", 44, 0)
    rect(-1,-20,30,40)
    rect(-32,-20,30,40)
    rect(-63,-20,30,40)
    fill(255, 255, 255, 255)
    thousand = math.floor(self.val / 1000)
    hundred = math.floor((self.val - thousand * 1000) / 100)
    ten = math.floor((self.val - thousand * 1000 - hundred * 100) / 10)
    one = math.fmod(self.val, 10)
    -- tens
    clip(self.x - 63, self.y - 20 * w, self.x - 33, 40 * w)
    text(ten, 12, -one * 4 )
    s = ten + 1
    s = string.sub(s, string.len(s), string.len(s))
    text(s, 12, -one * 4 + 40)
    -- hundreds
    clip(self.x - 32, self.y - 20 * w, self.x + 1, 40 * w)
    text(hundred, -18, -ten * 4 )
    s = hundred + 1
    s = string.sub(s, string.len(s), string.len(s))
    text(s, -18, -ten * 4 + 40)
    -- thousand
    clip(self.x - 63, self.y - 20 * w, self.x + 1, 40 * w)
    text(thousand, -50, -hundred * 4 )
    s = thousand + 1
    s = string.sub(s, string.len(s), string.len(s))
    text(s, -50, -hundred * 4 + 40)
    noClip()
    fontSize(12)
    fill(0, 0, 0, 255)
    text("Altitude", 0, 40)
    text("Feet", 0, -40)
    popStyle()
    popMatrix()
end

Deleted – see new version below

Compass = class()

function Compass:init(x, y)
    self.x = x
    self.y = y
    self.w = WIDTH * 0.22
end

function Compass:draw()
    pushMatrix()
    pushStyle()
    translate(self.x, self.y)
    fill(61, 61, 61, 255)
    stroke(84, 84, 84, 255)
    rect(0, 0, self.w, 40)
    ellipse(0, 20, 20)
    ellipse(self.w, 20, 20)
    strokeWidth(3)
    fill(192, 192, 192, 255)
    stroke(219, 191, 149, 255)
    rect(5, 20, self.w - 10, 18)
    fill(80, 80, 80, 255)
    ellipse(-2, 20, 8)
    ellipse(self.w + 2, 20, 8)
    font("CourierNewPS-BoldMT")
    fontSize(22)
    fill(104, 31, 31, 255)
    textMode(CENTER)
    textAlign(CENTER)
    text(math.floor(heading), self.w / 2, 27)
    font("Verdana")
    fontSize(14)
    fill(232, 218, 218, 255)
    text("Direction", self.w / 2, 10)
    popStyle()
    popMatrix()
end
Frame = class()

-- Frame 
-- ver. 1.0
-- a simple rectangle for holding controls.
-- ====================

function Frame:init(left, bottom, right, top)
    self.left = left
    self.right = right
    self.bottom = bottom
    self.top = top
end

function Frame:inset(dx, dy)
    self.left = self.left + dx
    self.right = self.right - dx
    self.bottom = self.bottom + dy
    self.top = self.top - dy
end

function Frame:offset(dx, dy)
    self.left = self.left + dx
    self.right = self.right + dx
    self.bottom = self.bottom + dy
    self.top = self.top + dy
end
    
function Frame:draw()
    pushStyle()
    rectMode(CORNERS)
    rect(self.left, self.bottom, self.right, self.top)
    popStyle()
end

function Frame:roundRect(r)
    pushStyle()
    insetPos = vec2(self.left + r,self.bottom + r)
    insetSize = vec2(self:width() - 2 * r,self:height() - 2 * r)

    rectMode(CORNER)
    rect(insetPos.x, insetPos.y, insetSize.x, insetSize.y)

    if r > 0 then
        smooth()
        lineCapMode(ROUND)
        strokeWidth(r * 2)

        line(insetPos.x, insetPos.y, 
             insetPos.x + insetSize.x, insetPos.y)
        line(insetPos.x, insetPos.y,
             insetPos.x, insetPos.y + insetSize.y)
        line(insetPos.x, insetPos.y + insetSize.y,
             insetPos.x + insetSize.x, insetPos.y + insetSize.y)
        line(insetPos.x + insetSize.x, insetPos.y,
             insetPos.x + insetSize.x, insetPos.y + insetSize.y)            
    end
    popStyle()
end

function Frame:gloss(baseclr)
    local i, t, r, g, b, y
    pushStyle()
    if baseclr == nil then baseclr = color(194, 194, 194, 255) end
    fill(baseclr)
    rectMode(CORNERS)
    rect(self.left, self.bottom, self.right, self.top)
    r = baseclr.r
    g = baseclr.g
    b = baseclr.b
    for i = 1 , self:height() / 2 do
        r = r - 1
        g = g - 1
        b = b - 1
        stroke(r, g, b, 255)
        y = (self.bottom + self.top) / 2
        line(self.left, y + i, self.right, y + i)
        line(self.left, y - i, self.right, y - i)
    end
    popStyle()
end

function Frame:shade(base, step)
    pushStyle()
    strokeWidth(1)
    for y = self.bottom, self.top do
        i = self.top - y
        stroke(base - i * step, base - i * step, base - i * step, 255)
        line(self.left, y, self.right, y)
    end
    popStyle()
end

function Frame:touched(touch)
    if touch.x >= self.left and touch.x <= self.right then
        if touch.y >= self.bottom and touch.y <= self.top then
            return true
        end
    end
    return false
end

function Frame:ptIn(x, y)
    if x >= self.left and x <= self.right then
        if y >= self.bottom and y <= self.top then
            return true
        end
    end
    return false
end

function Frame:width()
    return self.right - self.left
end

function Frame:height()
    return self.top - self.bottom
end

function Frame:midX()
    return (self.left + self.right) / 2
end
    
function Frame:midY()
    return (self.bottom + self.top) / 2
end
FlightTimer = class()

function FlightTimer:init(x, y, title)
    self.x = x
    self.y = y
    self.w = WIDTH * 0.22
    self.title = title
    self.val = 0
end

function FlightTimer:draw(alt)
    pushMatrix()
    pushStyle()
    if alt > 0 then self.val = self.val + 0.03 end
    translate(self.x, self.y)
    
    fill(61, 61, 61, 255)
    stroke(84, 84, 84, 255)
    rect(0, 0, self.w, 40)
    ellipse(0, 20, 20)
    ellipse(self.w, 20, 20)
    strokeWidth(3)
    fill(192, 192, 192, 255)
    stroke(219, 191, 149, 255)
    rect(5, 20, self.w-10, 18)
    fill(80, 80, 80, 255)
    ellipse(-2, 20, 8)
    ellipse(self.w+2, 20, 8)
    font("CourierNewPS-BoldMT")
    fontSize(22)
    fill(104, 31, 31, 255)
    textMode(CENTER)
    textAlign(CENTER)
    text(math.floor(self.val), self.w/2, 27)
    font("Verdana")
    fontSize(14)
    fill(232, 218, 218, 255)
    text(self.title, self.w/2, 10)
    
    popStyle()
    popMatrix()
end

Deleted – see new version below


Horizon = class()

function Horizon:init(x, y, r)
    self.x = x
    self.y = y
    self.r = r
    self.roll=0
    self.pitch=0
end

function Horizon:draw()
    local x, y, r
    x = self.x
    y = self.y
    r = self.r
    pushMatrix()
    pushStyle()
    translate(x, y)
    fill(61, 61, 61, 255)
    stroke(84, 84, 84, 255)
    rect(-r/2-5, -r/2-5, r+10, r+10)
    noStroke()
    fill(19, 19, 19, 255)
    ellipse(-r/2+5, -r/2+5, 10)
    ellipse(-r/2+5, r/2-5, 10)
    ellipse(r/2-5, -r/2+5, 10)
    ellipse(r/2-5, r/2-5, 10)
    strokeWidth(3)
    fill(176, 176, 176, 255)
    stroke(219, 191, 149, 255)
    ellipse(0, 0, r)
    stroke(141, 139, 139, 255)
    ellipse(0,0,r-10)
    strokeWidth(3)
    stroke(98, 98, 98, 255)
    for i=0,6 do
        line(r/2-20,0,r/2-35,0)
        rotate(30)
    end
    rotate(-210)
    font("ArialMT")
    textAlign(CENTER)
    fill(0, 0, 0, 255)
    fontSize(12)
    text("Horizon", 0, 25)
    self.roll = Gravity.x * 90
    self.pitch = Gravity.y 
    rotate(self.roll)
    strokeWidth(10)
    stroke(0, 206, 255, 255)
    x = r / 2 * ( 1 - math.abs(self.pitch / 2.4)) - 5
    y = r / 2 * (self.pitch * 0.8)
    line(0,y+2,-x,y+2)
    line(0,y+2,x,y+2)
    stroke(157, 124, 86, 255)
    line(0,y-3,-x,y-3)
    line(0,y-3,x,y-3)
    stroke(175, 148, 63, 255)
    fill(127, 127, 127, 255)
    rotate(-self.roll)
    stroke(230, 230, 230, 113)
    line(0,0,-r/4,-r/4)
    line(0,0,r/4,-r/4)
    line(0,0,0,-r/4)
    stroke(255, 102, 0, 255)
    line(-r/4,0, -20,0)
    line(r/4,0, 20,0)
    popStyle()
    popMatrix()
end
-- Main
--
-- Plane Crazy 0.2 --

supportedOrientations(PORTRAIT)
displayMode(FULLSCREEN)

function setup()
    local i
    -- targets
    target = {}
    for i = 1, 3 do newTarget(i) end
    -- instrument panel
    w = (WIDTH - 80) / 4
    panelTop = w + 130
    throttle = Throttle(w * 3 + 100, 60, WIDTH - 30, panelTop - 10)
    horizon = Horizon(w * 3 - w/2 + 50, panelTop - w/ 2 - 10, w)
    altimeter = Altimeter(w * 2 - w/2 + 30, panelTop - w/ 2 - 10, w)
    speedo = Speedometer(w - w/2 + 10, panelTop - w/ 2 - 10, w)
    compass = Compass( 10, 60)
    flightTimer = FlightTimer(w + 30, 60, "Flight Time")
    bestFlightTimer = FlightTimer(w * 2 + 50, 60, "Best Flight Time")
    map = Map(WIDTH-200, HEIGHT-200, 190, 190)
    logFrame = Frame(WIDTH - 145, panelTop + 10, 
    WIDTH - 10, panelTop + 65)
    -- meshes used for ground
    trees = mesh()
    trees2 = mesh()
    ground = mesh()
    mud = mesh()
    bushes = mesh()
    rocks = mesh()
    createMapElements()

    -- state of game variables
    status = 1
    showLog = false
    stall = false
    oldTouch = nil
    accelMode = false
    
    -- button for log book check box
    accelFrame = Frame(WIDTH / 2 + 10, HEIGHT - 433, 
    WIDTH / 2 + 35, HEIGHT - 395)
    
    mapFrame = Frame(0, panelTop, WIDTH, HEIGHT)
    
    -- read high score
    i = readLocalData("PlaneCrazyBest")
    if i ~= nil then
        bestFlightTimer.val = i
    end
    
    -- Start new game
    newGame()
end

function drawPlane()
    local x, y
    pushMatrix()
    translate(WIDTH/2, HEIGHT/2-100)
    rotate(180 + spinTimer)
    x=70
    if altimeter.val < 10 then y = 70 else
        y=70-math.abs(50*horizon.pitch)
    end
    if crashed then
        sprite("Tyrian Remastered:Explosion Huge", 0, 0, 120, 120)
        timer = 0
        if flightTimer.val > bestFlightTimer.val then
            bestFlightTimer.val = flightTimer.val
            saveLocalData("PlaneCrazyBest", bestFlightTimer.val)
        end
    else
        tint(56, 56, 56, 37)
        sprite("Tyrian Remastered:Enemy Ship B", 0, 
        altimeter.val/2, 70,y)
        noTint()
        if horizon.roll < -20 then
            sprite("Tyrian Remastered:Enemy Ship B R1", 0, 0, 70,y)
        elseif horizon.roll > 20 then
            sprite("Tyrian Remastered:Enemy Ship B L1", 0, 0, 70,y)
        else
            sprite("Tyrian Remastered:Enemy Ship B", 0, 0, 70,y)
        end
    end
    popMatrix()
end

function drawMap()
    -- draw the map features
    pushStyle()
    pushMatrix()
    rotate(heading)
    translate(east + WIDTH / 2, north + HEIGHT / 2)
    s = 1 - (altimeter.val / 5000)
    -- scaling disabled. .
    --scale(s)
    line(-10000, 0, 10000, 0)
    line(0, -10000, 0, 10000)
    ground:draw()
    mud:draw()
    rocks:draw()
    bushes:draw()
    trees:draw()
    trees2:draw()
    runway:draw()
    -- runway details
    stroke(213, 193, 129, 255)
    strokeWidth(5)
    for i=1,10 do
        line(0, i * 100 - 400, 0, i * 100 - 350)
    end
    stroke(0, 138, 255, 255)
    line(-80, 800, 80, 800)
    font("Futura-CondensedExtraBold")
    fill(151, 151, 151, 135)
    fontSize(96)
    text("0 0", 0, -200)
    -- targets
    noFill()
    strokeWidth(20)
    stroke(255, 240, 0, 255)
    for i=1,#target do
        if target[i].z == 500 then
            stroke(255, 36, 0, 104)
        elseif target[i].z == 1000 then
            stroke(253, 224, 3, 255)
        else
            stroke(0, 255, 253, 255)
        end
        ellipse(target[i].x, target[i].y, target[i].z)
        line(target[i].x - target[i].z / 3.2, 
        target[i].y - target[i].z / 3.2, 
        target[i].x + target[i].z / 3.2, 
        target[i].y + target[i].z / 3.2)
        line(target[i].x - target[i].z / 3.2, 
        target[i].y + target[i].z / 3.2, 
        target[i].x + target[i].z / 3.2, 
        target[i].y - target[i].z / 3.2)
    end
    popMatrix()
    -- intro screen if in intro mode
    if status == 1 then
        font("MarkerFelt-Wide")
        fontSize(222 * WIDTH / 768)
        fill(0, 0, 0, 123)
        text("Plane", WIDTH / 2, HEIGHT - 100)
        text("Crazy", WIDTH / 2, HEIGHT - 300)
        fill(255, 252, 0, 255)
        text("Plane", WIDTH / 2 - 10, HEIGHT - 90)
        text("Crazy", WIDTH / 2 - 10, HEIGHT - 290)
        fontSize(72 * WIDTH / 768)
        text("Touch here to begin", WIDTH /2, HEIGHT - 450)
    end
    popStyle()
end

function drawInstruments()
line(0, panelTop, WIDTH, panelTop)
    fill(89, 89, 89, 255)
    rect(0,0,WIDTH,panelTop)
    -- instruments
    horizon:draw()
    throttle:draw()
    altimeter:draw()
    speedo:draw()
    compass:draw()
    flightTimer:draw(altimeter.val)
    bestFlightTimer:draw(0)
end

function drawLogBook()
    -- pilot log book
    pushStyle()
    fill(0, 0, 0, 86)
    logFrame:draw()
    stroke(255, 230, 0, 255)
    strokeWidth(1)
    rect(logFrame.right - 75, logFrame.bottom + 5, 10, 10)
    fill(255, 230, 0, 255)
    fontSize(14)
    if accelMode then
        text("Accel. Mode", throttle.frame:midX(), 
        throttle.frame.bottom + 5)
    end
    fontSize(12)
    text("Pilot Log", logFrame.left + 100, logFrame.bottom + 10)
    font("HoeflerText-BlackItalic")
    fill(216, 216, 216, 142)
    noStroke()
    ellipse(logFrame.left + 30, logFrame.bottom + 28, 44)
    fill(0, 0, 0, 112)
    fontSize(44)
    text("i", logFrame.left + 30, logFrame.bottom + 22)
    popStyle()
end

function draw()
    noSmooth()
    background(31, 89, 36, 255)
    
    drawMap()
    drawPlane()
    drawInstruments()
    if status > 1 and not crashed then calcPosition() end
    if status > 1 then map:draw(east, north, target) end
    drawLogBook()

    if showLog then
        showInstructions()
    end
    
    -- touch handle
    if not accelMode then
        throttle:touched(CurrentTouch)
    else
        throttle.val = throttle.val + UserAcceleration.y * 2
        if throttle.val > 100 then throttle.val = 100 
        elseif throttle.val < 0 then throttle.val = 0 end
    end
        
    -- check targets
    for i = 1, #target do
        if math.abs(north + target[i].y + 300) < target[i].z / 1.5
        and math.abs(east + target[i].x) < target[i].z / 1.5 then
            if target[i].z == 500 then
                timer = timer + 50
            elseif target[i].z == 1000 then
                timer = timer + 40
            else
                timer = timer + 25
            end
            sound(SOUND_POWERUP, 26628)
            newTarget(i)
        end
    end
    
    if mapFrame:touched(CurrentTouch)  then
        if status == 1 and oldTouch ~= CurrentTouch.x then 
            status = 2
            throttle.val = 0
            newGame()
            
        elseif showLog and oldTouch ~= CurrentTouch.x 
            and CurrentTouch.state == BEGAN then
            if accelFrame:touched(CurrentTouch) then 
                accelMode = not accelMode
            else
                showLog = false
            end
        end
        
    end
    
    if logFrame:touched(CurrentTouch) then
        if oldTouch ~= CurrentTouch.x then
            showLog = true
        end
    end
    
    oldTouch = CurrentTouch.x
end

Deleted – see new version below

-- minimap for Plane Crazy

Map = class()

function Map:init(x, y, w, h)
    self.x = x
    self.y = y
    self.w = w
    self.h = h
end

function Map:convert(east, north)
    x = (self.w + self.x) - (east+20000) * self.w/40000
    y = (self.y + self.h) -(north+20000) * self.h/40000
    return x, y
end

function Map:draw(east, north, target)
    local x, y, i
    pushStyle()
    fill(195, 195, 195, 155)
    strokeWidth(2)
    stroke(94, 94, 94, 131)
    rect(self.x, self.y, self.w, self.h)
    stroke(235, 6, 6, 255)
    x, y = self:convert(east, north)
    
    if x < self.x then x= self.x end
    if x > self.x + self.w then x= self.x + self.w end
    if y < self.y then y= self.y end
    if y > self.y + self.h then y= self.y + self.h end
    line(x-2,y,x+2,y)
    line(x,y-2,x,y+2)
    stroke(127, 127, 127, 255)
    x, y = self:convert(0, 0)
    fill(126, 126, 126, 255)
    rect(x, y, 1, 10)
    
    fill(61, 61, 61, 255)
    fontSize(18)
    text("Map", self.x + 25, self.y + 15)
    noFill()
    for i=1,#target do
        if target[i].z == 500 then
            stroke(255, 36, 0, 104)
        elseif target[i].z == 1000 then
            stroke(253, 224, 3, 255)
        else
            stroke(50, 83, 239, 255)
        end
        x, y = self:convert(-target[i].x, -target[i].y)
        ellipse(x, y, target[i].z / 100)
    end
    popStyle()
end