Created
November 14, 2025 20:32
-
-
Save EncodeTheCode/dba3fe8417fb7962a7864aaed1e6e5a4 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
| """ | |
| 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