Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save EncodeTheCode/37a71e91d0bf9b25ae32dec96ac201ce to your computer and use it in GitHub Desktop.
Save EncodeTheCode/37a71e91d0bf9b25ae32dec96ac201ce to your computer and use it in GitHub Desktop.
import pygame
import numpy as np
import sys
import math
# Initialize Pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("3D Line of Sight with Mouse Look")
clock = pygame.time.Clock()
pygame.event.set_grab(True)
pygame.mouse.set_visible(False)
# Constants
WIDTH, HEIGHT = screen.get_size()
FOV = math.radians(45)
MOVE_SPEED = 3.0
MOUSE_SENSITIVITY = 0.001
max_danger = 100.0
danger_rate = 20.0
danger_cooldown = 80.0
BAR_ALPHA = 150
font = pygame.font.SysFont(None, 24)
# Colors
WHITE = (255, 255, 255)
GRAY = (180, 180, 180)
DARK_GRAY = (80, 80, 80)
ORANGE = (255, 165, 0)
BLACK = (0, 0, 0)
flashing_color = [(133,40,32), (136,41,31), (138,38,33), (130,39,31)]
def get_flashing_color():
return flashing_color[(pygame.time.get_ticks() // 500) % len(flashing_color)]
# State
camera_pos = np.array([0, 1, -5], dtype=np.float32)
yaw = 0.0
pitch = 0.0
danger = 0.0
blink = False
blink_timer = 1.5
enemy_pos = np.array([0, 1, 8], dtype=np.float32)
wall_pos = np.array([0, 1.5, 4], dtype=np.float32)
wall_size = np.array([4, 3, 0.5], dtype=np.float32)
def normalize(v):
norm = np.linalg.norm(v)
return v / norm if norm > 0 else v
# Projection with optional pitch override
def project_point(point, cam_pos, yaw, pitch, ignore_pitch=False):
rel = point - cam_pos
cos_y, sin_y = np.cos(yaw), np.sin(yaw)
cos_p, sin_p = np.cos(0.0 if ignore_pitch else pitch), np.sin(0.0 if ignore_pitch else pitch)
x = rel[0] * cos_y - rel[2] * sin_y
z = rel[0] * sin_y + rel[2] * cos_y
y = rel[1]
y2 = y * cos_p - z * sin_p
z2 = y * sin_p + z * cos_p
z2 = max(z2, 0.01)
f = HEIGHT / (2 * math.tan(FOV / 2))
x_proj = (x * f) / z2 + WIDTH / 2
y_proj = (-y2 * f) / z2 + HEIGHT / 2
return int(x_proj), int(y_proj), z2
# Draw any box; if force_draw, ignore pitch to keep visible when near
def draw_box(center, size, color, cam_pos, yaw, pitch, force_draw=False):
w, h, d = size
offsets = np.array([
[-w/2, -h/2, -d/2], [ w/2, -h/2, -d/2], [ w/2, h/2, -d/2], [-w/2, h/2, -d/2],
[-w/2, -h/2, d/2], [ w/2, -h/2, d/2], [ w/2, h/2, d/2], [-w/2, h/2, d/2],
], dtype=np.float32)
corners = offsets + center
projected = []
for c in corners:
try:
projected.append(project_point(c, cam_pos, yaw, pitch, ignore_pitch=force_draw))
except:
projected.append((0, 0, -1))
faces = [[0,1,2,3], [4,5,6,7], [0,1,5,4], [2,3,7,6], [1,2,6,5], [0,3,7,4]]
for face in faces:
pts = [projected[i] for i in face]
# Skip faces fully behind camera
if not force_draw and any(z <= 0.1 for _,_,z in pts):
continue
pts2d = [(p[0], p[1]) for p in pts]
pygame.draw.polygon(screen, color, pts2d)
# Draw floor grid within 25-tile radius
def draw_floor(y, spacing, cam_pos, yaw, pitch):
cam_x, _, cam_z = cam_pos
radius = 25
for x in range(int(cam_x-radius), int(cam_x+radius)+1):
for z in range(int(cam_z-radius), int(cam_z+radius)+1):
p1 = np.array([x, y, z], dtype=np.float32)
p2 = np.array([x+spacing, y, z], dtype=np.float32)
p3 = np.array([x, y, z+spacing], dtype=np.float32)
try:
p1p = project_point(p1, cam_pos, yaw, pitch)
p2p = project_point(p2, cam_pos, yaw, pitch)
p3p = project_point(p3, cam_pos, yaw, pitch)
pygame.draw.line(screen, GRAY, p1p[:2], p2p[:2])
pygame.draw.line(screen, GRAY, p1p[:2], p3p[:2])
except:
continue
# Ray-AABB intersection
def line_intersects_box(start, end, box_pos, box_size):
dir = end - start
t_min, t_max = -np.inf, np.inf
for i in range(3):
bmin = box_pos[i]-box_size[i]/2
bmax = box_pos[i]+box_size[i]/2
if abs(dir[i])<1e-6:
if start[i]<bmin or start[i]>bmax: return False
else:
t1=(bmin-start[i])/dir[i]; t2=(bmax-start[i])/dir[i]
t1,t2=min(t1,t2),max(t1,t2)
t_min,max_t = max(t_min,t1), min(t_max,t2)
if max_t<t_min: return False
return True
# Main loop
running=True
while running:
dt=clock.tick(60)/1000.0
SEL_COLOR=get_flashing_color()
for ev in pygame.event.get():
if ev.type==pygame.QUIT: running=False
elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_ESCAPE: running = False
mx,my=pygame.mouse.get_rel()
yaw+=mx*MOUSE_SENSITIVITY; pitch+=my*MOUSE_SENSITIVITY
pitch=np.clip(pitch,-np.pi/2+0.01,np.pi/2-0.01)
# Movement
keys=pygame.key.get_pressed(); move=np.zeros(3,dtype=np.float32)
fwd=np.array([np.sin(yaw),0,np.cos(yaw)],dtype=np.float32)
rt =np.array([np.cos(yaw),0,-np.sin(yaw)],dtype=np.float32)
if keys[pygame.K_w]: move+=fwd
if keys[pygame.K_s]: move-=fwd
if keys[pygame.K_a]: move-=rt
if keys[pygame.K_d]: move+=rt
if keys[pygame.K_q]: move[1]-=1
if keys[pygame.K_e]: move[1]+=1
if np.linalg.norm(move)>0: camera_pos+=normalize(move)*MOVE_SPEED*dt
# Danger logic
dist=np.linalg.norm(camera_pos-enemy_pos)
efwd=np.array([0,0,-1],dtype=np.float32)
topl=normalize(camera_pos-enemy_pos)
in_fov=np.dot(efwd,topl)>-0.5
los=not line_intersects_box(enemy_pos,camera_pos,wall_pos,wall_size)
if dist<=50 and in_fov and los:
danger=min(max_danger,danger+danger_rate*dt) if danger else 11.67
else: danger=max(0,danger-danger_cooldown*dt)
# Blink
if danger>=max_danger:
blink_timer+=dt
if blink_timer>=0.5: blink_timer=0; blink=not blink
else: blink=False; blink_timer=0
# Render
screen.fill(BLACK)
draw_floor(0,1,camera_pos,yaw,pitch)
# Always show if near
if np.linalg.norm(camera_pos-wall_pos)<=15:
draw_box(wall_pos,wall_size,DARK_GRAY,camera_pos,yaw,pitch,force_draw=True)
if np.linalg.norm(camera_pos-enemy_pos)<=15:
draw_box(enemy_pos,(1,2,1),ORANGE,camera_pos,yaw,pitch,force_draw=True)
# HUD
bar_w,bar_h=200,16
w=int(bar_w*(danger/max_danger))
if danger>0 and (danger<max_danger or blink):
pygame.draw.rect(screen,SEL_COLOR,pygame.Rect(10,40,w,bar_h))
screen.blit(font.render("Danger",True,WHITE),(10,20))
pygame.display.flip()
pygame.quit();sys.exit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment