Skip to content

Commit

Permalink
examples: add a gg raycaster demo (#21904)
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-ward authored Jul 21, 2024
1 parent 69bc4be commit 9179038
Showing 1 changed file with 279 additions and 0 deletions.
279 changes: 279 additions & 0 deletions examples/gg/raycaster.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// Demonstrates how raycasting works. The left side shows
// the 2D layout of the walls and player. The green lines
// represent the field of view of the player.
//
// The right side is a simple 3D projection of the field
// of view.
//
// There is no collision detection so yes, you can walk
// through walls.
//
// Watch https://www.youtube.com/watch?v=gYRrGTC7GtA to
// learn more on how this code works. There are some silly
// digressons in the video but the tech content is spot on.
import gg
import gx
import math

const player_size = 8
const player_move_delta = 10
const map_x_size = 8
const map_y_size = 8
const map_square = 64

struct App {
mut:
ctx &gg.Context = unsafe { nil }
player_x f32
player_y f32
player_dx f32
player_dy f32
player_angle f32
map []int
}

fn main() {
mut app := App{
player_x: 230
player_y: 320
// each number represents an 8x8 square
// 1 is a wall cube, 0 is empty space
map: [
// vfmt off
1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 1, 1, 0, 0, 0, 1,
1, 0, 1, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 1, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1,
// vfmt on
]
}

calc_deltas(mut app)

app.ctx = gg.new_context(
user_data: &app
window_title: 'Raycaster Demo'
width: 1024
height: 512
bg_color: gx.gray
frame_fn: draw
event_fn: handle_events
)

app.ctx.run()
}

fn draw(mut app App) {
app.ctx.begin()
draw_map_2d(app)
draw_player(app)
draw_rays_and_walls(app)
draw_instructions(app)
app.ctx.end()
}

fn draw_map_2d(app App) {
for y := 0; y < map_y_size; y++ {
for x := 0; x < map_x_size; x++ {
color := if app.map[y * map_x_size + x] == 1 { gx.white } else { gx.black }
app.ctx.draw_rect_filled(x * map_square, y * map_square, map_square - 1, map_square - 1,
color)
}
}
}

fn draw_player(app App) {
app.ctx.draw_rect_filled(app.player_x, app.player_y, player_size, player_size, gx.yellow)
cx := app.player_x + player_size / 2
cy := app.player_y + player_size / 2
app.ctx.draw_line(cx, cy, cx + app.player_dx * 5, cy + app.player_dy * 5, gx.yellow)
}

fn draw_rays_and_walls(app App) {
pi2 := math.pi / 2
pi3 := 3 * math.pi / 2
degree_radian := f32(0.0174533)
max_depth_of_field := 8
field_of_view := 60 // 60 degrees

mut distance := f32(0)
mut depth_of_field := 0
mut ray_x := f32(0)
mut ray_y := f32(0)
mut offset_x := f32(0)
mut offset_y := f32(0)
mut map_x := 0
mut map_y := 0
mut map_pos := 0
mut color := gx.red
mut ray_angle := clamp_ray_angle(app.player_angle - degree_radian * field_of_view / 2)

// each step = 1/2 degree
steps := field_of_view * 2

for step := 0; step < steps; step++ {
// check horizontal lines
mut hd := f32(max_int)
mut hx := app.player_x
mut hy := app.player_y
depth_of_field = 0
arc_tan := -1.0 / math.tanf(ray_angle)
if ray_angle > math.pi { // looking up
ray_y = f32(int(app.player_y) / map_square * map_square) - .0001
ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
offset_y = -map_square
offset_x = -offset_y * arc_tan
} else if ray_angle < math.pi { // looking down
ray_y = f32(int(app.player_y) / map_square * map_square + map_square)
ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
offset_y = map_square
offset_x = -offset_y * arc_tan
} else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight left/right
ray_x = app.player_x
ray_y = app.player_y
depth_of_field = max_depth_of_field
}
for depth_of_field < max_depth_of_field {
map_x = int(ray_x) / map_square
map_y = int(ray_y) / map_square
map_pos = map_y * map_x_size + map_x
if app.map[map_pos] or { 0 } == 1 {
// hit a wall
hx = ray_x
hy = ray_y
hd = hypotenuse(app.player_x, app.player_y, hx, hy)
depth_of_field = max_depth_of_field
} else { // go to next line
ray_x += offset_x
ray_y += offset_y
depth_of_field += 1
}
}
// check vertical lines
mut vd := f32(max_int)
mut vx := app.player_x
mut vy := app.player_y
depth_of_field = 0
neg_tan := -math.tanf(ray_angle)
if ray_angle > pi2 && ray_angle < pi3 { // looking left
ray_x = f32(int(app.player_x) / map_square * map_square) - .0001
ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
offset_x = -map_square
offset_y = -offset_x * neg_tan
} else if ray_angle < pi2 || ray_angle > pi3 { // looking right
ray_x = f32(int(app.player_x) / map_square * map_square + map_square)
ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
offset_x = map_square
offset_y = -offset_x * neg_tan
} else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight up/down
ray_x = app.player_x
ray_y = app.player_y
depth_of_field = max_depth_of_field
}
for depth_of_field < max_depth_of_field {
map_x = int(ray_x) / map_square
map_y = int(ray_y) / map_square
map_pos = map_y * map_x_size + map_x
if app.map[map_pos] or { 0 } == 1 {
// hit a wall
vx = ray_x
vy = ray_y
vd = hypotenuse(app.player_x, app.player_y, vx, vy)
depth_of_field = max_depth_of_field
} else { // go to next line
ray_x += offset_x
ray_y += offset_y
depth_of_field += 1
}
}
// use the shorter of the horizontal and vertical distances to draw rays
// use different colors for the two sides of the walls for lighting effect
if vd < hd {
ray_x = vx
ray_y = vy
distance = vd
color = gx.rgb(0, 100, 0)
} else if hd < vd {
ray_x = hx
ray_y = hy
distance = hd
color = gx.rgb(0, 120, 0)
}
// draw ray
cx := app.player_x + player_size / 2
cy := app.player_y + player_size / 2
app.ctx.draw_line(cx, cy, ray_x, ray_y, gx.green)
// draw wall section
mut ca := clamp_ray_angle(app.player_angle - ray_angle)
distance *= math.cosf(ca) // remove fish eye
offset_3d_view := 530
line_thickeness := 4
max_wall_height := 320
wall_height := math.min((map_square * max_wall_height) / distance, max_wall_height)
wall_offset := max_wall_height / 2 - wall_height / 2
app.ctx.draw_line_with_config(step * line_thickeness + offset_3d_view, wall_offset,
step * line_thickeness + offset_3d_view, wall_offset + wall_height, gg.PenConfig{
color: color
thickness: line_thickeness
})
// step to next ray angle
ray_angle = clamp_ray_angle(ray_angle + degree_radian / 2)
}
}

