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.json file and merges them with defaults. Current configurable values include fov, sensitivity, volume, render_distance, and underwater_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 the Scene and Player instances.

  • 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:

  1. Seeding: Retrieving an existing seed from the SQLite database or generating a fresh procedural seed.

  2. Instantiating the Scene: Constructing a new Scene and World object.

  3. Pre-loading: Engaging a blocking loop that forces initial chunks to load, build, and mesh.

  4. 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:

  1. Event Polling (handle_events): Intercepts raw OS hardware events (mouse clicks, key presses, window closes). It delegates these events dynamically based on the current game_state (e.g., passing WASD inputs to the Player in-game, or slider drags to the Options Menu).

  2. Logic Updates (update): Advances internal systems. This increments the world_session_time, processes physics boundaries, and evaluates UI animations. Delta time is strictly capped to prevent extreme physics desynchronization during performance spikes.

  3. 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 the Scene to 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:

  1. Initialize Pygame and request an OpenGL 3.3 core context.

  2. Create a fullscreen window at the current desktop resolution.

  3. Create a ModernGL context and enable depth testing, face culling, and blending.

  4. Initialize the frame clock and engine timers.

  5. Load default configuration values, then override them from config.json.

  6. Initialize the texture atlas, shader pipeline, and audio mixer.

  7. 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!")