Player Systems

This document details player control schemes, AABB collision detection, physics simulations, and survival mechanics (health, hunger, oxygen).

Player Class Hierarchy

Player (inherits from Camera)

The player object manages:

  • Position, velocity, view direction

  • Physics (gravity, jump, collisions)

  • Input handling (keyboard, mouse)

  • Survival stats (health, hunger, oxygen)

  • Inventory and held item

Architecture:

class Player(Camera):
    __init__(self, world, start_pos=(0, 50, 0)):
        # Inherited from Camera
        self.position: glm.vec3                    # Camera position (eye)
        self.yaw, self.pitch: float                # Rotation angles (radians)

        # Physics state
        self.feet_pos: glm.vec3                    # Position at feet (for collisions)
        self.velocity: glm.vec3                    # Movement velocity
        self.on_ground: bool                       # Contact with solid block
        self.in_water: bool                        # Submerged
        self.head_in_water: bool                   # Head above surface

        # Survival stats
        self.health: float                         # 0-20
        self.hunger: float                         # 0-20
        self.oxygen: float                         # 0-20
        self.highest_y: float                       # For fall damage calculation

        # Inventory
        self.inventory: List[int]                  # 41 slots, voxel IDs
        self.inventory_counts: List[int]           # Stack sizes (1-64)
        self.hotbar_index: int                     # Selected slot (0-8)

        # Mode
        self.game_mode: str                        # 'SURVIVAL' or 'CREATIVE'

        # Animation
        self.step_counter: float                   # For view bobbing, footsteps
        self.held_item_swing: float                # 0-1, for swing animation

        # Input state
        self.keys_pressed: dict                    # Keys currently held

Coordinate System

Feet Position vs. Eye Position

  • feet_pos: Base of player bounding box (Y=0 of AABB)

  • position (inherited): Eye level = feet_pos.y + EYE_HEIGHT

PLAYER_WIDTH = 0.6         # XZ diameter
PLAYER_HEIGHT = 1.8        # Total height
PLAYER_HALF_W = 0.3        # Half-width for AABB
PLAYER_EYE_HEIGHT = 1.6    # Distance from feet to eyes

# Calculate AABB
def get_aabb():
    half_w = PLAYER_HALF_W
    return (
        feet_pos.x - half_w, feet_pos.y,     feet_pos.z - half_w,  # Min
        feet_pos.x + half_w, feet_pos.y + PLAYER_HEIGHT, feet_pos.z + half_w  # Max
    )

Controls and Input Handling

Keyboard Input (per update, based on held keys)

Key

Survival Mode

Creative Mode

W

Move forward

Move forward

A

Move left

Move left

S

Move backward

Move backward

D

Move right

Move right

Space

Jump (if grounded)

Move up (fly)

LShift

Toggle sprint

Move down (fly)

Mouse

Look around

Look around

LClick

Mine block

Destroy block

RClick

Place block

Place block (fill)

E

Toggle inventory

Toggle inventory

Scroll

Select hotbar

Select hotbar

1-9

Select hotbar

Select hotbar

Movement Calculation (Survival Mode)

def update_movement(delta_time):
    # Compute move direction (camera-relative)
    direction = glm.vec3(0)

    if keys['W']: direction += get_forward_vector()
    if keys['S']: direction -= get_forward_vector()
    if keys['A']: direction -= get_right_vector()
    if keys['D']: direction += get_right_vector()

    # Sprint multiplier
    if keys['LShift'] and on_ground:
        sprint_mult = PLAYER_SPRINT_MULTIPLIER  # 1.5
        is_sprinting = True
    else:
        sprint_mult = 1.0
        is_sprinting = False

    # Apply movement speed
    if len(direction) > 0:
        direction = normalize(direction)
        velocity.x += direction.x * PLAYER_SPEED * sprint_mult * delta_time
        velocity.z += direction.z * PLAYER_SPEED * sprint_mult * delta_time

        # View bobbing (sine wave based on distance traveled)
        step_counter += length(glm.vec2(velocity.x, velocity.z)) * delta_time

    # Jump (only on ground)
    if keys['Space'] and on_ground:
        velocity.y = JUMP_VELOCITY
        on_ground = False

    # Creative fly mode
    if game_mode == 'CREATIVE':
        if keys['Space']:
            velocity.y = PLAYER_SPEED * 5  # Upward
        if keys['LShift']:
            velocity.y = -PLAYER_SPEED * 5  # Downward