fn handle_events(event &gg.Event, mut app App) {
if event.typ == .key_down {
match event.key_code {
.up {
app.player_x += app.player_dx
app.player_y += app.player_dy
}
.down {
app.player_x -= app.player_dx
app.player_y -= app.player_dy
}
.left {
app.player_angle -= 0.1
if app.player_angle < 0 {
app.player_angle += 2 * math.pi
}
calc_deltas(mut app)
}
.right {
app.player_angle += 0.1
if app.player_angle > 2 * math.pi {
app.player_angle -= 2 * math.pi
}
calc_deltas(mut app)
}
else {}
}
}
}

fn calc_deltas(mut app App) {
app.player_dx = math.cosf(app.player_angle) * 5
app.player_dy = math.sinf(app.player_angle) * 5
}

fn hypotenuse(ax f32, ay f32, bx f32, by f32) f32 {
a2 := math.square(bx - ax)
b2 := math.square(by - ay)
return math.sqrtf(a2 + b2)
}

fn clamp_ray_angle(ra f32) f32 {
return match true {
ra < 0 { ra + 2 * math.pi }
ra > 2 * math.pi { ra - 2 * math.pi }
else { ra }
}
}

fn draw_instructions(app App) {
app.ctx.draw_text(700, app.ctx.height - 17, 'use arrow keys to move player')
}

0 comments on commit 9179038

Please sign in to comment.