3D FPS movement code—improved!

@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):

https://youtu.be/sjHjKibaOtg

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):

https://youtu.be/vhHFVqnsIJw

@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):

https://youtu.be/D28J6Bz74L4

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):

https://youtu.be/wSLxwRA45Vk

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):

https://youtu.be/DKty04MDaIM

Have fun moving in 3D in Codea Craft!

@SugarRay Great demos, you put a lot of work into them. I noticed the ~~~ aren’t working to format the code. That happens when posting from an iPhone. I have updated code that I’ll post here that allows smoother movement. Just need to finish some minor changes.

Thank you @dave1707.

@SugarRay Heres my updated version that allows a smoother continuous movement. To use, move your finger on any of the sliders to increase/decrease the speed of any of the movements. The Stop button stops all movement. LU/LD look up/down. LL/LR look left/right. ML/MR move left/right. RL/RR rotate left/right. MU/MD move up/down. MF/MB move forward/backward.

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)    
    
    -- create a number of cubes for reference
    for a=1,2000 do
        x=math.random(-400,400)
        y=math.random(-400,400)
        z=math.random(-400,400)
        createCube(vec3(x,y,z),vec3(255,255,255))
    end
    
    cam1=cameraClass()
end

function draw(s)
    background(0)
    xPos,yPos,zPos,xAng,yAng,zAng=cam1:updateCameraPos()
    scene.camera.position = vec3(xPos,yPos,zPos)
    scene.camera.eulerAngles=vec3(xAng,yAng,zAng)
    scene:draw()
    cam1:draw(self)
    translate(WIDTH/2,HEIGHT/2)
    rotate(zAng)
    sprite(asset.builtin.Tyrian_Remastered.Boss_A,0,0)
    translate()
end

function touched(t)
    cam1: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.LlLr,self.LuLd=0,0
    self.MlMr,self.MuMd,self.RlRr=0,0,0
    self.speed=0 
    self.cameraX,self.cameraY,self.cameraZ=0,0,0
    self:createButtons()
end

function cameraClass:createButtons()  
    self.btnTab={}   
    table.insert(self.btnTab,buttonClass(WIDTH/2-10,100,40,40,0,"Zero","",
    function() self:setZero() end))
    table.insert(self.btnTab,buttonClass(WIDTH/2+80,100,40,120,1,"LU","LD",
    function(val) self.LuLd=self.LuLd-val*.1 end))
    table.insert(self.btnTab,buttonClass(WIDTH/2-100,140,120,40,2,"LL","LR",
    function(val) self.LlLr=self.LlLr+val*.1 end))
    table.insert(self.btnTab,buttonClass(WIDTH/2-100,100,120,40,2,"ML","MR",
    function(val) self.MlMr=self.MlMr-val*.1 end))
    table.insert(self.btnTab,buttonClass(WIDTH/2-100,60,120,40,2,"RL","RR",
    function(val) self.RlRr=self.RlRr-val*.2 end))
    table.insert(self.btnTab,buttonClass(WIDTH/2+40,100,40,120,1,"MU","MD",
    function(val) self.MuMd=self.MuMd+val*.1 end))
    table.insert(self.btnTab,buttonClass(WIDTH/2+120,100,40,120,1,"MF","MB",
    function(val) 
        local abx=math.abs(self.angleX)%360
        if abx>=0 and abx<=90 or abx>=270 and abx<=360 then
            self.speed=self.speed+val*.2
        else
            self.speed=self.speed-val*.2
        end 
    end))
end

function cameraClass:draw()
    pushStyle()
    for a,b in pairs(self.btnTab) do
        b:draw(b)
    end
    fill(255)
    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
        b:touched(t)
    end
end

function cameraClass:setZero()
    self.LlLr,self.LuLd=0,0
    self.MlMr,self.MuMd,self.RlRr=0,0,0
    self.speed=0
end