Mouse Input (Camera Control)

def handle_mouse_motion(rel_x, rel_y, sensitivity):
    # rel_x, rel_y from pygame.mouse.get_rel()

    # Yaw (left-right, X-axis rotation)
    self.yaw -= rel_x * sensitivity

    # Pitch (up-down, Y-axis rotation)
    self.pitch -= rel_y * sensitivity

    # Clamp pitch to ±90 degrees to prevent over-rotation
    self.pitch = glm.clamp(self.pitch, -glm.pi() / 2, glm.pi() / 2)

    # Update camera view matrix accordingly

Physics: Gravity and Velocity

Applied each update:

def apply_gravity(delta_time):
    if not on_ground:
        # Fall acceleration
        velocity.y += GRAVITY * delta_time  # GRAVITY = -0.000025

    if in_water:
        # Water buoyancy (reduced gravity)
        velocity.y += GRAVITY * PLAYER_UNDERWATER_GRAVITY_MULTIPLIER * delta_time
        # Also apply drag to vertical movement
        velocity.y *= (1.0 - PLAYER_VERTICAL_WATER_DRAG)

    # Terminal velocity (optional clamping to prevent extreme speeds)
    velocity.y = max(velocity.y, -0.1)

Movement and Collision Resolution:

Movement is axis-separated to enable smooth wall-sliding:

def move_and_collide(delta_time):
    # Apply velocity to get new position candidates
    new_pos = feet_pos + velocity * delta_time

    # Resolve collisions per-axis
    resolve_axis('X', feet_pos, new_pos)
    resolve_axis('Y', feet_pos, new_pos)
    resolve_axis('Z', feet_pos, new_pos)

    # Update final position
    feet_pos = new_pos
    position = feet_pos + glm.vec3(0, PLAYER_EYE_HEIGHT, 0)

AABB Collision Detection

Axis-Aligned Bounding Box (AABB)

def resolve_axis(axis, current_pos, new_pos):
    # Get AABB after movement
    aabb_min, aabb_max = get_aabb_at(new_pos)

    # Scan all voxels within and around AABB
    for vx in range(floor(aabb_min.x), ceil(aabb_max.x)):
        for vy in range(floor(aabb_min.y), ceil(aabb_max.y)):
            for vz in range(floor(aabb_min.z), ceil(aabb_max.z)):
                voxel_id = world.get_voxel(vx, vy, vz)

                # Skip transparent voxels (water allows movement)
                if is_transparent(voxel_id):
                    continue

                # Check AABB intersection
                voxel_box = (vx, vy, vz, vx+1, vy+1, vz+1)

                if aabb_intersect(aabb_min, aabb_max, voxel_box):
                    # Collision: snap position to voxel boundary
                    if axis == 'X':
                        if velocity.x > 0:  # Moving +X
                            new_pos.x = voxel_box[0] - (aabb_max.x - current_pos.x)
                        else:  # Moving -X
                            new_pos.x = voxel_box[3] + (current_pos.x - aabb_min.x)
                        velocity.x = 0

                    elif axis == 'Y':
                        if velocity.y > 0:  # Moving +Y
                            new_pos.y = voxel_box[1] - (aabb_max.y - current_pos.y)
                        else:  # Moving -Y
                            new_pos.y = voxel_box[4] + (current_pos.y - aabb_min.y)
                            on_ground = True
                        velocity.y = 0

                    elif axis == 'Z':
                        if velocity.z > 0:  # Moving +Z
                            new_pos.z = voxel_box[2] - (aabb_max.z - current_pos.z)
                        else:  # Moving -Z
                            new_pos.z = voxel_box[5] + (current_pos.z - aabb_min.z)
                        velocity.z = 0

AABB Intersection Test:

