From 3260df72cd357ec836f5a190c5793c30add6df96 Mon Sep 17 00:00:00 2001 From: Sem van der Hoeven Date: Thu, 2 Jul 2020 12:01:07 +0200 Subject: [PATCH] added raycaster project --- raycaster.py | 390 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 raycaster.py diff --git a/raycaster.py b/raycaster.py new file mode 100644 index 0000000..dc4c4b9 --- /dev/null +++ b/raycaster.py @@ -0,0 +1,390 @@ +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 30 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()