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()