function cameraClass:updateCameraPos()
    -- angleX=angle up/down,        yy=+- look up/down
    self.angleX=self.angleX+self.LuLd
    self.angleX=self:normalizeXYZ(self.angleX)
    
    -- angleY=angle left/right,     xx=+- look left/right
    self.angleY=self.angleY-self.LlLr
    self.angleY=self:normalizeXYZ(self.angleY)
    
    -- angleZ=rotate left/right
    self.angleZ=self.angleZ+self.RlRr
    self.angleZ=self:normalizeXYZ(self.angleZ)
    
    -- calc distance moved
    local x=math.sin(math.rad(self.angleY))
    local y=-math.tan(math.rad(self.angleX))
    local z=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.speed
        y=y*dist*self.speed
        z=z*dist*self.speed
    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.MuMd
    
    -- camera move left/right
    self.cameraX=self.cameraX+math.cos(math.rad(self.angleY))*self.MlMr
    self.cameraZ=self.cameraZ-math.sin(math.rad(self.angleY))*self.MlMr   
    return self.cameraX,self.cameraY,self.cameraZ,self.angleX,self.angleY,self.angleZ
end

function cameraClass:normalizeXYZ(deg)
    -- keep angle in a -180 to 180 degree range
    if deg>180 then
        deg=deg-360
    elseif deg<-180 then
        deg=deg+360
    end
    return deg
end

buttonClass=class()

function buttonClass:init(x,y,w,h,type,name1,name2,a)
    self.x=x
    self.y=y
    self.w=w
    self.h=h
    self.type=type
    self.name1=name1
    self.name2=name2
    self.action=a
    self.tStart=0
end

function buttonClass:draw()
    rectMode(CENTER)
    fill(244)
    rect(self.x,self.y,self.w,self.h)
    fill(0)
    if self.type==0 then
        text(self.name1,self.x,self.y)
    elseif self.type==1 then   -- slider goes left/right
        text(self.name1,self.x,self.y+self.h*.4)
        text(self.name2,self.x,self.y-self.h*.4)
    elseif self.type==2 then   -- slider goes up/down
        text(self.name1,self.x-self.w*.38,self.y)
        text(self.name2,self.x+self.w*.38,self.y)
    end    
end

function buttonClass:touched(t)
    if t.state==BEGAN then
        self.tStart=vec2(t.x,t.y)
        if t.x>=self.x-self.w/2 and t.x<=self.x+self.w/2 and
        t.y>=self.y-self.h/2 and t.y<=self.y+self.h/2 then
            if self.type==0 then
                self.action()
            end
        end
    end        
    if t.state==CHANGED then
        if t.x>=self.x-self.w/2 and t.x<=self.x+self.w/2 and
        t.y>=self.y-self.h/2 and t.y<=self.y+self.h/2 then
            if self.type==1 then
                if t.y>self.tStart.y and t.deltaY<0 or 
                t.y<self.tStart.y and t.deltaY>0 then
                    self.tStart.y=t.y
                end
                local val=(t.y-self.tStart.y)*.0005
                self.action(val)
            elseif self.type==2 then
                if t.x>self.tStart.x and t.deltaX<0 or 
                t.x<self.tStart.x and t.deltaX>0 then
                    self.tStart.x=t.x
                end
                local val=(t.x-self.tStart.x)*.0005
                self.action(val)
            end
        end
    end  
end

Thanks, @dave1707, I’ll check it out!

@SugarRay can you pretty please put the assets inside the projects themselves instead of in the documents folder?

When the assets are in the documents folder the projects won’t run, they just throw errors.

These look like some great demos and I’d love to run them.

Thanks, @UberGoober, I tried to do this previously but the 3D models seemed to be too big for the forum (i.e. when I zipped up the code with the 3D models and tried to upload the zip file to the forum, the forum said it was too big).

Any other ideas how I could upload zip files with these larger assets to the forum?

@dave1707, I made one other small change on your original movement code that seems to provide more balanced movement (i.e. turning speed more balanced with forward and back movement):

table.insert(self.btnTab,buttonClass(WIDTH/20,150,"Look Left",
    function() self:setZero() self.xx=-self.maxDist*2 end))

I just changed the turning button speed to be twice the speed of the backward or forward speed (e.g. in above code self.xx=-self.maxDist*2)

Fyi in case anyone would like to have a little more balanced movement from your original 3D FPS movement code.

@SugarRay my first recommendation would be WebRepo. Otherwise I’d try to reduce the complexity (or just size) of the assets to get them small enough to post.

@SugarRay The values I picked were mostly just to get it to work. I figured anyone who used it would change the values to their likings just as you did. It would all depend on what the camera was used for.

Perhaps you could put a large OBJ model file on github and get it in your code using http.request(), as I recall some of the sample projects on the forum were, you can refer to the modules they have already written.

The code like below:

