Initial commit from tutorial by Coder Space

This commit is contained in:
Michał Gdula 2023-01-27 23:39:49 +00:00
parent 1a55189db7
commit ccb5394b00
16 changed files with 377 additions and 0 deletions

View file

View file

51
main.py Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
resources/textures/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

BIN
resources/textures/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
resources/textures/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
resources/textures/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
resources/textures/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
resources/textures/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
resources/textures/sky.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

33
settings.py Normal file
View 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