Ray Cast Example
This example demonstrates a simple first-person perspective game using ray casting in akesi lili. Ray casting is a technique used to render a 3D perspective from a 2D map, creating the illusion of depth. This example showcases how to implement basic movement, rotation, and rendering of a first-person view.

Explanation
The code initializes a game state with a map, player position, direction, and movement speed. It updates the player's position and direction based on button inputs and renders the scene using a simple ray casting algorithm. The map is represented as a 2D grid, where # represents walls and . represents empty space.
Code
m=[
"##############################",
"#..........#..............#..#",
"#.######.#.#########.#####.#.#",
"#.#......#.#.............#.#.#",
"#.#.######.#.###.###.###.#.#.#",
"#.#.#......#.#.....#...#.#.#.#",
"#.#.#.######.#.###.#.###.#.#.#",
"#.#...#......#.....#...#.#.#.#",
"#.#####.#########.###.#.#.#.#.#",
"#...#...#.............#...#.#.#",
"#.###.#.#########.###.#.###.#.#",
"#.....#.#.............#...#...#",
"#.#######.#########.###.#.###.#",
"#.......#...#...........#...#.#",
"#.#######.###.###########.###.#",
"#........#...#..............#.#",
"#.#########.###.#############.#",
"#........#.....#............#.#",
"#.#########.###.#############.#",
"##############################"
]
val_pos=|x,y|
x=x.floor()
y=y.floor()
x>=0
and x<m.iter().count()
and y>=0
and y<m[0].iter().count()
and m[x][y]=='.'
poi=|x,y|
x:x
y:y
s={
pos:poi(2,2),
dir:poi(0,-1),
pla:poi(0.66,0),
mo_sp:0.2,
ro_sp:0.1,
}
export update=||
# Turn right
if btn 5
old_dir_x = s.dir.x
s.dir.x = s.dir.x * s.ro_sp.cos() - s.dir.y * s.ro_sp.sin()
s.dir.y = old_dir_x * s.ro_sp.sin() + s.dir.y * s.ro_sp.cos()
old_pla_x = s.pla.x
s.pla.x = s.pla.x * s.ro_sp.cos() - s.pla.y * s.ro_sp.sin()
s.pla.y = old_pla_x * s.ro_sp.sin() + s.pla.y * s.ro_sp.cos()
# Turn left
if btn 4
old_dir_x = s.dir.x
s.dir.x = s.dir.x * (-s.ro_sp).cos() - s.dir.y * (-s.ro_sp).sin()
s.dir.y = old_dir_x* (-s.ro_sp).sin() + s.dir.y * (-s.ro_sp).cos()
old_pla_x = s.pla.x
s.pla.x = s.pla.x * (-s.ro_sp).cos() - s.pla.y * (-s.ro_sp).sin()
s.pla.y = old_pla_x * (-s.ro_sp).sin() + s.pla.y * (-s.ro_sp).cos()
# Move forward
if btn 6
new_pos_x = s.pos.x + s.dir.x * s.mo_sp
new_pos_y = s.pos.y + s.dir.y * s.mo_sp
if val_pos(new_pos_x, s.pos.y)
s.pos.x = new_pos_x
if val_pos(s.pos.x, new_pos_y)
s.pos.y = new_pos_y
# Move backward
if btn 7
new_pos_x = s.pos.x - s.dir.x * s.mo_sp
new_pos_y = s.pos.y - s.dir.y * s.mo_sp
if val_pos new_pos_x, s.pos.y
s.pos.x = new_pos_x
if val_pos s.pos.x, new_pos_y
s.pos.y = new_pos_y
export draw=||
clear 0
# Simple raycasting algorithm
for x in 0..DIS_W
cam_x = 2.0 * x / DIS_W - 1.0
ray_dir_x = s.dir.x + s.pla.x * cam_x
ray_dir_y = s.dir.y + s.pla.y * cam_x
map_x = s.pos.x.floor()
map_y = s.pos.y.floor()
del_dis_x = (1.0 + (ray_dir_y * ray_dir_y) / (ray_dir_x * ray_dir_x)).sqrt()
del_dis_y = (1.0 + (ray_dir_x * ray_dir_x) / (ray_dir_y * ray_dir_y)).sqrt()
side_dis_x=0
side_dis_y=0
step_x=0
step_y=0
if ray_dir_x < 0.0
step_x = -1
side_dis_x = (s.pos.x - map_x * 1.0) * del_dis_x
else
step_x = 1
side_dis_x = (map_x + 1.0 - s.pos.x) * del_dis_x
if ray_dir_y < 0.0
step_y = -1
side_dis_y = (s.pos.y - map_y * 1.0) * del_dis_y
else
step_y = 1
side_dis_y = (map_y + 1.0 - s.pos.y) * del_dis_y
side=null
while true
if side_dis_x < side_dis_y
side_dis_x += del_dis_x
map_x += step_x
side = 0
else
side_dis_y += del_dis_y
map_y += step_y
side = 1
# Check bounds before access the map
if map_x < 0 or map_x >= size m or map_y < 0 or map_y >= size m[0]
break
if m[map_x][map_y] == '#'
break
per_wal_dis=null
if side == 0
per_wal_dis = (map_x - s.pos.x + (1.0 - step_x) / 2.0) / ray_dir_x
else
per_wal_dis = (map_y - s.pos.y + (1.0 - step_y) / 2.0) / ray_dir_y
# Ensure per_wal_dis is not too small to avoid overflow
if per_wal_dis > 0.0001
lin_hei = (DIS_H / per_wal_dis).floor()
draw_sta = (-lin_hei / 2.0 + DIS_H / 2.0).floor()
draw_end = (lin_hei / 2.0 + DIS_H / 2.0).floor()
if draw_sta < 0
draw_sta = 0
if draw_end >= DIS_H
draw_end = DIS_H - 1
col = if side==1 then 2 else 1
for y in draw_sta..draw_end
pix(x, y, col)
How It Works
This example implements a classic ray casting algorithm to create a 3D first-person view from a 2D map:
-
Map Representation: The map
mis a 2D grid of strings where#represents walls and.represents walkable space. Theval_pos()function checks if a position is valid (within bounds and not a wall). -
Player State: The
sobject stores:pos: Player position in the map (starts at 2,2)dir: Direction vector the player is facingpla: Camera plane vector (perpendicular to direction, determines field of view)mo_sp: Movement speed (0.2 units per frame)ro_sp: Rotation speed (0.1 radians per frame)
-
Rotation (Left/Right buttons): Uses 2D rotation matrices to rotate both the direction and plane vectors:
- Applies
cos(angle)andsin(angle)transformations - Right button rotates clockwise, left button rotates counter-clockwise
- Both vectors must rotate together to maintain proper camera perspective
- Applies
-
Movement (Up/Down buttons):
- Forward movement moves in the direction vector, backward movement moves opposite
- Each axis is validated separately using
val_pos()to enable sliding along walls - Movement is scaled by
mo_spfor consistent speed
-
Ray Casting Algorithm: For each vertical pixel column (x-coordinate):
- Ray Direction: Calculates a ray direction by interpolating across the camera plane using
cam_x - DDA Setup: Initializes Digital Differential Analysis (DDA) algorithm variables:
del_dis_x/y: Distance the ray travels to cross one grid squareside_dis_x/y: Distance to the next grid linestep_x/y: Direction to step in the grid (+1 or -1)
- Grid Traversal: Steps through grid squares using DDA until hitting a wall (
#) - Distance Calculation: Computes perpendicular wall distance to avoid fisheye distortion
- Wall Height: Projects the 3D wall height based on distance (
DIS_H / per_wal_dis) - Color Selection: Uses different colors for horizontal vs. vertical walls (color 2 vs. 1) for depth perception
- Vertical Line Drawing: Draws a vertical line of pixels for each column to represent the wall
- Ray Direction: Calculates a ray direction by interpolating across the camera plane using
This creates a pseudo-3D effect where closer walls appear taller and farther walls appear shorter, similar to classic games like Wolfenstein 3D.