--Assets
Models={

    {name="captainCodea",
    mtl="https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaStand.mtl",
    actions={
        default={
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaStand.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaStand2.obj",},
        walk={ --these key identifiers used to cue up anim
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaWalk_000000.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaWalk_000005.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaWalk_000010.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaWalk_000015.obj"},
        kick={
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaKick_000003.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaKick_000005.obj",
        "https://raw.githubusercontent.com/Utsira/assets/master/CaptainCodeaKick_000008.obj",},
    }},

}

Thanks, @UberGoober, @dave1707, and @binaryblues. I’ll start by looking into @binaryblues’ suggestion about glutting on github and also look if any similar options. May take me a couple of weeks but will try and find a solution, as I hope to keep making more 3D demos (and hopefully games) in the future.

@dave1707, one more housekeeping question:

If I do find a way to link to larger 3D models then redo the projects to incorporate those links and make new zip files of the projects, how do I then post the updated zip files on this post? I tried making a minor change to one project and wanted to remove the old zip file and post a new one on the post but couldn’t figure out how to remove an old zip file that I posted.

Thanks.

@UberGoober, @dave1707, @binaryblues: it looks like GitHub’s limit is 100MB and many of the 3D models I used are larger than that.

@Steppers, would it be okay for me to upload the above demos that I made with their assets included in each project to the Codea WebRepo? Some of the 3D model assets in the projects are several hundred MB in size. All of the models are downloaded from free 3D model sites (e.g. TurboSquid) but not sure using Codea WebRepo would be an allowable place to upload my demos.

Thanks for the consideration of this request.

@SugarRay If you tap on a post you want to change something in, a little gear icon will show to the upper right of the post. Tap that and select Edit and from there you should be able to change or delete what you want.

Thanks, @dave1707!

Hi, Everyone (@UberGoober, @binaryblues, @dave1707, @Steppers).

With all of your help and suggestions (and dave1707’s recent help in writing a brief script that helped me import .mtl files into a project’s asset’s folder), I’ve manage to package my SeaBottom demo as a Codea Zip file for others to try out. It’s unfortunately too big for Stepper’s WebRepo (147 MB, max WebRepo zip size 100 MB), so I uploaded it to Dropbox to distribute. Whereas the 3D model size maybe a bit excessive for what it recreates (a sea bottom), I think some of my other demos that are more functional also likely will be larger than 100MB so would like to see if the Dropbox option works for others as a distribution method for larger 3D model projects. Here is the dropbox link:

https://www.dropbox.com/s/lmhctk6uldo2ufv/CraftSeaBottmX.zip?dl=0

Please let me know if that link works for others and you are able to download the zip file of the Codea project. Please also let me know how the project runs for you and any suggestions for improvement. Note that with the larger size of the 3D model, there is a small delay before the program renders the scene.

If the dropbox method works, I’m happy to work on loading up other of my 3D demos into Dropbox and posting those links onto the forum.

Fyi, this is video of my latest Codea 3D FPS movement scene:

https://youtu.be/Qkx_TS5yQKU

Have a good rest of your weekend :slight_smile:

PS, one thing I forgot to mention is that sphereCast method of collision detection still needs a lot of fine tuning. You may need to therefore do a bit of maneuvering more than you’d expect to move through doors or around some objects in these 3D demos.

@SugarRay Ok, I was wrong! It WAS 100MB but I had to change the backend upload service in use to bayfiles at some point which has a 20GB limit instead https://bayfiles.com/faq

I’m currently testing how well it works with a large project but will update you if it works :smile:

Thanks, @Steppers!

@dave1707 mentioned that it was taking a long time for project I uploaded to dropbox on the above link to export into Codea. Thus, I just uploaded one of my smaller 3D Demo projects (CraftSpace: appx 1/3 size [approximately 50 MB] of the CraftSeaBottm file I posted above) both to Dropbox:

https://www.dropbox.com/s/2k7gd8wh615bcxf/CraftSpaceX.zip?dl=0

and (since met the original size limits you had told me initially for your WebRepo) to the WebRepo (just submitted for consideration and approval)

Hopefully, others on the forum will be able to download and try out one of the two projects with the full 3D models contained within them either from one of the two dropbox links or to your WebRepo if it is approved.

Fyi :slight_smile:
Cc: @UberGoober, @binaryblues, @Bri_G, @piinthesky