A Wolfenstein-style raycaster written entirely in R: WASD movement,
mouse look, textured walls, depth-based fog, and an optional top-down
debug view showing the live ray grid. The renderer fires a configurable
number of rays per frame using vectorized R arithmetic, then draws all
wall slices in a single draw_texture_pro() call with
per-slice source/destination rectangles and tint colors. It demonstrates
advanced use of draw_texture_pro() vectorization,
load_font_ex(), draw_text_ex() for multi-line
HUD text, toggle_fullscreen(), and
set_window_state().

Try it
Click the canvas to focus it. Move the mouse to look around, WASD to move. Tab toggles the debug map view.
Run
demo("raycaster", package = "raylibr")Source code
library(raylibr)
set_trace_log_level(log$none)
# custom functions -------------------------------------------------------------
blend_colors <- function(c1, c2, p = 0.5) {
p <- min(1, max(0, p))
if (is.null(c1)) c1 <- "black"
if (is.null(c2)) c2 <- "black"
c1 <- as_color(c1)
c2 <- as_color(c2)
r <- (c1$r * p) + (c2$r * (1-p))
g <- (c1$g * p) + (c2$g * (1-p))
b <- (c1$b * p) + (c2$b * (1-p))
color(r, g, b, c1$a)
}
get_tile <- function(x, y) {
tile_grid[floor(y / 64) * tile_dims[1] + floor(x / 64) + 1]
}
get_diff <- function(pos) {
diff <- pos - 64 * floor(pos / 64)
over_half <- diff > (tile_size / 2)
diff[over_half] <- 64 - diff[over_half]
diff
}
# settings --------------------------------------------------------------------
window_size <- c(1024, 512)
window_fps <- 100
viewport_size <- c(512, 384)
viewport_position <- c(512, 64)
viewport_resolution <- 128
viewport_fov <- 70
viewport_dof <- 8
viewport_fog <- 0.15
player_position <- c(320, 256)
player_angle <- 2.3
player_offset <- 8
player_walk_speed <- 2
player_strafe_speed <- 1
tile_dims <- c(8, 8)
tile_size <- 64
tile_grid <- c(1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,1,
1,0,2,2,2,0,0,1,
1,0,2,0,0,0,0,1,
1,0,2,0,0,0,0,1,
1,0,0,0,0,1,0,1,
1,0,0,0,0,1,0,1,
1,1,1,1,1,1,1,1)
color_fog <- blend_colors("grey", "black", 0.5)
color_floor <- blend_colors("darkgrey", "olivegreen", 0.1)
color_floor <- "moccasin"
color_ceiling <- "darkgrey"
color_wall <- list(fade(color_floor, 0), "tan1", "tan2", "dodgerblue3",
"dodgerblue4", "tomato")
textures <- c("transparent.png", "Bricks_01-128x128.png",
"Wood_20-128x128.png")
texture_size <- 128
# pre-compute constants -------------------------------------------------------
frame <- 0
fps <- 0
two_pi <- pi * 2
half_pi <- pi / 2
tile_xz <- as.matrix(expand.grid(seq(tile_dims[1]) - 1, seq(tile_dims[2]) - 1))
tile_colors <- ifelse(tile_grid, "white", "black")
tile_colors <- lapply(color_wall[tile_grid+1], blend_colors, "black", 0.8)
tile_sizes <- matrix(rep(tile_size, tile_dims[1] * tile_dims[2] * 2), ncol = 2)
player_offsets <- as.matrix(expand.grid(c(-1, 1), c(-1, 1))) * player_offset
help_lines <- c(
sprintf("W: FORWARD"),
sprintf("S: BACKPEDAL ENTER: TOGGLE FULLSCREEN"),
sprintf("A: STRAFE LEFT TAB: TOGGLE DEBUG"),
sprintf("D: STRAFE RIGHT MOUSE: ROTATE ESCAPE: EXIT")
)
# window setup -----------------------------------------------------------------
update_view <- TRUE
debug <- TRUE
init_window(window_size[1], window_size[2], "R & Raylib: Raycaster")
set_window_state(flag$window_resizable)
set_target_fps(window_fps)
hide_cursor()
base_path <- system.file(package = "raylibr")
font <- load_font_ex(file.path(base_path,
"demo_resources", "ShareTechMono-Regular.ttf"),
32)
textures <- lapply(paste0(base_path, "/demo_resources/", textures), load_texture)
# game loop --------------------------------------------------------------------
while (!window_should_close()) {
if (is_window_resized() && !is_window_fullscreen()) {
window_size <- c(get_screen_width(), get_screen_height())
update_view <- TRUE
}
## movement ------------------------------------------------------------------
player_angle <- (player_angle + get_mouse_delta()[1] / 60) %% two_pi
player_angle_s <- (player_angle + half_pi) %% two_pi
player_delta <- c(0, 0)
if (is_key_down(key$w)) player_delta <- player_delta +
c(cos(player_angle), sin(player_angle)) * player_walk_speed
if (is_key_down(key$s)) player_delta <- player_delta -
c(cos(player_angle), sin(player_angle)) * player_walk_speed
if (is_key_down(key$d)) player_delta <- player_delta +
c(cos(player_angle_s), sin(player_angle_s)) * player_strafe_speed
if (is_key_down(key$a)) player_delta <- player_delta -
c(cos(player_angle_s), sin(player_angle_s)) * player_strafe_speed
player_position <- player_position + player_delta
## collision detection -------------------------------------------------------
player_bb <- rep(player_position, each = 4) + player_offsets
player_bbi <- floor(player_bb / 64)
bc <- tile_grid[player_bbi[,2] * tile_dims[1] + player_bbi[,1] + 1] > 0
if (sum(bc) == 1) {
if (bc[1]) {
diff <- get_diff(player_bb[1,])
bc[ifelse(diff[2] < diff[1], 2, 3)] <- TRUE
} else if (bc[2]) {
diff <- get_diff(player_bb[2,])
bc[ifelse(diff[2] < diff[1], 1, 4)] <- TRUE
} else if (bc[3]) {
diff <- get_diff(player_bb[3,])
bc[ifelse(diff[2] < diff[1], 4, 1)] <- TRUE
} else if (bc[4]) {
diff <- get_diff(player_bb[4,])
bc[ifelse(diff[2] < diff[1], 3, 2)] <- TRUE
}
}
player_diff <- get_diff(player_position)
if (bc[1] && bc[2])
player_position[2] <- player_position[2] - player_diff[2] + player_offset
if (bc[3] && bc[4])
player_position[2] <- player_position[2] + player_diff[2] - player_offset
if (bc[1] && bc[3])
player_position[1] <- player_position[1] - player_diff[1] + player_offset
if (bc[2] && bc[4])
player_position[1] <- player_position[1] + player_diff[1] - player_offset
player_bb <- rep(player_position, each = 4) + player_offsets
## keys for changing settings ------------------------------------------------
k <- get_key_pressed()
if (k == key$minus) {
update_view <- TRUE
viewport_resolution <- viewport_resolution / 2
} else if (k == key$equal) {
update_view <- TRUE
viewport_resolution <- viewport_resolution * 2
} else if (k == key$tab) {
update_view <- TRUE
debug <- !debug
viewport_position <- c(0, 0)
viewport_size <- window_size
} else if (k == key$enter) {
update_view <- TRUE
toggle_fullscreen()
if (is_window_fullscreen()) {
monitor <- get_current_monitor()
old_window_size <- window_size
window_size <- c(get_monitor_width(monitor), get_monitor_height(monitor)) * 0.8
} else {
window_size <- old_window_size
}
}
if (k == key$one) { update_view <- TRUE; viewport_dof <- 1 }
if (k == key$two) { update_view <- TRUE; viewport_dof <- 2 }
if (k == key$three) { update_view <- TRUE; viewport_dof <- 3 }
if (k == key$four) { update_view <- TRUE; viewport_dof <- 4 }
if (k == key$five) { update_view <- TRUE; viewport_dof <- 5 }
if (k == key$six) { update_view <- TRUE; viewport_dof <- 6 }
if (k == key$seven) { update_view <- TRUE; viewport_dof <- 7 }
if (k == key$eight) { update_view <- TRUE; viewport_dof <- 8 }
if (k == key$nine) { update_view <- TRUE; viewport_dof <- 9 }
if (is_key_down(key$left_bracket)) {
update_view <- TRUE;
viewport_fov <- viewport_fov - 1
}
if (is_key_down(key$right_bracket)) {
update_view <- TRUE;
viewport_fov <- viewport_fov + 1
}
if (is_key_down(key$period)) {
update_view <- TRUE;
viewport_fog <- viewport_fog + 0.01
}
if (is_key_down(key$comma)) {
update_view <- TRUE;
viewport_fog <- viewport_fog - 0.01
}
## update view ---------------------------------------------------------------
if (update_view) {
if (debug) {
viewport_size[1] <- window_size[1] / 2
viewport_size[2] <- window_size[2] - 128
viewport_position[1] <- window_size[1] / 2
viewport_position[2] <- 64
} else {
viewport_size <- window_size
viewport_position <- c(0, 0)
}
viewport_center <- viewport_position + (viewport_size / 2)
viewport_resolution <- max(2, min(viewport_resolution, viewport_size[1]))
viewport_fov <- max(30, min(viewport_fov, 170))
viewport_fog <- max(0, min(viewport_fog, 1))
angles <- atan(seq(-viewport_fov / 2, viewport_fov / 2, length.out =
(viewport_resolution + 2))[2:(viewport_resolution +
1)] * pi / 180)
bars_width <- viewport_size[1] / viewport_resolution
bars_x <- viewport_position[1] + head(seq(from = 0, to = viewport_size[1],
length.out = viewport_resolution +
1), -1)
sixty_fours <- rep(64, viewport_resolution)
color_fogs <- rep(list(color_fog), length.out = viewport_resolution)
lines_x <- rep(viewport_position[1] + 6, 8)
lines_y <- c(seq(4) * 14 - 12,
seq(4) * 14 + viewport_size[2] + viewport_position[2] - 11)
lines_xy <- cbind(lines_x, lines_y)
zeros_vector <- rep(0, viewport_resolution)
zeros_matrix <- matrix(0, nrow = viewport_resolution, ncol = 2)
}
update_view <- FALSE
## raycasting ----------------------------------------------------------------
rays_angle <- (player_angle + angles) %% two_pi
rays_quadrant <- floor(rays_angle / half_pi)
rays_up <- rays_quadrant >= 2
rays_left <- (rays_quadrant == 1) | (rays_quadrant == 2)
### horizontal check ---------------------------------------------------------
rays_atan <- -1 / tan(rays_angle)
ry <- bitwShiftL(bitwShiftR(player_position[2], 6), 6) +
ifelse(rays_up, -0.0000001, 64)
rx <- (player_position[2] - ry) * rays_atan + player_position[1]
yo <- sixty_fours; yo[rays_up] <- -64
xo <- -yo * rays_atan
todo <- rep(TRUE, viewport_resolution)
mx <- rep(0, viewport_resolution)
my <- rep(0, viewport_resolution)
tile <- rep(0, viewport_resolution)
dof <- 0
while (any(todo) & (dof < viewport_dof)) {
mx[todo] <- floor(rx[todo] / 64)
my[todo] <- floor(ry[todo] / 64)
mp <- ifelse(mx >= 0 & my >= 0 & mx < tile_dims[1] & my < tile_dims[2],
my * tile_dims[1] + mx + 1, NA)
tile[todo] <- tile_grid[mp[todo]]
todo <- todo & (is.na(tile) | (tile == 0))
rx[todo] <- rx[todo] + xo[todo]
ry[todo] <- ry[todo] + yo[todo]
dof <- dof + 1
}
texture_h <- tile + 1
dists_h <- sqrt((rx - player_position[1])^2 +
(ry - player_position[2])^2) * cos(angles)
h_rx <- rx
h_ry <- ry
h_ti <- floor(2 * (rx %% tile_size))
h_ti[!rays_up] <- texture_size - h_ti[!rays_up]
### vertical check ---------------------------------------------------------
rays_ntan <- -tan(rays_angle)
rx <- bitwShiftL(bitwShiftR(player_position[1], 6), 6) +
ifelse(rays_left, -0.0000001, 64)
ry <- (player_position[1] - rx) * rays_ntan + player_position[2]
xo <- sixty_fours; xo[rays_left] <- -64
yo <- -xo * rays_ntan
todo <- rep(TRUE, viewport_resolution)
dof <- 0
while (any(todo) & (dof < viewport_dof)) {
mx[todo] <- floor(rx[todo] / 64)
my[todo] <- floor(ry[todo] / 64)
mp <- ifelse(mx >= 0 & my >= 0 & mx < tile_dims[1] & my < tile_dims[2],
my * tile_dims[1] + mx + 1, NA)
tile[todo] <- tile_grid[mp[todo]]
todo <- todo & (is.na(tile) | (tile == 0))
rx[todo] <- rx[todo] + xo[todo]
ry[todo] <- ry[todo] + yo[todo]
dof <- dof + 1
}
texture_v <- tile + 1
dists_v <- sqrt((rx - player_position[1])^2 +
(ry - player_position[2])^2) * cos(angles)
v_rx <- rx
v_ry <- ry
v_ti <- floor(2 * (ry %% tile_size))
v_ti[rays_left] <- texture_size - v_ti[rays_left]
### combine horizontal and vertical ------------------------------------------
t_light <- rep(1, viewport_resolution)
h_wins <- dists_h < dists_v
bars_texture <- texture_v
bars_texture[h_wins] <- texture_h[h_wins]
dists_t <- dists_v
dists_t[h_wins] <- dists_h[h_wins]
t_light[h_wins] <- 0.7
t_rx <- ifelse(h_wins, h_rx, v_rx)
t_ry <- ifelse(h_wins, h_ry, v_ry)
t_ti <- ifelse(h_wins, h_ti, v_ti)
### compute wall textures ----------------------------------------------------
bars_height <- tile_size * viewport_size[2] / dists_t
height_fractions <- viewport_size[2] / bars_height
height_fractions[height_fractions > 1] <- 1
bars_height[bars_height > viewport_size[2]] <- viewport_size[2]
rects_src <- Map(rectangle, t_ti, tile_size - (tile_size * height_fractions),
1, texture_size * height_fractions)
rects_dst <- Map(rectangle, bars_x, viewport_center[2] - (bars_height / 2),
bars_width, bars_height)
fog_ratio <- dists_t * viewport_fog / 100
fog_ratio[fog_ratio > 1] <- 1
textures_light <- Map(blend_colors, "white", "black", t_light)
textures_tint <- Map(blend_colors, color_fogs, textures_light, fog_ratio)
## drawing -------------------------------------------------------------------
begin_drawing()
clear_background("black")
if (debug) {
### draw tiles -------------------------------------------------------------
draw_rectangle_v(tile_xz * tile_size + 1, tile_sizes - 2, tile_colors)
### draw rays --------------------------------------------------------------
draw_line_ex(player_position, cbind(t_rx, t_ry), 1, textures_tint)
### draw player ------------------------------------------------------------
draw_triangle(player_position + c(cos(player_angle), sin(player_angle)) * 8,
player_position - c(cos(player_angle), sin(player_angle)) * 7 -
c(cos(player_angle_s), sin(player_angle_s)) * 4,
player_position - c(cos(player_angle), sin(player_angle)) * 7 +
c(cos(player_angle_s), sin(player_angle_s)) * 4,
"white")
if (bc[1] && bc[2]) { draw_line_ex(player_bb[1,], player_bb[2,], 2, "red") }
if (bc[3] && bc[4]) { draw_line_ex(player_bb[3,], player_bb[4,], 2, "red") }
if (bc[1] && bc[3]) { draw_line_ex(player_bb[1,], player_bb[3,], 2, "red") }
if (bc[2] && bc[4]) { draw_line_ex(player_bb[2,], player_bb[4,], 2, "red") }
}
### draw floor and ceiling ---------------------------------------------------
color_floor_far <- blend_colors(color_fog, color_floor, viewport_fog * 5)
color_floor_near <- blend_colors(color_fog, color_floor, viewport_fog * 0.9)
color_ceiling_far <- blend_colors(color_fog, color_ceiling, viewport_fog * 5)
color_ceiling_near <- blend_colors(color_fog, color_ceiling, viewport_fog * 0.9)
draw_rectangle_gradient_v(viewport_position[1], viewport_center[2],
viewport_size[1], viewport_size[2] / 2,
color_floor_far, color_floor_near)
draw_rectangle_gradient_v(viewport_position[1], viewport_position[2],
viewport_size[1], viewport_size[2] / 2,
color_ceiling_near, color_ceiling_far)
### draw walls ---------------------------------------------------------------
draw_texture_pro(textures[bars_texture], rects_src, rects_dst, zeros_matrix,
zeros_vector, textures_tint)
### draw stats and help ------------------------------------------------------
if (frame > 20) {
fps <- get_fps()
frame <- 0
}
frame <- frame + 1
if (debug) {
lines <- c(
sprintf("RES -/+: %3d FPS: %3d", viewport_resolution, fps),
sprintf("FOG ,/.: %3.f", viewport_fog * 100),
sprintf("DOF 1-9: %3d", viewport_dof),
sprintf("FOV [/]: %3d", viewport_fov),
help_lines
)
draw_text_ex(font, lines, lines_xy, 16, 0, "darkgrey")
}
end_drawing()
}
show_cursor()
close_window()