Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Ray Casting

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:

  1. Map Representation: The map m is a 2D grid of strings where # represents walls and . represents walkable space. The val_pos() function checks if a position is valid (within bounds and not a wall).

  2. Player State: The s object stores:

    • pos: Player position in the map (starts at 2,2)
    • dir: Direction vector the player is facing
    • pla: 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)
  3. Rotation (Left/Right buttons): Uses 2D rotation matrices to rotate both the direction and plane vectors:

    • Applies cos(angle) and sin(angle) transformations
    • Right button rotates clockwise, left button rotates counter-clockwise
    • Both vectors must rotate together to maintain proper camera perspective
  4. 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_sp for consistent speed
  5. 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 square
      • side_dis_x/y: Distance to the next grid line
      • step_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

This creates a pseudo-3D effect where closer walls appear taller and farther walls appear shorter, similar to classic games like Wolfenstein 3D.