Skip to content

Commit 9179038

Browse files
authored
examples: add a gg raycaster demo (#21904)
1 parent 69bc4be commit 9179038

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed

examples/gg/raycaster.v

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// Demonstrates how raycasting works. The left side shows
2+
// the 2D layout of the walls and player. The green lines
3+
// represent the field of view of the player.
4+
//
5+
// The right side is a simple 3D projection of the field
6+
// of view.
7+
//
8+
// There is no collision detection so yes, you can walk
9+
// through walls.
10+
//
11+
// Watch https://www.youtube.com/watch?v=gYRrGTC7GtA to
12+
// learn more on how this code works. There are some silly
13+
// digressons in the video but the tech content is spot on.
14+
import gg
15+
import gx
16+
import math
17+
18+
const player_size = 8
19+
const player_move_delta = 10
20+
const map_x_size = 8
21+
const map_y_size = 8
22+
const map_square = 64
23+
24+
struct App {
25+
mut:
26+
ctx &gg.Context = unsafe { nil }
27+
player_x f32
28+
player_y f32
29+
player_dx f32
30+
player_dy f32
31+
player_angle f32
32+
map []int
33+
}
34+
35+
fn main() {
36+
mut app := App{
37+
player_x: 230
38+
player_y: 320
39+
// each number represents an 8x8 square
40+
// 1 is a wall cube, 0 is empty space
41+
map: [
42+
// vfmt off
43+
1, 1, 1, 1, 1, 1, 1, 1,
44+
1, 0, 0, 0, 0, 0, 0, 1,
45+
1, 0, 1, 1, 0, 0, 0, 1,
46+
1, 0, 1, 0, 0, 0, 0, 1,
47+
1, 0, 0, 0, 0, 0, 0, 1,
48+
1, 0, 0, 0, 0, 1, 0, 1,
49+
1, 0, 0, 0, 0, 0, 0, 1,
50+
1, 1, 1, 1, 1, 1, 1, 1,
51+
// vfmt on
52+
]
53+
}
54+
55+
calc_deltas(mut app)
56+
57+
app.ctx = gg.new_context(
58+
user_data: &app
59+
window_title: 'Raycaster Demo'
60+
width: 1024
61+
height: 512
62+
bg_color: gx.gray
63+
frame_fn: draw
64+
event_fn: handle_events
65+
)
66+
67+
app.ctx.run()
68+
}
69+
70+
fn draw(mut app App) {
71+
app.ctx.begin()
72+
draw_map_2d(app)
73+
draw_player(app)
74+
draw_rays_and_walls(app)
75+
draw_instructions(app)
76+
app.ctx.end()
77+
}
78+
79+
fn draw_map_2d(app App) {
80+
for y := 0; y < map_y_size; y++ {
81+
for x := 0; x < map_x_size; x++ {
82+
color := if app.map[y * map_x_size + x] == 1 { gx.white } else { gx.black }
83+
app.ctx.draw_rect_filled(x * map_square, y * map_square, map_square - 1, map_square - 1,
84+
color)
85+
}
86+
}
87+
}
88+
89+
fn draw_player(app App) {
90+
app.ctx.draw_rect_filled(app.player_x, app.player_y, player_size, player_size, gx.yellow)
91+
cx := app.player_x + player_size / 2
92+
cy := app.player_y + player_size / 2
93+
app.ctx.draw_line(cx, cy, cx + app.player_dx * 5, cy + app.player_dy * 5, gx.yellow)
94+
}
95+
96+
fn draw_rays_and_walls(app App) {
97+
pi2 := math.pi / 2
98+
pi3 := 3 * math.pi / 2
99+
degree_radian := f32(0.0174533)
100+
max_depth_of_field := 8
101+
field_of_view := 60 // 60 degrees
102+
103+
mut distance := f32(0)
104+
mut depth_of_field := 0
105+
mut ray_x := f32(0)
106+
mut ray_y := f32(0)
107+
mut offset_x := f32(0)
108+
mut offset_y := f32(0)
109+
mut map_x := 0
110+
mut map_y := 0
111+
mut map_pos := 0
112+
mut color := gx.red
113+
mut ray_angle := clamp_ray_angle(app.player_angle - degree_radian * field_of_view / 2)
114+
115+
// each step = 1/2 degree
116+
steps := field_of_view * 2
117+
118+
for step := 0; step < steps; step++ {
119+
// check horizontal lines
120+
mut hd := f32(max_int)
121+
mut hx := app.player_x
122+
mut hy := app.player_y
123+
depth_of_field = 0
124+
arc_tan := -1.0 / math.tanf(ray_angle)
125+
if ray_angle > math.pi { // looking up
126+
ray_y = f32(int(app.player_y) / map_square * map_square) - .0001
127+
ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
128+
offset_y = -map_square
129+
offset_x = -offset_y * arc_tan
130+
} else if ray_angle < math.pi { // looking down
131+
ray_y = f32(int(app.player_y) / map_square * map_square + map_square)
132+
ray_x = (app.player_y - ray_y) * arc_tan + app.player_x
133+
offset_y = map_square
134+
offset_x = -offset_y * arc_tan
135+
} else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight left/right
136+
ray_x = app.player_x
137+
ray_y = app.player_y
138+
depth_of_field = max_depth_of_field
139+
}
140+
for depth_of_field < max_depth_of_field {
141+
map_x = int(ray_x) / map_square
142+
map_y = int(ray_y) / map_square
143+
map_pos = map_y * map_x_size + map_x
144+
if app.map[map_pos] or { 0 } == 1 {
145+
// hit a wall
146+
hx = ray_x
147+
hy = ray_y
148+
hd = hypotenuse(app.player_x, app.player_y, hx, hy)
149+
depth_of_field = max_depth_of_field
150+
} else { // go to next line
151+
ray_x += offset_x
152+
ray_y += offset_y
153+
depth_of_field += 1
154+
}
155+
}
156+
// check vertical lines
157+
mut vd := f32(max_int)
158+
mut vx := app.player_x
159+
mut vy := app.player_y
160+
depth_of_field = 0
161+
neg_tan := -math.tanf(ray_angle)
162+
if ray_angle > pi2 && ray_angle < pi3 { // looking left
163+
ray_x = f32(int(app.player_x) / map_square * map_square) - .0001
164+
ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
165+
offset_x = -map_square
166+
offset_y = -offset_x * neg_tan
167+
} else if ray_angle < pi2 || ray_angle > pi3 { // looking right
168+
ray_x = f32(int(app.player_x) / map_square * map_square + map_square)
169+
ray_y = (app.player_x - ray_x) * neg_tan + app.player_y
170+
offset_x = map_square
171+
offset_y = -offset_x * neg_tan
172+
} else if ray_angle == 0 || ray_angle == 2 * math.pi { // looking straight up/down
173+
ray_x = app.player_x
174+
ray_y = app.player_y
175+
depth_of_field = max_depth_of_field
176+
}
177+
for depth_of_field < max_depth_of_field {
178+
map_x = int(ray_x) / map_square
179+
map_y = int(ray_y) / map_square
180+
map_pos = map_y * map_x_size + map_x
181+
if app.map[map_pos] or { 0 } == 1 {
182+
// hit a wall
183+
vx = ray_x
184+
vy = ray_y
185+
vd = hypotenuse(app.player_x, app.player_y, vx, vy)
186+
depth_of_field = max_depth_of_field
187+
} else { // go to next line
188+
ray_x += offset_x
189+
ray_y += offset_y
190+
depth_of_field += 1
191+
}
192+
}
193+
// use the shorter of the horizontal and vertical distances to draw rays
194+
// use different colors for the two sides of the walls for lighting effect
195+
if vd < hd {
196+
ray_x = vx
197+
ray_y = vy
198+
distance = vd
199+
color = gx.rgb(0, 100, 0)
200+
} else if hd < vd {
201+
ray_x = hx
202+
ray_y = hy
203+
distance = hd
204+
color = gx.rgb(0, 120, 0)
205+
}
206+
// draw ray
207+
cx := app.player_x + player_size / 2
208+
cy := app.player_y + player_size / 2
209+
app.ctx.draw_line(cx, cy, ray_x, ray_y, gx.green)
210+
// draw wall section
211+
mut ca := clamp_ray_angle(app.player_angle - ray_angle)
212+
distance *= math.cosf(ca) // remove fish eye
213+
offset_3d_view := 530
214+
line_thickeness := 4
215+
max_wall_height := 320
216+
wall_height := math.min((map_square * max_wall_height) / distance, max_wall_height)
217+
wall_offset := max_wall_height / 2 - wall_height / 2
218+
app.ctx.draw_line_with_config(step * line_thickeness + offset_3d_view, wall_offset,
219+
step * line_thickeness + offset_3d_view, wall_offset + wall_height, gg.PenConfig{
220+
color: color
221+
thickness: line_thickeness
222+
})
223+
// step to next ray angle
224+
ray_angle = clamp_ray_angle(ray_angle + degree_radian / 2)
225+
}
226+
}
227+
228+
fn handle_events(event &gg.Event, mut app App) {
229+
if event.typ == .key_down {
230+
match event.key_code {
231+
.up {
232+
app.player_x += app.player_dx
233+
app.player_y += app.player_dy
234+
}
235+
.down {
236+
app.player_x -= app.player_dx
237+
app.player_y -= app.player_dy
238+
}
239+
.left {
240+
app.player_angle -= 0.1
241+
if app.player_angle < 0 {
242+
app.player_angle += 2 * math.pi
243+
}
244+
calc_deltas(mut app)
245+
}
246+
.right {
247+
app.player_angle += 0.1
248+
if app.player_angle > 2 * math.pi {
249+
app.player_angle -= 2 * math.pi
250+
}
251+
calc_deltas(mut app)
252+
}
253+
else {}
254+
}
255+
}
256+
}
257+
258+
fn calc_deltas(mut app App) {
259+
app.player_dx = math.cosf(app.player_angle) * 5
260+
app.player_dy = math.sinf(app.player_angle) * 5
261+
}
262+
263+
fn hypotenuse(ax f32, ay f32, bx f32, by f32) f32 {
264+
a2 := math.square(bx - ax)
265+
b2 := math.square(by - ay)
266+
return math.sqrtf(a2 + b2)
267+
}
268+
269+
fn clamp_ray_angle(ra f32) f32 {
270+
return match true {
271+
ra < 0 { ra + 2 * math.pi }
272+
ra > 2 * math.pi { ra - 2 * math.pi }
273+
else { ra }
274+
}
275+
}
276+
277+
fn draw_instructions(app App) {
278+
app.ctx.draw_text(700, app.ctx.height - 17, 'use arrow keys to move player')
279+
}

0 commit comments

Comments
 (0)