Key Code Examples
1. Game Loop (Pygame)
import pygame
import sys
from dataclasses import dataclass, field
from typing import List
import time
pygame.init()
# Display setup
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
FPS = 60
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("EduVerse Game")
clock = pygame.time.Clock()
@dataclass
class Vector2:
x: float = 0.0
y: float = 0.0
def __add__(self, other):
return Vector2(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector2(self.x * scalar, self.y * scalar)
@dataclass
class GameObject:
"""Base class for all game objects."""
position: Vector2 = field(default_factory=Vector2)
velocity: Vector2 = field(default_factory=Vector2)
acceleration: Vector2 = field(default_factory=Vector2)
width: int = 32
height: int = 32
color: tuple = (255, 255, 255)
mass: float = 1.0
@property
def rect(self):
return pygame.Rect(self.position.x, self.position.y,
self.width, self.height)
def update(self, dt: float):
"""Update physics."""
self.velocity = self.velocity + self.acceleration * dt
self.position = self.position + self.velocity * dt
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.rect)
class Player(GameObject):
def __init__(self):
super().__init__(position=Vector2(400, 300),
width=32, height=32,
color=(0, 128, 255))
self.speed = 300 # pixels per second
self.jump_force = 500
self.on_ground = False
def handle_input(self, keys):
"""Process keyboard input."""
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.velocity.x = -self.speed
elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.velocity.x = self.speed
else:
self.velocity.x = 0
if (keys[pygame.K_SPACE] or keys[pygame.K_UP]) and self.on_ground:
self.velocity.y = -self.jump_force
self.on_ground = False
class Game:
"""Main game class managing the game loop."""
def __init__(self):
self.running = True
self.game_objects: List[GameObject] = []
self.player = Player()
self.gravity = Vector2(0, 800) # pixels per second^2
self.score = 0
self.font = pygame.font.Font(None, 36)
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.running = False
def update(self, dt: float):
keys = pygame.key.get_pressed()
self.player.handle_input(keys)
# Apply gravity
self.player.acceleration = self.gravity
# Update all objects
for obj in self.game_objects:
obj.update(dt)
self.player.update(dt)
# Ground collision
if self.player.position.y >= SCREEN_HEIGHT - 64:
self.player.position.y = SCREEN_HEIGHT - 64
self.player.velocity.y = 0
self.player.on_ground = True
# Boundary checks
self.player.position.x = max(0, min(SCREEN_WIDTH - 32,
self.player.position.x))
def draw(self):
screen.fill((30, 30, 30)) # Dark background
for obj in self.game_objects:
obj.draw(screen)
self.player.draw(screen)
# Draw HUD
score_text = self.font.render(f"Score: {self.score}", True, (255, 255, 255))
screen.blit(score_text, (10, 10))
pygame.display.flip()
def run(self):
"""Main game loop with fixed timestep."""
previous_time = time.time()
while self.running:
current_time = time.time()
dt = min(current_time - previous_time, 0.1) # Cap at 100ms
previous_time = current_time
self.handle_events()
self.update(dt)
self.draw()
clock.tick(FPS)
pygame.quit()
sys.exit()
# Run game
if __name__ == "__main__":
game = Game()
game.run()
2. Collision Detection (AABB & Circle)
import math
from dataclasses import dataclass
from typing import Optional
@dataclass
class AABB:
"""Axis-Aligned Bounding Box."""
x: float
y: float
width: float
height: float
@property
def left(self): return self.x
@property
def right(self): return self.x + self.width
@property
def top(self): return self.y
@property
def bottom(self): return self.y + self.height
@property
def center(self): return (self.x + self.width/2, self.y + self.height/2)
@dataclass
class Circle:
cx: float
cy: float
radius: float
def aabb_collision(a: AABB, b: AABB) -> bool:
"""Check if two AABBs overlap."""
return (a.left < b.right and a.right > b.left and
a.top < b.bottom and a.bottom > b.top)
def circle_collision(a: Circle, b: Circle) -> bool:
"""Check if two circles overlap."""
dx = a.cx - b.cx
dy = a.cy - b.cy
dist_sq = dx*dx + dy*dy
radius_sum = a.radius + b.radius
return dist_sq < radius_sum * radius_sum
def circle_aabb_collision(circle: Circle, box: AABB) -> bool:
"""Check if a circle overlaps with an AABB."""
# Find closest point on box to circle center
closest_x = max(box.left, min(circle.cx, box.right))
closest_y = max(box.top, min(circle.cy, box.bottom))
# Distance from circle center to closest point
dx = circle.cx - closest_x
dy = circle.cy - closest_y
return (dx*dx + dy*dy) < (circle.radius * circle.radius)
def resolve_aabb_collision(a: AABB, b: AABB) -> Optional[tuple]:
"""Calculate minimum translation vector to separate AABBs."""
a_cx, a_cy = a.center
b_cx, b_cy = b.center
# Overlap on each axis
overlap_x = (a.width + b.width) / 2 - abs(a_cx - b_cx)
overlap_y = (a.height + b.height) / 2 - abs(a_cy - b_cy)
if overlap_x <= 0 or overlap_y <= 0:
return None # No collision
# Resolve along axis of least penetration
if overlap_x < overlap_y:
direction = 1 if a_cx < b_cx else -1
return (overlap_x * direction, 0)
else:
direction = 1 if a_cy < b_cy else -1
return (0, overlap_y * direction)
# Example usage
player = AABB(100, 100, 32, 32)
enemy = AABB(120, 110, 32, 32)
print(aabb_collision(player, enemy)) # True
ball = Circle(50, 50, 10)
wall = AABB(55, 40, 20, 20)
print(circle_aabb_collision(ball, wall)) # True
mtv = resolve_aabb_collision(player, enemy)
print(f"Push player by: {mtv}")
3. Entity Component System (ECS)
from dataclasses import dataclass, field
from typing import Dict, List, Type, TypeVar, Set
import uuid
T = TypeVar('T')
# ===== Components (pure data) =====
@dataclass
class Position:
x: float = 0.0
y: float = 0.0
@dataclass
class Velocity:
vx: float = 0.0
vy: float = 0.0
@dataclass
class Health:
current: int = 100
maximum: int = 100
@property
def is_alive(self): return self.current > 0
@property
def percent(self): return self.current / self.maximum * 100
@dataclass
class Renderable:
sprite: str = "default.png"
layer: int = 0
color: tuple = (255, 255, 255)
@dataclass
class Collider:
radius: float = 16.0
is_trigger: bool = False
# ===== Entity (just an ID) =====
class Entity:
_next_id = 0
def __init__(self):
self.id = Entity._next_id
Entity._next_id += 1
def __hash__(self):
return self.id
def __eq__(self, other):
return isinstance(other, Entity) and self.id == other.id
# ===== ECS World =====
class World:
def __init__(self):
self.entities: Set[Entity] = set()
self.components: Dict[Type, Dict[Entity, any]] = {}
self.systems: List[System] = []
def create_entity(self, *components) -> Entity:
entity = Entity()
self.entities.add(entity)
for component in components:
self.add_component(entity, component)
return entity
def add_component(self, entity: Entity, component):
comp_type = type(component)
if comp_type not in self.components:
self.components[comp_type] = {}
self.components[comp_type][entity] = component
def remove_component(self, entity: Entity, comp_type: Type):
if comp_type in self.components:
self.components[comp_type].pop(entity, None)
def get_component(self, entity: Entity, comp_type: Type[T]) -> T:
return self.components.get(comp_type, {}).get(entity)
def query(self, *component_types: Type) -> List[tuple]:
"""Get all entities with specified components."""
if not component_types:
return []
# Start with entities having the first component
result = set(self.components.get(component_types[0], {}).keys())
# Intersect with remaining component types
for comp_type in component_types[1:]:
result &= set(self.components.get(comp_type, {}).keys())
# Return entities with their components
return [(entity, *[self.get_component(entity, ct)
for ct in component_types])
for entity in result]
def add_system(self, system):
system.world = self
self.systems.append(system)
def update(self, dt: float):
for system in self.systems:
system.update(dt)
# ===== Systems =====
class MovementSystem:
def __init__(self):
self.world = None
def update(self, dt: float):
for entity, pos, vel in self.world.query(Position, Velocity):
pos.x += vel.vx * dt
pos.y += vel.vy * dt
class HealthSystem:
def __init__(self):
self.world = None
def update(self, dt: float):
dead = []
for entity, health in self.world.query(Health):
if not health.is_alive:
dead.append(entity)
for entity in dead:
print(f"Entity {entity.id} died!")
self.world.entities.discard(entity)
# Usage
world = World()
world.add_system(MovementSystem())
world.add_system(HealthSystem())
# Create entities
player = world.create_entity(
Position(x=0, y=0),
Velocity(vx=100, vy=0),
Health(current=100, maximum=100),
Renderable(sprite="player.png")
)
enemy = world.create_entity(
Position(x=200, y=0),
Velocity(vx=-50, vy=0),
Health(current=50, maximum=50),
Collider(radius=20)
)
# Game loop
import time
for _ in range(60): # 60 frames
world.update(dt=1/60)
pos = world.get_component(player, Position)
print(f"Player position: ({pos.x:.1f}, {pos.y:.1f})")
4. Procedural Dungeon Generation
import random
import numpy as np
from typing import List, Tuple
from enum import IntEnum
class Tile(IntEnum):
WALL = 0
FLOOR = 1
DOOR = 2
CHEST = 3
ENEMY = 4
PLAYER = 5
class DungeonGenerator:
"""Procedural dungeon generation using BSP (Binary Space Partition)."""
def __init__(self, width=60, height=40, min_room_size=5):
self.width = width
self.height = height
self.min_room_size = min_room_size
self.grid = np.full((height, width), Tile.WALL, dtype=int)
self.rooms: List[Tuple[int, int, int, int]] = [] # x, y, w, h
self.doors: List[Tuple[int, int]] = []
def generate(self, num_rooms=10, seed=None) -> np.ndarray:
"""Generate a dungeon with rooms and corridors."""
if seed is not None:
random.seed(seed)
# BSP partitioning
self._split_bsp(0, 0, self.width, self.height, depth=0, max_depth=4)
# Connect rooms with corridors
self._connect_rooms()
# Place features
self._place_features()
return self.grid
def _split_bsp(self, x, y, w, h, depth, max_depth):
"""Recursively split space using BSP."""
if depth >= max_depth or w < self.min_room_size * 2 or h < self.min_room_size * 2:
self._create_room(x, y, w, h)
return
# Choose split direction (horizontal or vertical)
if w > h:
split = random.randint(self.min_room_size, w - self.min_room_size)
self._split_bsp(x, y, split, h, depth+1, max_depth)
self._split_bsp(x+split, y, w-split, h, depth+1, max_depth)
else:
split = random.randint(self.min_room_size, h - self.min_room_size)
self._split_bsp(x, y, w, split, depth+1, max_depth)
self._split_bsp(x, y+split, w, h-split, depth+1, max_depth)
def _create_room(self, x, y, w, h):
"""Create a room within the given bounds."""
# Shrink to ensure walls around room
room_w = random.randint(self.min_room_size, w-2)
room_h = random.randint(self.min_room_size, h-2)
room_x = x + random.randint(1, max(1, w - room_w - 1))
room_y = y + random.randint(1, max(1, h - room_h - 1))
# Carve room
self.grid[room_y:room_y+room_h, room_x:room_x+room_w] = Tile.FLOOR
self.rooms.append((room_x, room_y, room_w, room_h))
def _connect_rooms(self):
"""Connect rooms with L-shaped corridors."""
for i in range(len(self.rooms) - 1):
x1 = self.rooms[i][0] + self.rooms[i][2] // 2
y1 = self.rooms[i][1] + self.rooms[i][3] // 2
x2 = self.rooms[i+1][0] + self.rooms[i+1][2] // 2
y2 = self.rooms[i+1][1] + self.rooms[i+1][3] // 2
# L-shaped corridor
if random.random() < 0.5:
self._carve_h_corridor(x1, x2, y1)
self._carve_v_corridor(y1, y2, x2)
else:
self._carve_v_corridor(y1, y2, x1)
self._carve_h_corridor(x1, x2, y2)
def _carve_h_corridor(self, x1, x2, y):
for x in range(min(x1, x2), max(x1, x2) + 1):
if 0 <= x < self.width and 0 <= y < self.height:
self.grid[y][x] = Tile.FLOOR
def _carve_v_corridor(self, y1, y2, x):
for y in range(min(y1, y2), max(y1, y2) + 1):
if 0 <= y < self.height and 0 <= x < self.width:
self.grid[y][x] = Tile.FLOOR
def _place_features(self):
"""Place player, enemies, and chests."""
if not self.rooms:
return
# Player in first room
r = self.rooms[0]
px = r[0] + r[2] // 2
py = r[1] + r[3] // 2
self.grid[py][px] = Tile.PLAYER
# Enemies and chests in other rooms
for room in self.rooms[1:]:
rx = room[0] + random.randint(0, room[2]-1)
ry = room[1] + random.randint(0, room[3]-1)
if self.grid[ry][rx] == Tile.FLOOR:
if random.random() < 0.5:
self.grid[ry][rx] = Tile.ENEMY
else:
self.grid[ry][rx] = Tile.CHEST
def display(self):
"""ASCII display of the dungeon."""
chars = {Tile.WALL: '#', Tile.FLOOR: '.', Tile.DOOR: '+',
Tile.CHEST: '$', Tile.ENEMY: 'E', Tile.PLAYER: '@'}
for row in self.grid:
print(''.join(chars[Tile(cell)] for cell in row))
# Generate and display dungeon
generator = DungeonGenerator(width=60, height=20)
dungeon = generator.generate(num_rooms=8, seed=42)
generator.display()
5. Steering Behaviors (Autonomous Agents)
import math
import random
from dataclasses import dataclass
from typing import List
@dataclass
class Vector2:
x: float
y: float
def __add__(self, other): return Vector2(self.x+other.x, self.y+other.y)
def __sub__(self, other): return Vector2(self.x-other.x, self.y-other.y)
def __mul__(self, s): return Vector2(self.x*s, self.y*s)
@property
def magnitude(self): return math.sqrt(self.x**2 + self.y**2)
@property
def normalized(self):
mag = self.magnitude
if mag == 0: return Vector2(0, 0)
return Vector2(self.x/mag, self.y/mag)
def limit(self, max_mag):
if self.magnitude > max_mag:
return self.normalized * max_mag
return self
class Boid:
"""Autonomous agent for flocking behavior."""
def __init__(self, x, y):
self.position = Vector2(x, y)
self.velocity = Vector2(random.uniform(-1, 1), random.uniform(-1, 1))
self.acceleration = Vector2(0, 0)
self.max_speed = 4.0
self.max_force = 0.1
self.perception_radius = 50
def apply_force(self, force: Vector2):
self.acceleration = self.acceleration + force
def seek(self, target: Vector2, weight=1.0) -> Vector2:
"""Steer towards target position."""
desired = target - self.position
desired = desired.normalized * self.max_speed
steer = desired - self.velocity
return steer.limit(self.max_force) * weight
def flee(self, target: Vector2, weight=1.0) -> Vector2:
"""Steer away from target."""
return self.seek(target, -weight)
def arrive(self, target: Vector2, slowing_radius=100, weight=1.0) -> Vector2:
"""Seek target but slow down when close."""
diff = target - self.position
dist = diff.magnitude
if dist < slowing_radius:
speed = self.max_speed * (dist / slowing_radius)
else:
speed = self.max_speed
desired = diff.normalized * speed
steer = desired - self.velocity
return steer.limit(self.max_force) * weight
def separate(self, boids: List['Boid'], weight=1.5) -> Vector2:
"""Avoid crowding neighbors."""
steer = Vector2(0, 0)
count = 0
for other in boids:
if other is self: continue
d = (self.position - other.position).magnitude
if 0 < d < self.perception_radius:
diff = (self.position - other.position).normalized / d
steer = steer + diff
count += 1
if count > 0:
steer = steer * (1.0 / count)
steer = steer.normalized * self.max_speed
steer = steer - self.velocity
return steer.limit(self.max_force) * weight
return steer
def align(self, boids: List['Boid'], weight=1.0) -> Vector2:
"""Match velocity of neighbors."""
avg = Vector2(0, 0)
count = 0
for other in boids:
if other is self: continue
if (other.position - self.position).magnitude < self.perception_radius:
avg = avg + other.velocity
count += 1
if count > 0:
avg = avg * (1.0 / count)
avg = avg.normalized * self.max_speed
steer = avg - self.velocity
return steer.limit(self.max_force) * weight
return avg
def cohesion(self, boids: List['Boid'], weight=1.0) -> Vector2:
"""Move toward center of neighbors."""
center = Vector2(0, 0)
count = 0
for other in boids:
if other is self: continue
if (other.position - self.position).magnitude < self.perception_radius:
center = center + other.position
count += 1
if count > 0:
center = center * (1.0 / count)
return self.seek(center, weight)
return center
def update(self, dt=1.0):
self.velocity = (self.velocity + self.acceleration * dt).limit(self.max_speed)
self.position = self.position + self.velocity * dt
self.acceleration = Vector2(0, 0)
def draw(self, surface):
angle = math.atan2(self.velocity.y, self.velocity.x)
# Draw triangle pointing in movement direction
tip = self.position + Vector2(math.cos(angle)*10, math.sin(angle)*10)
left = self.position + Vector2(math.cos(angle+2.5)*7, math.sin(angle+2.5)*7)
right = self.position + Vector2(math.cos(angle-2.5)*7, math.sin(angle-2.5)*7)
# pygame.draw.polygon(surface, (0, 150, 255), [tip, left, right])
6. Game State Management
from typing import Dict, Callable, Any
from enum import Enum, auto
import copy
class GameState(Enum):
LOADING = auto()
MAIN_MENU = auto()
PLAYING = auto()
PAUSED = auto()
GAME_OVER = auto()
SETTINGS = auto()
class GameStateMachine:
"""Hierarchical game state manager."""
def __init__(self):
self.current_state: GameState = GameState.LOADING
self.previous_state: GameState = None
self.state_data: Dict[GameState, Any] = {}
self.transitions: Dict[tuple, Callable] = {}
self.global_data: Dict[str, Any] = {}
# Define valid transitions
self._setup_transitions()
def _setup_transitions(self):
"""Define allowed state transitions."""
valid = {
GameState.LOADING: [GameState.MAIN_MENU],
GameState.MAIN_MENU: [GameState.PLAYING, GameState.SETTINGS],
GameState.PLAYING: [GameState.PAUSED, GameState.GAME_OVER],
GameState.PAUSED: [GameState.PLAYING, GameState.MAIN_MENU],
GameState.GAME_OVER: [GameState.MAIN_MENU, GameState.PLAYING],
GameState.SETTINGS: [GameState.MAIN_MENU],
}
for from_state, to_states in valid.items():
for to_state in to_states:
self.transitions[(from_state, to_state)] = None
def can_transition(self, new_state: GameState) -> bool:
"""Check if transition is valid."""
return (self.current_state, new_state) in self.transitions
def transition(self, new_state: GameState, data: Any = None):
"""Change to a new state."""
if not self.can_transition(new_state):
raise ValueError(
f"Invalid transition: {self.current_state.name} -> {new_state.name}"
)
# Save current state data
self.state_data[self.current_state] = self._capture_state()
self.previous_state = self.current_state
self.current_state = new_state
# Restore new state data if exists
if new_state in self.state_data:
self._restore_state(self.state_data[new_state])
print(f"State: {self.previous_state.name} -> {new_state.name}")
return self
def _capture_state(self) -> dict:
"""Capture current state for restoration."""
return copy.deepcopy(self.global_data)
def _restore_state(self, data: dict):
"""Restore state from saved data."""
self.global_data = copy.deepcopy(data)
def update(self, dt: float):
"""Update current state."""
handler = getattr(self, f'_update_{self.current_state.name.lower()}', None)
if handler:
handler(dt)
def _update_playing(self, dt):
"""Update gameplay."""
pass
def _update_paused(self, dt):
"""Update pause menu."""
pass
# Usage
gsm = GameStateMachine()
gsm.transition(GameState.MAIN_MENU)
gsm.global_data["player_health"] = 100
gsm.transition(GameState.PLAYING)
# Player takes damage
gsm.global_data["player_health"] -= 20
print(f"Health: {gsm.global_data['player_health']}")
gsm.transition(GameState.PAUSED)
gsm.transition(GameState.PLAYING)
7. Shader Programming (GLSL)
// vertex_shader.glsl
#version 330 core
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uTime;
out vec2 vTexCoord;
out vec3 vNormal;
out vec3 vWorldPos;
void main() {
vec4 worldPos = uModel * vec4(aPosition, 1.0);
vWorldPos = worldPos.xyz;
vTexCoord = aTexCoord;
vNormal = mat3(uModel) * aNormal;
gl_Position = uProjection * uView * worldPos;
}
// fragment_shader.glsl
#version 330 core
in vec2 vTexCoord;
in vec3 vNormal;
in vec3 vWorldPos;
uniform sampler2D uTexture;
uniform vec3 uLightPos;
uniform vec3 uCameraPos;
uniform float uTime;
out vec4 FragColor;
void main() {
// Ambient
float ambientStrength = 0.3;
vec3 ambient = ambientStrength * vec3(1.0, 1.0, 1.0);
// Diffuse
vec3 lightDir = normalize(uLightPos - vWorldPos);
float diff = max(dot(vNormal, lightDir), 0.0);
vec3 diffuse = diff * vec3(1.0, 0.95, 0.8);
// Specular
vec3 viewDir = normalize(uCameraPos - vWorldPos);
vec3 reflectDir = reflect(-lightDir, vNormal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = 0.5 * spec * vec3(1.0);
// Texture
vec4 texColor = texture(uTexture, vTexCoord);
// Animated glow effect
float glow = sin(uTime * 2.0 + vWorldPos.x) * 0.1 + 0.9;
vec3 result = (ambient + diffuse + specular) * texColor.rgb * glow;
FragColor = vec4(result, texColor.a);
}
// Python code to compile and use shaders (Pygame/ModernGL)
import moderngl
def create_shader_program(ctx, vertex_src, fragment_src):
prog = ctx.program(
vertex_shader=vertex_src,
fragment_shader=fragment_src,
)
return prog
8. Audio System โ Sound Manager
import pygame
import os
from typing import Dict, Optional
from enum import Enum, auto
class SoundType(Enum):
SFX = auto()
MUSIC = auto()
VOICE = auto()
class AudioManager:
"""Centralized audio management system."""
def __init__(self, sfx_volume=0.7, music_volume=0.5):
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
self.sounds: Dict[str, pygame.mixer.Sound] = {}
self.current_music: Optional[str] = None
self.music_paused = False
# Channels for different sound types
self.channels = {
SoundType.SFX: pygame.mixer.Channel(0),
SoundType.MUSIC: pygame.mixer.Channel(1),
SoundType.VOICE: pygame.mixer.Channel(2),
}
self.set_sfx_volume(sfx_volume)
self.set_music_volume(music_volume)
def load_sound(self, name: str, filepath: str, sound_type=SoundType.SFX):
"""Preload a sound effect."""
if os.path.exists(filepath):
self.sounds[name] = pygame.mixer.Sound(filepath)
self.sounds[name].set_volume(
0.7 if sound_type == SoundType.SFX else 0.5
)
print(f"Loaded sound: {name}")
def play_sfx(self, name: str, loops=0, maxtime=0, fade_ms=0):
"""Play a one-shot sound effect."""
if name in self.sounds:
channel = self.channels[SoundType.SFX]
channel.play(self.sounds[name], loops=loops,
maxtime=maxtime, fade_ms=fade_ms)
def play_music(self, filepath: str, loops=-1, fade_in_ms=1000):
"""Play background music with smooth transition."""
if self.current_music == filepath:
return
# Fade out current music
if pygame.mixer.music.get_busy():
pygame.mixer.music.fadeout(500)
pygame.time.wait(500)
pygame.mixer.music.load(filepath)
pygame.mixer.music.set_volume(0.5)
pygame.mixer.music.play(loops=loops, fade_ms=fade_in_ms)
self.current_music = filepath
self.music_paused = False
def pause_music(self):
if pygame.mixer.music.get_busy() and not self.music_paused:
pygame.mixer.music.pause()
self.music_paused = True
def resume_music(self):
if self.music_paused:
pygame.mixer.music.unpause()
self.music_paused = False
def stop_music(self, fade_out_ms=500):
pygame.mixer.music.fadeout(fade_out_ms)
self.current_music = None
def set_sfx_volume(self, volume: float):
self.channels[SoundType.SFX].set_volume(max(0.0, min(1.0, volume)))
def set_music_volume(self, volume: float):
pygame.mixer.music.set_volume(max(0.0, min(1.0, volume)))
# Usage
audio = AudioManager()
audio.load_sound("jump", "assets/sounds/jump.wav")
audio.load_sound("explosion", "assets/sounds/explosion.wav")
audio.play_music("assets/music/level1.ogg")
# In game events:
# audio.play_sfx("jump")
# audio.play_sfx("explosion", fade_ms=50)
9. Tile-Based Level System
import json
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from enum import IntEnum
class TileType(IntEnum):
EMPTY = 0
GROUND = 1
WALL = 2
WATER = 3
LAVA = 4
COIN = 5
SPIKE = 6
CHEST = 7
@dataclass
class Tile:
type: TileType = TileType.EMPTY
walkable: bool = True
deadly: bool = False
collectible: bool = False
sprite_id: str = ""
@dataclass
class TileLayer:
name: str
width: int
height: int
tiles: List[List[Tile]] = field(default_factory=list)
visible: bool = True
opacity: float = 1.0
def __post_init__(self):
if not self.tiles:
self.tiles = [[Tile() for _ in range(self.width)]
for _ in range(self.height)]
def get_tile(self, x: int, y: int) -> Optional[Tile]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.tiles[y][x]
return None
def set_tile(self, x: int, y: int, tile: Tile):
if 0 <= x < self.width and 0 <= y < self.height:
self.tiles[y][x] = tile
@dataclass
class Level:
name: str
tile_width: int = 32
tile_height: int = 32
layers: List[TileLayer] = field(default_factory=list)
entities: List[dict] = field(default_factory=list)
player_start: tuple = (2, 2)
@property
def pixel_width(self):
if self.layers:
return self.layers[0].width * self.tile_width
return 0
@property
def pixel_height(self):
if self.layers:
return self.layers[0].height * self.tile_height
return 0
def is_walkable(self, tile_x: int, tile_y: int) -> bool:
"""Check if a tile position is walkable."""
for layer in self.layers:
tile = layer.get_tile(tile_x, tile_y)
if tile and not tile.walkable:
return False
return True
def save(self, filepath: str):
"""Save level to JSON."""
data = {
"name": self.name,
"tileWidth": self.tile_width,
"tileHeight": self.tile_height,
"playerStart": self.player_start,
"layers": [],
"entities": self.entities,
}
for layer in self.layers:
layer_data = {
"name": layer.name,
"width": layer.width,
"height": layer.height,
"visible": layer.visible,
"tiles": [[tile.type for tile in row] for row in layer.tiles]
}
data["layers"].append(layer_data)
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
@classmethod
def load(cls, filepath: str) -> 'Level':
"""Load level from JSON."""
with open(filepath, 'r') as f:
data = json.load(f)
level = cls(
name=data["name"],
tile_width=data["tileWidth"],
tile_height=data["tileHeight"],
player_start=tuple(data["playerStart"]),
entities=data.get("entities", []),
)
for layer_data in data["layers"]:
layer = TileLayer(
name=layer_data["name"],
width=layer_data["width"],
height=layer_data["height"],
visible=layer_data["visible"],
)
for y, row in enumerate(layer_data["tiles"]):
for x, tile_type in enumerate(row):
layer.tiles[y][x] = Tile(type=TileType(tile_type))
level.layers.append(layer)
return level
# Usage
level = Level(name="Forest Level", tile_width=32, tile_height=32)
ground = TileLayer(name="Ground", width=30, height=20)
decoration = TileLayer(name="Decoration", width=30, height=20)
# Set ground tiles
for x in range(30):
for y in range(15, 20):
ground.set_tile(x, y, Tile(type=TileType.GROUND, walkable=True))
# Add some walls
for x in range(10, 20):
decoration.set_tile(x, 10, Tile(type=TileType.WALL, walkable=False))
level.layers = [ground, decoration]
level.player_start = (2, 14)
level.save("levels/forest_level.json")
10. Unity C# โ Player Controller
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float sprintSpeed = 8f;
[SerializeField] private float rotationSpeed = 720f;
[SerializeField] private float jumpForce = 8f;
[SerializeField] private float gravity = -20f;
[Header("Ground Check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundDistance = 0.2f;
[SerializeField] private LayerMask groundMask;
// Components
private CharacterController controller;
private Camera mainCamera;
// State
private Vector3 velocity;
private bool isGrounded;
private bool isSprinting;
// Animation
private Animator animator;
private static readonly int SpeedHash = Animator.StringToHash("Speed");
private static readonly int IsGroundedHash = Animator.StringToHash("IsGrounded");
private static readonly int JumpHash = Animator.StringToHash("Jump");
private void Awake()
{
controller = GetComponent
();
animator = GetComponent();
mainCamera = Camera.main;
Cursor.lockState = CursorLockMode.Locked;
}
private void Update()
{
HandleGroundCheck();
HandleMovement();
HandleJump();
ApplyGravity();
UpdateAnimation();
}
private void HandleGroundCheck()
{
isGrounded = Physics.CheckSphere(
groundCheck.position, groundDistance, groundMask);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f; // Small downward force to keep grounded
}
}
private void HandleMovement()
{
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
// Camera-relative movement
Vector3 camForward = mainCamera.transform.forward;
Vector3 camRight = mainCamera.transform.right;
camForward.y = 0;
camRight.y = 0;
camForward.Normalize();
camRight.Normalize();
Vector3 moveDir = (camForward * vertical + camRight * horizontal).normalized;
isSprinting = Input.GetKey(KeyCode.LeftShift);
float speed = isSprinting ? sprintSpeed : moveSpeed;
controller.Move(moveDir * speed * Time.deltaTime);
// Rotate player to face movement direction
if (moveDir.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDir);
transform.rotation = Quaternion.RotateTowards(
transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
}
}
private void HandleJump()
{
if (Input.GetButtonDown("Jump") && isGrounded)
{
velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
animator.SetTrigger(JumpHash);
}
}
private void ApplyGravity()
{
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
private void UpdateAnimation()
{
float speed = new Vector2(controller.velocity.x,
controller.velocity.z).magnitude;
animator.SetFloat(SpeedHash, speed);
animator.SetBool(IsGroundedHash, isGrounded);
}
}
Core Concepts
Game Engine Architecture
Definition: A software framework that provides tools and systems for building games efficiently.
- Rendering Engine: 2D/3D graphics, shaders, lighting
- Physics Engine: Collision detection, rigid body dynamics
- Audio Engine: Sound effects, music, spatial audio
- Input System: Keyboard, mouse, gamepad, touch
- Scene Management: Level loading, entity management
- Scripting: Game logic, AI, UI scripting
Popular Game Engines
- Unity: C#, cross-platform, massive asset store โ most popular for indie/AA
- Unreal Engine: C++, Blueprints, AAA graphics โ high-end 3D games
- Godot: GDScript/C#, open-source, lightweight โ growing fast
- GameMaker: GML, 2D-focused โ great for beginners
- Pygame: Python, 2D โ educational and prototyping
Game Physics
Rigid Body Dynamics: Objects with mass, velocity, and forces (gravity, friction, impulse).
Collision Detection: Broad phase (BVH, spatial hash) + narrow phase (SAT, GJK algorithms).
Common Algorithms: Verlet integration, impulse-based resolution, raycasting.
Artificial Intelligence in Games
- State Machines: Simple AI with discrete states (idle, patrol, chase, attack)
- Behavior Trees: Hierarchical decision-making for complex NPCs
- Pathfinding: A*, navigation meshes, dynamic obstacle avoidance
- Flocking: Boids algorithm for crowd/swarm behavior
- GOAP: Goal-Oriented Action Planning โ dynamic decision making
- Machine Learning: Neural networks for NPC behavior, procedural content
Game Design Patterns
- Game Loop: Input โ Update โ Render cycle running at 30-60 FPS
- Entity Component System: Composition over inheritance for game objects
- Object Pool: Reuse objects instead of creating/destroying (performance)
- State Pattern: Game states (menu, playing, paused) managed cleanly
- Observer Pattern: Event system for decoupled communication
- Command Pattern: Input handling, undo system, replay system
Rendering Techniques
- Forward Rendering: Traditional, per-object lighting โ simple but limited lights
- Deferred Rendering: G-buffer, many lights โ AAA games
- 2D Tilemap: Grid-based rendering for pixel art games
- Particle Systems: Fire, smoke, magic effects โ GPU-accelerated
- Post-Processing: Bloom, motion blur, color grading โ screen-space effects
Multiplayer Game Development
Network Models:
- Client-Server: Authoritative server โ most common for competitive games
- Peer-to-Peer: Direct connection between players โ simpler but less secure
- Listen Server: One player hosts โ common in co-op
Key Challenges: Latency, prediction, reconciliation, cheating prevention
Game Optimization
- Performance: Profile first, optimize bottlenecks โ CPU vs GPU vs memory
- Draw Calls: Batch sprites, use atlases, LOD (Level of Detail)
- Memory: Object pooling, asset streaming, garbage collection management
- Loading: Async loading, level streaming, preloading assets
Worked Examples
Example 1: Build a Complete Pong Game
Problem: Create a Pong game with paddles, ball physics, scoring, and AI opponent.
import pygame
pygame.init()
W, H = 800, 600
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
font = pygame.font.Font(None, 74)
# Paddle class
class Paddle:
def __init__(self, x, y, is_ai=False):
self.rect = pygame.Rect(x, y, 15, 100)
self.speed = 7
self.score = 0
self.is_ai = is_ai
def move(self, direction):
self.rect.y += direction * self.speed
self.rect.clamp_ip(screen.get_rect())
def ai_move(self, ball):
# Simple AI: track ball with maximum speed
if ball.rect.centery < self.rect.centery:
self.move(-1)
elif ball.rect.centery > self.rect.centery:
self.move(1)
# Ball class
class Ball:
def __init__(self):
self.rect = pygame.Rect(W//2, H//2, 15, 15)
self.vx = 5 * random.choice([-1, 1])
self.vy = 5 * random.choice([-1, 1])
def update(self, paddle1, paddle2):
self.rect.x += self.vx
self.rect.y += self.vy
# Bounce off top/bottom
if self.rect.top <= 0 or self.rect.bottom >= H:
self.vy = -self.vy
# Bounce off paddles
if self.rect.colliderect(paddle1.rect) and self.vx < 0:
self.vx = -self.vx * 1.05 # Speed up slightly
offset = (self.rect.centery - paddle1.rect.centery) / 50
self.vy += offset * 3
if self.rect.colliderect(paddle2.rect) and self.vx > 0:
self.vx = -self.vx * 1.05
offset = (self.rect.centery - paddle2.rect.centery) / 50
self.vy += offset * 3
# Score
if self.rect.left <= 0:
paddle2.score += 1
self.reset()
elif self.rect.right >= W:
paddle1.score += 1
self.reset()
def reset(self):
self.rect.center = (W//2, H//2)
self.vx = 5 * random.choice([-1, 1])
self.vy = 5 * random.choice([-1, 1])
# Game setup
paddle1 = Paddle(30, H//2 - 50)
paddle2 = Paddle(W-45, H//2 - 50, is_ai=True)
ball = Ball()
# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
if keys[pygame.K_w]: paddle1.move(-1)
if keys[pygame.K_s]: paddle1.move(1)
paddle2.ai_move(ball)
ball.update(paddle1, paddle2)
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255,255,255), paddle1.rect)
pygame.draw.rect(screen, (255,255,255), paddle2.rect)
pygame.draw.ellipse(screen, (255,255,255), ball.rect)
pygame.draw.aaline(screen, (255,255,255), (W//2, 0), (W//2, H))
score_text = font.render(f"{paddle1.score} - {paddle2.score}", True, (255,255,255))
screen.blit(score_text, (W//2 - score_text.get_width()//2, 20))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Example 2: Implement A* for Game Pathfinding
Problem: Create an A* pathfinding system for an RTS game where units navigate around obstacles.
Solution:
- Grid-based map with walkable/non-walkable tiles
- 8-directional movement (including diagonals)
- Diagonal cost = โ2 โ 1.414, orthogonal cost = 1
- Heuristic: Octile distance (optimal for 8-directional grids)
- Smooth path by removing unnecessary waypoints
f(n) = g(n) + h(n)
g(n) = cost from start to node n
h(n) = octile distance = max(dx, dy) + (โ2 - 1) ร min(dx, dy)
Example 3: Object Pool Pattern
Problem: Optimize bullet management in a shooter game that spawns thousands of bullets.
Solution: Pre-allocate bullet objects and reuse them instead of creating/destroying constantly.
class BulletPool:
def __init__(self, size=100):
self.pool = [Bullet() for _ in range(size)]
self.active = set()
self.available = set(self.pool)
def spawn(self, x, y, vx, vy):
if not self.available:
return None # Pool exhausted
bullet = self.available.pop()
bullet.activate(x, y, vx, vy)
self.active.add(bullet)
return bullet
def update(self, dt):
for bullet in list(self.active):
bullet.update(dt)
if bullet.is_expired():
bullet.deactivate()
self.active.remove(bullet)
self.available.add(bullet)
def draw(self, surface):
for bullet in self.active:
bullet.draw(surface)
# Performance: ~10x faster than creating new objects each time
Example 4: Implement Game Save System
Problem: Design a save system that serializes game state to disk with versioning.
Solution:
- JSON format: Human-readable, easy to debug
- Save slots: Multiple save slots with metadata (timestamp, level, screenshot)
- Auto-save: Periodic background saves
- Migration: Version field allows loading old saves with upgrade logic
Example 5: Particle System
Problem: Create a fire particle system with realistic behavior.
Solution:
- Emitter spawns 50 particles/second
- Each particle: position, velocity, lifetime, size, color
- Particles rise (negative gravity) with random spread
- Color transitions: white โ yellow โ orange โ red โ gray โ transparent
- Lifetime: 1-3 seconds with random variance
- GPU-accelerated using point sprites or instanced rendering
Practice Problems
Problem 1: Implement Spatial Hash for Collision
Create a spatial hash grid for efficient collision detection with many objects.
class SpatialHash:
def __init__(self, cell_size=64):
self.cell_size = cell_size
self.grid = {}
def _hash(self, x, y):
return (int(x // self.cell_size), int(y // self.cell_size))
def insert(self, obj):
min_cell = self._hash(obj.x, obj.y)
max_cell = self._hash(obj.x + obj.width, obj.y + obj.height)
for cx in range(min_cell[0], max_cell[0] + 1):
for cy in range(min_cell[1], max_cell[1] + 1):
key = (cx, cy)
if key not in self.grid:
self.grid[key] = set()
self.grid[key].add(obj)
def query(self, x, y, width, height):
min_cell = self._hash(x, y)
max_cell = self._hash(x + width, y + height)
results = set()
for cx in range(min_cell[0], max_cell[0] + 1):
for cy in range(min_cell[1], max_cell[1] + 1):
key = (cx, cy)
if key in self.grid:
results |= self.grid[key]
return results
Problem 2: Implement Quadtree
Build a quadtree for 2D spatial partitioning.
class Quadtree:
def __init__(self, boundary, capacity=4):
self.boundary = boundary # (x, y, w, h)
self.capacity = capacity
self.objects = []
self.divided = False
def insert(self, obj):
if not self._contains(self.boundary, obj):
return False
if len(self.objects) < self.capacity:
self.objects.append(obj)
return True
if not self.divided:
self._subdivide()
return (self.nw.insert(obj) or self.ne.insert(obj) or
self.sw.insert(obj) or self.se.insert(obj))
def _subdivide(self):
x, y, w, h = self.boundary
hw, hh = w/2, h/2
self.nw = Quadtree((x, y, hw, hh), self.capacity)
self.ne = Quadtree((x+hw, y, hw, hh), self.capacity)
self.sw = Quadtree((x, y+hh, hw, hh), self.capacity)
self.se = Quadtree((x+hw, y+hh, hw, hh), self.capacity)
self.divided = True
Problem 3: Implement Game Timer
Create a timer system supporting countdown, stopwatch, and cooldown mechanics.
class GameTimer:
def __init__(self):
self.timers = {}
def add_cooldown(self, name, duration):
self.timers[name] = {
'type': 'cooldown',
'duration': duration,
'start': time.time()
}
def is_ready(self, name):
if name not in self.timers:
return True
timer = self.timers[name]
if time.time() - timer['start'] >= timer['duration']:
return True
return False
def get_remaining(self, name):
if name not in self.timers:
return 0
timer = self.timers[name]
elapsed = time.time() - timer['start']
return max(0, timer['duration'] - elapsed)
def trigger(self, name):
if self.is_ready(name):
self.timers[name]['start'] = time.time()
return True
return False
Problem 4: Behavior Tree for NPC AI
Implement a simple behavior tree for an enemy NPC with patrol, chase, and attack behaviors.
class Node:
def tick(self, blackboard): return False
class Selector(Node):
def __init__(self, children):
self.children = children
def tick(self, bb):
for child in self.children:
if child.tick(bb): return True
return False
class Sequence(Node):
def __init__(self, children):
self.children = children
def tick(self, bb):
for child in self.children:
if not child.tick(bb): return False
return True
# Build tree: Enemy AI
tree = Selector([
Sequence([ # Attack behavior
IsPlayerInRange(radius=50),
Attack()
]),
Sequence([ # Chase behavior
IsPlayerInRange(radius=200),
ChasePlayer()
]),
Patrol() # Default: patrol
])
Problem 5: Implement Object Pooling
Create an object pool for efficiently managing frequently created/destroyed game objects.
class ObjectPool:
def __init__(self, factory, initial_size=10):
self._factory = factory
self._available = [factory() for _ in range(initial_size)]
self._in_use = set()
self._all = set(self._available)
def acquire(self):
if not self._available:
obj = self._factory()
self._all.add(obj)
else:
obj = self._available.pop()
self._in_use.add(obj)
return obj
def release(self, obj):
if obj in self._in_use:
obj.reset()
self._in_use.remove(obj)
self._available.append(obj)
@property
def active_count(self):
return len(self._in_use)
@property
def total_count(self):
return len(self._all)
Problem 6: Implement Screen Shake
Add a camera screen shake effect to a 2D game.
import random
import math
class ScreenShake:
def __init__(self, max_offset=20, decay=0.9):
self.max_offset = max_offset
self.decay = decay
self.offset = (0, 0)
self.trauma = 0
def shake(self, intensity=1.0):
self.trauma = min(1.0, self.trauma + intensity)
def update(self, dt):
self.trauma = max(0, self.trauma - dt * 1.5)
shake_amount = self.trauma ** 2 * self.max_offset
self.offset = (
random.uniform(-1, 1) * shake_amount,
random.uniform(-1, 1) * shake_amount
)
return self.offset
Problem 7: Implement Dialogue System
Create a branching dialogue system for an RPG.
@dataclass
class DialogueNode:
speaker: str
text: str
choices: List['DialogueChoice'] = field(default_factory=list)
next_node: Optional[str] = None
conditions: dict = field(default_factory=dict)
effects: dict = field(default_factory=dict)
@dataclass
class DialogueChoice:
text: str
next_node: str
conditions: dict = field(default_factory=dict)
class DialogueSystem:
def __init__(self, dialogue_data):
self.nodes = {k: DialogueNode(**v) for k, v in dialogue_data.items()}
self.variables = {}
self.current_node = None
def start(self, start_node):
self.current_node = start_node
def advance(self, choice_index=None):
node = self.nodes[self.current_node]
if choice_index is not None:
node = self.nodes[node.choices[choice_index].next_node]
elif node.next_node:
node = self.nodes[node.next_node]
else:
return None # End of dialogue
self.current_node = node
return node
Problem 8: Procedural Terrain Generation
Generate terrain heightmaps using Perlin noise.
import numpy as np
import random
def generate_perlin_noise(width, height, octaves=6, persistence=0.5, scale=1.0):
"""Generate 2D Perlin noise for terrain."""
noise = np.zeros((height, width))
amplitude = 1.0
frequency = scale
for o in range(octaves):
# Generate base gradient grid
grid_w = int(frequency) + 2
grid_h = int(frequency) + 2
gradients = np.random.uniform(-1, 1, (grid_h, grid_w, 2))
for y in range(height):
for x in range(width):
gx, gy = x * frequency / width, y * frequency / height
# Smooth interpolation
noise[y, x] += _interpolate(gx, gy, gradients) * amplitude
amplitude *= persistence
frequency *= 2
# Normalize to [0, 1]
noise = (noise - noise.min()) / (noise.max() - noise.min())
return noise
def _interpolate(x, y, gradients):
x0, y0 = int(x), int(y)
fx, fy = x - x0, y - y0
# Smoothstep
fx = fx * fx * (3 - 2 * fx)
fy = fy * fy * (3 - 2 * fy)
# Bilinear interpolation
return gradients[y0, x0, 0] # Simplified
Problem 9: Implement HUD System
Create a heads-up display with health bars, ammo counter, and minimap.
class HUD:
def __init__(self, screen_width, screen_height):
self.elements = []
def add_health_bar(self, x, y, width=200, height=20):
bar = HealthBar(x, y, width, height)
self.elements.append(bar)
def add_ammo_counter(self, x, y, font_size=36):
ammo = TextElement(x, y, "Ammo: 30/120", font_size)
self.elements.append(ammo)
def add_minimap(self, x, y, size=150, world_size=2000):
minimap = Minimap(x, y, size, world_size)
self.elements.append(minimap)
def draw(self, surface):
for element in self.elements:
element.draw(surface)
def update(self, game_state):
for element in self.elements:
element.update(game_state)
Problem 10: Implement Raycasting Engine
Create a simple raycasting 3D engine (Wolfenstein 3D style).
import pygame
import math
class Raycaster:
def __init__(self, screen, map_data):
self.screen = screen
self.map = map_data
self.width, self.height = map_data.shape
def cast_ray(self, px, py, angle):
"""DDA raycasting algorithm."""
sin_a = math.sin(angle)
cos_a = math.cos(angle)
# DDA algorithm
map_x, map_y = int(px), int(py)
delta_dist_x = abs(1 / cos_a) if cos_a != 0 else 1e30
delta_dist_y = abs(1 / sin_a) if sin_a != 0 else 1e30
# Step direction
step_x = 1 if cos_a > 0 else -1
step_y = 1 if sin_a > 0 else -1
side_dist_x = (map_x + 1 - px) * delta_dist_x if cos_a > 0 else (px - map_x) * delta_dist_x
side_dist_y = (map_y + 1 - py) * delta_dist_y if sin_a > 0 else (py - map_y) * delta_dist_y
# Walk until hitting wall
for _ in range(100):
if side_dist_x < side_dist_y:
side_dist_x += delta_dist_x
map_x += step_x
side = 0
else:
side_dist_y += delta_dist_y
map_y += step_y
side = 1
if self.map[map_y, map_x] > 0:
break
# Calculate distance
if side == 0:
dist = (map_x - px + (1 - step_x) / 2) / cos_a
else:
dist = (map_y - py + (1 - step_y) / 2) / sin_a
return max(dist, 0.01), side
def render(self, player_x, player_y, player_angle):
num_rays = self.screen.get_width()
for i in range(num_rays):
ray_angle = player_angle - math.pi/3 + (math.pi/3) * i / (num_rays/2)
dist, side = self.cast_ray(player_x, player_y, ray_angle)
# Fix fisheye
corrected_dist = dist * math.cos(ray_angle - player_angle)
# Wall height
wall_height = min(int(self.screen.get_height() / corrected_dist),
self.screen.get_height())
# Color based on distance and side
shade = max(50, min(255, int(255 / corrected_dist)))
color = (shade, shade, shade) if side == 0 else (shade//2, shade//2, shade//2)
x = i
y_start = (self.screen.get_height() - wall_height) // 2
pygame.draw.line(self.screen, color, (x, y_start),
(x, y_start + wall_height))