A 3D game for the Codea app
--# Main | |
-- Dimensions of screen: 1024,768 | |
Worlds = {} | |
function Worlds.addTri(pos1,pos2,pos3,addTex,col,world) | |
local col,world = color(255),Worlds.town | |
local m = world.mesh | |
do | |
verts = m.vertices | |
verts[#verts+1],verts[#verts+2],verts[#verts+3] = pos1,pos2,pos3 | |
m.vertices = verts | |
end | |
do | |
local cols = {} | |
for i=1, m.size-3 do | |
cols[i] = m:color(i) | |
end | |
cols[#cols+1],cols[#cols+2],cols[#cols+3] = col,col,col | |
m.colors = cols | |
end | |
do | |
local tex = {} | |
for i=1, m.size-3 do | |
tex[i] = m:texCoord(i) | |
end | |
tex[#tex+1],tex[#tex+2],tex[#tex+3] = addTex[1],addTex[2],addTex[3] | |
m.texCoords = tex | |
end | |
end | |
function Worlds.addWall(world,x,z,w,h,r,addTex,col,y,solid) | |
local solid,y,col = solid or true, y or 0,col or color(255) | |
local m = world.mesh | |
do | |
local verts = m.vertices | |
local cos,sin = math.cos,math.sin | |
local tl,tr,bl,br = vec3(x+sin(math.rad(r))*w*-.5,y+h,z+cos(math.rad(r))*w*-.5),vec3(x+sin(math.rad(r))*w*.5,y+h,z+cos(math.rad(r))*w*.5),vec3(x+sin(math.rad(r))*w*-.5,y,z+cos(math.rad(r))*w*-.5),vec3(x+sin(math.rad(r))*w*.5,y,z+cos(math.rad(r))*w*.5) | |
verts[#verts+1],verts[#verts+2],verts[#verts+3] = tl,tr,br | |
verts[#verts+1],verts[#verts+2],verts[#verts+3] = br,bl,tl | |
m.vertices = verts | |
end | |
do | |
local cols = {} | |
for i=1, m.size-6 do | |
cols[i] = m:color(i) | |
end | |
cols[#cols+1],cols[#cols+2],cols[#cols+3] = col,col,col | |
cols[#cols+1],cols[#cols+2],cols[#cols+3] = col,col,col | |
m.colors = cols | |
end | |
do | |
local tex = {} | |
for i=1, m.size-6 do | |
tex[i] = m:texCoord(i) | |
end | |
local tl,tr,bl,br = addTex[1],addTex[2],addTex[3],addTex[4] | |
tex[#tex+1],tex[#tex+2],tex[#tex+3] = tl,tr,br | |
tex[#tex+1],tex[#tex+2],tex[#tex+3] = br,bl,tl | |
m.texCoords = tex | |
end | |
end | |
function Worlds.addFloor(x,z,w,d,y,addTex,col,world) | |
local col,addTex,r,y = col or color(255), addTex ~= false and {vec2(0,1),vec2(1,1),vec2(0,0),vec2(1,0)}, r or 0, y or 0 | |
local m = world.mesh | |
do | |
local verts = m.vertices | |
local torad = math.pi/180 | |
--local cos = math.cos | |
local tl,tr,bl,br = vec3(x+w*-.5,y,z+d*.5),vec3(x+w*.5,y,z+d*.5),vec3(x+w*-.5,y,z+d*-.5),vec3(x+w*.5,y,z+d*-.5) | |
verts[#verts+1],verts[#verts+2],verts[#verts+3] = tr,tl,bl | |
verts[#verts+1],verts[#verts+2],verts[#verts+3] = bl,br,tr | |
m.vertices = verts | |
end | |
do | |
local cols = {} | |
for i=1, m.size-6 do | |
cols[i] = m:color(i) | |
end | |
cols[#cols+1],cols[#cols+2],cols[#cols+3] = col,col,col | |
cols[#cols+1],cols[#cols+2],cols[#cols+3] = col,col,col | |
m.colors = cols | |
end | |
do | |
local tex = {} | |
for i=1, m.size-6 do | |
tex[i] = m:texCoord(i) | |
end | |
local tl,tr,bl,br = addTex[1],addTex[2],addTex[3],addTex[4] | |
tex[#tex+1],tex[#tex+2],tex[#tex+3] = tl,tr,br | |
tex[#tex+1],tex[#tex+2],tex[#tex+3] = br,bl,tl | |
m.texCoords = tex | |
end | |
end | |
function Worlds.addHouse(x,z,walTex,doorTex,rooTex,door,world,y) | |
local door,world,y = 1,Worlds.town,0 | |
world.houses[#world.houses+1] = {x = x, z = z} | |
Worlds.addWall(world,x,z-200,400,300,270,walTex) | |
Worlds.addWall(world,x,z+200,400,300,90,walTex) | |
Worlds.addWall(world,x-200,z,400,300,0,walTex) | |
Worlds.addWall(world,x+200,z,400,300,180,walTex) | |
do | |
local dz = door*201 | |
Worlds.addWall(world,x,z+dz,120,200,math.deg(math.atan(dz,0)),doorTex,color(200)) | |
end | |
local m = world.mesh | |
do | |
local v = m.vertices | |
local addV = { | |
vec3(x,400,z-200),vec3(x,400,z+200),vec3(x-200,300,z+200), | |
vec3(x-200,300,z+200),vec3(x-200,300,z-200),vec3(x,400,z-200), | |
vec3(x,400,z+200),vec3(x,400,z-200),vec3(x+200,300,z-200), | |
vec3(x+200,300,z-200),vec3(x+200,300,z+200),vec3(x,400,z+200), | |
vec3(x-200,300,z+200),vec3(x,400,z+200),vec3(x+200,300,z+200), | |
vec3(x+200,300,z-200),vec3(x,400,z-200),vec3(x-200,300,z-200) | |
} | |
table.move(addV,1,#addV,#v+1,v) | |
local cols = {} | |
for i=1,m.size do | |
cols[i] = m:color(i) | |
end | |
local w = color(255) | |
for i=#cols+1,#cols + #addV do | |
cols[i] = w | |
end | |
local tc = {} | |
for i=1,m.size do | |
tc[i] = m:texCoord(i) | |
end | |
local tl,tr,bl,br = rooTex[1],rooTex[2],rooTex[3],rooTex[4] | |
local addTc = {tl,tr,br,br,bl,tl,tl,tr,br,br,bl,tl,bl,vec2(bl.x+(br.x-bl.x)/2,bl.y+(tl.y-bl.y)/3),br,bl,vec2(bl.x+(br.x-bl.x)/2,bl.y+(tl.y-bl.y)/3),br} | |
table.move(addTc,1,#addTc,#tc+1,tc) | |
m.vertices,m.colors,m.texCoords = v,cols,tc | |
end | |
end | |
Player = {x=0,y=0,z=0,vel = 0, hp = 20, rotation=vec2(0,0),inventory = {}} | |
do | |
local s = Player | |
function s.draw() -- Player.draw | |
do | |
s.onGround = false | |
local oldx,oldy,oldz = s.x,s.y,s.z -- Store x,y,z positions for collision sensing | |
s.vel = math.max(s.vel - 0.77,-55) | |
s.y = s.y + s.vel -- Simulate gravity | |
if s.y < 0 then -- if below ground level, | |
s.vel = 0 -- set velocity to 0 | |
s.y = 0 -- and teleport to ground level | |
s.onGround = true -- Set onGround to true for the jump button | |
end | |
if World == Worlds.town then | |
local x,y,z = s.x,s.y,s.z | |
local moving = World.jx ~= 0 or World.jy ~= 0 -- jx and jy are the x,y positions of the joystick | |
if moving then | |
local new = vec2(World.jy/6,World.jx/6):rotate(Player.rotation.x) -- vec2 of next step of player | |
x,z = x+new.x,z+new.y -- add the step to current position (and assign it to the local variables) | |
s.x,s.z = x,z -- set the position of the player to the local variables x and z | |
end | |
for i,v in ipairs(World.houses) do | |
local oldY = (((oldx < v.x + 30 and oldx > v.x - 30) and 400) or ((oldx > v.x and 400 - math.abs(v.x - oldx-30)/2) or 400 - math.abs(oldx - v.x-30)/2)) -- formula for the y position whilst on the roof depending on the x position of the last frame. | |
local Y = (((x < v.x + 30 and x > v.x - 30) and 400) or ((x > v.x and 400 - math.abs(x - v.x-30)/2) or 400 - math.abs(v.x - x-30)/2)) -- formula for the y position whilst on the roof depending on the x position of the current frame. | |
if x < v.x + 230 and x > v.x - 230 and z > v.z - 230 and z < v.z + 230 then -- x,z within the walls | |
if oldy >= oldY and y < Y then -- old y is higher than the roof and current y is below the roof | |
s.vel = math.max(s.vel, 0) | |
s.y = Y -- Set y to where the roof y would be | |
s.onGround = true | |
break | |
elseif moving then | |
if oldy < oldY and oldx > v.x - 230 and oldx < v.x + 230 then -- if old y was below old roof y and also within the walls of -x and +x then | |
Player.z = v.z + (oldz > v.z and 230 or -230) -- set z to outside +z and -z walls | |
break | |
elseif oldy < 300 and oldz > v.z - 230 and oldz < v.z + 230 then | |
Player.x = v.x + (oldx > v.x and 230 or -230) | |
break | |
end | |
end | |
end | |
end | |
end | |
end | |
local posx,posy,posz = s.x,s.y+160,s.z | |
perspective() | |
local lookAtX = posx+math.cos(s.rotation.x) | |
local lookAtY = posy+math.tan(s.rotation.y) | |
local lookAtZ = posz+math.sin(s.rotation.x) | |
camera(posx,posy,posz,lookAtX,lookAtY,lookAtZ,0,1,0) | |
end | |
end | |
do | |
local intro = {mesh = mesh(),sky = color(0),noUI = true, dialogTimer = 5, text = 0, details = {t="Greetings, Player."}, dialog = {{t="Do you know who this is?",delay=3},{t="This is Corwin.",delay=3},{t="He has become the overlord of a town called Corwinia.",delay=4},{t="Those who inhabit this town are called Corwinians.",delay=4},{t="Corwin does not consider his people, people.",delay=3},{t="He treats them unfairly.",delay=3},{t="The Corwinians are not fond of Corwin.",delay=3},{t="However, they cannot tell anyone that because Corwin executes anyone who displeases him.",delay=6},{t="He has built a wall surrounding the town to keep people from leaving.",delay=4},{t="He also has guards scattered across the town to execute those who break the rules.",delay=6},{t="The town feels like it's from centuries in the past when it's actually the year 220.",delay=6},{t="Corwin is so cruel, he has even mutated his people to make them look similar to him.",delay=5},{t="You are a trained Corwinian guard and must end the madness.",delay=5},{t="Defeat the various guards and ultimately, Corwin.",delay=3},{t="Go, our hero!",delay=2}}, draw = function() | |
local s = Worlds.intro | |
s.dialogTimer = s.dialogTimer - DeltaTime | |
if s.dialogTimer <= 0 then | |
if s.text == #s.dialog then | |
World = Worlds.town | |
else | |
s.text = s.text + 1 | |
s.details = s.dialog[s.text] | |
s.dialogTimer = s.dialogTimer + s.details.delay | |
end | |
end | |
Player.draw() -- Set up the camera and everything | |
if s.text ~= 0 and s.text < 14 then | |
Worlds.intro.mesh:draw() | |
translate(400,60,0) | |
else | |
translate(400,160,0) | |
end | |
fill(255) | |
fontSize(25) | |
textWrapWidth(300) | |
font("ArialRoundedMTBold") | |
rotate(270,0,1,0) | |
text(s.details.t) | |
end} | |
intro.mesh.texture = readImage("Platformer Art:Guy Standing") | |
Worlds.addWall(intro,400,0,168,224,0,{vec2(0,1),vec2(1,1),vec2(0,0),vec2(1,0)},color(255),120) | |
Worlds.intro = intro | |
end | |
do | |
local emptyMatrix = matrix() | |
local guardM = mesh() -- mesh to be drawn for all the guards | |
guardM.vertices = {vec3(-40,180,0),vec3(40,180,0),vec3(40,0,0),vec3(40,0,0),vec3(-40,0,0),vec3(-40,180,0)} | |
guardM.texture = readImage("Platformer Art:Guy Standing") | |
guardM.texCoords = {vec2(0,1),vec2(1,1),vec2(1,0),vec2(1,0),vec2(0,0),vec2(0,1)} | |
guardM:setColors(255,255,255) | |
local town = {sortTimer = 25, jx = 0, jy = 0, cButton = {}, mesh = mesh(),sky = color(255, 42.5, 85, 255),houses = {}, guardM = guardM, guards = {}} | |
-- Each guard table contains the keys: pos (vec2), hp (number), hitTime (number) | |
local gSort = function(a,b) -- sort function for sorting the guards | |
local pDist = vec2(Player.x,Player.z) | |
return a.pos:dist(pDist) > b.pos:dist(pDist) | |
end | |
local pl = Player -- upValue for town.draw | |
local guards = town.guards -- upValue for town.draw | |
local nHouses = #town.houses -- upValue for town.draw | |
town.draw = function() -- ************* town.draw ******************** | |
town.sortTimer = town.sortTimer - 1 -- time before the code sorts the guards | |
if #guards > 1 and town.sortTimer == 0 then | |
town.sortTimer = 25 | |
table.sort(guards,gSort) -- sort the guards from frathest to closest | |
end | |
pl.draw() | |
town.mesh:draw() | |
if pl.shooting then -- If shoot button was pressed | |
pl.shooting = false | |
for i = #guards, 1, -1 do -- check each guard individually from closest to farthest | |
local v = guards[i] | |
local dist = ((pl.x-v.pos.x)^2 + (pl.z-v.pos.y)^2)^.5 -- pythagorean theorem to calculate dist | |
local y = pl.y + 160 + math.tan(pl.rotation.y) * dist -- find the y of the point we're looking at | |
if y < 180 and y > 0 and vec2(math.cos(pl.rotation.x)*dist+pl.x,math.sin(pl.rotation.x)*dist+pl.z):dist(v.pos) < 30 then -- check if we're aiming at the enemy. | |
v.hp = v.hp - 4 -- lower HP. | |
v.hitTime = 0.5 -- set the timer for the red tint. | |
break -- We don't want the bullets to be able to pierce. | |
end | |
end | |
end | |
for gi,g in ipairs(guards) do -- iterate over each guard from farthest to closest | |
if g.hp < 1 then -- if the guard's health is below 1, remove it | |
table.remove(guards,gi) | |
else | |
local ch = vec2(pl.x - g.pos.x,pl.z - g.pos.y):normalize() -- direction to player | |
local old = vec2(g.pos.x,g.pos.y) -- old position | |
g.pos.x,g.pos.y = 10 * ch.x + g.pos.x, 10 * ch.y + g.pos.y -- move in direction to player | |
local x,z = g.pos.x,g.pos.y | |
for _,v in ipairs(town.houses) do -- iterate over each house to see if the guards colliding. | |
if x > v.x - 230 and x < v.x + 230 and z < v.z + 230 and z > v.z - 230 then -- if inside house | |
if old.x > v.x - 230 and old.x < v.x + 230 then -- We can check like as if the world is 2D because the guards will never be able to jump. | |
g.pos.y = v.z + (old.y > v.z and 230 or -230) | |
elseif old.y > v.z - 230 and old.y < v.z + 230 then | |
g.pos.x = v.x + (old.x > v.x and 230 or -230) | |
end | |
break -- The houses won't be close enough for a guard to be colliding with 2 simultaneously | |
end | |
end | |
for _,v in ipairs(guards) do -- iterate over the guards to see if this guards colliding w/ another | |
if g ~= v and v.pos:dist(g.pos) < 80 then -- if colliding then | |
local ch = vec2(v.pos.x - g.pos.x,v.pos.y - g.pos.y):normalize() * 80 | |
v.pos.x,v.pos.y = g.pos.x + ch.x, g.pos.y + ch.y | |
end | |
end | |
translate(g.pos.x,0,g.pos.y) -- Now we translate to the guard's position. | |
rotate(math.deg(math.atan(pl.x-g.pos.x,pl.z-g.pos.y)),0,1,0) -- rotate towards the player. | |
if g.hitTime > 0 then -- if the guards timer for the red tint | |
g.hitTime = g.hitTime - DeltaTime | |
guardM:setColors(255,0,0) | |
else | |
guardM:setColors(255,255,255) | |
end | |
guardM:draw() | |
--[[ -- uncomment to display all the guards positions | |
local dist = ((pl.x-g.pos.x)^2 + (pl.z-g.pos.y)^2)^.5 | |
local yt = pl.y + 160 + math.tan(pl.rotation.y) * dist | |
stroke(0) | |
line(-40,yt,40,yt) | |
translate(0,90,20) | |
fill(0) | |
text(gi, 0,0) | |
--]] | |
resetMatrix() | |
end | |
end | |
viewMatrix(emptyMatrix) -- go back to 2D drawing for the UI | |
ortho() | |
UI.draw() -- draw the buttons | |
translate(512,384) -- middle of the screen | |
stroke(255) | |
strokeWidth(5) | |
line(-30,0,30,0) -- draw the crosshair | |
line(0,-30,0,30) | |
translate(-392,-264) -- goto bottom left of screen | |
stroke(0) | |
fill(200) | |
ellipse(0,0,160) -- draw the joystick | |
translate(town.jx,town.jy) | |
ellipse(0,0,50) | |
end | |
--[ -- Uncomment to spawn guards | |
for i=1, 8 do -- Change the 8 to change the amount of guards are spawned | |
town.guards[i] = {hitTime = 0, pos = vec2(math.random(-3000,-1000),math.random(0,2000)),hp=15} | |
end | |
--]] | |
Worlds.town = town | |
town.mesh.texture = readImage("SpaceCute:Background") | |
local t = {vec2(0,1),vec2(1,1),vec2(0,0),vec2(1,0)} | |
Worlds.addFloor(0,0,15000,15000,0,{vec2(0,1),vec2(1,1),vec2(0,0),vec2(1,0)},color(170, 80, 40),town) -- ground | |
Worlds.addHouse(0,0,t,t,t) | |
Worlds.addHouse(500,0,t,t,t) | |
Worlds.addHouse(1000,0,t,t,t) | |
end | |
-----------------------------------------UI------------------------------------------ | |
UI = {} | |
function UI.createImage(t) | |
local img = image(#t[1],#t) | |
for x=1, img.width do | |
for y=1, img.height do | |
img:set(x,y,t[x][y]) | |
end | |
end | |
return img | |
end | |
function UI.draw() | |
if World.noUI then return end | |
noSmooth() | |
if World.cButton then | |
World.cButton.mesh:draw() | |
fill(230) | |
font("ArialMT") | |
for i,b in pairs(World.cButton.all) do | |
if b.text then | |
fontSize(0.3*b.size) | |
text(b.text,b.pos.x,b.pos.y) | |
end | |
end | |
end | |
end | |
local inRect = UI.inRect | |
local posX,posY = 120,120 -- joystick position | |
local pi = math.pi | |
function UI.touch() -- touch detection for the UI | |
for tIndex,t in pairs(touches) do | |
local usingUi = false | |
if not World.noUI and World.cButton then | |
local cb = World.cButton | |
for _,b in pairs(cb.all) do | |
if vec2(t.x,t.y):dist(b.pos) < b.size*.5 and t.state == BEGAN then | |
b.touch = t | |
cb.mesh:setRectColor(b.id,30,50,127.5) | |
end | |
if b.touch == t then | |
usingUi = true | |
if t.state == ENDED then | |
b.touch = nil | |
touches[tIndex] = nil | |
if b.callback then b.callback() end | |
cb.mesh:setRectColor(b.id,60,100,255) | |
break | |
end | |
end | |
end | |
end | |
-- touch detection for joystick | |
if World == Worlds.town then | |
if vec2(t.x,t.y):dist(vec2(posX,posY)) < 80 then | |
if t.state == BEGAN and World.jTouch == nil then | |
World.jTouch = t | |
end | |
if World.jTouch == t then | |
World.jx,World.jy = t.x-posX,t.y-posY | |
usingUi = true | |
end | |
elseif World.jTouch == t then | |
usingUi = true | |
local pos = vec2(t.x-posX,t.y-posY):normalize() | |
World.jx,World.jy = pos.x * 80, pos.y * 80 | |
end | |
if t == World.jTouch then | |
if t.state == ENDED then | |
World.jTouch = nil | |
World.jx,World.jy = 0,0 | |
touches[t.id] = nil | |
usingUi = true | |
end | |
end | |
end | |
if not usingUi then -- if this touch is not pressing any buttons or anything, then | |
local rot = Player.rotation | |
local deltX = t.deltaX | |
local change = rot.x + deltX*.012 | |
rot.x = change % 360 == 0 and (deltX < 0 and -1 or 1) or change | |
rot.y = rot.y + t.deltaY*.012 | |
touches[t.id] = nil | |
if rot.y > pi*.5 then | |
rot.y = pi*.5 | |
elseif rot.y < pi*-.5 then | |
rot.y = pi*-.5 | |
end | |
end | |
end | |
end | |
displayMode(FULLSCREEN) | |
supportedOrientations(LANDSCAPE_ANY) | |
function setup() | |
World = Worlds.intro -- set this to Worlds.town to skip the intro | |
touches = {} -- table to keep all the touches | |
do | |
local img = image(150,150) -- texture for the buttons | |
setContext(img) | |
stroke(200) | |
strokeWidth(8) | |
fill(255) | |
ellipse(75,75,150) | |
setContext() | |
UI.cButtonImg = img | |
--[[ FUNCTION DESCRIPTION: UI.addCircleButton =================================== | |
Below, the parameters between <> are mandatory and those between [] are optional. | |
3 parameters: | |
<world (table)>: The world you wish to own this button | |
<b (table)>: The details of the button | |
<key> (string): The key of the button | |
b can include details such as: | |
<pos (2D vector)>: The x and y position of the button | |
[text (string)]: The text to be displayed over the button | |
[callback (function)]: The function to be called when the button is pressed | |
[size (number)]: The size of the button (defaults to 100) | |
NOTE: world must contain a table, cButton. If not, an error WILL occur | |
--]] | |
function UI.addCircleButton(world,b,key) | |
local cb = world.cButton | |
if not cb.mesh then | |
local m = mesh() | |
cb.mesh = m | |
m.texture = img | |
end | |
b.size = b.size or 100 | |
b.id = cb.mesh:addRect(b.pos.x,b.pos.y,b.size,b.size) | |
cb.mesh:setRectTex(b.id,0,0,1,1) | |
cb.mesh:setRectColor(b.id,60,100,255) | |
if cb.all then | |
cb.all[key] = b | |
else | |
cb.all = {[key] = b} | |
end | |
end | |
end | |
local town = Worlds.town | |
UI.addCircleButton(town, {size = 100, pos = vec2(70,708), text = "❚❚"}, "pause") | |
local pl = Player | |
UI.addCircleButton(town, {size = 100, pos = vec2(939,240), text = "⇪",callback = function() | |
if pl.onGround then | |
pl.vel = 26 -- height of jump | |
end | |
end}, "jump") | |
UI.addCircleButton(town, {size = 120, pos = vec2(929,698), text = "⇵"}, "changeWeapon") | |
local torad = math.pi/180 | |
UI.addCircleButton(town, {size = 150, pos = vec2(914,110), text = "⚡️",callback = function() | |
pl.shooting = true | |
end}, "shoot") | |
end | |
function draw() | |
background(World.sky) | |
UI.touch() | |
World.draw() -- Draw the current world (there are two) | |
end | |
function touched(t) | |
local T = touches[t.id] | |
if T then | |
T.state,T.x,T.y,T.deltaX,T.deltaY = t.state,t.x,t.y,t.deltaX,t.deltaY -- Avoid creating a new table. | |
else | |
touches[t.id] = {state=t.state,x=t.x,y=t.y,id=t.id,deltaX=t.deltaX,deltaY=t.deltaY} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment