Wolfenstein 3D-style Ray Casting

Hi guys. This is a simple pseudo-3D ray caster that I’ve been working on. I’d like to eventually turn it into a game of some sort but I wanted to get some feedback before I go much further.

Main:

touches = {}
used = {}
function setup()
    displayMode(FULLSCREEN)
    -- turn off gravity
    physics.gravity(0, 0)
    -- creates a 16x16 map with a rock texture strip width of five.
    -- lower strip widths produce higher resolution renders
    map = Map(16, 16, readImage("Documents:rock"), 4)
    map:setRect(3, 3, 12, 12, 1)
    map:setRect(4, 4, 10, 10, 0)
    map:setRect(5, 5, 8, 8, 1)
    map:setRect(6, 6, 6, 6, 0)
    map:set(5, 3, 0)
    map:set(3, 7, 0)
    map:set(5, 7, 0)
    map:set(8, 5, 0)  
    -- create a camera with a move speed of 10, turn speed of 0.5,
    -- initial position of (1.5, 1.5), and initial rotation of 0
    cam = Camera(10, 0.2, 1.5, 1.5, 0.0)
    -- set the map's active camera to the one we just made
    map:setCamera(cam)    
    -- make the background
    bg = makeBg()
end
function draw()
    -- make sure there's no tint before drawing background
    tint(255, 255, 255, 255)
    -- draw background
    sprite(bg, WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT)
    cam:move()
    map:draw()  
end
function touched(touch)
    touches[touch.id] = touch
    if touch.state == BEGAN then used[touch.id] = 0 end
end
-- produces a simple vertical gradient outward from center of screen
function makeBg()
    bg = image(1, 100)
    for i = 1, bg.height / 2 do
        color = 170 - i * 3
        bg:set(1, i, color, color, color)
        bg:set(1, bg.height + 1 - i, color, color, color)
    end
    return bg
end

Camera:

Camera = class()
--makes a camera
function Camera:init(moveSpeed, turnSpeed, x, y, angle)
    self.moveSpeed = moveSpeed
    self.turnSpeed = turnSpeed
    self.x = x
    self.y = y
    self.angle = angle
    self.body = physics.body(CIRCLE, 0.01)
    self.body.x = x
    self.body.y = y
end
function Camera:move()
    -- if touching screen
    if CurrentTouch.state == MOVING then
        -- rotate according to amount touch is moving on x axis
        self.angle = self.angle - DeltaTime * CurrentTouch.deltaX * self.turnSpeed   
        -- velocity according to amount touch is moving on y axis
        vx = DeltaTime * CurrentTouch.deltaY * self.moveSpeed * math.cos(self.angle)
        vy = DeltaTime * CurrentTouch.deltaY * self.moveSpeed * math.sin(self.angle)
        --self.body:applyForce(vec2(math.cos(self.angle) * 0.0001, math.sin(self.angle) * 0.0001))
        -- set body velocity for physics sim for collision
        self.body.linearVelocity = vec2(vx, vy)    
    else
        -- stop moving
       self.body.linearVelocity = vec2(0, 0)
    end
    -- update camera position to body position to account for collision
    self.x = self.body.x
    self.y = self.body.y
end

Map:

Map = class()
-- creates map of width w, height h, texture, and res (strip width)
-- texture isn't required
function Map:init(w, h, texture, res)
    self.w = w
    self.h = h
    self.texture = texture
    -- if texture was given, split into columns to make drawing easier
    if texture ~= nil then
        self.sprites = {}
        for i = 1, texture.width do
            self.sprites[i] = col(texture, i)
        end
    end
    self.res = res
    -- initialize data, create a default wall around edge of map
    -- d contains 1 for wall, else 0
    -- bodies contains corresponding physical bodies for collision
    self.d = {}
    self.bodies = {}
    for i = 1, w do
        self.d[i] = {}
        self.bodies[i] = {}
        for j = 1, h do
            if i == 1 or i == w or j == 1 or j == h then
                self.d[i][j] = 1
                self.bodies[i][j] = physics.body(POLYGON, vec2(0, 0), vec2(0, 1), vec2(1, 1), vec2(1, 0))
                self.bodies[i][j].type = STATIC
                self.bodies[i][j].x = i - 1
                self.bodies[i][j].y = j - 1
            else
                self.d[i][j] = 0
            end
        end
    end 
    self.cam = nil