def aabb_intersect(aabb1_min, aabb1_max, aabb2_min, aabb2_max):
    # No separation on any axis = collision
    return (aabb1_min.x < aabb2_max.x and aabb1_max.x > aabb2_min.x and
            aabb1_min.y < aabb2_max.y and aabb1_max.y > aabb2_min.y and
            aabb1_min.z < aabb2_max.z and aabb1_max.z > aabb2_min.z)

Water Physics

When in water:

def update_in_water():
    # Check if feet_pos in water voxel
    voxel = world.get_voxel(int(feet_pos.x), int(feet_pos.y), int(feet_pos.z))

    if voxel == WATER:
        in_water = True

        # Check if head in water (for oxygen drain)
        head_voxel = world.get_voxel(int(position.x), int(position.y), int(position.z))
        head_in_water = (head_voxel == WATER)

        # Horizontal drag
        velocity.x *= PLAYER_WATER_DRAG_MULTIPLIER  # 0.5
        velocity.z *= PLAYER_WATER_DRAG_MULTIPLIER

        # Vertical drag
        velocity.y *= (1.0 - PLAYER_VERTICAL_WATER_DRAG)  # Additional damping
    else:
        in_water = False
        head_in_water = False

Dolphin Leap (jump near surface):

def try_jump_in_water():
    if in_water and keys['Space']:
        # Check if near surface (head not in water)
        if not head_in_water:
            # Apply upward boost
            velocity.y = JUMP_VELOCITY * PLAYER_DOLPHIN_LEAP_MULTIPLIER  # 1.05x
        else:
            # Normal swimming upward
            velocity.y = PLAYER_SPEED * 3

Survival Stats Management

Health (0-20)

  • Damaged by: Fall > 3 blocks, void (Y < -64), drowning, contact with hazards

  • Healed by: Food (not implemented in basic version)

  • Death: respawn at spawn point

def apply_fall_damage(delta_time):
    if not on_ground:
        highest_y = max(highest_y, feet_pos.y)

    if on_ground and feet_pos.y < highest_y - 3.0:
        # Fall distance > 3 blocks
        fall_distance = highest_y - feet_pos.y
        damage = int(fall_distance - 3.0)
        take_damage(damage)
        highest_y = feet_pos.y

Hunger (0-20)

  • Drains when sprinting or traveling

  • Restored by consuming food

HUNGER_DRAIN_SPRINT = 0.002       # per millisecond
HUNGER_DRAIN_WALK = 0.0005

def update_hunger(delta_time):
    if is_sprinting:
        hunger -= HUNGER_DRAIN_SPRINT * delta_time
    elif is_moving:
        hunger -= HUNGER_DRAIN_WALK * delta_time

    hunger = clamp(hunger, 0, MAX_HUNGER)

Oxygen (0-20)

  • Depletes when head in water

  • Regenerates when above water surface

OXYGEN_LOSE_TIMER = 1000    # ms, depletes 1 oxygen per second
OXYGEN_GAIN_TIMER = 200     # ms, regenerates 1 oxygen per 200ms

def update_oxygen(delta_time):
    if head_in_water:
        oxygen_drain_time += delta_time
        if oxygen_drain_time >= OXYGEN_LOSE_TIMER:
            oxygen -= 1
            oxygen_drain_time = 0
    else:
        oxygen_refill_time += delta_time
        if oxygen_refill_time >= OXYGEN_GAIN_TIMER:
            oxygen += 1
            oxygen_refill_time = 0

    oxygen = clamp(oxygen, 0, MAX_OXYGEN)

    # Drowned at 0 oxygen
    if oxygen == 0:
        take_damage(1)  # 1 damage per frame

Void Damage (Y < -64)

VOID_DEATH_Y = -64
VOID_DAMAGE = 1
VOID_DAMAGE_INTERVAL = 500  # ms

def apply_void_damage(delta_time):
    if feet_pos.y < VOID_DEATH_Y:
        void_damage_time += delta_time
        if void_damage_time >= VOID_DAMAGE_INTERVAL:
            take_damage(VOID_DAMAGE)
            void_damage_time = 0

Inventory Management

