Skip to content

Commit 58a1f08

Browse files
authored
Darkscott/movement (#13)
* Minor changes to get things to work on Windows. * Switch to Chebyshev distance model instead of Manhatten distance for computing pathing cost. Switch to constant interpolation instead of linear interpolation for moving between tiles. Set walking speed to 3 tiles per second, which felt OK, but might need to be changed later. * Make keyboard walking feel natural and work with the updated walking system. * Make mouse movement work with the keyboard movement by bypassing the grace period for the keypresses. Mouse clicks are instantaneous.
1 parent 4784756 commit 58a1f08

4 files changed

Lines changed: 126 additions & 41 deletions

File tree

dyrah_client/src/game.rs

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,17 @@ pub struct Game {
4747
auth_password: String,
4848
auth_error: Option<String>,
4949
hovered_tile: Option<IVec2>,
50+
// time since each direction key was last held (rises when released)
51+
dir_age: [f32; 4], // left, up, right, down
52+
// when starting from a stop, wait this long before sending to allow diagonal input
53+
move_start_grace: f32,
54+
was_moving: bool,
5055
}
5156

5257
impl Game {
5358
pub fn new() -> Self {
5459
Self {
55-
client: Client::new(Transport::new("0.0.0.0:8080"), "0.0.0.0:0"),
60+
client: Client::new(Transport::new("127.0.0.1:8080"), "0.0.0.0:0"),
5661
world: World::default(),
5762
map: Map::new("assets/map.json"),
5863
lobby: HashMap::new(),
@@ -67,6 +72,9 @@ impl Game {
6772
auth_password: String::new(),
6873
auth_error: None,
6974
hovered_tile: None,
75+
dir_age: [f32::MAX; 4],
76+
move_start_grace: 0.0,
77+
was_moving: false,
7078
}
7179
}
7280

@@ -179,27 +187,35 @@ impl Game {
179187
}
180188

181189
let egui_focused = egui_ctx.wants_keyboard_input();
182-
let left = if egui_focused {
183-
false
190+
let raw_dirs = if egui_focused {
191+
[false; 4]
184192
} else {
185-
input.keys_held(&[KeyCode::KeyA, KeyCode::ArrowLeft])
186-
};
187-
let up = if egui_focused {
188-
false
189-
} else {
190-
input.keys_held(&[KeyCode::KeyW, KeyCode::ArrowUp])
191-
};
192-
let right = if egui_focused {
193-
false
194-
} else {
195-
input.keys_held(&[KeyCode::KeyD, KeyCode::ArrowRight])
196-
};
197-
let down = if egui_focused {
198-
false
199-
} else {
200-
input.keys_held(&[KeyCode::KeyS, KeyCode::ArrowDown])
193+
[
194+
input.keys_held(&[KeyCode::KeyA, KeyCode::ArrowLeft]),
195+
input.keys_held(&[KeyCode::KeyW, KeyCode::ArrowUp]),
196+
input.keys_held(&[KeyCode::KeyD, KeyCode::ArrowRight]),
197+
input.keys_held(&[KeyCode::KeyS, KeyCode::ArrowDown]),
198+
]
201199
};
202200

201+
// ~3 frames at 60fps
202+
let diagonal_buffer = 3.0 / 60.0;
203+
for i in 0..4 {
204+
if raw_dirs[i] {
205+
self.dir_age[i] = 0.0;
206+
} else {
207+
self.dir_age[i] += timer.delta;
208+
}
209+
}
210+
211+
let buffered = [
212+
self.dir_age[0] < diagonal_buffer,
213+
self.dir_age[1] < diagonal_buffer,
214+
self.dir_age[2] < diagonal_buffer,
215+
self.dir_age[3] < diagonal_buffer,
216+
];
217+
let [left, up, right, down] = buffered;
218+
203219
let mouse_world_pos = gfx.camera().screen_to_world(input.mouse_position().into());
204220
let mouse_tile_pos = input
205221
.mouse_released(MouseButton::Left)
@@ -213,25 +229,32 @@ impl Game {
213229
*age < 10.0
214230
});
215231

232+
// 3 tiles/sec × 32 px/tile = 96 px/sec
233+
let move_speed = 3.0 * dyrah_shared::TILE_SIZE;
234+
let dt = timer.delta;
235+
216236
self.world.query(
217237
|_,
218238
_: &Player,
219239
pos: &mut WorldPos,
220240
target_pos: &mut TargetWorldPos,
221241
spr: &mut Sprite| {
222242
if pos.vec != target_pos.vec {
223-
pos.vec = pos.vec.lerp(target_pos.vec, 0.1);
243+
let diff = target_pos.vec - pos.vec;
244+
let dist = diff.length();
245+
let step = move_speed * dt;
224246

225-
let delta = target_pos.vec - pos.vec;
226-
if delta.x != 0.0 {
227-
spr.anim.flip_x(delta.x < 0.0);
247+
if step >= dist {
248+
pos.vec = target_pos.vec;
249+
} else {
250+
pos.vec += diff * (step / dist);
228251
}
229252

230-
spr.anim.update(timer.delta);
231-
232-
if pos.vec.distance(target_pos.vec) < 1.0 {
233-
pos.vec = target_pos.vec;
253+
if diff.x != 0.0 {
254+
spr.anim.flip_x(diff.x < 0.0);
234255
}
256+
257+
spr.anim.update(dt);
235258
} else {
236259
spr.anim.set_frame(0);
237260
target_pos.path = None;
@@ -240,20 +263,51 @@ impl Game {
240263
);
241264

242265
self.last_input_time += timer.delta;
243-
if self.last_input_time >= 0.3 && moving {
266+
267+
// mouse clicks send immediately — no grace delay or rate limiting
268+
if mouse_tile_pos.is_some() {
244269
self.last_input_time = 0.0;
245270

246271
let msg = ClientMessage::PlayerUpdate {
247272
input: ClientInput {
248-
left,
249-
up,
250-
right,
251-
down,
273+
left: false,
274+
up: false,
275+
right: false,
276+
down: false,
252277
mouse_tile_pos,
253278
},
254279
};
255280
self.client
256281
.send(&serialize(&msg).unwrap(), Reliability::Unreliable);
282+
} else {
283+
let keyboard_moving = left || up || right || down;
284+
285+
// when keyboard movement starts from a stop, wait the buffer window
286+
// so the player can add a second key for diagonal
287+
if keyboard_moving && !self.was_moving {
288+
self.move_start_grace = diagonal_buffer;
289+
}
290+
self.was_moving = keyboard_moving;
291+
292+
if self.move_start_grace > 0.0 {
293+
self.move_start_grace -= timer.delta;
294+
}
295+
296+
if self.last_input_time >= 0.3 && keyboard_moving && self.move_start_grace <= 0.0 {
297+
self.last_input_time = 0.0;
298+
299+
let msg = ClientMessage::PlayerUpdate {
300+
input: ClientInput {
301+
left,
302+
up,
303+
right,
304+
down,
305+
mouse_tile_pos: None,
306+
},
307+
};
308+
self.client
309+
.send(&serialize(&msg).unwrap(), Reliability::Unreliable);
310+
}
257311
}
258312
}
259313

dyrah_client/src/map.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ impl Map {
2424

2525
pub fn load(&mut self, gfx: &mut Graphics) {
2626
for tileset in &self.tiled.tilesets {
27-
if let Some(path) = &tileset.image {
28-
let bytes = std::fs::read(format!("assets/{}", path)).unwrap();
27+
if let Some(image_path) = &tileset.image {
28+
let filename = std::path::Path::new(image_path).file_name().unwrap();
29+
let bytes = std::fs::read(std::path::Path::new("assets").join(filename)).unwrap();
2930
let img = image::load_from_memory(&bytes).unwrap().to_rgba8();
3031
let (w, h) = img.dimensions();
3132
let tex = gfx.load_texture(&bytes);
@@ -38,7 +39,7 @@ impl Map {
3839
},
3940
);
4041

41-
println!("Loaded tileset: {} ({}x{})", path, w, h);
42+
println!("Loaded tileset: {} ({}x{})", image_path, w, h);
4243
}
4344
}
4445
}

dyrah_server/src/game.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,22 @@ impl Game {
162162
let dir = input.to_direction();
163163
if dir != IVec2::ZERO {
164164
let next_pos = tile_pos.vec + dir;
165-
if self.map.is_walkable(next_pos, &self.collision_grid) {
165+
let walkable = if dir.x != 0 && dir.y != 0 {
166+
// diagonal: destination + both adjacent cardinals must be clear
167+
self.map.is_walkable(next_pos, &self.collision_grid)
168+
&& self.map.is_walkable(
169+
tile_pos.vec + IVec2::new(dir.x, 0),
170+
&self.collision_grid,
171+
)
172+
&& self.map.is_walkable(
173+
tile_pos.vec + IVec2::new(0, dir.y),
174+
&self.collision_grid,
175+
)
176+
} else {
177+
self.map.is_walkable(next_pos, &self.collision_grid)
178+
};
179+
180+
if walkable {
166181
target_pos.vec = next_pos;
167182
target_pos.path = None;
168183
tile_pos.vec = next_pos;

dyrah_server/src/map.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,36 @@ impl Map {
7171
grid.is_walkable(tile_pos)
7272
}
7373

74-
fn manhattan_distance(&self, a: IVec2, b: IVec2) -> u32 {
75-
((a.x - b.x).abs() + (a.y - b.y).abs()) as u32
74+
fn chebyshev_distance(&self, a: IVec2, b: IVec2) -> u32 {
75+
let dx = (a.x - b.x).abs() as u32;
76+
let dy = (a.y - b.y).abs() as u32;
77+
// cardinal cost=2, diagonal cost=3; Chebyshev: max(dx,dy)*2 + min(dx,dy)*(3-2)
78+
let (min, max) = if dx < dy { (dx, dy) } else { (dy, dx) };
79+
max * 2 + min
7680
}
7781

7882
fn get_walkable_successors(&self, tile_pos: IVec2, grid: &CollisionGrid) -> Vec<(IVec2, u32)> {
7983
let mut successors = Vec::new();
80-
for (dx, dy) in &[(0, 1), (1, 0), (0, -1), (-1, 0)] {
84+
for &(dx, dy, cost) in &[
85+
(0, 1, 2), (1, 0, 2), (0, -1, 2), (-1, 0, 2),
86+
(1, 1, 3), (1, -1, 3), (-1, 1, 3), (-1, -1, 3),
87+
] {
8188
let neighbor = IVec2::new(tile_pos.x + dx, tile_pos.y + dy);
8289
if neighbor.x >= 0
8390
&& neighbor.y >= 0
8491
&& neighbor.x < self.tiled.width as i32
8592
&& neighbor.y < self.tiled.height as i32
8693
&& grid.is_walkable(neighbor)
8794
{
88-
successors.push((neighbor, 1));
95+
// for diagonals, also require both adjacent cardinal tiles to be walkable
96+
if dx != 0 && dy != 0 {
97+
let adj_x = IVec2::new(tile_pos.x + dx, tile_pos.y);
98+
let adj_y = IVec2::new(tile_pos.x, tile_pos.y + dy);
99+
if !grid.is_walkable(adj_x) || !grid.is_walkable(adj_y) {
100+
continue;
101+
}
102+
}
103+
successors.push((neighbor, cost));
89104
}
90105
}
91106
successors
@@ -95,7 +110,7 @@ impl Map {
95110
let result = astar(
96111
&start,
97112
|&pos| self.get_walkable_successors(pos, grid),
98-
|&pos| self.manhattan_distance(pos, end),
113+
|&pos| self.chebyshev_distance(pos, end),
99114
|&pos| pos == end,
100115
);
101116
result.map(|(path, _)| path.into_iter().skip(1).collect())

0 commit comments

Comments
 (0)