-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
282 lines (238 loc) · 11.8 KB
/
main.py
File metadata and controls
282 lines (238 loc) · 11.8 KB
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
import sys, pygame
import pygame.surfarray as surfarray
from math import cos
from math import sin
from math import floor
import numpy as np
from pygame.locals import *
# Initialize pygame
pygame.init()
# Create clock
clock = pygame.time.Clock()
# Define colors
background_color = (129, 138, 145)
# Width and height of the map
width = 400
height = 300
size = (width, height)
# Width and height of textures
# Here we use 32x32 textures, but other sizes can be used
texture_width = 32
texture_height = 32
# Replace zero by a sufficiently small value when division by zero occurs
dbz = 0.0000001
# Define rotational and movement speed
rot_speed = 0.01
mov_speed = 0.02
# Define the world map
# As you can see, the world map is just a two-dimensional Python tuple
# where zeroes represent empty space, and non-zero integers represent
# different textures
world_map = (
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 0, 1),
(1, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 4, 4, 0, 0, 4, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 5, 5, 0, 0, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 5, 5, 0, 0, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1),
(1, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1),
(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1),
(1, 4, 4, 0, 0, 4, 0, 0, 5, 5, 5, 5, 0, 1, 0, 0, 1, 0, 1, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1),
(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1),
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
)
# Create a screen
screen = pygame.display.set_mode(size, DOUBLEBUF, 24)
# Texture loading
# This is done using surfarray.array3d, which converts the images into two-dimensional arrays of RGB colors
# which then can be easily accessed just as a plain old numpy array
bricks = pygame.surfarray.array3d(pygame.image.load('./textures/bricks.png').convert())
dirt = pygame.surfarray.array3d(pygame.image.load('./textures/dirt.png'))
grass = pygame.surfarray.array3d(pygame.image.load('./textures/grass_block_side.png').convert())
cobblestone = pygame.surfarray.array3d(pygame.image.load('./textures/cobblestone.png').convert())
netherbrick = pygame.surfarray.array3d(pygame.image.load('./textures/chiseled_nether_bricks.png').convert())
# This function takes a surfarray and obscures it
# To do so, it removes the last bit in order to divide the RGB value by two, then sets the first bit of every byte to zero
# in order to prevent screwing up the colors
def darken_texture(texture):
return (texture>> 1) & 8355711
# Create tuple of textures
textures = np.asarray(
(
bricks,
dirt,
grass,
cobblestone,
netherbrick
), dtype=object
)
# Tuple of precalculated darkened textures
# This is precalculated here because it is it is very costly to do it during the game loop
dark_textures = np.asarray(
(
darken_texture(bricks),
darken_texture(dirt),
darken_texture(grass),
darken_texture(cobblestone),
darken_texture(netherbrick)
), dtype=object
)
# This raycasting implementation relies on vector calculations
# The position of the player, its direction, and the camera plane are all vectors
pos_x, pos_y = 8.0, 5.0 # Position vector
dir_x, dir_y = -1, 1 # Direction vector
plane_x, plane_y = 0, 0.66 # Camera plane vector, it must be perpendicular to the direction vector
# If a key is kept pressed down, the event will keep on being sent repeteadly
pygame.key.set_repeat(1,10)
# GAME LOOP
while True:
# Create a surface canvas
canvas = pygame.Surface( ( width, height ) )
canvas.fill( background_color ) # The background is created
canvas_array = pygame.surfarray.array3d(canvas) # Convert the surface canvas into an array for direct pixel manipulation
# Keystroke event-handling logic
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
# Quit the game by pressing q
if event.key == pygame.K_q:
sys.exit()
# In order for the player to look to the left or right, a standard matrix rotation formula is used
if event.key == pygame.K_LEFT:
old_dir_x = dir_x
dir_x = dir_x * cos(rot_speed) - dir_y * sin(rot_speed)
dir_y = old_dir_x * sin(rot_speed) + dir_y * cos(rot_speed)
old_plane_x = plane_x
plane_x = plane_x * cos(rot_speed) - plane_y * sin(rot_speed)
plane_y = old_plane_x * sin(rot_speed) + plane_y * cos(rot_speed)
if event.key == pygame.K_RIGHT:
old_dir_x = dir_x
dir_x = dir_x * cos(-rot_speed) - dir_y * sin(-rot_speed)
dir_y = old_dir_x * sin(-rot_speed) + dir_y * cos(-rot_speed)
old_plane_x = plane_x
plane_x = plane_x * cos(-rot_speed) - plane_y * sin(-rot_speed)
plane_y = old_plane_x * sin(-rot_speed) + plane_y * cos(-rot_speed)
# In order to move forwards or backwards, a vector sum is applied in order to change the x and y coordinates
# The position vector is added to the direction vector multiplied by the movement speed
if event.key == pygame.K_UP:
if not world_map[int(pos_x + dir_x * mov_speed)][int(pos_y)]:
pos_x += dir_x * mov_speed
if not world_map[int(pos_x)][int(pos_y + dir_y * mov_speed)]:
pos_y += dir_y * mov_speed
if event.key == pygame.K_DOWN:
if not world_map[int(pos_x - dir_x * mov_speed)][int(pos_y)]:
pos_x -= dir_x * mov_speed
if not world_map[int(pos_x)][int(pos_y - dir_y * mov_speed)]:
pos_y -= dir_y * mov_speed
# RAYCASTING LOGIC
# We iterate through every pixel on the screen, from left to right
for i in range(0, width):
# From the camera plane x coordinates, we find the direction vector of the ray
camera_x = 2*i / float(width) - 1
ray_direction_x = dir_x + plane_x * camera_x
ray_direction_y = dir_y + plane_y * camera_x
# Calculate which box of the map we are in
map_x = int(pos_x)
map_y = int(pos_y)
# Each box of the map has a x-side and a y-side
# It is best to move the ray from one side to another, so that we don't miss any box
# Here we calculate the length of ray from its current position to the next x or y-side
side_dist_x = 0
side_dist_y = 0
# And here we calculate the length of ray from one x or y-side to next x or y-side
delta_dist_x = dbz if ray_direction_x == 0 else abs(1/ray_direction_x)
delta_dist_y = dbz if ray_direction_y == 0 else abs(1/ray_direction_y)
# This represents the distance from the player to the wall
perpwall_dist = 0
# These parameters indicate in which x or y-direction to move each step of the ray (either +1 or -1)
step_x = 0
step_y = 0
hit = 0 # Check if a wall was hit
side = 0 # Check if it was an x-side or a y-side
# Calculate step and initial sideDist
if(ray_direction_x < 0):
step_x = -1
side_dist_x = (pos_x - map_x) * delta_dist_x
else:
step_x = 1
side_dist_x = (map_x + 1.0 - pos_x) * delta_dist_x
if(ray_direction_y < 0):
step_y = -1
side_dist_y = (pos_y - map_y) * delta_dist_y
else:
step_y = 1
side_dist_y = (map_y + 1.0 - pos_y) * delta_dist_y
# Perform the ray-steps, until a wall is hit
while (hit == 0):
# Basically, the x and y-distances from the current side to the other are updated
# then the box of the map we are in is updated accordingly by one x or y-step
# and finally we check if a wall was hit
if (side_dist_x < side_dist_y):
side_dist_x += delta_dist_x
map_x += step_x
side = 0
else:
side_dist_y += delta_dist_y
map_y += step_y
side = 1 # If we are closer to a y-side, then update to 1
# Wall check
if(world_map[map_x][map_y]>0):
hit = 1
# Calculate the distance between the wall and the player
if(side == 0):
perpwall_dist = (side_dist_x - delta_dist_x)
else:
perpwall_dist = (side_dist_y - delta_dist_y)
# From that, calculate height of line to be drawn on screen
line_height = height / (perpwall_dist + dbz)
# This calculates the first and the last pixel of the current line
draw_start = -line_height / 2 + height / 2
if(draw_start < 0):
draw_start = 0
draw_end = line_height / 2 + height / 2
if(draw_end >= height):
draw_end = height - 1
# TEXTURING LOGIC
# Affine texture mapping is applied by first finding which coordinate of the wall was hit
# and then the corresponding texture coordinate
# Here we find wall_x, which is the x wall-coordinte hit by the ray
wall_x = 0.0
if(side == 0):
wall_x = pos_y + perpwall_dist * ray_direction_y
else:
wall_x = pos_x + perpwall_dist * ray_direction_x
wall_x -= floor(wall_x)
# From wall_x, it is easy to infer the corresponding x-texture coordinate
tex_x = int(wall_x * texture_width)
if((side == 0 and ray_direction_y > 0) or (side == 1 and ray_direction_y < 0)):
tex_x = texture_width - tex_x - 1
# How much to increase the texture coordinate per screen pixel
step = 1.0 * texture_height / line_height
# Starting texture coordinate
tex_pos = (draw_start - height / 2 + line_height / 2) * step
# Current texture number
tex_num = world_map[map_x][map_y] - 1
# RENDERING LOGIC
for y in range(int(draw_start), int(draw_end)): # For each y wall coordinate, calculate the corresponding y texture coordinate
tex_y = int(tex_pos) & (texture_height - 1)
# Retrieve the RGB color to draw from the textures array
# First, from the map box number, get the index of the texture to draw
# Second, from the x and y texture coordinates, retrieve the exact RGB color to draw
if(side == 1):
color = dark_textures[tex_num][tex_x][tex_y] # If it is a side wall, decreases the color brightness
else:
color = textures[tex_num][tex_x][tex_y]
tex_pos += step # Increase the step by one pixel
canvas_array[i][y] = color # Assign the pixel with the right color into the canvas array
canvas = pygame.pixelcopy.make_surface(canvas_array) # Convert canvas array to surface
screen.blit( canvas, ( 0, 0 ) ) # Blit the canvas into the screen
pygame.display.update() # Refresh the display
clock.tick(15)