Created
June 28, 2025 10:00
-
-
Save EncodeTheCode/41926a6d940a374981f87ffe46364c15 to your computer and use it in GitHub Desktop.
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
import pygame as g, sys, math | |
import numpy as n | |
from pathlib import Path | |
# Init | |
# Initialize pygame | |
g.init() | |
s = g.display.set_mode((800, 600)) | |
w, h = s.get_size() | |
g.display.set_caption("3D LOS with NumPy Optimization") | |
c = g.time.Clock() | |
g.event.set_grab(True) # lock mouse to window | |
g.mouse.set_visible(True) # we'll hide/show on toggle | |
# --- Crosshair setup --- | |
gfx = Path("gfx") | |
crosshair_img = g.image.load(gfx / "xhair.png").convert_alpha() | |
# center of your 285×128 image is at (142,63): | |
crosshair_offset = g.math.Vector2(142, 63) | |
crosshair_mode = False | |
def enter_first_person_mode(): | |
g.mouse.set_visible(False) | |
def enter_third_person_mode(): | |
g.mouse.set_visible(True) | |
# Constants | |
tile_view = 25 | |
entity_view = 15 | |
danger_range = 50 | |
FOV = math.radians(45) | |
MOVE_SPEED = 3.0 | |
MOUSE_SENS = 0.001 | |
MAX_D = 100.0 | |
D_RATE = 20.0 | |
D_COOLDOWN = 80.0 | |
# Colors & Fonts | |
cols = { 'WHITE':(255,255,255),'GRAY':(180,180,180),'DGRAY':(80,80,80), | |
'ORANGE':(255,165,0),'BLACK':(0,0,0) } | |
font = g.font.SysFont(None,24) | |
flash_colors = [(133,40,32),(136,41,31),(138,38,33),(130,39,31)] | |
# State | |
p = n.array([0.0,1.0,-5.0], float) | |
yaw = pitch = 0.0 | |
d = 0.0 | |
blink = False | |
bt = 0.0 | |
e = n.array([0.0,1.0,8.0], float) | |
wpos = n.array([0.0,1.5,4.0], float) | |
wsize = n.array([4.0,3.0,0.5], float) | |
enemy_dir = n.array([0.0,0.0,-1.0], float) | |
# Helpers | |
def flash_col(): | |
return flash_colors[(g.time.get_ticks() // 500) % 4] | |
def normalize(v): | |
return v / n.linalg.norm(v) if n.linalg.norm(v) > 0 else v | |
def project(pt): | |
rel = pt - p | |
cy, sy = math.cos(yaw), math.sin(yaw) | |
cp, sp = math.cos(pitch), math.sin(pitch) | |
x = rel[0]*cy - rel[2]*sy | |
z = rel[0]*sy + rel[2]*cy | |
y_ = rel[1] | |
y2 = y_*cp - z*sp | |
z2 = y_*sp + z*cp | |
z2 = max(z2, 0.01) | |
f = h / (2*math.tan(FOV/2)) | |
return int(x * f / z2 + w/2), int(-y2 * f / z2 + h/2), z2 | |
def draw_box(center, size, color): | |
offsets = n.array([ | |
[-.5,-.5,-.5],[ .5,-.5,-.5],[ .5, .5,-.5],[-.5, .5,-.5], | |
[-.5,-.5, .5],[ .5,-.5, .5],[ .5, .5, .5],[-.5, .5, .5] | |
]) * size | |
corners = [project(center + o) for o in offsets] | |
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 f_ in faces: | |
face = [corners[i] for i in f_] | |
if any(z <= 0 for *_, z in face): continue | |
poly = [(x, y) for x, y, _ in face] | |
g.draw.polygon(s, color, poly) | |
def draw_grid(): | |
cx, _, cz = p | |
for X in range(int(cx - tile_view), int(cx + tile_view) + 1): | |
for Z in range(int(cz - tile_view), int(cz + tile_view) + 1): | |
a = project(n.array([X,0,Z], float)) | |
b = project(n.array([X+1,0,Z], float)) | |
c_ = project(n.array([X,0,Z+1], float)) | |
if a[2]>0 and b[2]>0 and c_[2]>0: | |
g.draw.line(s, cols['GRAY'], a[:2], b[:2]) | |
g.draw.line(s, cols['GRAY'], a[:2], c_[:2]) | |
def intersects(a, b, pos, sz): | |
d = b - a | |
tmin, tmax = -n.inf, n.inf | |
for i in range(3): | |
mn = pos[i] - sz[i]/2; mx = pos[i] + sz[i]/2 | |
if abs(d[i])<1e-6: | |
if a[i]<mn or a[i]>mx: return False | |
else: | |
t1 = (mn - a[i]) / d[i]; t2 = (mx - a[i]) / d[i] | |
tmin = max(tmin, min(t1,t2)) | |
tmax = min(tmax, max(t1,t2)) | |
if tmax < tmin: return False | |
return True | |
# Main loop | |
while True: | |
dt = c.tick(60) / 1000 | |
col = flash_col() | |
for ev in g.event.get(): | |
if ev.type == g.QUIT: | |
sys.exit() | |
elif ev.type == g.KEYDOWN and ev.key == g.K_F3: | |
crosshair_mode = not crosshair_mode | |
if crosshair_mode: | |
enter_first_person_mode() | |
else: | |
enter_third_person_mode() | |
# Mouse-look | |
mx, my = g.mouse.get_rel() | |
yaw += mx * MOUSE_SENS | |
pitch += my * MOUSE_SENS | |
pitch = max(-math.pi/2+0.01, min(math.pi/2-0.01, pitch)) | |
# Movement | |
keys = g.key.get_pressed() | |
mv = n.zeros(3) | |
fwd = n.array([math.sin(yaw), 0, math.cos(yaw)]) | |
rt = n.array([math.cos(yaw), 0, -math.sin(yaw)]) | |
if keys[g.K_w]: mv += fwd | |
if keys[g.K_s]: mv -= fwd | |
if keys[g.K_a]: mv -= rt | |
if keys[g.K_d]: mv += rt | |
if keys[g.K_q]: mv[1] -= 1 | |
if keys[g.K_e]: mv[1] += 1 | |
if n.linalg.norm(mv)>0: | |
p += normalize(mv) * MOVE_SPEED * dt | |
# Danger | |
dist = n.linalg.norm(p - e) | |
vis = n.dot(enemy_dir, normalize(p - e)) > -0.5 | |
los = not intersects(e, p, wpos, wsize) | |
if dist<=danger_range and vis and los: | |
if d==0: d=11.67 | |
d = min(MAX_D, d + D_RATE*dt) | |
else: | |
d = max(0, d - D_COOLDOWN*dt) | |
if d>=MAX_D: | |
bt += dt | |
if bt>=0.5: | |
bt=0; blink=not blink | |
else: | |
blink=False; bt=0 | |
# Render world | |
s.fill(cols['BLACK']) | |
draw_grid() | |
if n.linalg.norm(p - wpos)<= entity_view: | |
draw_box(wpos, wsize, cols['DGRAY']) | |
if n.linalg.norm(p - e)<= entity_view: | |
draw_box(e, [1,2,1], cols['ORANGE']) | |
# HUD | |
fw = int(200 * (d / MAX_D)) | |
if d>0 and (d<MAX_D or blink): | |
g.draw.rect(s, col, (10, 40, fw, 16)) | |
s.blit(font.render("Danger", True, cols['WHITE']), (10, 20)) | |
# Crosshair fixed at screen center | |
if crosshair_mode: | |
cx, cy = w // 2, h // 2 | |
s.blit(crosshair_img, | |
(cx - crosshair_offset.x, | |
cy - crosshair_offset.y)) | |
g.display.flip() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Centers crosshair on the center of the screen with the crosshair offset position with the x and y positions. It fixes the functionality of the mouse look while also some issues remain with the looking around part. Still no fix in mind at the moment, working on a solution for third person mode and the viewing angles the player character can have.