How to make your app universal (short guide)

This started out as a question about making an app universal, but I ended up figuring it out.

So here is a short little guide for anyone trying to figure it out.

If you want to make your app universal, you need to set every value according to the WIDTH and HEIGHT as in the code below, then it works as a universal app.

Whether the app runs in landscape or portrait, make sure you set values in the horizontal axis (x) relative to the WIDTH and values in the vertical axis (y) relative to the HEIGHT. Else you will run into scaling problems.

Here is the original code before being converted to universal, so you can compare and see what I did. All fixed values have been converted to values that are relative to WIDTH or HEIGHT.


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)
function setup()
    ast={}
    ship=readImage("Space Art:Red Ship")
    asteroid=readImage("Space Art:Asteroid Large")
    astFreq=1.5
    astSpeed=-9
    score=0 -- starting score
    timer=0
    rectMode(CENTER)
    if readLocalData("HS")== nil then
        saveLocalData("HS", 0)
    end
    music("A Hero's Quest:Battle",true,.5)
    gameState=0
    font("Futura-CondensedExtraBold")
    fontSize(64)
    fill(0, 255, 0, 255)
    shipX=WIDTH/2
    shipY=HEIGHT/4
end
function spawn()
    table.insert(ast,vec2(math.random(96,WIDTH-96),1152))
end
function draw()
    if gameState==0 then
        background(0, 0, 0, 255)
        text("PLAY",WIDTH/2,HEIGHT/2)
        if CurrentTouch.state==BEGAN then
            if CurrentTouch.x>WIDTH/2-256 and CurrentTouch.x<WIDTH/2+256
            and CurrentTouch.y>HEIGHT/2-128 and CurrentTouch.y<HEIGHT/2+128 then
                gameState=1
            end
        end
    end
    if gameState==1 then
        background(0, 0, 0, 255)
        pushStyle()
        text("Score: "..score, WIDTH/2, HEIGHT/1.2)
        popStyle()
    for a,b in pairs(ast) do
        sprite(asteroid,b.x,b.y,192,256)
        b.y=b.y+astSpeed
    end
    timer=timer+DeltaTime
    if timer>astFreq then
        spawn()
        spawn()
        spawn()
        timer=0 
        end
    for a,b in pairs(ast) do
        if b.y<-128 then
            table.remove(ast,a)
            score=score+10
        end
    end
    if CurrentTouch.x <= WIDTH / 2 and CurrentTouch.state ~= ENDED and CurrentTouch.state ~= CANCELLED then
            shipX=shipX-12
        end
    if CurrentTouch.x>WIDTH/2 and CurrentTouch.state ~= ENDED and CurrentTouch.state ~= CANCELLED then
            shipX=shipX+12
        end
    sprite(ship,shipX,shipY,64,96)
    for a,b in pairs(ast) do
            if b.y-96<=shipY+48 and b.y+160>=shipY-48 and
            b.x-96<=shipX+32 and b.x+96>=shipX-32 then
                gameState=2
            end
        end
    if shipX-32<0 or shipX+32>WIDTH then
            gameState=2
        end
    end
    if gameState==2 then
        background(0, 0, 0, 255)
        text("RETRY",WIDTH/2,HEIGHT/4)
        if score > readLocalData("HS") then
            saveLocalData("HS", score)
            end
        text("Score: "..score, WIDTH/2,HEIGHT/1.2)
        text("HighScore: "..readLocalData("HS"),WIDTH/2,HEIGHT/2)
        if CurrentTouch.state==BEGAN then
            if CurrentTouch.x>WIDTH/2-128 and CurrentTouch.x<WIDTH/2+128
            and CurrentTouch.y>HEIGHT/4-64 and CurrentTouch.y<HEIGHT/4+64 then
                score=0
                gameState=0
                shipX=WIDTH/2
                for i,t in pairs(ast) do ast[i]= nil
                end
            end
        end
    end
end

And here is the app converted to Universal so it runs on iphones or ipads.

