Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/AleksandrHovhannisyan/2d-raycasting
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
https://github.com/AleksandrHovhannisyan/2d-raycasting
coding-train lua raycasting tic-80
Last synced: 2 months ago
JSON representation
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
- Host: GitHub
- URL: https://github.com/AleksandrHovhannisyan/2d-raycasting
- Owner: AleksandrHovhannisyan
- Created: 2024-06-19T23:08:49.000Z (7 months ago)
- Default Branch: master
- Last Pushed: 2024-06-23T03:26:40.000Z (7 months ago)
- Last Synced: 2024-06-23T05:57:45.380Z (7 months ago)
- Topics: coding-train, lua, raycasting, tic-80
- Homepage: https://tic80.com/play?cart=3881
- Size: 23.4 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# 2D Raycasting in TIC-80
Demo of 2D raycasting in the TIC-80 fantasy console, using Lua.
Based on this video tutorial by Daniel Shiffman: [Coding Challenge #145: 2D Raycasting](https://www.youtube.com/watch?v=TOEi6T2mtHo)
![Black lines representing walls, with yellow shaded regions representing rays cast out from the player location.](./thumbnail.png)
## Code
```lua
-- author: Aleksandr Hovhannisyan
-- desc: Lua implementation of this tutorial by Daniel Shiffman: https://www.youtube.com/watch?v=TOEi6T2mtHo
-- site: aleksandrhovhannisyan.com
-- license: MIT License
-- version: 0.1
-- script: luafunction BOOT()
Color = {
BLACK = 0,
RED = 2,
YELLOW = 4,
WHITE = 12
}
Screen = {
WIDTH = 240,
HEIGHT = 136,
PADDING = 2
}
Btn = {
UP = 0,
DOWN = 1,
LEFT = 2,
RIGHT = 3
}Vector = {}
Vector.__index = Vector-- Creates a new instance of a vector
function Vector:new(x, y)
local instance = setmetatable({}, Vector)
instance.x = x
instance.y = y
return instance
end-- Returns the length (magnitude) of this vector
function Vector:length()
return math.sqrt(self.x ^ 2 + self.y ^ 2)
end-- Normalizes this vector so it has a length of 1
function Vector:normalize()
local length = self:length()
self.x = self.x / length
self.y = self.y / length
end-- Scales this vector by the given scalar
function Vector:scale(scalar)
self.x = self.x * scalar
self.y = self.y * scalar
endfunction Vector:fromAngle(angleRadians)
local x = math.cos(angleRadians)
local y = math.sin(angleRadians)
return Vector:new(x, y)
endRay = {}
Ray.__index = Ray-- Creates a new ray at the specified position, pointing in the specified direction
-- as an angle in radians relative to the horizontal.
function Ray:new(position, angleRadians)
local instance = setmetatable({}, Ray)
instance.position = position
instance:setAngle(angleRadians)
return instance
endfunction Ray:setAngle(angleRadians)
self.angle = angleRadians
self.direction = Vector:fromAngle(self.angle)
self.direction:normalize()
end-- Casts this ray onto the specified boundary and returns the intersection
-- point and distance to the intersection, if one is found.
-- https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment
-- https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/565282#565282
function Ray:cast(boundary)
-- Boundary, L1
local x1 = boundary.a.x
local y1 = boundary.a.y
local x2 = boundary.b.x
local y2 = boundary.b.y-- Ray, L2
local x3 = self.position.x
local y3 = self.position.y
local x4 = self.position.x + self.direction.x
local y4 = self.position.y - self.direction.y-- Zero implies the two lines are parallel and never intersect
local denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)if denominator == 0 then
return nil
endlocal t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator
local u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominatorif t >= 0 and t <= 1 and u >= 0 then
local intersection = Vector:new()
intersection.x = x1 + t * (x2 - x1)
intersection.y = y1 + t * (y2 - y1)
return {intersection = intersection, distance = u}
endreturn nil
endBoundary = {}
Boundary.__index = Boundaryfunction Boundary:new(a, b)
local instance = setmetatable({}, Boundary)
instance.a = Vector:new(a.x, a.y)
instance.b = Vector:new(b.x, b.y)
return instance
endfunction Boundary:draw()
line(self.a.x, self.a.y, self.b.x, self.b.y, Color.BLACK)
end-- A moving particle/player that casts rays out towards boundaries
Particle = {}
Particle.__index = Particlefunction Particle:new(fovDegrees)
local instance = setmetatable({}, Particle)
-- The particle's current position on the screen
instance.position = Vector:new(Screen.WIDTH / 2, Screen.HEIGHT / 2)
-- Angle in radians representing direction the particle is facing
instance.angle = math.rad(0)
-- Field of view (in degrees) of the particle, representing how far out to the sides it can cast its rays
instance.fov = fovDegrees
-- The rays that the particle will cast out within its FOV cone
instance.rays = {}
for angleDegrees = -fovDegrees / 2, fovDegrees / 2 - 1, 1 do
table.insert(instance.rays, Ray:new(instance.position, math.rad(angleDegrees)))
end
return instance
end-- Casts this particle's rays towards the given boundaries
function Particle:cast(boundaries)
for i, ray in pairs(self.rays) do
local closestIntersection = nil
local shortestDistance = math.huge
--trace("Ray: x="..ray.position.x..", y="..ray.position.y)
-- Check each boundary for intersections, and find the closest intersecting boundary
for j, boundary in pairs(boundaries) do
local result = ray:cast(boundary)if result ~= nil then
local intersection = result.intersection
local distance = result.distanceif distance < shortestDistance then
shortestDistance = distance
closestIntersection = intersection
end
end
end-- If we found an intersection with a boundary,
-- draw a line from this particle's position to that intersection
-- to represent the ray being cast
if closestIntersection ~= nil then
line(self.position.x, self.position.y, closestIntersection.x, closestIntersection.y, Color.YELLOW)
end
end
endfunction Particle:draw()
-- Draw a red dot for the particle as a point of reference
circ(self.position.x, self.position.y, 1, Color.RED)
-- Draw a direction vector
direction = Vector:fromAngle(self.angle)
direction:normalize()
direction:scale(8)
line(self.position.x, self.position.y, self.position.x + direction.x, self.position.y + direction.y, Color.RED)
endfunction Particle:setAngle(angleRadians)
local angleDelta = angleRadians - self.angle
trace("angleRadians=" .. angleRadians .. ", angleDelta=" .. angleDelta)
self.angle = angleRadians
for k, ray in pairs(self.rays) do
-- FIXME: why do I need to do -angleDelta?
ray:setAngle(ray.angle - angleDelta)
end
end-- Updates the particle's position and orientation
function Particle:update()
-- Force the particle to face the direction of the mouse cursor
local mouseX, mouseY = mouse()
local angle = math.atan2(mouseY - self.position.y, mouseX - self.position.x)
self:setAngle(angle)
if btn(Btn.UP) then
self.position.y = self.position.y - 1
end
if btn(Btn.DOWN) then
self.position.y = self.position.y + 1
end
if btn(Btn.LEFT) then
self.position.x = self.position.x - 1
end
if btn(Btn.RIGHT) then
self.position.x = self.position.x + 1
end
endfunction createBoundaries()
boundaries = {}
-- Randomly generate boundaries in the bounds of the screen
for i = 1, 5 do
local x1 = math.random(Screen.WIDTH)
local y1 = math.random(Screen.HEIGHT)
local x2 = math.random(Screen.WIDTH)
local y2 = math.random(Screen.HEIGHT)
local boundary = Boundary:new(Vector:new(x1, y1), Vector:new(x2, y2))
table.insert(boundaries, boundary)
end
-- Outer walls
local wallTop =
Boundary:new(
Vector:new(Screen.PADDING, Screen.PADDING),
Vector:new(Screen.WIDTH - Screen.PADDING, Screen.PADDING)
)
local wallRight =
Boundary:new(
Vector:new(Screen.WIDTH - Screen.PADDING, Screen.PADDING),
Vector:new(Screen.WIDTH - Screen.PADDING, Screen.HEIGHT - Screen.PADDING)
)
local wallBottom =
Boundary:new(
Vector:new(Screen.WIDTH - Screen.PADDING, Screen.HEIGHT - Screen.PADDING),
Vector:new(Screen.PADDING, Screen.HEIGHT - Screen.PADDING)
)
local wallLeft =
Boundary:new(
Vector:new(Screen.PADDING, Screen.HEIGHT - Screen.PADDING),
Vector:new(Screen.PADDING, Screen.PADDING)
)
table.insert(boundaries, wallTop)
table.insert(boundaries, wallRight)
table.insert(boundaries, wallBottom)
table.insert(boundaries, wallLeft)
end-- Main globals
createBoundaries()
particle = Particle:new(90)
endfunction TIC()
cls(Color.WHITE)
particle:update()
particle:cast(boundaries)
particle:draw()
-- Draw boundaries last so they show on top
for i, boundary in pairs(boundaries) do
boundary:draw()
end
end
```