Files
python-raycaster/raycaster.py
Sem van der Hoeven 37904b560b typo
2020-07-02 12:13:12 +02:00

391 lines
15 KiB
Python

import array
import pygame
import math
from tkinter import *
# tutorial from https://lodev.org/cgtutor/raycasting.html
# constants
mapWidth = 24
mapHeight = 24
screenWidth = 900 #640
screenHeight = 600 #480
# world map
worldMap = [
[1, 1, 1, 1, 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, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 2, 0, 2, 0, 2, 0, 0, 0, 0, 3, 0, 3, 0, 3, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 3, 0, 3, 0, 3, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 0, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 0, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 0, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
# initialize pygame
pygame.init()
# initialize font
pygame.font.init()
# create the screen
screen = pygame.display.set_mode((screenWidth, screenHeight))
pygame.display.set_caption("Python Raycaster")
# create game clock
clock = pygame.time.Clock()
def switch(number: int):
"""acts like a switch statement. switches the ints of the map with a color value to display.
Args:
number: the number to switch on
"""
switcher = {
1: (255, 0, 0), # red
2: (0, 255, 0), # green
3: (0, 0, 255), # blue
4: (255, 255, 255) # white
}
return switcher.get(number, pygame.Color("#ffff00")) # default is yellow
def stop():
pygame.quit()
quit()
def display_text(text: str, position: tuple, color: tuple):
"""displays text to the screen at the given position.
Args:
text: the text to display.
position: the position to display the text.
color: the color of the text.
"""
fpsFont = pygame.font.SysFont('Consolas',15)
screen.blit(fpsFont.render(text, False, color), position)
def display_text_centered(text: str, position: tuple, color: tuple, size: int, font: str):
"""displays text to the screen with the given parameters
Args:
text: the text to display
position: the position to display the text.
color: the color of the text.
size: the font size
font: the system font to use
"""
font_obj = pygame.font.SysFont(font, size)
rendered = font_obj.render(text, False, color)
rect = rendered.get_rect().width
screen.blit(rendered, (position[0] - rect // 2,position[1]))
return rendered
def get_text_object(text: str, color: tuple, fontSize: int, font: str):
font_obj = pygame.font.SysFont(font, fontSize)
return font_obj.render(text, False, color)
def game_intro():
intro = True
while intro:
screen.fill((0, 0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
print("exiting...")
intro = False
stop()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
intro = False
display_text_centered(text="Python raycaster", position=(screenWidth // 2, 100), color=(200, 200, 200), size=30, font='Consolas')
display_text_centered(text="Press 'SPACE' to start", position=(screenWidth // 2, 200), color=(200, 40, 40), size=20, font='Consolas')
display_text_centered(text="WASD to move, left and right arrow keys to look", position=(screenWidth//2, 250), color=(50, 100, 100), size=16, font='Consolas')
copy_right_text = get_text_object(text="made by Sem van der Hoeven", color=(100, 100, 100), fontSize=12, font='Consolas')
rect = copy_right_text.get_rect()
screen.blit(copy_right_text, (0, screenHeight - rect.height))
pygame.display.update()
clock.tick(30)
def game_loop(clock, screen):
"""main game loop that runs the game.
Args:
clock: The pygame clock object.
screen: the pygame screen object.
"""
posX = 22.0 # x start position
posY = 12.0 # y start position
dirX = -1.0 # initial x of direction vector
dirY = 0.0 # initial y of direction vector
planeX = 0.0 # 2d raycaster version of camera plane x
planeY = 0.66 # 2d raycaster version of camera plane y
running = True
rotateLeft = False
rotateRight = False
moveForward = False
moveBackward = False
moveLeft = False
moveRight = False
debugMode = False
# main game loop
print("starting game...")
while running:
# update the next frame at 60 fps
ms = clock.tick(60) / 1000.0
moveSpeed = ms * 3.0
rotSpeed = ms * 3.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
print("exiting...")
del screen
del clock
running = False
if event.type == pygame.KEYDOWN:
if event.key == ord('a'):
moveLeft = True
if event.key == ord('d'):
moveRight = True
if event.key == ord('w'):
moveForward = True
if event.key == ord('s'):
moveBackward = True
if event.key == pygame.K_LEFT:
rotateLeft = True
if event.key == pygame.K_RIGHT:
rotateRight = True
if event.key == pygame.K_TAB:
debugMode = not debugMode
print("setting debug mode to" ,debugMode)
if event.type == pygame.KEYUP:
if event.key == ord('a'):
moveLeft = False
if event.key == ord('d'):
moveRight = False
if event.key == ord('w'):
moveForward = False
if event.key == ord('s'):
moveBackward = False
if event.key == pygame.K_LEFT:
rotateLeft = False
if event.key == pygame.K_RIGHT:
rotateRight = False
if running == False:
continue # if the user has pressed the quit key, stop the loop
# loop that goes over every x (vertical line)
screen.fill((0, 0, 0))
for x in range(screenWidth):
# calculate camera x, the x coordinate on the camera plane that the current
# x-coordinate represents.
# This way, the right side of the screen will get coordinate 1, center will get coordinate 0,
# and left will get coordinate -1
cameraX = 2 * x / float(screenWidth) - 1
rayDirX = dirX + planeX * cameraX
rayDirY = dirY + planeY * cameraX
# which box of the map we're in
mapX = int(posX)
mapY = int(posY)
# length of the ray from current position to next x or y side
sideDistX = 0.0 # (double)
sideDistY = 0.0 # (double)
# length of ray from one x or y-side to next x or y-side
# rayDirX and rayDirY can be 0, so then division by 0 would occur if
# we did only abs(1/rayDirX), so we need to check if it is 0
deltaDistX = 0 if rayDirY == 0 else (
1 if rayDirX == 0 else abs(1/rayDirX))
deltaDistY = 0 if rayDirX == 0 else (
1 if rayDirY == 0 else abs(1/rayDirY))
perpWallDist = 0.0 # used to calculate the length of the ray (double)
# what direction to step in x or y-direction (either +1 or -1 for positive or negative direction)
# if the ray direction has a negative x-component, stepX will be -1. If it has a positive x-component,
# it will be +1. If the x-component is 0 the value of stepX won't matter because it won't be used.
# same for stepY
stepX = 0 # (int)
stepY = 0 # (int)
hit = 0 # was there a wall hit?
side = 0 # (int) was a vertical or horizontal wall hit?
# calculate step and initial sideDist
# is the x-component of the ray is negative, sideDistX is the distance to the first vertical line to the left.
if rayDirX < 0:
# if the x-component of the ray is positive, sideDistX is the distance to the first vertical line to the richt.
# same for the y-component, but then to the first horizontal line to the top or bottom
stepX = -1
sideDistX = (posX - mapX) * deltaDistX
else:
stepX = 1
sideDistX = (mapX + 1.0 - posX) * deltaDistX
if rayDirY < 0:
stepY = -1
sideDistY = (posY - mapY) * deltaDistY
else:
stepY = 1
sideDistY = (mapY + 1.0 - posY) * deltaDistY
# actual DDA algorithm
while hit == 0: # while the ray has not hit a wall
# jump to the next map square, OR in x direction OR in y direction
if (sideDistX < sideDistY): # the ray is going morea horizontal than vertical
sideDistX += deltaDistX
mapX += stepX
side = 0
else:
sideDistY += deltaDistY
mapY += stepY
side = 1
# check if the ray has hit a wall
# if it is not 0, so the square does not contain a walkable square, so a wall
if worldMap[mapX][mapY] > 0:
hit = 1
# calculate the distance projected on camera direction (Euclidean distance will give fisheye effect!)
if side == 0: # vertical wall hit
perpWallDist = (mapX - posX + (1 - stepX) / 2) / rayDirX
else: # horizontal wall hit
perpWallDist = (mapY - posY + (1 - stepY) / 2) / rayDirY
# calculate the height of the line to draw on the screen
if (perpWallDist != 0):
lineHeight = int(screenHeight / perpWallDist)
# calculate the lowest and highest pixel to fill in current vertical stripe
drawStart = -lineHeight / 2 + screenHeight / 2
if drawStart < 0:
drawStart = 0
drawEnd = lineHeight / 2 + screenHeight / 2
if drawEnd >= screenHeight:
drawEnd = screenHeight - 1
color = switch(worldMap[mapX][mapY])
if side == 1:
color = (color[0] / 2, color[1] / 2, color[2] / 2)
pygame.draw.line(screen, color, (x, int(drawStart)),
(x, int(drawEnd)), 1)
if moveForward:
movePosX = int(posX + dirX * moveSpeed) # x position to move to next
movePosY = int(posY + dirY * moveSpeed) # y position to move to next
if worldMap[movePosX][movePosY] == 0:
posX += dirX * moveSpeed
posY += dirY * moveSpeed
if moveBackward:
movePosX = int(posX - dirX * moveSpeed)
movePosY = int(posY - dirY * moveSpeed)
if worldMap[movePosX][movePosY] == 0:
posX -= dirX * moveSpeed
posY -= dirY * moveSpeed
if moveLeft:
oldDirX = dirX
oldDirY = dirY
dirX = dirX * math.cos(math.pi/2) - dirY * math.sin(math.pi/2)
dirY = oldDirX * math.sin(math.pi/2) + dirY * math.cos(math.pi/2)
movePosX = int(posX + dirX * moveSpeed) # x position to move to next
movePosY = int(posY + dirY * moveSpeed) # y position to move to next
if worldMap[movePosX][movePosY] == 0:
posX += dirX * moveSpeed
posY += dirY * moveSpeed
dirX = oldDirX
dirY = oldDirY
if moveRight:
oldDirX = dirX
oldDirY = dirY
dirX = dirX * math.cos(-math.pi/2) - dirY * math.sin(-math.pi/2) # rotate 90 degrees to the right
dirY = oldDirX * math.sin(-math.pi/2) + dirY * math.cos(-math.pi/2) # so change the direction to 90 degrees
# apply the move, so move to the right
movePosX = int(posX + dirX * moveSpeed) # x position to move to next
movePosY = int(posY + dirY * moveSpeed) # y position to move to next
if worldMap[movePosX][movePosY] == 0:
posX += dirX * moveSpeed
posY += dirY * moveSpeed
# reset the direction vector because we still want to look forward
dirX = oldDirX
dirY = oldDirY
if rotateRight:
oldDirX = dirX
dirX = dirX * math.cos(-rotSpeed) - dirY * math.sin(-rotSpeed)
dirY = oldDirX * math.sin(-rotSpeed) + dirY * math.cos(-rotSpeed)
oldPlaneX = planeX
planeX = planeX * math.cos(-rotSpeed) - planeY * math.sin(-rotSpeed)
planeY = oldPlaneX * math.sin(-rotSpeed) + planeY * math.cos(-rotSpeed)
if rotateLeft:
oldDirX = dirX
dirX = dirX * math.cos(rotSpeed) - dirY * math.sin(rotSpeed)
dirY = oldDirX * math.sin(rotSpeed) + dirY * math.cos(rotSpeed)
oldPlaneX = planeX
planeX = planeX * math.cos(rotSpeed) - planeY * math.sin(rotSpeed)
planeY = oldPlaneX * math.sin(rotSpeed) + planeY * math.cos(rotSpeed)
textToRender = "FPS: " + str(int(1 // ms))
if debugMode: textToRender = textToRender + " posX: " + str(int(
posX)) + " posY: " + str(int(
posY)) + " dirX: " + str(dirX) + " dirY: " + str(dirY)
display_text(textToRender, (0, 0), (0, 255, 255))
copy_right_text = get_text_object(text="press TAB for debug info", color=(100, 100, 100), fontSize=12, font='Consolas')
rect = copy_right_text.get_rect()
screen.blit(copy_right_text, (0, screenHeight - rect.height))
# uodate the display
pygame.display.update()
game_intro()
game_loop(clock,screen)
stop()