Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created November 14, 2025 20:32
Show Gist options
  • Select an option

  • Save EncodeTheCode/dba3fe8417fb7962a7864aaed1e6e5a4 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/dba3fe8417fb7962a7864aaed1e6e5a4 to your computer and use it in GitHub Desktop.
"""
Minimal 3D game example using Pygame + PyOpenGL.
Features:
- Perspective camera tied to player (pitch, yaw, roll)
- WASD movement, mouse look
- Player size: width=32, length=32, height=80, speed=0.5
- FOV similar to Half-Life (90 degrees)
- Back-face culling and simple view-frustum culling
- 60 FPS cap
- Easy constructors for walls, boxes, ramps (triangles), and a cylindrical trash can
- Simple AABB collision system and slope handling
Run:
pip install pygame PyOpenGL PyOpenGL_accelerate
python3 3D_game_player_camera.py
Controls:
- WASD: move
- Space: jump
- Mouse: look around (click to grab mouse)
- Esc or close window: exit
This is intentionally compact but commented enough to modify.
"""
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import math
# ----- Small math helpers -----
class Vec3:
def __init__(self,x=0,y=0,z=0):
self.x=float(x); self.y=float(y); self.z=float(z)
def __add__(self,o): return Vec3(self.x+o.x,self.y+o.y,self.z+o.z)
def __sub__(self,o): return Vec3(self.x-o.x,self.y-o.y,self.z-o.z)
def __mul__(self,s): return Vec3(self.x*s,self.y*s,self.z*s)
def dot(self,o): return self.x*o.x + self.y*o.y + self.z*o.z
def norm(self):
l=math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z)
return self*(1/l) if l else Vec3()
# ----- Basic bounding box for collisions -----
class AABB:
def __init__(self,center,size):
self.c=center; self.h=size*0.5
def min(self): return Vec3(self.c.x-self.h.x, self.c.y-self.h.y, self.c.z-self.h.z)
def max(self): return Vec3(self.c.x+self.h.x, self.c.y+self.h.y, self.c.z+self.h.z)
def aabb_intersect(a,b):
amin,amax=a.min(),a.max(); bmin,bmax=b.min(),b.max()
return (amin.x<=bmax.x and amax.x>=bmin.x) and (amin.y<=bmax.y and amax.y>=bmin.y) and (amin.z<=bmax.z and amax.z>=bmin.z)
# ----- Scene objects -----
class Box:
def __init__(self,center,size,color=(0.7,0.7,0.7),solid=True):
self.center=Vec3(*center); self.size=Vec3(*size); self.color=color; self.solid=solid
self.aabb=AABB(self.center,self.size)
def draw(self):
glColor3f(*self.color)
x,y,z=self.size.x/2,self.size.y/2,self.size.z/2
cx,cy,cz=self.center.x,self.center.y,self.center.z
# 8 vertices
v=[(cx+-x,cy+-y,cz+-z),(cx+-x,cy+-y,cz+z),(cx+-x,cy+y,cz+-z),(cx+-x,cy+y,cz+z),
(cx+x,cy+-y,cz+-z),(cx+x,cy+-y,cz+z),(cx+x,cy+y,cz+-z),(cx+x,cy+y,cz+z)]
# faces (as indices)
faces=[(0,1,3,2),(4,6,7,5),(0,4,5,1),(2,3,7,6),(0,2,6,4),(1,5,7,3)]
glBegin(GL_QUADS)
for f in faces:
# back-face culling handled by GL if enabled
for idx in f:
glVertex3fv(v[idx])
glEnd()
class Cylinder:
def __init__(self,center,radius,height,color=(0.3,0.3,0.3),solid=True,segments=16):
self.c=Vec3(*center); self.r=radius; self.h=height; self.color=color; self.solid=solid; self.seg=segments
self.aabb=AABB(self.c,Vec3(radius*2,height, radius*2))
def draw(self):
glColor3f(*self.color)
cx,cy,cz=self.c.x,self.c.y,self.c.z
# side
glBegin(GL_QUAD_STRIP)
for i in range(self.seg+1):
a=2*math.pi*i/self.seg
x=math.cos(a)*self.r; z=math.sin(a)*self.r
glVertex3f(cx+x, cy-self.h/2, cz+z)
glVertex3f(cx+x, cy+self.h/2, cz+z)
glEnd()
# top
glBegin(GL_TRIANGLE_FAN)
glVertex3f(cx, cy+self.h/2, cz)
for i in range(self.seg+1):
a=2*math.pi*i/self.seg
glVertex3f(cx+math.cos(a)*self.r, cy+self.h/2, cz+math.sin(a)*self.r)
glEnd()
class Ramp:
# triangular slope defined by three Vec3 vertices (triangle) and thickness
def __init__(self,a,b,c,thickness=2,color=(0.6,0.4,0.2),solid=True):
self.a=Vec3(*a); self.b=Vec3(*b); self.c=Vec3(*c); self.t=thickness; self.color=color; self.solid=solid
# aabb
minx=min(self.a.x,self.b.x,self.c.x); maxx=max(self.a.x,self.b.x,self.c.x)
miny=min(self.a.y,self.b.y,self.c.y)-thickness; maxy=max(self.a.y,self.b.y,self.c.y)+thickness
minz=min(self.a.z,self.b.z,self.c.z); maxz=max(self.a.z,self.b.z,self.c.z)
center=Vec3((minx+maxx)/2,(miny+maxy)/2,(minz+maxz)/2)
size=Vec3((maxx-minx),(maxy-miny),(maxz-minz))
self.aabb=AABB(center,size)
def draw(self):
glColor3f(*self.color)
glBegin(GL_TRIANGLES)
glVertex3f(self.a.x,self.a.y,self.a.z)
glVertex3f(self.b.x,self.b.y,self.b.z)
glVertex3f(self.c.x,self.c.y,self.c.z)
glEnd()
# ----- Player and camera -----
class Player:
def __init__(self,pos=(0,0,0),speed=0.5, size=(32,80,32)):
self.pos=Vec3(*pos); self.speed=float(speed)
self.width=size[0]; self.height=size[1]; self.length=size[2]
self.yaw=0; self.pitch=0; self.roll=0
self.vel=Vec3(0,0,0); self.on_ground=False
def aabb(self):
return AABB(Vec3(self.pos.x, self.pos.y+self.height/2, self.pos.z), Vec3(self.width,self.height,self.length))
def forward_vec(self):
cy=math.cos(math.radians(self.yaw)); sy=math.sin(math.radians(self.yaw))
cp=math.cos(math.radians(self.pitch)); sp=math.sin(math.radians(self.pitch))
return Vec3(cp*sy, -sp, cp*cy).norm()
def right_vec(self):
y=math.radians(self.yaw+90)
return Vec3(math.sin(y),0,math.cos(y)).norm()
# ----- World -----
class World:
def __init__(self):
self.objects=[]
def add(self,o): self.objects.append(o)
def draw(self,cam):
for o in self.objects:
# simple frustum culling: if object behind camera skip
dir_to = Vec3(o.aabb.c.x-cam.pos.x, o.aabb.c.y-cam.pos.y, o.aabb.c.z-cam.pos.z)
if cam.forward.dot(dir_to.norm()) < -0.2: continue
o.draw()
def colliders(self):
return [o for o in self.objects if getattr(o,'solid',False)]
# ----- Camera wrapper tied to player -----
class Camera:
def __init__(self,player,fov=90,near=0.1,far=10000):
self.player=player; self.pos=player.pos; self.fov=fov; self.near=near; self.far=far; self.forward=Vec3(0,0,1)
def update(self):
# camera at player's eye height
eye = Vec3(self.player.pos.x, self.player.pos.y + self.player.height*0.9, self.player.pos.z)
self.pos=eye
# compute forward vector from pitch/yaw
yaw=math.radians(self.player.yaw); pitch=math.radians(self.player.pitch)
fx = math.cos(pitch)*math.sin(yaw)
fy = -math.sin(pitch)
fz = math.cos(pitch)*math.cos(yaw)
self.forward=Vec3(fx,fy,fz).norm()
# set GL view
glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(self.fov, WIDTH/HEIGHT, self.near, self.far)
glMatrixMode(GL_MODELVIEW); glLoadIdentity()
look = self.pos + self.forward
gluLookAt(self.pos.x,self.pos.y,self.pos.z, look.x,look.y,look.z, 0,1,0)
# ----- Collision helpers -----
def resolve_collisions(player,colliders):
paabb = player.aabb()
for obj in colliders:
if aabb_intersect(paabb, obj.aabb):
# simple push back along smallest overlap axis
amin,amax=paabb.min(),paabb.max(); bmin,bmax=obj.aabb.min(),obj.aabb.max()
ox = min(amax.x-bmin.x, bmax.x-amin.x)
oy = min(amax.y-bmin.y, bmax.y-amin.y)
oz = min(amax.z-bmin.z, bmax.z-amin.z)
# choose smallest
if ox<=oy and ox<=oz:
# push on x
if paabb.c.x < obj.aabb.c.x: player.pos.x -= ox
else: player.pos.x += ox
elif oy<=ox and oy<=oz:
if paabb.c.y < obj.aabb.c.y:
player.pos.y -= oy; player.on_ground=True
else:
player.pos.y += oy
player.vel.y=0
else:
if paabb.c.z < obj.aabb.c.z: player.pos.z -= oz
else: player.pos.z += oz
paabb = player.aabb()
# ----- Rendering helpers: ground grid -----
def draw_grid(size=500,step=100):
glBegin(GL_LINES)
glColor3f(0.2,0.2,0.2)
for i in range(-size, size+1, step):
glVertex3f(i,0,-size); glVertex3f(i,0,size)
glVertex3f(-size,0,i); glVertex3f(size,0,i)
glEnd()
# ----- Main initialization and loop -----
WIDTH, HEIGHT = 1280, 720
def main():
pygame.init(); pygame.display.set_mode((WIDTH,HEIGHT), DOUBLEBUF|OPENGL)
pygame.mouse.set_visible(False)
# GL setup
glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK)
glEnable(GL_NORMALIZE)
player=Player(pos=(0,100,200), speed=0.5)
cam=Camera(player,fov=90)
world=World()
# build level: floor, walls, table ramp, trash can
world.add(Box((0,-40,0),(2000,80,2000),(0.3,0.5,0.3))) # large ground
# wooden table elevated
table_top_height=20
world.add(Box((300,table_top_height+20,0),(200,40,120),(0.6,0.4,0.2)))
# ramp to table (triangle)
a=(200, -40, -50); b=(400, -40, -50); c=(300, table_top_height+20, -50)
ramp=Ramp(a,b,c, color=(0.5,0.35,0.2))
world.add(ramp)
# walls
world.add(Box((800,0,0),(40,400,1000),(0.7,0.7,0.7)))
world.add(Box((-800,0,0),(40,400,1000),(0.7,0.7,0.7)))
world.add(Box((0,0,800),(1600,400,40),(0.7,0.7,0.7)))
# trash can (cylinder)
world.add(Cylinder((320, -40+25,30), 20, 50, color=(0.2,0.2,0.2)))
clock=pygame.time.Clock(); running=True; mouse_grab=False
while running:
dt = clock.tick(60) / 1000.0 # limit to 60 FPS and get seconds
for e in pygame.event.get():
if e.type==QUIT: running=False
if e.type==KEYDOWN and e.key==K_ESCAPE: running=False
if e.type==MOUSEBUTTONDOWN:
mouse_grab=True; pygame.event.set_grab(True)
if e.type==MOUSEBUTTONUP:
pass
# mouse look
if mouse_grab:
mx,my = pygame.mouse.get_rel()
player.yaw += mx * 0.1
player.pitch += my * 0.1
player.pitch = max(-89, min(89, player.pitch))
# movement
keys=pygame.key.get_pressed()
move=Vec3()
fwd=player.forward_vec(); right=player.right_vec()
if keys[K_w]: move = move + fwd
if keys[K_s]: move = move - fwd
if keys[K_d]: move = move + right
if keys[K_a]: move = move - right
# normalize move and apply speed
if move.x or move.y or move.z:
mvnorm = move.norm()
player.pos = player.pos + mvnorm * (player.speed * dt * 60) # scaled so speed feels consistent
# gravity simple
player.vel.y -= 9.81 * dt * 40
player.pos.y += player.vel.y * dt
player.on_ground=False
# collisions
resolve_collisions(player, world.colliders())
# simple ground clamp
if player.pos.y < -39: player.pos.y=-39; player.on_ground=True; player.vel.y=0
# update camera
cam.update()
# clear
glClearColor(0.53,0.81,0.92,1.0); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
# draw world
draw_grid()
world.draw(cam)
# draw player bounding box for debug
glPushMatrix(); glTranslatef(player.pos.x, player.pos.y+player.height/2, player.pos.z); glScalef(player.width, player.height, player.length)
glColor3f(1,0,0); glutWireCube = None
try:
from OpenGL.GLUT import glutWireCube as gw
glPushMatrix(); glScalef(1,1,1); gw(1); glPopMatrix()
except Exception:
# fallback: draw as small cube
glBegin(GL_QUADS)
glVertex3f(-0.5,-0.5,-0.5); glVertex3f(0.5,-0.5,-0.5); glVertex3f(0.5,0.5,-0.5); glVertex3f(-0.5,0.5,-0.5)
glEnd()
glPopMatrix()
pygame.display.flip()
pygame.quit()
if __name__=="__main__": main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment