Porting rd-132328 to Rust: The Entity Extraction

In which we discover that translating Java inheritance to Rust composition is less about fighting the borrow checker and more about deciding how much convenience you want to pay for.

This is version 2 of 878. The zombies have arrived.

What Changed

The analysis post covered what rd-132328 is: the same-day sequel to rd-132211 where Notch added 100 zombies with random walk AI, extracted an Entity base class from Player, and built a skeletal character model system. Six new files, 306 new lines of Java, and the birth of the mob system that would eventually support Creepers, Endermen, and every other entity in the game.

For the Rust port, the scope is smaller than it looks. The level, physics, timer, and save/load systems are completely unchanged between rd-132211 and rd-132328 — eleven of thirteen original Java files are byte-identical. Our existing code carries forward untouched. The work is: extract Entity from Player, implement Zombie, wire them into the game loop, and render them somehow.

The “somehow” is doing a lot of work in that sentence. More on that later.

The Inheritance Problem

Java’s entity system is straightforward OOP inheritance:

Entity (base class)
  ├── Player (extends Entity)
  └── Zombie (extends Entity)

Entity owns position, velocity, bounding box, collision detection, and physics. Player adds keyboard input. Zombie adds random walk AI. Both inherit move(), moveRelative(), and all the shared physics constants. Clean, simple, idiomatic Java circa 2009.

Rust does not have inheritance. This is not a limitation — it is a design choice with real consequences. The question is not “how do we fake inheritance” but “what does this inheritance actually mean, and what is the right Rust pattern for that meaning?”

In this case, the inheritance means exactly one thing: Player and Zombie share physics code. They are not polymorphically dispatched (nothing calls entity.tick() on a generic Entity reference). They do not form a meaningful type hierarchy (nothing accepts “any entity”). They just share a chunk of implementation. The relationship is “has physics” not “is an entity.”

That points directly at composition.

The Composition Pattern

The Rust structure we landed on:

pub struct Entity {
    pub xo: f32, pub yo: f32, pub zo: f32,
    pub x: f32, pub y: f32, pub z: f32,
    pub xd: f32, pub yd: f32, pub zd: f32,
    pub y_rot: f32, pub x_rot: f32,
    pub bb: AABB,
    pub on_ground: bool,
    pub height_offset: f32,
}

pub struct Player {
    pub entity: Entity,
}

pub struct Zombie {
    pub entity: Entity,
    pub rot: f32,
    pub time_offs: f32,
    pub speed: f32,
    pub rot_a: f32,
}

Entity is a struct that owns all the shared state and provides apply_movement(), move_relative(), save_prev_pos(), turn(), reset_pos(), and set_pos(). Player and Zombie each contain an Entity field and delegate physics to it.

The physics constants that were previously defined in player.rs moved to entity.rs where they belong:

pub const GRAVITY: f32 = 0.005;
pub const JUMP_VELOCITY: f32 = 0.12;
pub const GROUND_SPEED: f32 = 0.02;
pub const AIR_SPEED: f32 = 0.005;
pub const GROUND_FRICTION: f32 = 0.8;
pub const HORIZONTAL_DRAG: f32 = 0.91;
pub const VERTICAL_DRAG: f32 = 0.98;
pub const MOUSE_SENSITIVITY: f32 = 0.15;

Player re-exports these for backward compatibility, so existing test code that imports from player::GRAVITY continues to work without modification. This matters: all 34 rd-132211 tests must pass unchanged. If porting a new version breaks existing tests, we have introduced a regression, and the whole point of this project is behavioral parity across the version history.

The Deref Trick

Raw composition works but produces ugly call sites. Without any syntactic sugar, accessing a zombie’s position requires zombie.entity.x. The renderer, the game loop, and every test would need this double-dereference. It is correct but noisy.

Rust’s Deref trait provides a way to delegate field access:

impl std::ops::Deref for Zombie {
    type Target = Entity;
    fn deref(&self) -> &Entity {
        &self.entity
    }
}

With this, zombie.x transparently resolves to zombie.entity.x. The Zombie struct “looks like” an Entity for read access. Player gets both Deref and DerefMut, since the game loop needs to mutate entity state through the player (mouse look via turn(), direct velocity writes for jumping).

This is a somewhat controversial Rust pattern. The Deref trait is primarily intended for smart pointer types (Box, Arc, Rc), and using it for composition-based delegation is technically an abuse of its purpose. The Rust API Guidelines explicitly recommend against this for public API design.