The comments in the beginning of setup were used for testing. Delete the double dash before the WIDTH and HEIGHT for the device size that you want to test in Codea. Make sure that you don’t have the WIDTH and HEIGHT set to values before exporting the code to Xcode, or the final app will not work as universal and will be set to whatever size you set WIDTH and HEIGHT to.


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)
function setup()
    -- iphone 4
    --WIDTH=320
    --HEIGHT=480
    -- iphone 5
    --WIDTH=320
    --HEIGHT=568
    ast={}
    ship=readImage("Space Art:Red Ship")
    asteroid=readImage("Space Art:Asteroid Large")
    bg=readImage("SpaceCute:Background")
    astFreq=1.5
    astSpeed=-HEIGHT/114
    score=0 -- starting score
    timer=0
    rectMode(CENTER)
    if readLocalData("HS")== nil then
        saveLocalData("HS", 0)
    end
    music("A Hero's Quest:Battle",true,.5)
    gameState=0
    font("Futura-CondensedExtraBold")
    fontSize(HEIGHT/16)
    fill(0, 255, 0, 255)
    shipX=WIDTH/2
    shipY=HEIGHT/4
end
function spawn()
    table.insert(ast,vec2(math.random(WIDTH/8,WIDTH-WIDTH/8),HEIGHT+HEIGHT/8))