Structure:

inventory = [0] * 41  # 41 slots, 0 = empty, 1-255 = voxel ID
inventory_counts = [0] * 41  # Stack sizes (1-64)

# Slots breakdown:
# 0-8:   Hotbar
# 9-35:  Main storage (27 slots)
# 36-39: Crafting 2x2 grid
# 40:    Crafting output

Add Item (pickup or craft):

def add_item(voxel_id):
    # Try to stack on existing
    for slot in range(41):
        if inventory[slot] == voxel_id and inventory_counts[slot] < 64:
            space = 64 - inventory_counts[slot]
            inventory_counts[slot] += 1
            return

    # Add to empty slot (hotbar first)
    for slot in range(41):
        if inventory[slot] == 0:
            inventory[slot] = voxel_id
            inventory_counts[slot] = 1
            return

Get Held Item:

def get_held_item():
    return inventory[hotbar_index], inventory_counts[hotbar_index]

Mining and Placing

Mining (LClick):

  1. Raycast from player camera to find target block

  2. Calculate break time based on block hardness and held tool

  3. If held long enough, remove block and drop item

BLOCK_HARDNESS = {
    STONE: 1500,        # ms to break
    WOOD: 1000,
    DIRT: 500,
    GRASS: 300,
    ...
}

TOOL_MULTIPLIER = {
    BARE_HAND: 1.0,
    WOODEN_PICKAXE: 0.2,   # 5x faster than bare hand
    STONE_PICKAXE: 0.1,
    ...
}

def mine_block(target_block_id):
    hardness = BLOCK_HARDNESS.get(target_block_id, 1000)
    held_item = get_held_item()
    multiplier = TOOL_MULTIPLIER.get(held_item, 1.0)

    break_time = hardness * multiplier  # ms

    # Player holds LClick; after break_time, remove block
    if held_click_duration >= break_time:
        world.remove_voxel(target_pos)
        # Drop item if not in creative mode or not holding wrong tool
        if game_mode == 'SURVIVAL':
            drop_item(target_block_id)

Placing (RClick):

  1. Raycast to find target face

  2. Place block on adjacent empty voxel

  3. Consume from inventory

def place_block():
    target_face_normal = raycast_result.normal
    target_pos = raycast_result.pos

    # Compute placement position (adjacent to target)
    place_pos = target_pos + target_face_normal

    # Check empty and no player collision
    if not world.is_solid(place_pos) and not aabb_intersect_voxel(place_pos):
        voxel_id = get_held_item()
        world.place_voxel(place_pos, voxel_id)

        if game_mode == 'SURVIVAL':
            consume_inventory(hotbar_index, 1)

View Bobbing and Hand Animation

View Bobbing (Camera offset):

def update_view_bobbing():
    # Sine wave based on step_counter
    bob_y = math.sin(step_counter * BOB_FREQ) * BOB_AMPLITUDE

    # Vertical head offset
    position.y += bob_y

Held Item Swing (for visual feedback):

def update_held_item_animation():
    if held_item_swing > 0:
        # Decrease swing over time
        held_item_swing -= delta_time / SWING_DURATION  # e.g., 0.2 seconds
        held_item_swing = max(held_item_swing, 0)

    # During swing, rotate/bob held item in camera space
    swing_rotation = held_item_swing * 45  # degrees
    swing_bob = sin(held_item_swing * PI) * 0.1

Dynamic FOV During Sprint

def update_fov():
    target_fov = BASE_FOV  # e.g., 70 degrees

    if is_sprinting:
        target_fov = BASE_FOV + 10  # Expand FOV while running

    # Smooth lerp
    current_fov = lerp(current_fov, target_fov, 0.1 * delta_time)

Integration with Game Loop

def player_update(delta_time):
    update_movement(delta_time)
    handle_mouse_motion()
    apply_gravity(delta_time)
    move_and_collide(delta_time)
    update_in_water()
    update_oxygen(delta_time)
    update_hunger(delta_time)
    apply_fall_damage(delta_time)
    apply_void_damage(delta_time)
    update_held_item_animation()
    update_view_bobbing()