We use it anyway, for three reasons.

First, the alternative is worse. Writing zombie.entity.x everywhere obscures the code’s intent. The analysis post established that rd-132328’s design point is that players and zombies share the same physics. Making the physics fields directly accessible through Deref reflects that design intent.

Second, the scope is small. Entity has a fixed, stable set of fields that will not grow unboundedly. We are not wrapping a complex trait hierarchy with dozens of methods — we are delegating access to 13 fields on a struct that represents “thing with physics.”

Third, the project is private and internal. These are not public API types consumed by downstream crates. If the pattern causes confusion later, we can unwind it.

Player additionally implements DerefMut, which Zombie does not. This is deliberate: the game loop modifies player state directly (mouse look adjustments, velocity writes), while zombie state is only modified through zombie.tick(). The asymmetry reflects a real difference in how the two entity types are used.

Zombie AI: Seven Lines of Interesting Math

The zombie tick method translates directly from Java:

pub fn tick(&mut self, level: &Level) {
    self.entity.save_prev_pos();

    self.rot += self.rot_a;
    self.rot_a *= 0.99;
    self.rot_a += (random() - random()) * random() * random() * 0.01;

    let xa = self.rot.sin();
    let za = self.rot.cos();

    if self.entity.on_ground && random() < 0.01 {
        self.entity.yd = JUMP_VELOCITY;
    }

    let speed = if self.entity.on_ground { GROUND_SPEED } else { AIR_SPEED };
    self.entity.move_relative(xa, za, speed);
    self.entity.yd -= GRAVITY;
    let (xd, yd, zd) = (self.entity.xd, self.entity.yd, self.entity.zd);
    self.entity.apply_movement(level, xd, yd, zd);
    // ... friction, drag, bounds check
}

The AI is two state variables: rot (current facing angle) and rot_a (angular velocity, called speed in the Java source — we renamed it rot_a to avoid confusion with the linear speed constant). Each tick, rotation advances by the angular velocity. Angular velocity decays by 1% and gets a small random perturbation. Movement direction is (sin(rot), cos(rot)).

The random perturbation formula deserves attention: (random() - random()) * random() * random(). This is not a uniform distribution. Subtracting two uniform random values produces a triangular distribution centered on zero. Multiplying by two more uniform values compresses the result toward zero. The effect: most perturbations are tiny (angular velocity changes slowly), but occasional larger ones cause the zombie to curve noticeably. This produces the lazy, drifting random walks described in the analysis post.

The safety valve — if self.entity.y > 100.0 { self.entity.reset_pos(level); } — teleports zombies that somehow escape above Y=100. In practice this never triggers with normal physics, but it prevents edge-case escapes from accumulating zombies in the sky.

The random() Function

Java has Math.random() built in. Rust does not include a random number generator in its standard library. We needed one for the zombie AI, and we did not want to add a rand crate dependency for what amounts to (random float between 0 and 1).

The solution is a minimal LCG (linear congruential generator) seeded from system time:

fn rand_float() -> f32 {
    use std::time::SystemTime;
    static SEED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

    let mut s = SEED.load(std::sync::atomic::Ordering::Relaxed);
    if s == 0 {
        s = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_nanos() as u64;
    }
    s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
    SEED.store(s, std::sync::atomic::Ordering::Relaxed);
    ((s >> 33) as f32) / (u32::MAX as f32 / 2.0)
}

The constants are from Knuth’s MMIX LCG. It is not cryptographically secure, not reproducible across runs, and not suitable for anything that matters. But zombie random walks do not care about statistical quality. They need numbers that look random enough to produce natural-looking movement. An LCG with a 64-bit state delivers that.

The pub fn random() wrapper returns rand_float().abs(), matching Math.random()‘s [0, 1) contract. This is exposed publicly because zombie AI needs it, and future mob types will too.

Is this the right long-term solution? Probably not. When we need reproducible seeds (for world generation, for deterministic testing), we will want a proper PRNG. But for rd-132328, where randomness is used exclusively for zombie wandering, this is sufficient.

Refactoring Player Without Breaking Anything

The Player struct went from a monolith (position + velocity + bounding box + collision + physics + input) to a thin wrapper around Entity:

pub struct Player {
    pub entity: Entity,
}

impl Player {
    pub fn new(x: f32, y: f32, z: f32) -> Self {
        Player {
            entity: Entity::new(x, y, z, EYE_HEIGHT),
        }
    }

    pub fn tick(&mut self, level: &Level, input: &PlayerInput) {
        self.entity.save_prev_pos();
        // ... keyboard input → xa, za ...
        self.entity.move_relative(xa, za, speed);
        self.entity.yd -= GRAVITY;
        let (xd, yd, zd) = (self.entity.xd, self.entity.yd, self.entity.zd);
        self.entity.apply_movement(level, xd, yd, zd);
        // ... friction, drag ...
    }
}

The key difference from the Java refactor: in Java, Player extends Entity inherits all fields and methods automatically. In Rust, we had to make a choice at every access point. Fields like x, y, z, bb, on_ground are accessed via Deref. Methods like save_prev_pos(), move_relative(), apply_movement() are called on self.entity explicitly.

The tick() method body is line-for-line identical to the pre-refactor version — same constants, same physics, same order of operations. The only difference is that self.xd became self.entity.xd (though Deref lets the former work too for reads). This is important. The refactor must be a pure restructuring, not a behavior change. If player physics feel different after the refactor, we have a bug.

To ensure backward compatibility, player.rs re-exports all the physics constants that tests and the renderer import:

pub use crate::entity::{
    GRAVITY, JUMP_VELOCITY, GROUND_SPEED, AIR_SPEED,
    GROUND_FRICTION, HORIZONTAL_DRAG, VERTICAL_DRAG, MOUSE_SENSITIVITY,
};

This means use rustcraft::player::GRAVITY still works. No test file needed updating for import paths. Zero-friction migration.

Rendering: Green Boxes (For Now)

The Java version renders zombies with a full skeletal model system: six textured cubes (head, body, two arms, two legs) with sine-wave joint rotations, reading from a char.png texture atlas. The analysis post covers this system in detail — 273 lines of Java across four classes in the character/ package.

We did not port the skeletal model system. Not yet.

Instead, zombies render as solid green boxes. Each zombie’s interpolated position is computed from its previous and current tick positions (the same interpolation the player uses for smooth camera movement), and a box of the entity’s bounding box dimensions (0.6 wide, 1.8 tall) is drawn at that position using the existing highlight pipeline:

let zombie_color = HighlightUniforms {
    color: [0.2, 0.6, 0.2, 1.0],  // dark green
};

for zombie in zombies {
    let (zx, zy, zz) = zombie.get_interpolated_pos(partial_tick);
    let hw = 0.3f32;
    let hh = 0.9f32;
    let verts = build_box_vertices(
        zx - hw, zy - hh, zz - hw,
        zx + hw, zy + hh, zz + hw,
    );
    // ... create buffer, draw ...
}

This is a deliberate shortcut. The skeletal model system (Cube, Polygon, Vertex, Vec3) is a rendering feature. It does not affect game logic. Zombies walk, jump, collide, and exist in the world identically whether they are rendered as green boxes or articulated humanoids. The game logic port is complete; the rendering is a placeholder.

Why green? Because they are zombies. The box color is [0.2, 0.6, 0.2, 1.0] — a muted forest green that is easily distinguishable from the terrain and from the block selection highlight. It is placeholder art, but it is intentional placeholder art.

The full skeletal model system will arrive when we build the entity rendering pipeline properly. That pipeline needs to handle textured meshes with per-joint transforms, which requires either instanced rendering with a bone transform buffer or per-entity vertex buffer rebuilds each frame. For 100 zombies with 6 cubes each, either approach works. The decision depends on how the rendering architecture evolves over the next several versions. Committing to a design now, when we do not know what future versions will demand, is premature optimization of the wrong kind.

The char.png and terrain.png textures were extracted from the rd-132328 JAR file. The terrain texture was already extracted for rd-132211; the character texture is new. Both are checked into the assets directory and loaded by the renderer at startup. When we implement the skeletal model, the texture is ready.

Test Suite: 46 Tests, All Green

The rd-132328 test file adds 12 new tests on top of the existing 34 from rd-132211:

Entity tests (4):
test_entity_height_offset_default — Entity with height_offset=0.0 reports zero.
test_entity_height_offset_player — Entity with height_offset=1.62 reports the player eye height.
test_entity_dimensions — Entity bounding box is 0.6 wide and 1.8 tall.
test_entity_gravity — Entity in the air falls on successive ticks.

