| Crates.io | hewn |
| lib.rs | hewn |
| version | 0.1.0-alpha.3 |
| created_at | 2025-08-14 21:29:06.874006+00 |
| updated_at | 2025-09-03 09:18:17.37861+00 |
| description | A simple game engine built for educational purposes. |
| homepage | |
| repository | https://github.com/joshua-mason/hewn |
| max_upload_size | |
| id | 1795836 |
| size | 197,424 |
Hewn is a minimal Rust game engine for learning and tinkering, with support for terminal, desktop, and web platforms.
examples/asciijump, examples/asciibird, examples/snake[!WARNING] Hewn has only been tested on macOS so far. Windows and Linux support is untested and may have issues.
[!NOTE] Complete tutorial code is available in
examples/tutorial/. The following tutorial builds up from the simplest possible game.
Let's start with the simplest possible game - showing debug text in the terminal.
First, let's create the basic game structure:
use hewn::{
runtime::GameHandler, // 1.
ecs::ECS, // 2.
};
use std::time::Duration; // NEW: dt for frame time
struct HelloGame {
ecs: ECS, // 3.
}
impl HelloGame {
fn new() -> Self {
Self { ecs: ECS::new() }
}
}
impl GameHandler for HelloGame { // 4.
fn start_game(&mut self) {}
fn next(&mut self, _dt: Duration) {}
fn handle_key(&mut self, _key: hewn::runtime::Key, _pressed: bool) -> bool { true }
fn ecs(&self) -> &ECS { &self.ecs }
fn debug_str(&self) -> Option<String> {
Some("Hello Hewn! Press Q to exit.".to_string()) // 5.
}
}
GameHandler trait - the core interface all Hewn games implementECS - the Entity Component System that manages game objectsHelloGame struct holds our game state (just an ECS for now)GameHandler trait with required methodsdebug_str() returns text that appears at the bottom of the terminalNext, let's run our game:
use hewn::terminal::runtime::TerminalRuntime;
// ..
fn main() {
let mut game = HelloGame::new(); // 1.
let mut runtime = TerminalRuntime::new(20, 20); // 2.
runtime.start(&mut game); // 3.
}
This creates a minimal game that shows "Hello Hewn! Press Q to exit." at the bottom of your terminal. All Hewn games implement the GameHandler trait and need an ECS (Entity Component System) to manage game objects.
[!TIP] Run this with
cargo runand you'll see a field of.characters representing empty space, with your debug text at the bottom. We're about to add a character that moves around this world!
[!NOTE] Delta time:
next(dt: Duration)provides the time since the last frame. Treat velocities as "world units per second" — the ECS scales movement and collision bydtso motion is frame-rate independent.
Now let's add a character that appears on screen. This is where the ECS comes in - we'll create an entity with position and rendering components.
First, let's create the player entity:
// ..
use hewn::{
ecs::{ECS, EntityId, Components, PositionComponent, RenderComponent, SizeComponent}, // NEW!
};
struct HelloGame {
ecs: ECS,
player_id: EntityId, // 1.
}
impl HelloGame {
fn new() -> Self {
let mut ecs = ECS::new();
let player_id = ecs.add_entity_from_components(Components {
position: Some(PositionComponent { x: 5.0, y: 5.0 }), // 2.
render: Some(RenderComponent { // 3.
ascii_character: '@',
rgb: cgmath::Vector3 {
x: 0.0,
y: 0.0,
z: 0.0,
},
}),
velocity: None,
size: Some(SizeComponent { x: 1.0, y: 1.0 }), // 4.
camera_follow: None,
});
Self { ecs, player_id }
}
}
// ..
player_id field to store a reference to our character entityRenderComponent makes the entity appear as @ character on screen - we also include an rgb with the colour for wgpu rendering.SizeComponent defines the entity's collision box (1×1 unit)Next, let's update the game loop and debug display:
// ..
impl GameHandler for HelloGame {
// ..
fn next(&mut self, dt: Duration) {
self.ecs.step(dt); // 1.
}
// ..
fn debug_str(&self) -> Option<String> {
let player = self.ecs.get_entity_by_id(self.player_id)?; // 2.
let pos = player.components.position.as_ref()?;
Some(format!("Player @ ({}, {})", pos.x, pos.y)) // 3.
}
}
// ..
ecs.step() updates all entities each frame (position, rendering, etc.)Now you'll see an @ character in your terminal surrounded by . tiles! The debug text at the bottom shows its exact position.
Let's make our character respond to arrow keys using a controller pattern.
First, let's create a controller to track key states:
[!NOTE] Coordinate system:
xincreases to the right andyincreases upward. For example, pressing Up sets a positiveyvelocity.
// ..
use hewn::{
runtime::Key, // NEW!
ecs::VelocityComponent, // NEW!
};
use std::time::Duration; // NEW!
// Add a controller to track key states
pub struct GameController { // 1.
is_up_pressed: bool,
is_down_pressed: bool,
is_left_pressed: bool,
is_right_pressed: bool,
}
impl GameController {
pub fn new() -> Self {
Self {
is_up_pressed: false,
is_down_pressed: false,
is_left_pressed: false,
is_right_pressed: false,
}
}
pub fn handle_key(&mut self, key: Key, is_pressed: bool) -> bool {
match key { // 2.
Key::Up => { self.is_up_pressed = is_pressed; true }
Key::Down => { self.is_down_pressed = is_pressed; true }
Key::Left => { self.is_left_pressed = is_pressed; true }
Key::Right => { self.is_right_pressed = is_pressed; true }
_ => false,
}
}
}
// ..
GameController struct tracks the current state of arrow keys (pressed/not pressed)handle_key() updates the key states when keys are pressed or releasedNext, let's integrate the controller into our game and add velocity:
// ..
struct HelloGame {
ecs: ECS,
player_id: EntityId,
game_controller: GameController, // 1.
}
impl HelloGame {
fn new() -> Self {
let mut ecs = ECS::new();
let player_id = ecs.add_entity_from_components(Components {
position: Some(PositionComponent { x: 5.0, y: 5.0 }),
render: Some(RenderComponent {
ascii_character: '@',
rgb: cgmath::Vector3 {
x: 0.0,
y: 0.0,
z: 0.0,
},
}),
velocity: Some(VelocityComponent { x: 0.0, y: 0.0 }), // 2.
size: Some(SizeComponent { x: 2.0, y: 1.0 }), // 3.
camera_follow: None,
});
Self {
ecs,
player_id,
game_controller: GameController::new(), // 4.
}
}
}
impl GameHandler for HelloGame {
// ..
fn next(&mut self, dt: Duration) {
// Update player velocity based on controller state
let velocity = self.ecs.get_entity_by_id_mut(self.player_id)
.and_then(|player| player.components.velocity.as_mut());
if let Some(velocity) = velocity {
if self.game_controller.is_up_pressed { // 5.
velocity.y = 2.0;
} else if self.game_controller.is_down_pressed {
velocity.y = -2.0;
} else {
velocity.y = 0.0;
}
if self.game_controller.is_left_pressed {
velocity.x = -2.0;
} else if self.game_controller.is_right_pressed {
velocity.x = 2.0;
} else {
velocity.x = 0.0;
}
}
self.ecs.step(dt); // 6.
}
fn handle_key(&mut self, key: Key, pressed: bool) -> bool {
self.game_controller.handle_key(key, pressed) // 7.
}
}
// ..
game_controller field to track input stateVelocityComponent - the ECS automatically moves entities with velocity2x1 so it appears wider in the terminalnext() method reads controller state and updates player velocity accordinglyecs.step() applies the velocity to move the playerhandle_key() delegates to the controller for clean separation of concernsYour @ character now responds to arrow keys! Try moving around and watch the debug text update with your position. Now let's see the same game running in a desktop window...
Let's add a wall that blocks the player's movement to make it feel like a real game.
First, let's add a wall entity:
// ..
impl HelloGame {
fn new() -> Self {
let mut ecs = ECS::new();
// ..
// Add a wall
ecs.add_entity_from_components(Components {
position: Some(PositionComponent { x: 8.0, y: 5.0 }), // 1.
render: Some(RenderComponent { // 2.
ascii_character: '#',
rgb: cgmath::Vector3 {
x: 0.0,
y: 0.0,
z: 0.0,
},
}),
velocity: None, // 3.
size: Some(SizeComponent { x: 2.0, y: 1.0 }), // 4.
camera_follow: None,
});
// ..
}
}
// ..
# character on screen, or a black square in wgpu renderingVelocityComponent { x: 0, y: 0 }.## (2 units wide)If you run now, you'll see the player move through the wall. Next, let's add collision detection to the game loop:
// ..
impl GameHandler for HelloGame {
// ..
fn next(&mut self, dt: Duration) {
// .. Velocity update logic from Step 3 ..
// Check for collisions BEFORE moving entities
let collisions = self.ecs.collision_pass(dt); // 1.
for [a, b] in collisions.into_iter() { // 2.
if a == self.player_id || b == self.player_id {
let player_entity = self.ecs.get_entity_by_id_mut(self.player_id);
let Some(player_entity) = player_entity else { return; };
let Some(velocity) = &mut player_entity.components.velocity else { return; };
velocity.x = 0.0; // 3.
velocity.y = 0.0;
break; // Stop after first collision
}
}
self.ecs.step(dt); // 4. Move entities AFTER collision check
}
// ..
}
collision_pass() returns pairs of entities that are colliding[a, b](0, 0)ecs.step() AFTER collision detection to apply the movement[!IMPORTANT] The order of operations in
next()matters! Update velocity → Check collisions → Apply movement. This prevents the player from "tunneling" through walls.
Now you'll see a ## wall that blocks your @ character's movement! Try moving right into it.
🎉 Congratulations! You’ve built a simple game with movement and collision using Hewn. Explore, experiment, and have fun making your own games! Check the examples or docs for more advanced features.
Now we've built our game, it's possible to run in our WindowRuntime. Without changing our game, we use the wgpu runtime:
// ..
use hewn::wgpu::runtime::WindowRuntime; // NEW!
// ..
fn main() {
let mut game = HelloGame::new(); // Same game!
let mut runtime = WindowRuntime::new(); // 1.
let _ = runtime.start(&mut game);
}
TerminalRuntime for WindowRuntime - that's literally it!Your @ character now renders as a colored square in a desktop window.
| Platform | Runtime | Command | Use Case |
|---|---|---|---|
| Terminal | TerminalRuntime |
cargo run |
ASCII games, debugging, servers |
| Desktop | WindowRuntime |
cargo run |
Native apps, high performance |
| Web | WASM + Canvas | wasm-pack build |
Browser games |
Hewn games implement the GameHandler trait:
start_game() - Initialize your game statenext(dt: Duration) - Update game logic each frame with delta timehandle_key() - Process keyboard inputecs() - Access the Entity Component Systemdebug_str() - Show debug info (terminal only)The ECS manages entities with components:
PositionComponent - Where entities are locatedVelocityComponent - How entities moveRenderComponent - How entities lookSizeComponent - Entity collision boundsCameraFollow - Camera tracks this entity# Terminal snake game
cargo run -p snake
# Terminal platformer
cargo run -p asciijump
# Terminal flying game
cargo run -p asciibird
# Install wasm-pack if you haven't
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Build any example for web
cd examples/snake
wasm-pack build --release --target web
# Serve locally
python3 -m http.server
# Open http://localhost:8000
Happy game making! 🎮