-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
360 lines (305 loc) · 10.9 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import math
import pyxel
INTERLEAVE_RENDERING = True
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 136
MAP_WIDTH = 16
MAP_HEIGHT = 16
DEFAULT_FOV = math.pi / 4
DEFAULT_DEPTH = 16
MAP_1 = """
################
#..............#
#..#....######.#
#..............#
#..............#
#......##......#
#......##......#
#..............#
###....#####...#
##.............#
#..............#
#........###.###
#........#.....#
#..............#
#........#.....#
################
"""
class Game:
def __init__(self):
pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT, "Raycasting in progress...")
# Player staring position and angle
self.player_x = 7
self.player_y = 12
self.player_a = 4
self.fov = DEFAULT_FOV
self.depth = DEFAULT_DEPTH
# Walking speed
self.speed = 0.2
# Load the world map
self.world_map = MAP_1[1:-1].split("\n")
self.tp1 = pyxel.frame_count
self.tp2 = pyxel.frame_count
self.interleave = INTERLEAVE_RENDERING
self.render_alt = True
self.player_run = False
self.v_look = 0
self.show_map = True
self.show_boundries = True
def run(self):
pyxel.run(self.update, self.draw)
def update(self):
# Compute elapsed time
self.tp2 = pyxel.frame_count
ellapsed_time = self.tp2 - self.tp1
self.tp1 = self.tp2
# Handle player input
if pyxel.btnp(pyxel.KEY_ESCAPE):
pyxel.quit()
# Determine player speed
if pyxel.btnp(pyxel.KEY_SHIFT):
self.player_run = True
if pyxel.btnr(pyxel.KEY_SHIFT):
self.player_run = False
if self.player_run:
speed = self.speed
else:
speed = self.speed / 3
if pyxel.btn(pyxel.KEY_W):
self.move_forward(speed, ellapsed_time)
if pyxel.btn(pyxel.KEY_S):
self.move_backward(speed, ellapsed_time)
if pyxel.btn(pyxel.KEY_A):
self.player_a -= 0.04 * ellapsed_time
if pyxel.btn(pyxel.KEY_D):
self.player_a += 0.04 * ellapsed_time
if pyxel.btn(pyxel.KEY_Z):
self.move_left(speed, ellapsed_time)
if pyxel.btn(pyxel.KEY_C):
self.move_right(speed, ellapsed_time)
if pyxel.btnp(pyxel.KEY_P):
print(f"x: {self.player_x}, ", end="")
print(f"y: {self.player_y}, ", end="")
print(f"a: {self.player_a}")
if pyxel.btn(pyxel.KEY_1):
self.fov += 0.01
if pyxel.btn(pyxel.KEY_2):
self.fov -= 0.01
if pyxel.btn(pyxel.KEY_3):
self.fov = DEFAULT_FOV
if pyxel.btnp(pyxel.KEY_4):
self.interleave = not self.interleave
if pyxel.btn(pyxel.KEY_5):
self.depth += 1
if pyxel.btn(pyxel.KEY_6):
self.depth -= 1
if pyxel.btn(pyxel.KEY_7):
self.depth = DEFAULT_DEPTH
if pyxel.btnp(pyxel.KEY_8):
self.show_boundries = not self.show_boundries
if pyxel.btn(pyxel.KEY_Q):
self.v_look += 1
if pyxel.btn(pyxel.KEY_E):
self.v_look -= 1
if pyxel.btn(pyxel.KEY_X):
self.v_look = 0
if pyxel.btnp(pyxel.KEY_M):
self.show_map = not self.show_map
def draw(self):
if self.interleave:
inc = 2
start = 0
if self.render_alt:
start = 1
self.render_alt = not self.render_alt
else:
pyxel.cls(0)
start = 0
inc = 1
for x in range(start, SCREEN_WIDTH, inc):
dist_to_wall, boundary = self.cast_ray(x)
# Calculate distance to ceiling and floor
ceiling = SCREEN_HEIGHT / 2 - SCREEN_HEIGHT / dist_to_wall
floor = SCREEN_HEIGHT - ceiling
# Modify the ceiling and floor for the vertical look position
ceiling += self.v_look
floor += self.v_look
shade = 0
for y in range(SCREEN_HEIGHT):
if y < ceiling:
shade = 0 # ceiling shade
elif y > ceiling and y <= floor:
shade = self.compute_wall_shade(x, y, dist_to_wall, boundary)
else:
shade = self.compute_floor_shade(x, y)
pyxel.image(1).pset(x, y, shade)
# Blt the screen from the image bank being used as the display buffer
pyxel.blt(0, 0, 1, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
# Draw map
if self.show_map:
w = 2
startx = 0
starty = SCREEN_HEIGHT - MAP_WIDTH * w
for nx in range(MAP_HEIGHT):
for ny in range(MAP_WIDTH):
cell = self.world_map[ny][nx]
if cell == "#":
col = 6
else:
col = 7
pyxel.rect(w * nx + startx, w * ny + starty, w, w, col)
px = int(self.player_x)
py = int(self.player_y)
pyxel.rect(w * px + startx, w * py + starty, w, w, 12)
def cast_ray(self, x):
# https://www.youtube.com/watch?v=NbSee-XM7WA
# https://lodev.org/cgtutor/raycasting.html
ray_start_x = self.player_x
ray_start_y = self.player_y
ray_angle = (self.player_a - self.fov / 2) + (x / SCREEN_WIDTH) * self.fov
ray_dir_x = math.sin(ray_angle)
ray_dir_y = math.cos(ray_angle)
ray_unit_step_x = math.sqrt(
1 + (ray_dir_y / ray_dir_x) * (ray_dir_y / ray_dir_x)
)
ray_unit_step_y = math.sqrt(
1 + (ray_dir_x / ray_dir_y) * (ray_dir_x / ray_dir_y)
)
map_check_x = int(ray_start_x)
map_check_y = int(ray_start_y)
ray_len_x = 0
ray_len_y = 0
step_x = 0
step_y = 0
if ray_dir_x < 0:
step_x = -1
ray_len_x = (ray_start_x - float(map_check_x)) * ray_unit_step_x
else:
step_x = 1
ray_len_x = (float(map_check_x + 1) - ray_start_x) * ray_unit_step_x
if ray_dir_y < 0:
step_y = -1
ray_len_y = (ray_start_y - float(map_check_y)) * ray_unit_step_y
else:
step_y = 1
ray_len_y = (float(map_check_y + 1) - ray_start_y) * ray_unit_step_y
tile_found = False
max_dist = 64.0
dist = 0.0
boundary = False
while not tile_found and dist < max_dist:
if ray_len_x < ray_len_y:
map_check_x += step_x
dist = ray_len_x
ray_len_x += ray_unit_step_x
else:
map_check_y += step_y
dist = ray_len_y
ray_len_y += ray_unit_step_y
if self.check_collision(map_check_x, map_check_y):
tile_found = True
if self.show_boundries:
# Cast rays from each corner of the wall to find the boundaries
p = []
for tx in range(2):
for ty in range(2):
vy = map_check_y + ty - self.player_y
vx = map_check_x + tx - self.player_x
d = math.sqrt(vx * vx + vy * vy)
dot = (ray_dir_x * vx / d) + (ray_dir_y * vy / d)
p.append((d, dot))
# Sort the pairs to find the closest
p.sort()
# Looking for very small angles with closest corners
bound = 0.01
if math.acos(p[0][1]) < bound:
boundary = True
if math.acos(p[1][1]) < bound:
boundary = True
return dist, boundary
def compute_wall_shade(self, x, y, dist_to_wall, boundary=False):
if dist_to_wall <= self.depth / 7:
shade = 6
elif dist_to_wall < self.depth / 6:
if x % 2:
shade = 6 if y % 2 else 13
else:
shade = 13 if y % 2 else 6
elif dist_to_wall < self.depth / 5:
shade = 13
elif dist_to_wall < self.depth / 4:
if x % 2:
shade = 13 if y % 2 else 1
else:
shade = 1 if y % 2 else 13
elif dist_to_wall < self.depth / 3:
shade = 1
elif dist_to_wall < self.depth / 2:
if x % 2:
shade = 1 if y % 2 else 0
else:
shade = 0 if y % 2 else 1
elif dist_to_wall < self.depth:
shade = 0
if self.show_boundries and boundary:
shade = 0
return shade
def compute_floor_shade(self, x, y):
# v_look changes where the floor starts rendering
b = 1 - (y - self.v_look - SCREEN_HEIGHT / 2) / (SCREEN_HEIGHT / 2)
if b < 0.25:
shade = 14
elif b < 0.5:
if x % 2:
shade = 14 if y % 2 else 2
else:
shade = 2 if y % 2 else 14
elif b < 0.75:
shade = 2
elif b < 0.9:
if x % 2:
shade = 2 if y % 2 else 0
else:
shade = 0 if y % 2 else 2
else:
shade = 0
return shade
def move_forward(self, speed, time_delta):
new_x = self.player_x + math.sin(self.player_a) * speed * time_delta
new_y = self.player_y + math.cos(self.player_a) * speed * time_delta
if not self.check_collision(new_x, new_y):
self.player_x = new_x
self.player_y = new_y
def move_backward(self, speed, time_delta):
new_x = self.player_x - math.sin(self.player_a) * speed * time_delta
new_y = self.player_y - math.cos(self.player_a) * speed * time_delta
if not self.check_collision(new_x, new_y):
self.player_x = new_x
self.player_y = new_y
def move_left(self, speed, time_delta):
new_x = (
self.player_x + math.sin(self.player_a - math.pi / 2) * speed * time_delta
)
new_y = (
self.player_y + math.cos(self.player_a - math.pi / 2) * speed * time_delta
)
if not self.check_collision(new_x, new_y):
self.player_x = new_x
self.player_y = new_y
def move_right(self, speed, time_delta):
new_x = (
self.player_x + math.sin(self.player_a + math.pi / 2) * speed * time_delta
)
new_y = (
self.player_y + math.cos(self.player_a + math.pi / 2) * speed * time_delta
)
if not self.check_collision(new_x, new_y):
self.player_x = new_x
self.player_y = new_y
def check_collision(self, x, y):
if (x < 0 or x >= MAP_WIDTH) or (y < 0 or y >= MAP_HEIGHT):
return True
return self.world_map[int(y)][int(x)] == "#"
if __name__ == "__main__":
game = Game()
game.run()