Created
March 12, 2023 15:39
-
-
Save samclane/4048e9047bbdd6a26ef21e608291d4a4 to your computer and use it in GitHub Desktop.
Wolfenstein-esque raycasting on a tkinter canvas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
First person shooter game in python using just tkinter canavas | |
Based on https://github.com/jdah/doomenstein-3d/blob/main/src/main_wolf.c | |
""" | |
import tkinter as tk | |
import math | |
SCREEN_WIDTH = 72 | |
SCREEN_HEIGHT = 40 | |
FPS = 30 | |
class Vector2D: | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
def __add__(self, other): | |
return Vector2D(self.x + other.x, self.y + other.y) | |
def __sub__(self, other): | |
return Vector2D(self.x - other.x, self.y - other.y) | |
def __mul__(self, other): | |
if isinstance(other, Vector2D): | |
return self.x * other.x + self.y * other.y | |
else: | |
return Vector2D(self.x * other, self.y * other) | |
def dot(self, other): | |
return self.x * other.x + self.y * other.y | |
def length(self): | |
return math.sqrt(self.x * self.x + self.y * self.y) | |
def normalize(self): | |
length = self.length() | |
self.x /= length | |
self.y /= length | |
def sign(self, other): | |
return self.x * other.y - self.y * other.x | |
def min(self, other): | |
return Vector2D(min(self.x, other.x), min(self.y, other.y)) | |
def max(self, other): | |
return Vector2D(max(self.x, other.x), max(self.y, other.y)) | |
class Hit: | |
def __init__(self, val: int, side: int, pos: Vector2D): | |
self.val = val | |
self.side = side | |
self.pos = pos | |
# create a window | |
root = tk.Tk() | |
# create a canvas | |
canvas = tk.Canvas(root, width=SCREEN_WIDTH, height=SCREEN_HEIGHT) | |
canvas.pack() | |
MAP_SIZE = 8 | |
# create map data | |
map_data = [ | |
1, 1, 1, 1, 1, 1, 1, 1, | |
1, 0, 0, 0, 0, 0, 0, 1, | |
1, 0, 0, 0, 0, 3, 0, 1, | |
1, 0, 0, 0, 0, 0, 0, 1, | |
1, 0, 2, 0, 4, 4, 0, 1, | |
1, 0, 0, 0, 4, 0, 0, 1, | |
1, 0, 3, 0, 0, 0, 0, 1, | |
1, 1, 1, 1, 1, 1, 1, 1, | |
] | |
class State: | |
def __init__(self): | |
# create a pixel buffer | |
self.pixel_buffer = tk.PhotoImage(width=SCREEN_WIDTH, height=SCREEN_HEIGHT) | |
self.pos = Vector2D(2, 2) | |
self.dir = Vector2D(-1, 0.1) | |
self.plane = Vector2D(0, 0.66) | |
def draw_pixel(self, x, y, color): | |
x, y = int(x), int(y) | |
self.pixel_buffer.put(color, (x, y, x+1, y+1)) # type: ignore | |
def vertical_line(self, x: int, y1: int, y2: int, color): | |
x, y1, y2 = int(x), int(y1), int(y2) | |
for y in range(y1, y2): | |
self.draw_pixel(x, y, color) | |
def rotate(self, rot): | |
self.dir = Vector2D( | |
self.dir.x * math.cos(rot) - self.dir.y * math.sin(rot), | |
self.dir.x * math.sin(rot) + self.dir.y * math.cos(rot) | |
) | |
self.plane = Vector2D( | |
self.plane.x * math.cos(rot) - self.plane.y * math.sin(rot), | |
self.plane.x * math.sin(rot) + self.plane.y * math.cos(rot) | |
) | |
s = State() | |
def render(): | |
for x in range(SCREEN_WIDTH): | |
xcam = 2 * x / SCREEN_WIDTH - 1 | |
dir = Vector2D( | |
s.dir.x + s.plane.x * xcam, | |
s.dir.y + s.plane.y * xcam | |
) | |
pos = s.pos | |
ipos = Vector2D(int(pos.x), int(pos.y)) | |
# distance ray must travel from one x/y side to the next | |
delta_dist = Vector2D( | |
1e30 if abs(dir.x) < 1e-20 else abs(1 / dir.x), | |
1e30 if abs(dir.y) < 1e-20 else abs(1 / dir.y) | |
) | |
# distance from start to first x/y side | |
side_dist = Vector2D( | |
delta_dist.x * ((pos.x - ipos.x) if dir.x < 0 else (ipos.x + 1 - pos.x)), | |
delta_dist.y * ((pos.y - ipos.y) if dir.y < 0 else (ipos.y + 1 - pos.y)) | |
) | |
# integer direction to step in x/y calculated overall diff | |
step = Vector2D( | |
-1 if dir.x < 0 else 1, | |
-1 if dir.y < 0 else 1 | |
) | |
# dda hit | |
hit = Hit(0, 0, Vector2D(0, 0)) | |
while (hit.val == 0): | |
if side_dist.x < side_dist.y: | |
side_dist.x += delta_dist.x | |
ipos.x += step.x | |
hit.side = 0 | |
else: | |
side_dist.y += delta_dist.y | |
ipos.y += step.y | |
hit.side = 1 | |
# assert ipos.x >= 0 and ipos.x < MAP_SIZE and ipos.y >= 0 and ipos.y < MAP_SIZE, "out of bounds" | |
hit.val = map_data[ipos.x + ipos.y * MAP_SIZE] | |
color = "#000000" | |
if hit.val == 1: | |
color = "#FF0000" | |
elif hit.val == 2: | |
color = "#FF00FF" | |
elif hit.val == 3: | |
color = "#FFFF00" | |
elif hit.val == 4: | |
color = "#FF0F00" | |
else: | |
color = "#FFFFFF" | |
if hit.side == 1: | |
# darken color if hit side is y | |
r = int(color[1:3], 16) >> 1 | |
g = int(color[3:5], 16) >> 1 | |
b = int(color[5:7], 16) >> 1 | |
color = f"#{r:02x}{g:02x}{b:02x}" | |
hit.pos = Vector2D( | |
pos.x + side_dist.x, | |
pos.y + side_dist.y | |
) | |
dperp = (side_dist.x - delta_dist.x) if hit.side == 0 else (side_dist.y - delta_dist.y) | |
h = int(SCREEN_HEIGHT / dperp) | |
y0 = max((SCREEN_HEIGHT / 2) - (h / 2), 0) | |
y1 = min((SCREEN_HEIGHT / 2) + (h / 2), SCREEN_HEIGHT - 1) | |
s.vertical_line(x, 0, y0, "#ff2020") | |
s.vertical_line(x, y0, y1, color) | |
s.vertical_line(x, y1, SCREEN_HEIGHT - 1, "#ff2020") | |
# keypress handler | |
def keypress(event: tk.Event): | |
# print(event.keysym) | |
if event.keysym == "Up": | |
s.pos += s.dir * 0.1 | |
elif event.keysym == "Down": | |
s.pos -= s.dir * 0.1 | |
elif event.keysym == "Left": | |
s.rotate(0.1) | |
elif event.keysym == "Right": | |
s.rotate(-0.1) | |
# bind keypress handler | |
root.bind("<KeyPress>", keypress) | |
def main_loop(): | |
# render map | |
render() | |
canvas.create_image(0, 0, image=s.pixel_buffer, anchor=tk.NW) | |
canvas.update() | |
# handle events | |
root.update() | |
# schedule next frame | |
root.after(int(1000/FPS), main_loop) | |
main_loop() | |
# create main loop | |
root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment