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
|