Zombie tests (7):
test_zombie_spawns_at_position — Zombie’s entity position matches constructor arguments.
test_zombie_has_random_rotation — Two zombies created at the same position have different initial rotations.
test_zombie_height_offset_zero — Zombie’s height_offset is 0.0 (not the player’s 1.62).
test_zombie_tick_changes_position — After 60 ticks, a zombie has moved from its starting position.
test_zombie_falls_with_gravity — A zombie in the air falls on successive ticks.
test_zombie_uses_same_physics_as_player — All physics constants (GRAVITY, JUMP_VELOCITY, GROUND_SPEED, AIR_SPEED, HORIZONTAL_DRAG, VERTICAL_DRAG, GROUND_FRICTION) are shared between entity types.
test_100_zombies_spawn — Creating 100 zombies does not panic or fail.

Backward compatibility (1):
test_player_still_uses_eye_height — Player’s height_offset is EYE_HEIGHT (1.62), confirming the refactor preserved player semantics.

All 34 rd-132211 tests continue to pass without modification. This was the primary constraint: the entity extraction must not change player behavior. The re-export pattern for constants and the Deref/DerefMut implementations ensure that existing test code sees identical types and values.

Running cargo test produces:

running 34 tests ... test result: ok. 34 passed
running 12 tests ... test result: ok. 12 passed

46 tests. All green. Under a second total.

The Architectural Significance

The analysis post argued that rd-132328 matters less for what it adds (zombies are not gameplay) than for what it establishes (the entity system pattern). The same is true on the Rust side.

The composition-with-Deref pattern we chose here will be used for every future mob type. When Notch adds new entity types in later versions — and he will, rapidly — they will each get a struct with an entity: Entity field and a Deref implementation. The physics code lives in one place. The AI code lives in each mob’s tick() method. The separation is clean.

The module layout is now:

src/
  lib.rs              # Module declarations
  entity.rs           # Entity base struct, physics, constants   [NEW]
  player.rs           # Player struct, keyboard input             [REFACTORED]
  zombie.rs           # Zombie struct, random walk AI             [NEW]
  level.rs            # Unchanged from rd-132211
  phys.rs             # Unchanged from rd-132211
  timer.rs            # Unchanged from rd-132211
  renderer/
    mod.rs            # Added zombie rendering (green boxes)
    vertex.rs         # Unchanged
    chunk_mesh.rs     # Unchanged
    shader.wgsl       # Unchanged

Three source files changed or added. 157 lines for Entity, 76 for Zombie, 93 for the refactored Player. That is 326 lines of Rust for 306 lines of Java — a ratio close to 1:1, which is unusual. Rust code typically runs 20-50% longer than equivalent Java due to explicit error handling, lifetime annotations, and the absence of inherited-method syntactic brevity. The near-parity here reflects the simplicity of the game logic: there are no error paths, no lifetime challenges, no complex type relationships. It is struct fields and floating-point arithmetic.

What Carries Forward

The level, physics, and timer code passed through this version untouched, exactly as they did in the Java original. This validates the architectural split from rd-132211: game logic lives in the library crate, rendering lives in the binary crate, and they do not leak into each other. Adding entities required zero changes to existing game logic modules.

The green-box zombie rendering is a debt we carry forward. It does not affect parity (the game logic is identical regardless of rendering), but it is visually incomplete. The skeletal model system from the Java version’s character/ package remains unported. We will need it eventually — probably when the player character itself gets a visible model, which happens in a later version.

What Is Next

The version manifest says mc-161648 (May 14, 2009) is next. One day after the zombies arrived. Notch was not slowing down.

We have 876 versions to go. The entity system is in place. The test infrastructure scales. The rendering pipeline handles both terrain and entity primitives. Each subsequent version will continue to be a diff — new features, changed constants, new entity types — layered incrementally on top of what exists.

The green boxes stumble across the flat stone world, drifting in lazy arcs, occasionally hopping. They are not articulated humanoids with sine-wave arm swings. They are featureless colored rectangles. But they move with the same physics as the player, driven by the same gravity and friction constants, colliding with the same terrain. The behavior is right. The appearance is a placeholder.

In game development, getting the behavior right is the hard part. Appearance is just textures.

By Clara

Leave a Reply

Your email address will not be published. Required fields are marked *