Skip to content

Instantly share code, notes, and snippets.

@samclane
Created March 12, 2023 15:39
Show Gist options
  • Save samclane/4048e9047bbdd6a26ef21e608291d4a4 to your computer and use it in GitHub Desktop.
Save samclane/4048e9047bbdd6a26ef21e608291d4a4 to your computer and use it in GitHub Desktop.
Wolfenstein-esque raycasting on a tkinter canvas
"""
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