From 0c9136eaae595789577f8a8979a28e4f2fd9b4ea Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Sun, 18 Oct 2026 23:15:16 +0200 Subject: [PATCH] feat!: move characters freely instead of tile by tile Characters have a continuous Position simulated on the server with a square collision box, sliding along walls and corner assist into openings. GridPos is derived from the position, so interactions, building and reach stay tile-based. The exact position is saved. BREAKING CHANGE: protocol version 4 replaces SetMoveDir with SetMovement and replicates Position. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 7 +- README.md | 6 +- crates/game_client/src/input.rs | 52 +++--- crates/game_client/src/render.rs | 25 +-- crates/game_core/src/components.rs | 18 +- crates/game_core/src/grid.rs | 5 + crates/game_core/src/lib.rs | 2 +- crates/game_core/src/protocol.rs | 15 +- crates/game_server/src/actions.rs | 7 +- crates/game_server/src/lib.rs | 7 +- crates/game_server/src/movement.rs | 248 ++++++++++++++++++++++--- crates/game_server/src/persistence.rs | 8 +- crates/game_server/src/players.rs | 11 +- crates/game_server/src/survival.rs | 7 +- crates/game_server/tests/simulation.rs | 25 ++- 15 files changed, 345 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe01b44..19956fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed -- **Breaking:** network protocol version 3. Logins carry a player id and - password, so older clients and servers can't connect. Characters saved by name +- Characters move freely instead of tile by tile: diagonal movement, sliding + along walls and around corners. Their exact position is saved. +- **Breaking:** network protocol version 4. Logins carry a player id and + password, and movement is sent as a direction vector, so older clients and + servers can't connect. Characters saved by name in existing worlds are not carried over to player ids. ### Removed diff --git a/README.md b/README.md index 5ad6a2a..78f2736 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ with [Bevy](https://bevy.org) in Rust. - **Tiled maps** – maps are regular [Tiled](https://www.mapeditor.org) `.tmx` files. - **Persistent world** – no rounds. The world and every character (inventory, skills, vitals, position) are saved and restored across restarts. +- **Free movement** – characters walk anywhere, not tile by tile, and slide along + walls. Interactions still target the tile you face. - **Singleplayer and multiplayer** – one authoritative server simulation, used locally in singleplayer and over UDP in multiplayer ([bevy_replicon](https://crates.io/crates/bevy_replicon) + renet). @@ -23,7 +25,7 @@ with [Bevy](https://bevy.org) in Rust. | `crates/game_server` | Authoritative simulation, Rune scripting, persistence, `game-server` binary | | `crates/game_client` | Rendering, UI and input (`game` binary); embeds the server for singleplayer/host | -The client never changes game state directly. It sends requests (`Login`, `SetMoveDir`, +The client never changes game state directly. It sends requests (`Login`, `SetMovement`, `Interact`, `Craft`, …) and the server replicates the resulting state back. In singleplayer the same server systems run inside the client process, so singleplayer and multiplayer behave identically. @@ -144,7 +146,7 @@ in-game chat. Admin commands change `server.toml` (or `host.toml`) immediately. | Key | Action | |---------------------|------------------------------------------------------| -| WASD / arrows | Move (you face the direction you move) | +| WASD / arrows | Move freely, also diagonally (you face that way) | | E | Pick up (own tile, otherwise the faced tile) | | F / left click | Interact with the faced / clicked building; machines open their window | | U | Use the selected item (eat, heal, …) | diff --git a/crates/game_client/src/input.rs b/crates/game_client/src/input.rs index a1ba0d7..73b54ff 100644 --- a/crates/game_client/src/input.rs +++ b/crates/game_client/src/input.rs @@ -72,44 +72,34 @@ fn window_keys( } } -const MOVE_KEYS: [(KeyCode, Dir); 8] = [ - (KeyCode::KeyW, Dir::North), - (KeyCode::ArrowUp, Dir::North), - (KeyCode::KeyS, Dir::South), - (KeyCode::ArrowDown, Dir::South), - (KeyCode::KeyA, Dir::West), - (KeyCode::ArrowLeft, Dir::West), - (KeyCode::KeyD, Dir::East), - (KeyCode::ArrowRight, Dir::East), -]; - -/// Sends the held direction whenever it changes. The most recently pressed key wins. +/// Sends the held movement direction whenever it changes. fn movement( mut commands: Commands, keys: Res>, ui: Res, focus: Res, - mut held: Local>, - mut sent: Local>, + mut sent: Local, ) { - held.retain(|dir| { - MOVE_KEYS - .iter() - .any(|(key, d)| d == dir && keys.pressed(*key)) - }); - for (key, dir) in MOVE_KEYS { - if keys.just_pressed(key) && !held.contains(&dir) { - held.push(dir); - } - } - let dir = if ui.blocks_game() || ui.chat_open || focus.captures_keyboard() { - None - } else { - held.last().copied() + let axis = |neg: [KeyCode; 2], pos: [KeyCode; 2]| { + i8::from(keys.any_pressed(pos)) - i8::from(keys.any_pressed(neg)) }; - if *sent != dir { - *sent = dir; - commands.client_trigger(SetMoveDir(dir)); + let movement = if ui.blocks_game() || ui.chat_open || focus.captures_keyboard() { + SetMovement::default() + } else { + SetMovement { + x: axis( + [KeyCode::KeyA, KeyCode::ArrowLeft], + [KeyCode::KeyD, KeyCode::ArrowRight], + ), + y: axis( + [KeyCode::KeyW, KeyCode::ArrowUp], + [KeyCode::KeyS, KeyCode::ArrowDown], + ), + } + }; + if *sent != movement { + *sent = movement; + commands.client_trigger(movement); } } diff --git a/crates/game_client/src/render.rs b/crates/game_client/src/render.rs index ac4b8da..048e2d6 100644 --- a/crates/game_client/src/render.rs +++ b/crates/game_client/src/render.rs @@ -4,7 +4,7 @@ use bevy::prelude::*; use game_core::{ GameData, components::*, - grid::{Dir, TILE_SIZE, grid_to_world}, + grid::{Dir, TILE_SIZE, grid_to_world, position_to_world}, map::MapLayout, }; @@ -223,7 +223,7 @@ fn character_visuals( mut commands: Commands, asset_server: Res, local: Res, - added: Query<(Entity, &Character, &GridPos), Added>, + added: Query<(Entity, &Character, &Position), Added>, mut facing: Query<(&Facing, &mut Sprite), (With, Changed)>, ) { for (entity, character, pos) in &added { @@ -233,7 +233,7 @@ fn character_visuals( } commands.entity(entity).insert(( sprite, - Transform::from_translation(grid_to_world(*pos).extend(Z_CHARACTER)), + Transform::from_translation(position_to_world(pos.0).extend(Z_CHARACTER)), Visibility::default(), children![( Text2d::new(character.name.clone()), @@ -247,22 +247,23 @@ fn character_visuals( } } -/// Glides characters between tiles; teleports for large jumps (respawn, login). +/// Smooths replicated positions, which arrive at the server tick rate; teleports for large +/// jumps (respawn, login). fn move_characters( time: Res