-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
examples: add a gg raycaster demo (#21904)
- Loading branch information
Showing
1 changed file
with
279 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} |