Initial commit from tutorial by Coder Space
0
LICENSE → .github/LICENSE
vendored
0
README.md → .github/README.md
vendored
51
main.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import pygame as pg
|
||||||
|
import sys
|
||||||
|
from settings import *
|
||||||
|
from map import *
|
||||||
|
from player import *
|
||||||
|
from raycasting import *
|
||||||
|
from object_renderer import *
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
def __init__(self):
|
||||||
|
pg.init()
|
||||||
|
pg.mouse.set_visible(False)
|
||||||
|
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
|
||||||
|
self.clock = pg.time.Clock()
|
||||||
|
self.delta_time = 1
|
||||||
|
self.new_game()
|
||||||
|
|
||||||
|
def new_game(self):
|
||||||
|
self.map = Map(self)
|
||||||
|
self.player = Player(self)
|
||||||
|
self.object_renderer = ObjectRenderer(self)
|
||||||
|
self.raycasting = RayCasting(self)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.player.update()
|
||||||
|
self.raycasting.update()
|
||||||
|
pg.display.flip()
|
||||||
|
self.delta_time = self.clock.tick(FPS)
|
||||||
|
pg.display.set_caption(f"FPS: {self.clock.get_fps() : 0.2f}")
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
#self.screen.fill('black')
|
||||||
|
self.object_renderer.draw()
|
||||||
|
#self.map.draw()
|
||||||
|
#self.player.draw()
|
||||||
|
|
||||||
|
def check_events(self):
|
||||||
|
for event in pg.event.get():
|
||||||
|
if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
|
||||||
|
pg.quit()
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
self.check_events()
|
||||||
|
self.update()
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
game = Game()
|
||||||
|
game.run()
|
55
map.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import pygame as pg
|
||||||
|
|
||||||
|
_ = False
|
||||||
|
mini_map = [
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, 3, 3, 3, 3, _, _, _, 2, 2, 2, _, _, 1],
|
||||||
|
[1, _, _, _, _, _, 4, _, _, _, _, _, 2, _, _, 1],
|
||||||
|
[1, _, _, _, _, _, 4, _, _, _, _, _, 2, _, _, 1],
|
||||||
|
[1, _, _, 3, 3, 3, 3, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, _, 4, _, _, _, 4, _, _, _, _, _, _, 1],
|
||||||
|
[1, 1, 1, 3, 1, 3, 1, 1, 1, 3, _, _, 3, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 3, _, _, 3, 1, 1, 1],
|
||||||
|
[1, 1, 1, 1, 1, 1, 1, 1, 1, 3, _, _, 3, 1, 1, 1],
|
||||||
|
[1, 1, 3, 1, 1, 1, 1, 1, 1, 3, _, _, 3, 1, 1, 1],
|
||||||
|
[1, 4, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, 2, _, _, _, _, _, 3, 4, _, 4, 3, _, 1],
|
||||||
|
[1, _, _, 5, _, _, _, _, _, _, 3, _, 3, _, _, 1],
|
||||||
|
[1, _, _, 2, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1],
|
||||||
|
[1, 4, _, _, _, _, _, _, 4, _, _, 4, _, _, _, 1],
|
||||||
|
[1, 1, 3, 3, _, _, 3, 3, 1, 3, 3, 1, 3, 1, 1, 1],
|
||||||
|
[1, 1, 1, 3, _, _, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[1, 3, 3, 4, _, _, 4, 3, 3, 3, 3, 3, 3, 3, 3, 1],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, _, _, 5, _, _, _, 5, _, _, _, 5, _, _, _, 3],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 3],
|
||||||
|
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
|
||||||
|
]
|
||||||
|
|
||||||
|
class Map:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.mini_map = mini_map
|
||||||
|
self.world_map = {}
|
||||||
|
self.get_map()
|
||||||
|
|
||||||
|
def get_map(self):
|
||||||
|
for i, row in enumerate(self.mini_map):
|
||||||
|
for j, tile in enumerate(row):
|
||||||
|
if tile:
|
||||||
|
self.world_map[(j, i)] = tile
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
[pg.draw.rect(self.game.screen, 'darkgray', (pos[0] * 100, pos[1] * 100, 100, 100), 1)
|
||||||
|
for pos in self.world_map]
|
||||||
|
|
44
object_renderer.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import pygame as pg
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
class ObjectRenderer:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.screen = self.game.screen
|
||||||
|
self.wall_textures = self.load_wall_textures()
|
||||||
|
self.sky_texture = self.get_texture('resources/textures/sky.png', (WIDTH, HALF_HEIGHT))
|
||||||
|
self.sky_offset = 0
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
self.draw_background()
|
||||||
|
self.render_game_objects()
|
||||||
|
|
||||||
|
def draw_background(self):
|
||||||
|
# Sky
|
||||||
|
self.sky_offset = (self.sky_offset + 4.5 * self.game.player.rel) % WIDTH
|
||||||
|
self.screen.blit(self.sky_texture, (-self.sky_offset, 0))
|
||||||
|
self.screen.blit(self.sky_texture, (-self.sky_offset + WIDTH, 0))
|
||||||
|
# Ground
|
||||||
|
pg.draw.rect(self.screen, FLOOR_COLOR, (0, HALF_HEIGHT, WIDTH, HALF_HEIGHT))
|
||||||
|
|
||||||
|
def render_game_objects(self):
|
||||||
|
list_objects = self.game.raycasting.objects_to_render
|
||||||
|
for depth, image, pos in list_objects:
|
||||||
|
colour = [255 / (1 + depth ** 3.2 * 0.0002)] * 3
|
||||||
|
image.fill(colour, special_flags=pg.BLEND_MULT)
|
||||||
|
self.screen.blit(image, pos)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_texture(path, res=(TEXTURE_SIZE, TEXTURE_SIZE)):
|
||||||
|
texture = pg.image.load(path).convert_alpha()
|
||||||
|
texture = pg.transform.scale(texture, res)
|
||||||
|
return texture
|
||||||
|
|
||||||
|
def load_wall_textures(self):
|
||||||
|
return {
|
||||||
|
1: self.get_texture('resources/textures/1.png'),
|
||||||
|
2: self.get_texture('resources/textures/2.png'),
|
||||||
|
3: self.get_texture('resources/textures/3.png'),
|
||||||
|
4: self.get_texture('resources/textures/4.jpg'),
|
||||||
|
5: self.get_texture('resources/textures/1.jpg'),
|
||||||
|
}
|
79
player.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from settings import *
|
||||||
|
import pygame as pg
|
||||||
|
import math
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.x, self.y = PLAYER_POS
|
||||||
|
self.rot = PLAYER_ROT
|
||||||
|
|
||||||
|
def movement(self):
|
||||||
|
# Initialize variables
|
||||||
|
sin_a = math.sin(self.rot)
|
||||||
|
cos_a = math.cos(self.rot)
|
||||||
|
dx, dy = 0, 0
|
||||||
|
player_speed = PLAYER_SPEED * self.game.delta_time
|
||||||
|
player_speed_sin = player_speed * sin_a
|
||||||
|
player_speed_cos = player_speed * cos_a
|
||||||
|
|
||||||
|
# player movement
|
||||||
|
keys = pg.key.get_pressed()
|
||||||
|
if keys[pg.K_w]:
|
||||||
|
dx += player_speed_cos
|
||||||
|
dy += player_speed_sin
|
||||||
|
if keys[pg.K_s]:
|
||||||
|
dx -= player_speed_cos
|
||||||
|
dy -= player_speed_sin
|
||||||
|
if keys[pg.K_a]:
|
||||||
|
dx += player_speed_sin
|
||||||
|
dy -= player_speed_cos
|
||||||
|
if keys[pg.K_d]:
|
||||||
|
dx -= player_speed_sin
|
||||||
|
dy += player_speed_cos
|
||||||
|
|
||||||
|
# set player position
|
||||||
|
self.check_collision(dx, dy)
|
||||||
|
|
||||||
|
# player rotation
|
||||||
|
if keys[pg.K_LEFT]:
|
||||||
|
self.rot -= PLAYER_ROT_SPEED * self.game.delta_time
|
||||||
|
if keys[pg.K_RIGHT]:
|
||||||
|
self.rot += PLAYER_ROT_SPEED * self.game.delta_time
|
||||||
|
self.rot %= 2 * math.tau # tau = 2 * pi
|
||||||
|
|
||||||
|
def check_wall(self, x, y):
|
||||||
|
return (x, y) not in self.game.map.world_map
|
||||||
|
|
||||||
|
def check_collision(self, dx, dy):
|
||||||
|
scale = PLAYER_SIZE_SCALE / self.game.delta_time
|
||||||
|
if self.check_wall(int(self.x + dx * scale), int(self.y)):
|
||||||
|
self.x += dx
|
||||||
|
if self.check_wall(int(self.x), int(self.y + dy * scale)):
|
||||||
|
self.y += dy
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
pg.draw.line(self.game.screen, 'green', (self.x * 100, self.y * 100),
|
||||||
|
(self.x * 100 + WIDTH * math.cos(self.rot),
|
||||||
|
self.y * 100 + WIDTH * math.sin(self.rot)), 2)
|
||||||
|
pg.draw.circle(self.game.screen, 'red', (self.x * 100, self.y * 100), 15)
|
||||||
|
|
||||||
|
def mouse_control(self):
|
||||||
|
mx, my = pg.mouse.get_pos()
|
||||||
|
if mx < MOUSE_BORDER_LEFT or mx > MOUSE_BORDER_RIGHT:
|
||||||
|
pg.mouse.set_pos([HALF_WIDTH, HALF_HEIGHT])
|
||||||
|
self.rel = pg.mouse.get_rel()[0]
|
||||||
|
self.rel = max(-MOUSE_MAX_SPEED, min(MOUSE_MAX_SPEED, self.rel))
|
||||||
|
self.rot += self.rel * MOUSE_SENSITIVITY * self.game.delta_time
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.movement()
|
||||||
|
self.mouse_control()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pos(self):
|
||||||
|
return self.x, self.y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def map_pos(self):
|
||||||
|
return int(self.x), int(self.y)
|
115
raycasting.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import pygame as pg
|
||||||
|
import math
|
||||||
|
from settings import *
|
||||||
|
|
||||||
|
class RayCasting():
|
||||||
|
def __init__(self, game):
|
||||||
|
self.game = game
|
||||||
|
self.ray_casting_result = []
|
||||||
|
self.objects_to_render = []
|
||||||
|
self.textures = self.game.object_renderer.wall_textures
|
||||||
|
|
||||||
|
def get_objects_to_render(self):
|
||||||
|
self.objects_to_render = []
|
||||||
|
for ray, values in enumerate(self.ray_casting_result):
|
||||||
|
depth, proj_height, texture, offset = values
|
||||||
|
|
||||||
|
if proj_height < HEIGHT:
|
||||||
|
wall_column = self.textures[texture].subsurface(
|
||||||
|
offset * (TEXTURE_SIZE - SCALE), 0, SCALE, TEXTURE_SIZE
|
||||||
|
)
|
||||||
|
wall_column = pg.transform.scale(wall_column, (SCALE, proj_height))
|
||||||
|
wall_pos = (ray * SCALE, HALF_HEIGHT - proj_height // 2)
|
||||||
|
else:
|
||||||
|
texture_height = TEXTURE_SIZE * HEIGHT / proj_height
|
||||||
|
wall_column = self.textures[texture].subsurface(
|
||||||
|
offset * (TEXTURE_SIZE - SCALE), HALF_TEXTURE_SIZE - texture_height // 2,
|
||||||
|
SCALE, texture_height
|
||||||
|
)
|
||||||
|
wall_column = pg.transform.scale(wall_column, (SCALE, HEIGHT))
|
||||||
|
wall_pos = (ray * SCALE, 0)
|
||||||
|
|
||||||
|
self.objects_to_render.append((depth, wall_column, wall_pos))
|
||||||
|
|
||||||
|
def ray_cast(self):
|
||||||
|
self.ray_casting_result = []
|
||||||
|
ox, oy = self.game.player.pos
|
||||||
|
x_map, y_map = self.game.player.map_pos
|
||||||
|
|
||||||
|
texture_vert, texture_horz = 1, 1
|
||||||
|
|
||||||
|
ray_angle = self.game.player.rot - HALF_FOV + 0.0001 # 0.0001 to avoid division by zero
|
||||||
|
for ray in range(NUM_RAYS):
|
||||||
|
sin_a = math.sin(ray_angle)
|
||||||
|
cos_a = math.cos(ray_angle)
|
||||||
|
|
||||||
|
# horizontal
|
||||||
|
y_horz, dy = (y_map + 1, 1) if sin_a > 0 else (y_map - 1e-6, -1)
|
||||||
|
|
||||||
|
depth_horz = (y_horz - oy) / sin_a
|
||||||
|
x_horz = ox + depth_horz * cos_a
|
||||||
|
|
||||||
|
delta_depth = dy / sin_a
|
||||||
|
dx = delta_depth * cos_a
|
||||||
|
|
||||||
|
for i in range(MAX_DEPTH):
|
||||||
|
tile_horz = int(x_horz), int(y_horz)
|
||||||
|
if tile_horz in self.game.map.world_map:
|
||||||
|
texture_horz = self.game.map.world_map[tile_horz]
|
||||||
|
break
|
||||||
|
x_horz += dx
|
||||||
|
y_horz += dy
|
||||||
|
depth_horz += delta_depth
|
||||||
|
|
||||||
|
# verticals
|
||||||
|
x_vert, dx = (x_map + 1, 1) if cos_a > 0 else (x_map - 1e-6, -1)
|
||||||
|
|
||||||
|
depth_vert = (x_vert - ox) / cos_a
|
||||||
|
y_vert = oy + depth_vert * sin_a
|
||||||
|
|
||||||
|
delta_depth = dx / cos_a
|
||||||
|
dy = delta_depth * sin_a
|
||||||
|
|
||||||
|
for i in range(MAX_DEPTH):
|
||||||
|
tile_vert = int(x_vert), int(y_vert)
|
||||||
|
if tile_vert in self.game.map.world_map:
|
||||||
|
texture_vert = self.game.map.world_map[tile_vert]
|
||||||
|
break
|
||||||
|
x_vert += dx
|
||||||
|
y_vert += dy
|
||||||
|
depth_vert += delta_depth
|
||||||
|
|
||||||
|
# Select the shortest distance to the wall, and the corresponding texture
|
||||||
|
if depth_vert < depth_horz:
|
||||||
|
depth, texture = depth_vert, texture_vert
|
||||||
|
y_vert %= 1
|
||||||
|
offset = y_vert if cos_a > 0 else (1 - y_vert)
|
||||||
|
else:
|
||||||
|
depth, texture = depth_horz, texture_horz
|
||||||
|
x_horz %= 1
|
||||||
|
offset = (1 - x_horz) if sin_a > 0 else x_horz
|
||||||
|
|
||||||
|
# Draw the ray for debugging
|
||||||
|
#pg.draw.line(self.game.screen, 'blue', (ox * 100, oy * 100),
|
||||||
|
# (100 * ox + 100 * depth * cos_a, 100 * oy + 100 * depth * sin_a), 1)
|
||||||
|
|
||||||
|
# Remove fish-eye effect
|
||||||
|
depth *= math.cos(self.game.player.rot - ray_angle)
|
||||||
|
|
||||||
|
# Projection
|
||||||
|
proj_height = SCREEN_DIST / (depth + 0.0001) # 0.0001 to avoid division by zero
|
||||||
|
|
||||||
|
# Draw walls
|
||||||
|
#colour = [255 / (1 + depth ** 5 * 0.0002)] * 3
|
||||||
|
#pg.draw.rect(self.game.screen, colour,
|
||||||
|
# (ray * SCALE, HALF_HEIGHT - proj_height // 2, SCALE, proj_height))
|
||||||
|
|
||||||
|
# Ray casting result
|
||||||
|
self.ray_casting_result.append((depth, proj_height, texture, offset))
|
||||||
|
|
||||||
|
ray_angle += DELTA_ANGLE
|
||||||
|
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.ray_cast()
|
||||||
|
self.get_objects_to_render()
|
BIN
resources/textures/1.jpg
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
resources/textures/1.png
Normal file
After Width: | Height: | Size: 889 KiB |
BIN
resources/textures/2.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
resources/textures/3.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
resources/textures/4.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
resources/textures/4.png
Normal file
After Width: | Height: | Size: 247 KiB |
BIN
resources/textures/5.png
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
resources/textures/sky.png
Normal file
After Width: | Height: | Size: 588 KiB |
33
settings.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
# GAME SETTINGS
|
||||||
|
RES = WIDTH, HEIGHT = 1600, 900
|
||||||
|
HALF_WIDTH = WIDTH // 2
|
||||||
|
HALF_HEIGHT = HEIGHT // 2
|
||||||
|
FPS = 0 # 0 = unlimited
|
||||||
|
|
||||||
|
PLAYER_POS = 1.5, 5 # player position on the map
|
||||||
|
PLAYER_ROT = 0
|
||||||
|
PLAYER_SPEED = 0.004
|
||||||
|
PLAYER_ROT_SPEED = 0.004
|
||||||
|
PLAYER_SIZE_SCALE = 60
|
||||||
|
|
||||||
|
MOUSE_SENSITIVITY = 0.0003
|
||||||
|
MOUSE_MAX_SPEED = 40
|
||||||
|
MOUSE_BORDER_LEFT = 100
|
||||||
|
MOUSE_BORDER_RIGHT = WIDTH - MOUSE_BORDER_LEFT
|
||||||
|
|
||||||
|
FLOOR_COLOR = (69, 69, 69)
|
||||||
|
|
||||||
|
FOV = math.pi / 3
|
||||||
|
HALF_FOV = FOV / 2
|
||||||
|
NUM_RAYS = WIDTH // 2
|
||||||
|
HALF_NUM_RAYS = NUM_RAYS // 2
|
||||||
|
DELTA_ANGLE = FOV / NUM_RAYS
|
||||||
|
MAX_DEPTH = 20
|
||||||
|
|
||||||
|
SCREEN_DIST = HALF_WIDTH / math.tan(HALF_FOV)
|
||||||
|
SCALE = WIDTH // NUM_RAYS
|
||||||
|
|
||||||
|
TEXTURE_SIZE = 256
|
||||||
|
HALF_TEXTURE_SIZE = TEXTURE_SIZE // 2
|