end
-- sets d[x][y] to b
function Map:set(x, y, b)
    if x > 0 and y > 0 and x <= self.w and y <= self.h then     
        -- there wasn't a wall here before we have to make body
        if b == 1 and self.d[x][y] == 0 then
            self.d[x][y] = 1
                self.bodies[x][y] = physics.body(POLYGON, vec2(0, 0), vec2(0, 1), vec2(1, 1), vec2(1, 0))
                self.bodies[x][y].type = STATIC
                self.bodies[x][y].x = x - 1
                self.bodies[x][y].y = y - 1
        else           
            -- if there was a wall here before we have to destroy body
            if b == 0 and self.d[x][y] == 1 and self.bodies[x][y] then                
                self.bodies[x][y]:destroy()
                self.bodies[x][y] = nil
            end            
            self.d[x][y] = b
        end
    end
end
-- sets a rectangular section of the map to b
function Map:setRect(x, y, w, h, b)
    for i = x, x + w - 1 do
        for j = y, y + h - 1 do
            self:set(i, j, b)
        end
    end
end
-- sets map data from image, image must be same size as map,
-- black pixel = 0, white pixel = 1
function Map:setFromImage(i)
    for x = 1, self.w do
        for y = 1, self.h do
            r = i:get(x, y)
            if r > 0 then
                self:set(x, y, 1)
            else
                self:set(x, y, 0)
            end
        end
    end
end
-- DOESN'T WORK
-- eventually, will generate a dungeon randomly
-- right now, just uses setRect on random sections
function Map:generate()
    newMap = {}
    for x = 2, self.w - 1 do
        newMap[x] = {}
        for y =2, self.h - 1 do
            newMap[x][y] = 1
        end
    end
    newMap[2][2] = 0
    for i = 1, 200 do
        cellX = math.random(2, self.w - 2)
        cellY = math.random(2, self.h - 2)
        cellWidth = math.random(0, self.w - cellX)
        cellHeight = math.random(0, self.h - cellY)
        type = math.random(0, 1)
        for x = cellX, cellX + cellWidth - 1 do
            for y = cellY, cellY + cellHeight - 1 do
                newMap[x][y] = type
            end
        end       
    end
    for x = 2, self.w - 1 do
        for y = 2, self.h - 1 do
            self:set(x, y, newMap[x][y])
        end
    end
end
-- sets the active camera
function Map:setCamera(cam)
    self.cam = cam
end
-- draws the map
function Map:draw() 
    fill(255, 255, 255, 255)
    noSmooth()  
    -- iterate through columns of pixels of width self.res
    for x = 1, WIDTH, self.res do
        -- calculate angle based on column
        angle = self.cam.angle + math.atan2(WIDTH / 2 - x, WIDTH / 2)
        -- cast ray, t is the relative location of the ray's hit, used 
        -- to determine what part of the texture should be drawn
        dist, wall, t = self:castRay(angle)
        -- calculates component of ray's displacement that is perpendicular
        -- to view plane to correct fisheye
        dist = dist * math.cos(angle - self.cam.angle)
        -- calculate height of strip in pixels
        height = 0.5 / dist * HEIGHT        
        if self.texture == nil then
            -- if no texture then draw rectangle
            -- color based on which wall, for shading
            if wall == 1 then fill(255, 255, 255, 255) end
            if wall == 2 or wall == 4 then fill(176, 176, 176, 255) end
            if wall == 3 then fill(97, 97, 97, 255) end
            rect(x, HEIGHT / 2 - height / 2, self.res, height)
        else
            -- uses t to calculate which column of pixels we should use from texture
            -- t is between 0 and 1, 0 if ray hit very edge of wall, 1 if hit other edge
            pixelX = math.floor(t * table.getn(self.sprites) + 1)           
            -- tint based on which wall, for shading
            if wall == 1 then tint(255, 255, 255, 255) end
            if wall == 2 or wall == 4 then tint(185, 185, 185, 255) end
            if wall == 3 then tint(128, 127, 127, 255) end
            sprite(self.sprites[pixelX], x + self.res / 2, HEIGHT / 2, self.res, height)
        end
    end  
