@dave1707 was kind enough to create improved code to simulate 3D FPS (first person shooter) movement in Codea. It makes extensive use of trigonometry to allow and calculate movement for the scene.camera in 3D (note that @Ignatz previously provided instruction on how to do this in a Codea wiki 3D tutorial but incorporated quaternions). I’ve created some updated demos to show off the utility of @dave1707’s new code:
Updated 3D FPS movement code (CraftFPSultra.zip):
-- CraftFPSultra
-- code from @dave1707
viewer.mode=FULLSCREEN
function setup()
assert(craft, "Include Craft as a dependency")
fill(255)
scene = craft.scene()
scene.camera.position = vec3(cameraX,0,cameraZ)
scene.camera.eulerAngles=vec3(0,0,0)
scene.sun.rotation = quat.eulerAngles(90,0,0)
scene.ambientColor = color(90,90,90)
skyMaterial = scene.sky.material
skyMaterial.horizon = color(0, 203, 255, 255)
img = readImage(asset.builtin.Blocks.Missing)
for a=1,2000 do
x=math.random(-200,200)
y=math.random(-200,200)
z=math.random(-200,200)
createCube(vec3(x,y,z),vec3(255,255,255))
end
c1=cameraClass()
end
function draw(s)
background(0)
cx,cy,cz,ax,ay,az=c1:updateCameraPos()
scene.camera.position = vec3(cx,cy,cz)
scene.camera.eulerAngles=vec3(ax,ay,az)
scene:draw()
c1:draw()
text("O",WIDTH/2,HEIGHT/2)
end
function touched(t)
c1:touched(t)
end
function createCube(p,c)
local pt=scene:entity()
pt.position=vec3(p.x,p.y,p.z)
pt.model = craft.model.cube(vec3(2,2,2))
pt.material = craft.material(asset.builtin.Materials.Basic)
pt.material.map = img
pt.material.diffuse=color(c.x,c.y,c.z)
end
cameraClass=class()
function cameraClass:init()
self.angleX,self.angleY,self.angleZ=0,0,0
self.speed,self.xx,self.yy=0,0,0
self.x1,self.y1,self.z1=0,0,0
self.maxDist=.1 -- max speed per draw cycle, change for your needs
self.cameraX,self.cameraY,self.cameraZ=0,0,0
-- create button array using buttonClass
self.btnTab={}
table.insert(self.btnTab,buttonClass(WIDTH/2-100,150,"Look Left",
function() self:setZero() self.xx=-self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2+100,150,"Look Right",
function() self:setZero() self.xx=self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2,150,"Forward",
function()
self:setZero()
abx=math.abs(self.angleX)%360
if abx>=0 and abx<=90 or abx>=270 and abx<=360 then
self.speed=.1
else
self.speed=-.1
end
end))
table.insert(self.btnTab,buttonClass(WIDTH/2,100,"Backward",
function()
self:setZero()
abx=math.abs(self.angleX)%360
if abx>=0 and abx<=90 or abx>=270 and abx<=360 then
self.speed=-.1
else
self.speed=.1
end
end))
table.insert(self.btnTab,buttonClass(WIDTH/2+200,150,"Move Up",
function() self:setZero() self.y1=self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2+200,100,"Move Down",
function() self:setZero() self.y1=-self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2-200,150,"Look Up",
function() self:setZero() self.yy=-self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2-200,100,"Look Down",
function() self:setZero() self.yy=self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2-100,100,"Move Left",
function() self:setZero() self.x1=self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2+100,100,"Move Right",
function() self:setZero() self.x1=-self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2-100,50,"Rotate Left",
function() self:setZero() self.z1=self.maxDist end))
table.insert(self.btnTab,buttonClass(WIDTH/2+100,50,"Rotate Right",
function() self:setZero() self.z1=-self.maxDist end))
end
function cameraClass:draw()
pushStyle()
for a,b in pairs(self.btnTab) do
b:draw()
end
str1=string.format("%4.1f %4.1f %4.1f",self.cameraX,self.cameraY,self.cameraZ)
text("Camera position "..str1,WIDTH/2,HEIGHT-50)
str1=string.format("%4.1f %4.1f %4.1f",-self.angleX,self.angleY,self.angleZ)
text("Camera angle "..str1,WIDTH/2,HEIGHT-75)
popStyle()
end
function cameraClass:touched(t)
for a,b in pairs(self.btnTab) do
touchEnded=b:touched(t)
end
if touchEnded then
self:setZero()
end
end
function cameraClass:setZero()
self.xx,self.yy,self.zz,self.speed=0,0,0,0
self.x1,self.y1,self.z1=0,0,0
end
function cameraClass:updateCameraPos()
-- angleX=angle up/down, yy=+- look up/down
self.angleX=self.angleX+self.yy
-- angleY=angle left/right, xx=+- look left/right
self.angleY=self.angleY-self.xx
-- angleZ=rotate left/right
self.angleZ=self.angleZ+self.z1
-- calc distance
local x=self.speed*math.sin(math.rad(self.angleY))
local y=-self.speed*math.tan(math.rad(self.angleX))
local z=self.speed*math.cos(math.rad(self.angleY))
-- move the same distance per draw cycle irregardless of direction
if x~=0 or y~=0 or z~=0 then
local dist=1/math.sqrt(x^2+y^2+z^2)
x=x*dist*self.maxDist
y=y*dist*self.maxDist
z=z*dist*self.maxDist
end
-- camera move forward/backward, look left/right, look up/down
self.cameraX=self.cameraX+x
self.cameraY=self.cameraY+y
self.cameraZ=self.cameraZ+z
-- camera move up/down
self.cameraY=self.cameraY+self.y1
-- camera move left/right
self.cameraX=self.cameraX+math.cos(math.rad(self.angleY))*self.x1
self.cameraZ=self.cameraZ-math.sin(math.rad(self.angleY))*self.x1
return self.cameraX,self.cameraY,self.cameraZ,self.angleX,self.angleY,self.angleZ
end
buttonClass=class()
function buttonClass:init(x,y,n,a)
self.x=x
self.y=y
self.name=n
self.action=a
end
function buttonClass:draw()
pushStyle()
rectMode(CENTER)
fill(255)
rect(self.x,self.y,90,40)
fill(0)
text(self.name,self.x,self.y)
popStyle()
end
function buttonClass:touched(t)
if t.state==BEGAN or t.state==CHANGED then
if t.x>self.x-45 and t.x<self.x+45 and t.y>self.y-20 and t.y<self.y+20 then
self.action()
end
end
if t.state==ENDED then
return true
end
end
This is @dave1707’s demo running (CraftFPSultra.zip):
I created the following additional demos using @dave1707’s code; I’ve added Codea Craft 3D physics to detect when the FPS camera (or avatar attached to the FPS camera) collides into a solid object of the scene by adding a “rigidbody” to the 3D models and then using sphereCast to determine when the FPS camera is about to run into the 3D model in the scene. All 3D models are free downloads; unfortunately, the models are too large to attach to the zip files on this forum (please feel free to email me or reply to this post if you would like any of the links to the free 3D models that I used in the demos).
I posted this sea bottom demo previously on the forum but now have incorporated the updated 3D movement code which can handle underwater somersaults (CraftSeaBottom.zip):
@dave1707’s demo gave me the idea to create this simulator of a submarine navigating an underwater minefield. @dave1707 was additionally generous in sharing exploding mesh code that he previously created which I used to simulate an explosion after the submarine hits a mine (CraftSubSim.zip):
The following code simulates an astronaut on a tetherless space walk near a space station. I added a 4 color sphere above the astronaut’s head to use as a gyro to help the player keep themselves orientated in 3D space (CraftSpace.zip):
Finally, I’ve attached my favorite demo in which the player gets to explore a haunted house with a “ghoul” inside. The demo is a bit more complicated in that it shows off one way I came up with to allow the FPS player camera to enter an otherwise solid 3D model (e.g. the haunted house); I mapped the 3D coordinates of two of the doors on the house and instructed Codea to allow movement through the solid 3D model if sphereCast detected the FPS player camera was approaching the 3D coordinates of one of those doors. Note that much of the indoor decor was left unfinished by the 3D artist; however, I liked the rest of 3D model so much (and it was free) that I thought it was worth including in a demo (CraftHaunted.zip):
Have fun moving in 3D in Codea Craft!