Core Engine Flow
This document describes the runtime flow of the Pyrite engine, focusing on how the application initializes, updates, and renders each frame. It is intended as a system-level guide rather than a line-by-line walkthrough of main.py.
Engine Initialization
Pyrite boots by constructing a fullscreen OpenGL context and then initializing all major subsystems. The startup sequence is designed so that the render thread can remain responsive while heavier tasks are deferred or executed in background queues.
Multimedia & Context: Initializes Pygame and configures a hardware-accelerated ModernGL context. It requests a core OpenGL profile with depth testing, face culling, and alpha blending enabled.
Configuration Management: Loads user preferences from a local
config.jsonfile and merges them with defaults. Current configurable values includefov,sensitivity,volume,render_distance, andunderwater_tint.Subsystem Instantiation: Bootstraps all major managers and systems:
Textures: Loads and binds image assets to the GPU.Player: Initializes the physics and camera entity.Sounds: Interfaces with the Pygame audio mixer.ShaderProgram: Compiles and manages GLSL shaders.UI: Prepares all interactive menus (Main Menu, Pause, Options).
State Management
Pyrite operates using a robust finite state machine to delegate logic and rendering. The active state is stored in self.game_state and seamlessly transitions between the following modes:
MAIN_MENU: The front-end title screen and world selection interface.LOADING: A blocking transition state that flushes the event queue while heavy procedural generation and async database reads occur.IN_GAME: The active 3D voxel simulation, handing control over to theSceneandPlayerinstances.INVENTORY: Pauses player movement and frees the mouse cursor to allow for drag-and-drop item management.PAUSED: Suspends background logic and draws a darkened UI overlay, allowing players to adjust settings or exit the session.OPTIONS: Renders the configuration sliders, saving changes to disk upon exit.
World Generation and Sessions
When a player selects a world, init_game_session() is invoked. This process ensures a clean environment by:
Seeding: Retrieving an existing seed from the SQLite database or generating a fresh procedural seed.
Instantiating the Scene: Constructing a new
SceneandWorldobject.Pre-loading: Engaging a blocking loop that forces initial chunks to load, build, and mesh.
Safety Flushing: Discarding buffered inputs and relative mouse movements to prevent sudden camera snapping when the game begins.
During this phase, a minimal render_loading_screen() UI overlay is repeatedly drawn to prevent the operating system from flagging the application as unresponsive.
The Main Loop
The core execution cycle is governed by the run() method. Until the application halts, it continuously cycles through three primary stages:
Event Polling (
handle_events): Intercepts raw OS hardware events (mouse clicks, key presses, window closes). It delegates these events dynamically based on the currentgame_state(e.g., passing WASD inputs to the Player in-game, or slider drags to the Options Menu).Logic Updates (
update): Advances internal systems. This increments theworld_session_time, processes physics boundaries, and evaluates UI animations. Delta time is strictly capped to prevent extreme physics desynchronization during performance spikes.Rendering (
render): Clears the hardware frame buffer and dispatches draw calls. If in a menu, it disables depth testing to draw 2D quads. If in-game, it directs theSceneto perform its complex multi-pass rendering pipeline.
Application Control
Safely exiting the application is handled by the quit_game() sequence. It breaks the main execution loop, triggers the world to synchronously serialize all remaining chunks to the SQLite database, and cleanly releases multimedia hardware handles back to the OS.
Detailed Initialization Sequence
Step-by-Step Pyrite Startup:
Pyrite boots by creating a fullscreen OpenGL context, then layering the engine subsystems on top of it. The actual source initializes a fixed fullscreen window using the current desktop resolution, and the configuration file is only used for gameplay options that affect camera, audio, and render streaming.
The key startup stages are:
Initialize Pygame and request an OpenGL 3.3 core context.
Create a fullscreen window at the current desktop resolution.
Create a ModernGL context and enable depth testing, face culling, and blending.
Initialize the frame clock and engine timers.
Load default configuration values, then override them from
config.json.Initialize the texture atlas, shader pipeline, and audio mixer.
Instantiate the player, the menu system, and the main game state container.
The configuration keys currently supported are:
fov: Vertical field of view in degrees.sensitivity: Mouse look sensitivity.volume: Global sound volume.render_distance: Number of chunks loaded around the player.underwater_tint: Whether a blue tint is applied underwater.
Detailed State Machine
State Transitions and Logic:
stateDiagram-v2
direction LR
GameOpens --> MAIN_MENU
MAIN_MENU --> WORLD_SELECT : Play
MAIN_MENU --> OPTIONS : Settings
MAIN_MENU --> GameCloses : Quit
WORLD_SELECT --> LOADING : Load/Create World
state LOADING {
[*] --> GenerateChunks
GenerateChunks --> CompileNumba
CompileNumba --> Ready
}
LOADING --> IN_GAME : Ready
IN_GAME --> PAUSED : ESC
IN_GAME --> INVENTORY : E
INVENTORY --> IN_GAME : E
PAUSED --> IN_GAME : Resume
PAUSED --> OPTIONS : Settings
OPTIONS --> PAUSED : Back
State Handler Pseudocode:
def handle_state(state):
if state == 'MAIN_MENU':
main_menu.handle_events()
main_menu.update()
elif state == 'LOADING':
# Blocking loop: load chunks until ready
while not loading_complete:
# Generate/fetch chunks
# Mesh chunks
# Render loading screen UI
check_loading_progress()
elif state == 'IN_GAME':
player.handle_events()
world.update()
scene.update()
elif state == 'PAUSED':
pause_menu.handle_events()
pause_menu.update()
elif state == 'OPTIONS':
options_menu.handle_events()
options_menu.update()
The Main Game Loop (Pseudocode)
High-Level Loop Structure:
def run(self):
running = True
while running:
# 1. FRAME TIMING
frame_start_time = time.time()
delta_time = self.clock.tick(60) / 1000.0 # Cap at 60 FPS
delta_time = min(delta_time, 0.033) # Clamp to 33ms max to prevent physics explosion
# 2. EVENT HANDLING
running = self.handle_events() # Returns False if quit requested
# 3. STATE-SPECIFIC UPDATES
if self.game_state == 'IN_GAME':
self.update_in_game(delta_time)
elif self.game_state == 'PAUSED':
# No logic updates while paused
pass
elif self.game_state == 'LOADING':
self.update_loading(delta_time)
# 4. RENDER
self.render()
# 5. DISPLAY
pygame.display.flip()
Detailed Event Handling
Event Loop:
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False # Exit main loop
# Global events (all states)
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_F11:
self.toggle_fullscreen()
elif event.key == pygame.K_F3:
self.show_debug_overlay = not self.show_debug_overlay
# State-specific events
if self.game_state == 'MAIN_MENU':
self.main_menu.handle_event(event)
elif self.game_state == 'IN_GAME':
self.player.handle_event(event)
# Pause or inventory
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.game_state = 'PAUSED'
elif event.key == pygame.K_e:
# Toggle inventory (layer on top of IN_GAME)
self.show_inventory = not self.show_inventory
# Mining/placing
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # LClick
self.start_mining()
elif event.button == 3: # RClick
self.place_block()
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
self.stop_mining()
elif self.game_state == 'PAUSED':
self.pause_menu.handle_event(event)
elif self.game_state == 'OPTIONS':
self.options_menu.handle_event(event)
return True # Continue main loop
Detailed Update Step (In-Game)
Update Sequence (per frame):
def update_in_game(self, delta_time):
# 1. PLAYER UPDATE
self.player.update(delta_time)
# 2. WORLD STREAMING
player_chunk = self.world.get_chunk_containing(self.player.position)
self.world.update_loaded_chunks(player_chunk, RENDER_DISTANCE)
# 3. QUEUE PROCESSING (budgeted per frame)
# Pull from load_queue, build_queue, mesh_queue
self.process_mesh_queue(limit=MAIN_THREAD_MESH_PROCESS_LIMIT_INGAME)
self.process_chunk_queue(limit=MAIN_THREAD_CHUNK_PROCESS_LIMIT_INGAME)
# 4. VOXEL INTERACTIONS (mining/placing results)
if self.mining_duration > 0:
self.mining_duration += delta_time
if self.mining_duration >= self.current_block_hardness:
self.voxel_handler.remove_voxel(self.target_block_pos)
self.mining_duration = 0
# 5. SCENE UPDATE
self.scene.update(delta_time)
# 6. LIGHTING UPDATES (from block changes)
# Already queued during voxel_handler calls
# 7. UI ANIMATION
self.hud.update(delta_time)
if self.show_inventory:
self.inventory_ui.update(delta_time)
Detailed Render Step
Render Sequence (per frame):
def render(self):
# 1. CLEAR FRAMEBUFFER
self.ctx.clear(0.53, 0.81, 0.92) # Sky blue
self.ctx.clear_depth(1.0)
# 2. IN-GAME RENDERING (if applicable)
if self.game_state == 'IN_GAME':
self.ctx.enable(moderngl.DEPTH_TEST)
self.scene.render(self.ctx, self.shader_program)
# 3. DISABLE DEPTH FOR UI
self.ctx.disable(moderngl.DEPTH_TEST)
# 4. RENDER HUD OVERLAY
self.hud.render(self.ctx, self.shader_program)
# 5. RENDER INVENTORY (if open)
if self.show_inventory:
self.inventory_ui.render(self.ctx, self.shader_program)
# 6. RENDER DEBUG OVERLAY
if self.show_debug_overlay:
self.debug_overlay.render(self.ctx, self.shader_program)
# 3. MENU RENDERING
elif self.game_state == 'MAIN_MENU':
self.ctx.disable(moderngl.DEPTH_TEST)
self.main_menu.render(self.ctx, self.shader_program)
elif self.game_state == 'PAUSED':
# Render world darkened in background
self.ctx.enable(moderngl.DEPTH_TEST)
self.scene.render(self.ctx, self.shader_program)
# Dim overlay
self.ctx.disable(moderngl.DEPTH_TEST)
render_dim_quad((0, 0), (2, 2), (0, 0, 0, 0.5))
# Pause menu on top
self.pause_menu.render(self.ctx, self.shader_program)
elif self.game_state == 'LOADING':
self.ctx.disable(moderngl.DEPTH_TEST)
render_loading_screen(self.loading_progress)
Scene Rendering Pipeline
Scene.render() Detail (in-game world rendering):
def render(self, ctx, shader_program):
# 1. FRUSTUM CULLING
visible_chunks = shader_program.frustum_cull(
self.camera,
self.world.active_chunks
)
# 2. PREPARE SHADER UNIFORMS
shader_program.bind_matrices(
projection=self.camera.projection_matrix,
view=self.camera.view_matrix,
model=np.identity(4)
)
shader_program.bind_uniforms(
u_sun_direction=self.world.sun_direction,
u_time=self.world.session_time,
u_fog_density=self.config['render_distance'] * 48 * 0.7,
u_texture_array=self.textures.texture_array,
u_texture_map=self.textures.texture_map,
bg_color=(0.53, 0.81, 0.92) # Sky color
)
# 3. RENDER OPAQUE CHUNKS (depth write enabled)
ctx.enable(moderngl.DEPTH_TEST)
for chunk in visible_chunks:
if chunk.mesh and not chunk.mesh.is_hidden_by_occlusion_query:
chunk.mesh.render() # Render opaque faces
# 4. RENDER WATER (transparency, depth write disabled)
ctx.enable(moderngl.BLEND)
for chunk in visible_chunks:
if chunk.mesh:
chunk.mesh.render_water() # Render water faces only
ctx.disable(moderngl.BLEND)
# 5. RENDER SKY
self.sky.render(ctx, shader_program)
# 6. RENDER CLOUDS
self.clouds.render(ctx, shader_program)
# 7. RENDER ITEMS (dropped entities)
for item in self.world.items:
item.render(ctx, shader_program)
# 8. RENDER VOXEL MARKER (target block outline)
if self.voxel_marker.visible:
self.voxel_marker.render(ctx, shader_program)
# 9. RENDER HELD ITEM (in camera view)
self.held_item_mesh.render(ctx, shader_program)
World Session Initialization (LOADING state)
init_game_session() Pseudocode:
def init_game_session(self, world_name):
# 1. CLEAR PREVIOUS WORLD
if self.world:
self.world.unload_all_chunks() # Flush to DB
# 2. LOAD/CREATE WORLD
self.world = World(world_name)
self.world.load_metadata() # Load seed, spawn point
# 3. CREATE SCENE
self.scene = Scene(self.world)
# 4. LOAD PLAYER
player_data = self.world.load_player_data()
self.player = Player(self.world)
self.player.feet_pos = glm.vec3(player_data['x'], player_data['y'], player_data['z'])
self.player.inventory = player_data['inventory']
self.player.health = player_data['health']
# 5. INITIALIZE VOXEL HANDLER
self.voxel_handler = VoxelHandler(self.world)
# 6. CHUNK PRELOADING (blocking loop)
self.game_state = 'LOADING'
initial_chunks_needed = (RENDER_DISTANCE * 2) ** 2
chunks_loaded = 0
while chunks_loaded < initial_chunks_needed:
# Request chunk loads
for chunk_coord in self.world.get_chunks_near_player(self.player, RENDER_DISTANCE):
if not self.world.chunk_exists(chunk_coord) and not self.world.loading.contains(chunk_coord):
self.world.request_chunk_load(chunk_coord)
# Process queue (blocking)
self.process_mesh_queue(limit=64) # Load aggressively during init
self.process_chunk_queue(limit=10)
chunks_loaded = len(self.world.active_chunks)
loading_percent = int(100 * chunks_loaded / initial_chunks_needed)
# Render loading screen
self.render_loading_screen(loading_percent)
pygame.display.flip()
# 7. SAFETY FLUSH (clear buffered inputs)
pygame.event.clear()
pygame.mouse.get_rel() # Consume any accumulated mouse movement
# 8. TRANSITION TO GAMEPLAY
self.game_state = 'IN_GAME'
self.player_ready = True
Shutdown and Cleanup
quit_game() Pseudocode:
def quit_game(self):
print("Saving world...")
# 1. SAVE PLAYER STATE
if self.player:
player_data = {
'x': self.player.feet_pos.x,
'y': self.player.feet_pos.y,
'z': self.player.feet_pos.z,
'yaw': self.player.yaw,
'pitch': self.player.pitch,
'health': self.player.health,
'hunger': self.player.hunger,
'oxygen': self.player.oxygen,
'inventory': self.player.inventory,
'inventory_counts': self.player.inventory_counts,
}
self.world.save_player_data(player_data)
# 2. UNLOAD ALL CHUNKS (saves to DB)
if self.world:
self.world.unload_all_chunks()
# 3. CLOSE DATABASE
if hasattr(self, 'db'):
self.db.close()
# 4. RELEASE GPU RESOURCES
if hasattr(self, 'vbo_pool'):
for vbo in self.vbo_pool:
vbo.release()
# 5. CLEANUP AUDIO
if hasattr(self, 'sounds'):
pygame.mixer.stop()
# 6. CLOSE PYGAME
pygame.quit()
print("World saved. Goodbye!")