end
-- casts a ray at ang
-- returns distance, wall hit (1 - 4), and relative location of hit
function Map:castRay(ang)
    cosA = math.cos(ang)
    sinA = math.sin(ang)
    slope = sinA / cosA    
    -- does ray go left/right? up/down?
    hori = 0
    vert = 0    
    if cosA > 0 then hori = 1 else hori = 0 end
    if sinA > 0 then vert = 1 else vert = 0 end    
    x = 0
    y = 0
    dx = 0
    dy = 0    
    ----------------------------------------------
    -- first handle vertical walls    
    if hori == 1 then x = math.ceil(self.cam.x) else x = math.floor(self.cam.x) end
    y = self.cam.y + slope * (x - self.cam.x)    
    if hori == 1 then dx = 1 else dx = -1 end
    dy = slope * dx    
    d1 = 0
    h1 = 0
    t1 = 0    
    while x > 0 and x < self.w and y > 0 and y < self.h do
        mapX = 0
        mapY = 0
        if hori == 1 then mapX = math.floor(x) else mapX = math.floor(x) - 1 end
        mapY = math.floor(y)      
        if self.d[mapX + 1][mapY + 1] == 1 then
            d1 = (x - self.cam.x) * (x - self.cam.x) + (y - self.cam.y) * (y - self.cam.y)
            t1 = y - math.floor(y)
            h1 = 1
            break
        end        
        x = x + dx
        y = y + dy
    end
    ----------------------------------------------
    -- then horizontal    
    if vert == 1 then y = math.ceil(self.cam.y) else y = math.floor(self.cam.y) end
    x = self.cam.x + (1 / slope) * (y - self.cam.y)    
    if vert == 1 then dy = 1 else dy = -1 end
    dx = 1 / slope * dy    
    d2 = 0
    h2 = 0
    t2 = 0

Map, continued:

    while x > 0 and x < self.w and y > 0 and y < self.h do
        mapX = 0
        mapY = 0
        if vert == 1 then mapY = math.floor(y) else mapY = math.floor(y) - 1 end
        mapX = math.floor(x)     
        if self.d[mapX + 1][mapY + 1] == 1 then
            d2 = (x - self.cam.x) * (x - self.cam.x) + (y - self.cam.y) * (y - self.cam.y)
            h2 = 1
            t2 = x - math.floor(x)
            break
        end      
        x = x + dx
        y = y + dy
    end    
    -- if two hits, which is shorter
    if h1 == 1 and h2 == 1 then
        if d1 < d2 then
            -- which wall?
            if hori == 1 then
                return math.sqrt(d1), 1, t1
            else
                return math.sqrt(d1), 3, t1
            end
        else
            if vert == 1 then
                return math.sqrt(d2), 4, t2
            else
                return math.sqrt(d2), 2, t2
            end
        end    
    else
        if h1 == 1 then
            if hori == 1 then
                return math.sqrt(d1), 1, t1
            else
                return math.sqrt(d1), 3, t1
            end
        else
            if vert == 1 then
                return math.sqrt(d2), 4, t2
            else
                return math.sqrt(d2), 2, t2
            end
        end
    end
end
-- extracts column of pixels from image
function col(i, x)
    colimg = image(1, i.height)
    for y = 1, i.height do
         colimg:set(1, y, i:get(x, y))
    end
    return colimg
end

Screenshots:

http://postimage.org/image/xvi5hmqf9/

http://postimage.org/image/sy4kwiog5/

Texture (save this and import it into Codea as “rock” if you want the program to load it as a texture):

http://s18.postimage.org/qk1wyag5h/teture.png

Controls are pretty straightforward: slide up/down on the screen to move, or left/right to turn.

Feel free to use any of the code however you want. If you have any suggestions I’m open to ideas.

This is amazing. I have to say that I tried to do something like this some time ago and I failed,
Can’t wait to see a game made with it.

This is nice work, @Otterified. I made an almost identical project in qbasic back in my sophomore year of high school, and I remember I had to teach myself trigonometry in order to do it. It was, for me, that project that made me fall in love with programming.

As for your code, I suggest you learn everything you can about meshes. They will make your life easier and your program much faster even with higher resolution textures.

Cool! I could never do this…(I’m in 6th grade)

Cool :slight_smile:
What about adding some up and down ground meshes and textures?