{"id":13337742,"url":"https://github.com/AleksandrHovhannisyan/2d-raycasting","last_synced_at":"2025-03-11T07:30:59.275Z","repository":{"id":245192038,"uuid":"817526958","full_name":"AleksandrHovhannisyan/2d-raycasting","owner":"AleksandrHovhannisyan","description":"Follow-along of Daniel Shiffman's tutorial on 2D raycasting, with an implementation in Lua for the TIC-80 fantasy console: https://www.youtube.com/watch?v=TOEi6T2mtHo","archived":false,"fork":false,"pushed_at":"2024-06-23T03:26:40.000Z","size":24,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-06-23T05:57:45.380Z","etag":null,"topics":["coding-train","lua","raycasting","tic-80"],"latest_commit_sha":null,"homepage":"https://tic80.com/play?cart=3881","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AleksandrHovhannisyan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-19T23:08:49.000Z","updated_at":"2024-06-23T03:26:43.000Z","dependencies_parsed_at":"2024-06-20T12:25:05.323Z","dependency_job_id":null,"html_url":"https://github.com/AleksandrHovhannisyan/2d-raycasting","commit_stats":null,"previous_names":["aleksandrhovhannisyan/2d-raycasting"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AleksandrHovhannisyan%2F2d-raycasting","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AleksandrHovhannisyan%2F2d-raycasting/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AleksandrHovhannisyan%2F2d-raycasting/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AleksandrHovhannisyan%2F2d-raycasting/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AleksandrHovhannisyan","download_url":"https://codeload.github.com/AleksandrHovhannisyan/2d-raycasting/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":213069711,"owners_count":15532844,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coding-train","lua","raycasting","tic-80"],"created_at":"2024-07-29T19:14:56.411Z","updated_at":"2024-10-23T16:31:25.279Z","avatar_url":"https://github.com/AleksandrHovhannisyan.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# 2D Raycasting in TIC-80\n\nDemo of 2D raycasting in the TIC-80 fantasy console, using Lua.\n\nBased on this video tutorial by Daniel Shiffman: [Coding Challenge #145: 2D Raycasting](https://www.youtube.com/watch?v=TOEi6T2mtHo)\n\n![Black lines representing walls, with yellow shaded regions representing rays cast out from the player location.](./thumbnail.png)\n\n## Code\n\n```lua\n-- author:  Aleksandr Hovhannisyan\n-- desc:    Lua implementation of this tutorial by Daniel Shiffman: https://www.youtube.com/watch?v=TOEi6T2mtHo\n-- site:    aleksandrhovhannisyan.com\n-- license: MIT License\n-- version: 0.1\n-- script:  lua\n\nfunction BOOT()\n    Color = {\n        BLACK = 0,\n        RED = 2,\n        YELLOW = 4,\n        WHITE = 12\n    }\n    Screen = {\n        WIDTH = 240,\n        HEIGHT = 136,\n        PADDING = 2\n    }\n    Btn = {\n        UP = 0,\n        DOWN = 1,\n        LEFT = 2,\n        RIGHT = 3\n    }\n\n    Vector = {}\n    Vector.__index = Vector\n\n    -- Creates a new instance of a vector\n    function Vector:new(x, y)\n        local instance = setmetatable({}, Vector)\n        instance.x = x\n        instance.y = y\n        return instance\n    end\n\n    -- Returns the length (magnitude) of this vector\n    function Vector:length()\n        return math.sqrt(self.x ^ 2 + self.y ^ 2)\n    end\n\n    -- Normalizes this vector so it has a length of 1\n    function Vector:normalize()\n        local length = self:length()\n        self.x = self.x / length\n        self.y = self.y / length\n    end\n\n    -- Scales this vector by the given scalar\n    function Vector:scale(scalar)\n        self.x = self.x * scalar\n        self.y = self.y * scalar\n    end\n\n    function Vector:fromAngle(angleRadians)\n        local x = math.cos(angleRadians)\n        local y = math.sin(angleRadians)\n        return Vector:new(x, y)\n    end\n\n    Ray = {}\n    Ray.__index = Ray\n\n    -- Creates a new ray at the specified position, pointing in the specified direction\n    -- as an angle in radians relative to the horizontal.\n    function Ray:new(position, angleRadians)\n        local instance = setmetatable({}, Ray)\n        instance.position = position\n        instance:setAngle(angleRadians)\n        return instance\n    end\n\n    function Ray:setAngle(angleRadians)\n        self.angle = angleRadians\n        self.direction = Vector:fromAngle(self.angle)\n        self.direction:normalize()\n    end\n\n    -- Casts this ray onto the specified boundary and returns the intersection\n    -- point and distance to the intersection, if one is found.\n    -- https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment\n    -- https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/565282#565282\n    function Ray:cast(boundary)\n        -- Boundary, L1\n        local x1 = boundary.a.x\n        local y1 = boundary.a.y\n        local x2 = boundary.b.x\n        local y2 = boundary.b.y\n\n        -- Ray, L2\n        local x3 = self.position.x\n        local y3 = self.position.y\n        local x4 = self.position.x + self.direction.x\n        local y4 = self.position.y - self.direction.y\n\n        -- Zero implies the two lines are parallel and never intersect\n        local denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)\n\n        if denominator == 0 then\n            return nil\n        end\n\n        local t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator\n        local u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator\n\n        if t \u003e= 0 and t \u003c= 1 and u \u003e= 0 then\n            local intersection = Vector:new()\n            intersection.x = x1 + t * (x2 - x1)\n            intersection.y = y1 + t * (y2 - y1)\n            return {intersection = intersection, distance = u}\n        end\n\n        return nil\n    end\n\n    Boundary = {}\n    Boundary.__index = Boundary\n\n    function Boundary:new(a, b)\n        local instance = setmetatable({}, Boundary)\n        instance.a = Vector:new(a.x, a.y)\n        instance.b = Vector:new(b.x, b.y)\n        return instance\n    end\n\n    function Boundary:draw()\n        line(self.a.x, self.a.y, self.b.x, self.b.y, Color.BLACK)\n    end\n\n    -- A moving particle/player that casts rays out towards boundaries\n    Particle = {}\n    Particle.__index = Particle\n\n    function Particle:new(fovDegrees)\n        local instance = setmetatable({}, Particle)\n        -- The particle's current position on the screen\n        instance.position = Vector:new(Screen.WIDTH / 2, Screen.HEIGHT / 2)\n        -- Angle in radians representing direction the particle is facing\n        instance.angle = math.rad(0)\n        -- Field of view (in degrees) of the particle, representing how far out to the sides it can cast its rays\n        instance.fov = fovDegrees\n        -- The rays that the particle will cast out within its FOV cone\n        instance.rays = {}\n        for angleDegrees = -fovDegrees / 2, fovDegrees / 2 - 1, 1 do\n            table.insert(instance.rays, Ray:new(instance.position, math.rad(angleDegrees)))\n        end\n        return instance\n    end\n\n    -- Casts this particle's rays towards the given boundaries\n    function Particle:cast(boundaries)\n        for i, ray in pairs(self.rays) do\n            local closestIntersection = nil\n            local shortestDistance = math.huge\n            --trace(\"Ray: x=\"..ray.position.x..\", y=\"..ray.position.y)\n            -- Check each boundary for intersections, and find the closest intersecting boundary\n            for j, boundary in pairs(boundaries) do\n                local result = ray:cast(boundary)\n\n                if result ~= nil then\n                    local intersection = result.intersection\n                    local distance = result.distance\n\n                    if distance \u003c shortestDistance then\n                        shortestDistance = distance\n                        closestIntersection = intersection\n                    end\n                end\n            end\n\n            -- If we found an intersection with a boundary,\n            -- draw a line from this particle's position to that intersection\n            -- to represent the ray being cast\n            if closestIntersection ~= nil then\n                line(self.position.x, self.position.y, closestIntersection.x, closestIntersection.y, Color.YELLOW)\n            end\n        end\n    end\n\n    function Particle:draw()\n        -- Draw a red dot for the particle as a point of reference\n        circ(self.position.x, self.position.y, 1, Color.RED)\n        -- Draw a direction vector\n        direction = Vector:fromAngle(self.angle)\n        direction:normalize()\n        direction:scale(8)\n        line(self.position.x, self.position.y, self.position.x + direction.x, self.position.y + direction.y, Color.RED)\n    end\n\n    function Particle:setAngle(angleRadians)\n        local angleDelta = angleRadians - self.angle\n        trace(\"angleRadians=\" .. angleRadians .. \", angleDelta=\" .. angleDelta)\n        self.angle = angleRadians\n        for k, ray in pairs(self.rays) do\n            -- FIXME: why do I need to do -angleDelta?\n            ray:setAngle(ray.angle - angleDelta)\n        end\n    end\n\n    -- Updates the particle's position and orientation\n    function Particle:update()\n        -- Force the particle to face the direction of the mouse cursor\n        local mouseX, mouseY = mouse()\n        local angle = math.atan2(mouseY - self.position.y, mouseX - self.position.x)\n        self:setAngle(angle)\n        if btn(Btn.UP) then\n            self.position.y = self.position.y - 1\n        end\n        if btn(Btn.DOWN) then\n            self.position.y = self.position.y + 1\n        end\n        if btn(Btn.LEFT) then\n            self.position.x = self.position.x - 1\n        end\n        if btn(Btn.RIGHT) then\n            self.position.x = self.position.x + 1\n        end\n    end\n\n    function createBoundaries()\n        boundaries = {}\n        -- Randomly generate boundaries in the bounds of the screen\n        for i = 1, 5 do\n            local x1 = math.random(Screen.WIDTH)\n            local y1 = math.random(Screen.HEIGHT)\n            local x2 = math.random(Screen.WIDTH)\n            local y2 = math.random(Screen.HEIGHT)\n            local boundary = Boundary:new(Vector:new(x1, y1), Vector:new(x2, y2))\n            table.insert(boundaries, boundary)\n        end\n        -- Outer walls\n        local wallTop =\n            Boundary:new(\n            Vector:new(Screen.PADDING, Screen.PADDING),\n            Vector:new(Screen.WIDTH - Screen.PADDING, Screen.PADDING)\n        )\n        local wallRight =\n            Boundary:new(\n            Vector:new(Screen.WIDTH - Screen.PADDING, Screen.PADDING),\n            Vector:new(Screen.WIDTH - Screen.PADDING, Screen.HEIGHT - Screen.PADDING)\n        )\n        local wallBottom =\n            Boundary:new(\n            Vector:new(Screen.WIDTH - Screen.PADDING, Screen.HEIGHT - Screen.PADDING),\n            Vector:new(Screen.PADDING, Screen.HEIGHT - Screen.PADDING)\n        )\n        local wallLeft =\n            Boundary:new(\n            Vector:new(Screen.PADDING, Screen.HEIGHT - Screen.PADDING),\n            Vector:new(Screen.PADDING, Screen.PADDING)\n        )\n        table.insert(boundaries, wallTop)\n        table.insert(boundaries, wallRight)\n        table.insert(boundaries, wallBottom)\n        table.insert(boundaries, wallLeft)\n    end\n\n    -- Main globals\n    createBoundaries()\n    particle = Particle:new(90)\nend\n\nfunction TIC()\n    cls(Color.WHITE)\n    particle:update()\n    particle:cast(boundaries)\n    particle:draw()\n    -- Draw boundaries last so they show on top\n    for i, boundary in pairs(boundaries) do\n        boundary:draw()\n    end\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAleksandrHovhannisyan%2F2d-raycasting","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FAleksandrHovhannisyan%2F2d-raycasting","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FAleksandrHovhannisyan%2F2d-raycasting/lists"}