end
function draw()
    if gameState==0 then
        sprite(bg,WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        text("PLAY",WIDTH/2,HEIGHT/2)
        if CurrentTouch.state==BEGAN then
            if CurrentTouch.x>WIDTH/2-WIDTH/3 and CurrentTouch.x<WIDTH/2+WIDTH/3
            and CurrentTouch.y>HEIGHT/2-HEIGHT/8 and
            CurrentTouch.y<HEIGHT/2+HEIGHT/8 then
                gameState=1
            end
        end
    end
    if gameState==1 then
        sprite(bg,WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        pushStyle()
        text("Score: "..score, WIDTH/2, HEIGHT/1.2)
        popStyle()
    for a,b in pairs(ast) do
        sprite(asteroid,b.x,b.y,WIDTH/4,HEIGHT/4)
        b.y=b.y+astSpeed
    end
    timer=timer+DeltaTime
    if timer>astFreq then
        spawn()
        spawn()
        spawn()
        timer=0 
        end
    for a,b in pairs(ast) do
        if b.y<-HEIGHT/8 then
            table.remove(ast,a)
            score=score+10
        end
    end
    if CurrentTouch.x <= WIDTH / 2 and CurrentTouch.state ~= ENDED and 
        CurrentTouch.state ~= CANCELLED then
            shipX=shipX-WIDTH/64
        end
    if CurrentTouch.x>WIDTH/2 and CurrentTouch.state ~= ENDED and 
        CurrentTouch.state ~= CANCELLED then
            shipX=shipX+WIDTH/64
        end
    sprite(ship,shipX,shipY,WIDTH/12,HEIGHT/10.67)
    for a,b in pairs(ast) do
            if b.y-HEIGHT/10.667<=shipY+HEIGHT/21.33 and
            b.y+HEIGHT/6.4>=shipY-HEIGHT/21.33 and
            b.x-WIDTH/8<=shipX+WIDTH/24 and b.x+WIDTH/8>=shipX-WIDTH/24 then
                gameState=2
            end
        end
    if shipX-WIDTH/24<0 or shipX+WIDTH/24>WIDTH then
            gameState=2
        end
    end
    if gameState==2 then
        sprite(bg,WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        text("RETRY",WIDTH/2,HEIGHT/4)
        if score > readLocalData("HS") then
            saveLocalData("HS", score)
            end
        text("Score: "..score, WIDTH/2,HEIGHT/1.2)
        text("HighScore: "..readLocalData("HS"),WIDTH/2,HEIGHT/2)
        if CurrentTouch.state==BEGAN then
            if CurrentTouch.x>WIDTH/2-WIDTH/7.68 and 
            CurrentTouch.x<WIDTH/2+WIDTH/7.68
            and CurrentTouch.y>HEIGHT/4-HEIGHT/16 and 
                CurrentTouch.y<HEIGHT/4+HEIGHT/16 then
                score=0
                gameState=0
                shipX=WIDTH/2
                for i,t in pairs(ast) do ast[i]= nil
                end
            end
        end
    end
end

This may be easy stuff for the experienced programmers, but for a newbie like me it threw me of a bit. Hopefully this helps someone in the future.

My main concern is in a more complicated game with a lot more lines of code, this becomes a lot of calculations to perform 60 times per second, which may slow down the game. Not sure how valid of a concern this is though

This method will work well for most apps, but for apps displaying lots of data on the display, it will render stuff off of the screen. Assuming I decide to publish my app, it will end up being ipad only because of this issue (Mainly because I don’t want to start having to optimize variables that are set based on device type)(and I can’t move it over, there’s other necessary information to the left of it)

ellipse(WIDTH/2 + 300, HEIGHT/2, 150)

(Just wrote this code off the top of my head, sorry if there's a spelling mistake)

This code runs fine on he iPad resolution, but once you run it on an iPhone it, it goes off the screen. You’d have to do something like this is you want to run it on the iPhone (again, code is off the top of my head)

--just checking one variable, to my knowledge every device has a separate screen width
function setup()
if WIDTH >= 1024 then
size = 150
circleLocation = WIDTH/2 + 300
else
size = 75
circleLocation = WIDTH/2 + 100
end
end

function draw()
background (0,255,0,255)
fill(255,0,0,255)
ellipse(circleLocation, HEIGHT/2, size)
end()

That’s why you don’t use any fixed values. For example


WIDTH/2 - 100

Would be


WIDTH/2 - WIDTH/10.24

Which would scale it correctly no matter the width of the screen. That’s for landscape mode

You are dividing the width or height depending on if it’s an x or y value by the fixed value to get a multiple. Then you can divide any width or height screen dimension by that multiple and get a value that will be in the correct location on the screen.

So say that location is the center of a sprite on the x axis in landscape mode. On the iPad with a width of 1024 in landscape mode, the x location would be 1024/2-1024/10.24=412, which is the same as width/2 - 100. However on an iPhone 4 with a screen width of 480 in landscape mode, the x location would be at 480/2-480/10.24=193.125. In the correct location on both screens.

You can do this not only with locations, but with sprite sizes, font sizes, etc. as in my example in the original post, so it all scales nicely.

This is according to my limited ad hoc testing with this app in testflight, but it seems to work well.

@Crumble in more complex games you will use class() to define objects. So in the init() function you will call
self.width = WIDTH/10 and this will done only once, not 60 times/s.
The only gotcha: make sure to have setDisplayMode(whatever) in the script outside of the setup() and before the class() definition, otherwise you will get problems (error or not the right size, i dont remember)

@Crumble. I’m sorry, I didn’t see how you did that (didn’t pay enough attention reading over code). Also, thanks for the tutorial, I think Ill try making my soo universal based on this. It’s much simpiler than my method.

@Jmv38 - I actually ran into this one this morning whilst coding on the train :slight_smile:
Gotta love Codea!

Another thing that might help is actually work on a virtual mythical screen in the ranges of 0 to 100% or 0 to 1.

Then inside your object draw methods simply scale the x and y’s by WIDTH and HEIGHT respectively to get the actual draw position.

I’m trying to put together some guidelines for myself listing all the various resolutions of the different devices along with all the different aspect ratios.

The other thing to remember is that there are now several different aspect ratios ranging from 1.5 (iPhone 3GS), 4:3 (iPad) and (almost) 16:9 (iPhone 5 / 6) and (actual) 16:9 (iPhone 6+)

Yeah, I’m trying to get myself to place objects relative to WIDTH and HEIGHT as I make the apps now, so I don’t have to go back and fix it later.

As for the different aspect ratios, I did notice that sprite width and height sizes can get stretched in an ugly way if you size them relative to WIDTH and HEIGHT respectively because of the different aspect ratios. The solution, I think, is to size objects relative to one screen dimension, for example both the width and height size of an object with be relative to width.

Also I noticed this morning (note to self, RTFM a bit more), that if you only pass a width into the sprite function it automatically fixes the height based on the aspect ratio of the image and this works really well.

I’ve been playing with a setup to allow me to select which device I want to dev for and set up WIDTH & HEIGHT accordingly, I had a large background image and just called sprite(background,cx,cy,WIDTH) and this worked a treat.

Of course the other thing I’ve been experimenting with is actually programming the graphics using a mix of primatives / meshes etc directly into images and then blitting those - this would drastically lower the download size and also mean that higher resolution retina displays would automatically be catered for. I’ve done this before on flash games (especially for things like buttons, frames etc) and it worked really well.