Implementation Tutorials and Replication Guides
This section provides step-by-step guides to replicate core Pyrite systems from scratch. Each tutorial assumes knowledge of the relevant documentation section and focuses on practical implementation.
Tutorial 1: Build a Terrain Generation System
Objective: Implement a procedural terrain generator using OpenSimplex noise that produces biome-based, deterministic worlds.
Time: 2 hours | Difficulty: Medium
Prerequisites: - Understand OpenSimplex noise (read terrain.rst) - Familiarity with NumPy (for array operations) - Python 3.9+
Step 1: Set Up Noise Generator
import hashlib
from opensimplex import OpenSimplex
import numpy as np
class TerrainGenerator:
def __init__(self, seed_string):
# Convert string seed to integer
hash_obj = hashlib.md5(seed_string.encode())
self.seed = int(hash_obj.hexdigest(), 16) % (2**31 - 1)
# Initialize noise generator
self.noise_gen = OpenSimplex(self.seed)
# Constants
self.CHUNK_SIZE = 48
self.STONE_LVL = 8
self.WATER_LVL = 5.6
def noise_2d(self, x, z, scale):
"""Sample 2D OpenSimplex noise"""
return self.noise_gen.noise2(x * scale, z * scale)
def noise_3d(self, x, y, z, scale):
"""Sample 3D OpenSimplex noise"""
return self.noise_gen.noise3(x * scale, y * scale, z * scale)
Step 2: Implement Height Generation (FBm)
def get_height_at_column(self, world_x, world_z):
"""Calculate terrain height using fractional Brownian motion"""
continentalness = 0
amplitude = 1.0
frequency = 0.005
max_amplitude = 0 # For normalization
# 4 octaves of noise
for octave in [1, 2, 4, 8]:
freq = frequency * octave
noise = self.noise_2d(world_x, world_z, freq)
continentalness += noise * amplitude
max_amplitude += amplitude
amplitude *= 0.5
# Normalize
continentalness /= max_amplitude
continentalness = np.clip(continentalness, -1.0, 1.0)
# Compute height based on continentalness
if continentalness < -0.2:
height = 5.6 + continentalness * 3
elif continentalness <= 0.1:
height = 5.6 + continentalness * 8
elif continentalness <= 0.3:
height = 6.0 + continentalness * 30
else:
height = 15.0 + continentalness * 50
# Add jaggedness
jaggedness = self.noise_2d(world_x, world_z, 0.04)
height += jaggedness * 4
return np.clip(height, 0, 256)
Step 3: Implement Biome Selection
BLOCK_IDS = {'GRASS': 1, 'DIRT': 2, 'STONE': 3, 'SAND': 4, 'SNOW': 5, 'WATER': 6}
def get_biome_at_column(self, world_x, world_z):
"""Determine biome from temperature and moisture"""
temperature = self.noise_2d(world_x, world_z, 0.01)
moisture = self.noise_2d(world_x, world_z, 0.015)
# Dithering for natural transitions
dither = self.noise_2d(world_x, world_z, 0.1) * 0.05
temperature += dither
moisture += dither
if temperature > 0.3 and moisture < -0.2:
return 'DESERT'
elif temperature < -0.2:
return 'SNOW'
elif moisture > 0.2:
return 'FOREST'
else:
return 'GRASSLAND'
Step 4: Generate Chunk Voxels
def generate_chunk(self, chunk_x, chunk_y, chunk_z):
"""Generate all voxels for a chunk"""
chunk_voxels = np.zeros((self.CHUNK_SIZE ** 3,), dtype=np.uint8)
for local_x in range(self.CHUNK_SIZE):
for local_z in range(self.CHUNK_SIZE):
world_x = chunk_x * self.CHUNK_SIZE + local_x
world_z = chunk_z * self.CHUNK_SIZE + local_z
# Get column data
height = self.get_height_at_column(world_x, world_z)
biome = self.get_biome_at_column(world_x, world_z)
# Fill column
for local_y in range(self.CHUNK_SIZE):
world_y = chunk_y * self.CHUNK_SIZE + local_y
voxel_index = local_x + local_z * self.CHUNK_SIZE + local_y * self.CHUNK_SIZE ** 2
if world_y < self.STONE_LVL:
chunk_voxels[voxel_index] = BLOCK_IDS['STONE']
elif world_y < height:
if world_y <= height - 3:
chunk_voxels[voxel_index] = BLOCK_IDS['STONE']
elif world_y <= height - 1:
chunk_voxels[voxel_index] = BLOCK_IDS['DIRT']
else:
# Surface block
if biome == 'DESERT':
chunk_voxels[voxel_index] = BLOCK_IDS['SAND']
elif biome == 'SNOW':
chunk_voxels[voxel_index] = BLOCK_IDS['SNOW']
else:
chunk_voxels[voxel_index] = BLOCK_IDS['GRASS']
elif world_y < self.WATER_LVL:
chunk_voxels[voxel_index] = BLOCK_IDS['WATER']
else:
chunk_voxels[voxel_index] = 0 # AIR
return chunk_voxels
Verification: - Generate a test chunk at (0, 0, 0) and (1, 0, 1) - Verify same seed produces identical chunks - Visualize height map (different seeds = different patterns)
Tutorial 2: Implement Greedy Meshing
Objective: Build a greedy meshing algorithm to reduce polygon count by 90%+.
Time: 3 hours | Difficulty: Hard
Prerequisites: - Understand bit-packing (read meshes.rst) - Comfortable with 3D array indexing - Numba JIT knowledge (optional but helpful)
Step 1: Check Transparency
TRANSPARENT_BLOCKS = {0, 6, 7, 8, 9} # AIR, WATER, GLASS, LEAVES, VINE
def is_transparent(voxel_id):
return voxel_id in TRANSPARENT_BLOCKS
def is_solid(voxel_id):
return not is_transparent(voxel_id)
Step 2: Implement AO Calculation
def get_ao(chunk_voxels, local_y, local_z):
"""Calculate ambient occlusion for a corner"""
# Check 2x2 block neighbors
corners = [
(local_y - 1, local_z - 1),
(local_y, local_z - 1),
(local_y - 1, local_z),
(local_y, local_z),
]
ao_count = 0
for y, z in corners:
if 0 <= y < 48 and 0 <= z < 48:
voxel_id = chunk_voxels[y, z] # Simplified index
if not is_transparent(voxel_id):
ao_count += 1
return min(ao_count, 3) # Clamp to 0-3
Step 3: Build Greedy Quads
def build_greedy_quad(chunk_voxels, plane='Y'):
"""Extract greedy quads from a plane (simplified for XY plane)"""
vertices = []
# Build 2D mask
mask = np.zeros((48, 48), dtype=bool)
for y in range(48):
for z in range(48):
voxel_id = chunk_voxels[y, z]
if is_solid(voxel_id):
mask[y, z] = True
processed = set()
for y in range(48):
for z in range(48):
if (y, z) in processed or not mask[y, z]:
continue
# Find greedy width (extend along Z)
width = 1
while z + width < 48 and mask[y, z + width]:
width += 1
# Find greedy height (extend along Y)
height = 1
valid = True
while y + height < 48 and valid:
for z_check in range(z, z + width):
if not mask[y + height, z_check]:
valid = False
break
if valid:
height += 1
else:
break
# Mark as processed
for dy in range(height):
for dz in range(width):
processed.add((y + dy, z + dz))
mask[y + dy, z + dz] = False
# Add quad vertices
# (simplified: just record quad dimensions)
vertices.append({
'y': y, 'z': z,
'width': width, 'height': height
})
return vertices
Verification: - Mesh a simple grid of cubes - Count triangles (should be ~2/3 of naive implementation) - Visual check: uniform color cubes should render as expected
Tutorial 3: Implement AABB Collision Detection
Objective: Build player-terrain collision detection with axis-separated movement.
Time: 2 hours | Difficulty: Medium
Prerequisites: - Basic 3D math (vectors, AABBs) - Understand axis-separated movement (read player.rst)
Step 1: Define AABB
import glm
class AABB:
def __init__(self, min_pos, max_pos):
self.min = min_pos # glm.vec3
self.max = max_pos # glm.vec3
def intersects(self, other):
"""Check intersection with another AABB"""
return (self.min.x < other.max.x and self.max.x > other.min.x and
self.min.y < other.max.y and self.max.y > other.min.y and
self.min.z < other.max.z and self.max.z > other.min.z)
def get_voxel_aabbs(self, world):
"""Get all solid voxel AABBs within and around this AABB"""
min_x, min_y, min_z = floor(self.min.x), floor(self.min.y), floor(self.min.z)
max_x, max_y, max_z = ceil(self.max.x), ceil(self.max.y), ceil(self.max.z)
voxel_boxes = []
for vx in range(int(min_x), int(max_x) + 1):
for vy in range(int(min_y), int(max_y) + 1):
for vz in range(int(min_z), int(max_z) + 1):
voxel_id = world.get_voxel(vx, vy, vz)
if not is_transparent(voxel_id):
voxel_aabb = AABB(
glm.vec3(vx, vy, vz),
glm.vec3(vx + 1, vy + 1, vz + 1)
)
voxel_boxes.append(voxel_aabb)
return voxel_boxes
Step 2: Resolve Collisions Per-Axis
def resolve_axis(axis, player_pos, player_aabb, velocity, world):
"""Resolve collision for single axis"""
# Compute new position
old_pos = player_pos.copy()
player_pos[axis] += velocity[axis]
# Get new AABB
new_aabb = AABB(
player_pos - player_half_extents,
player_pos + player_half_extents
)
# Check collisions
voxel_boxes = new_aabb.get_voxel_aabbs(world)
for voxel_box in voxel_boxes:
if new_aabb.intersects(voxel_box):
# Collision detected: snap to voxel boundary
if velocity[axis] > 0: # Moving positive
player_pos[axis] = voxel_box.min[axis] - player_half_extents[axis]
else: # Moving negative
player_pos[axis] = voxel_box.max[axis] + player_half_extents[axis]
# Handle ground detection (for Y-axis)
if axis == 1 and velocity[axis] < 0: # Landing downward
on_ground = True
velocity[axis] = 0
break
return player_pos, velocity
Step 3: Integrate into Player Update
def move_and_collide(player, delta_time, world):
"""Apply velocity to player with collision"""
velocity = player.velocity.copy()
# Apply gravity
velocity.y += GRAVITY * delta_time
# Resolve per-axis
for axis in [0, 1, 2]: # X, Y, Z
player.feet_pos, velocity = resolve_axis(
axis, player.feet_pos, player.get_aabb(), velocity, world
)
player.velocity = velocity
player.position = player.feet_pos + glm.vec3(0, PLAYER_EYE_HEIGHT, 0)
Verification: - Place player in mid-air above ground - Verify gravity pulls down and stops at ground - Walk into walls (should stop, not clip through) - Jump and fall (should stick to ground when landing)
Tutorial 4: Implement Inventory and Crafting
Objective: Build a 41-slot inventory with drag-drop UI and crafting recipes.
Time: 1.5 hours | Difficulty: Easy
Prerequisites: - UI components (read ui.rst) - Basic event handling
Step 1: Inventory Data Structure
NUM_SLOTS = 41
HOTBAR_SIZE = 9
MAX_STACK = 64
class Inventory:
def __init__(self):
self.slots = [0] * NUM_SLOTS # 0 = empty, 1-255 = voxel ID
self.counts = [0] * NUM_SLOTS # Stack size
self.selected_slot = 0 # Hotbar index
def add_item(self, voxel_id):
"""Add item to inventory, stack if possible"""
# Try existing stacks
for i in range(NUM_SLOTS):
if self.slots[i] == voxel_id and self.counts[i] < MAX_STACK:
self.counts[i] += 1
return True
# Add to empty slot
for i in range(NUM_SLOTS):
if self.slots[i] == 0:
self.slots[i] = voxel_id
self.counts[i] = 1
return True
return False # Inventory full
def remove_item(self, slot_index, count=1):
"""Remove items from slot"""
if self.counts[slot_index] >= count:
self.counts[slot_index] -= count
if self.counts[slot_index] == 0:
self.slots[slot_index] = 0
return True
return False
Step 2: Crafting Recipes
RECIPES = {
# (slot36, slot37, slot38, slot39) → (output_id, count)
(WOOD, 0, 0, 0): (WOOD_PLANK, 4),
(WOOD_PLANK, WOOD_PLANK, STICK, 0): (WOODEN_PICKAXE, 1),
# ... more recipes
}
def update_crafting(inventory):
"""Check crafting grid and update output"""
inputs = (
inventory.slots[36],
inventory.slots[37],
inventory.slots[38],
inventory.slots[39],
)
if inputs in RECIPES:
output_id, count = RECIPES[inputs]
inventory.slots[40] = output_id
inventory.counts[40] = count
else:
inventory.slots[40] = 0
inventory.counts[40] = 0
Step 3: Drag-Drop UI Logic
class InventoryUI:
def __init__(self, inventory):
self.inventory = inventory
self.dragging_from = None
self.dragging_item_id = 0
self.dragging_count = 0
def on_click(self, slot_index, button):
if button == 1: # LClick
if self.dragging_from is None:
# Start drag
self.dragging_from = slot_index
self.dragging_item_id = self.inventory.slots[slot_index]
self.dragging_count = self.inventory.counts[slot_index]
else:
# Drop on target
self.swap_or_merge(slot_index)
self.dragging_from = None
elif button == 3: # RClick
# Split stack
if self.inventory.counts[slot_index] > 1:
half = self.inventory.counts[slot_index] // 2
self.inventory.counts[slot_index] -= half
# Create shadow item...
def swap_or_merge(self, target_slot):
"""Merge dragged items into target slot"""
drag_slot = self.dragging_from
if self.inventory.slots[target_slot] == self.inventory.slots[drag_slot]:
# Same item: merge
space = MAX_STACK - self.inventory.counts[target_slot]
transfer = min(space, self.inventory.counts[drag_slot])
self.inventory.counts[target_slot] += transfer
self.inventory.counts[drag_slot] -= transfer
if self.inventory.counts[drag_slot] == 0:
self.inventory.slots[drag_slot] = 0
else:
# Different items: swap
self.inventory.slots[drag_slot], self.inventory.slots[target_slot] = (
self.inventory.slots[target_slot], self.inventory.slots[drag_slot]
)
self.inventory.counts[drag_slot], self.inventory.counts[target_slot] = (
self.inventory.counts[target_slot], self.inventory.counts[drag_slot]
)
Verification: - Add items to inventory - Verify stacking works - Drag items around, test merge/swap - Check crafting recipe updates output slot