Skip to content

Commit a54f151

Browse files
authored
fix: 修复玩家不从主机请求新区块的bug
2 parents 812fcbf + 72c19d2 commit a54f151

14 files changed

Lines changed: 555 additions & 68 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,4 @@ docs/
8787
3. **文档冲突**:以本 README 决策表为准;其余冲突时 `docs/architecture.md` > `docs/modules/*` > `docs/features/*` > 其它
8888
4. **代码注释**:必须加上详细的中文注释以供没有图形学基础的人学习,但架构说明只放文档不写进注释
8989
5. **API 变化**:WGPU 等库新版 API 可能有变化,注意查阅最新文档
90-
6. **代码检查**:完成代码编辑后必须要通过 fmt 和 clippy 检查
90+
6. **代码检查**:完成代码编辑后必须要通过 fmt 和 clippy --all-targets 检查

crates/client/src/app.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,9 @@ pub struct Game {
288288
/// Phase 6:房间主机的 entity_id(Local-Only / Host 模式 = 自己;Remote 模式由 Welcome 填)。
289289
/// 0 表示尚未知晓(Remote 端 Welcome 到达前的瞬态)。
290290
pub host_entity_id: EntityId,
291+
/// Host 允许 Remote 使用的最大视距。Remote 在 Welcome/HostSettings 后填入;
292+
/// Local/Host 模式等于自己的设置。
293+
pub host_render_distance: u32,
291294
/// Phase 6:聊天历史(含本地合成的系统消息)。
292295
pub chat: ChatHistory,
293296
/// Phase 4:RTT(毫秒)。`None` 表示未测过 / 上次 Ping 还没回。Local 模式永远 None。
@@ -332,6 +335,7 @@ impl Game {
332335
let server = Rc::new(RefCell::new(Server::new(seed)));
333336
let eid = {
334337
let mut s = server.borrow_mut();
338+
s.set_host_render_distance(settings.render_distance);
335339
let id = s.add_player(display_name.to_string());
336340
let _ = s.drain_outbox();
337341
id
@@ -349,6 +353,7 @@ impl Game {
349353
game.entity_id = eid;
350354
// Local-Only:自己即 Host,立即填 host_entity_id 让 UI 不显示"未知主机"瞬态。
351355
game.host_entity_id = eid;
356+
game.host_render_distance = game.settings.render_distance;
352357
game
353358
}
354359

@@ -364,6 +369,7 @@ impl Game {
364369
let server = Rc::new(RefCell::new(Server::new(seed)));
365370
let eid = {
366371
let mut s = server.borrow_mut();
372+
s.set_host_render_distance(settings.render_distance);
367373
let id = s.add_player(display_name.to_string());
368374
let _ = s.drain_outbox();
369375
id
@@ -382,6 +388,7 @@ impl Game {
382388
game.entity_id = eid;
383389
// Host:自己即 Host,host_entity_id 直接填。
384390
game.host_entity_id = eid;
391+
game.host_render_distance = game.settings.render_distance;
385392
Ok(game)
386393
}
387394

@@ -398,15 +405,23 @@ impl Game {
398405
// server_inbox 在 Remote 模式不参与驱动;为保持 Game 字段不可空,造一对空 mpsc
399406
let (_net_local, dummy_inbox) = NetEndpoint::new_local_pair();
400407
let net = NetEndpoint::new_remote(signaling_url, room_id, display_name)?;
401-
Ok(Self::assemble(
408+
let mut game = Self::assemble(
402409
GameMode::Remote,
403410
server,
404411
dummy_inbox,
405412
net,
406413
settings,
407414
display_name.to_string(),
408415
room_id.to_string(),
409-
))
416+
);
417+
let spawn_center = crate::chunk_loader::chunk_pos_of(voxweb_server::DEFAULT_SPAWN);
418+
let bootstrap_radius = game
419+
.chunk_loader
420+
.render_distance
421+
.min(voxweb_server::INITIAL_SNAPSHOT_RADIUS);
422+
game.chunk_loader
423+
.mark_requested_square(spawn_center, bootstrap_radius);
424+
Ok(game)
410425
}
411426

412427
fn assemble(
@@ -445,6 +460,7 @@ impl Game {
445460
entity_id: 0, // 由 add_player(Local/Host)或 Welcome(Remote)填
446461
display_name,
447462
host_entity_id: 0, // Local/Host 模式在 new_* 里立即填;Remote 等 Welcome 回填
463+
host_render_distance: 0,
448464
chat: ChatHistory::default(),
449465
rtt_ms: None,
450466
last_ping_sent_ms: 0.0,
@@ -474,11 +490,28 @@ impl Game {
474490
self.interp
475491
.set_delay_ms(self.settings.interp_delay_ms as f64);
476492
self.chunk_loader
477-
.set_render_distance(self.settings.render_distance);
478-
self.server
479-
.borrow_mut()
480-
.world
481-
.set_chunk_cache_capacity(self.settings.chunk_cache_capacity);
493+
.set_render_distance(self.effective_render_distance());
494+
{
495+
let mut server = self.server.borrow_mut();
496+
server
497+
.world
498+
.set_chunk_cache_capacity(self.settings.chunk_cache_capacity);
499+
if matches!(self.mode, GameMode::Local | GameMode::Host) {
500+
server.set_host_render_distance(self.settings.render_distance);
501+
self.host_render_distance = self.settings.render_distance;
502+
}
503+
}
504+
}
505+
506+
/// 当前实际使用的区块视距。
507+
///
508+
/// Remote 端不能超过 Host 视距;Local/Host 端直接使用自己的设置。
509+
pub fn effective_render_distance(&self) -> u32 {
510+
if self.mode == GameMode::Remote && self.host_render_distance > 0 {
511+
self.settings.render_distance.min(self.host_render_distance)
512+
} else {
513+
self.settings.render_distance
514+
}
482515
}
483516
}
484517

crates/client/src/chunk_assembler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! ChunkSnapshot 分片组装器。
22
//!
3-
//! Host 用 `send_initial_snapshot` 把每个 chunk 的 bincode 编码结果切片发送;
3+
//! Host 用 bootstrap 快照或 `ChunkRequest` 响应把每个 chunk 的 bincode 编码结果切片发送;
44
//! Remote 端接收时通过本模块按 ChunkPos 汇集,齐了返回 concatenated bytes。
55
//!
66
//! Phase 5:不关心具体编码格式;解码(Vec<BlockID> → Chunk)在上层 `apply_server_message` 做。

crates/client/src/chunk_loader.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct ChunkLoader {
1818
pub render_distance: i32,
1919
pub unload_buffer: i32,
2020
pub loaded: HashSet<ChunkPos>,
21+
requested: HashSet<ChunkPos>,
2122
last_center: Option<ChunkPos>,
2223
}
2324

@@ -27,6 +28,7 @@ impl ChunkLoader {
2728
render_distance: render_distance as i32,
2829
unload_buffer: 3,
2930
loaded: HashSet::new(),
31+
requested: HashSet::new(),
3032
last_center: None,
3133
}
3234
}
@@ -102,6 +104,91 @@ impl ChunkLoader {
102104

103105
true
104106
}
107+
108+
/// Remote 模式的区块滚动加载。
109+
///
110+
/// Remote 没有权威地形生成权,只维护本地缓存集合:缺失 chunk 通过
111+
/// `ClientMessage::ChunkRequest` 向 Host 请求;Host 回传 `ChunkSnapshot` 后,
112+
/// 调用 [`ChunkLoader::mark_loaded`] 把它从 in-flight 集合移到 loaded 集合。
113+
pub fn update_remote(
114+
&mut self,
115+
camera_pos: Vec3,
116+
server: &mut Server,
117+
mesh_jobs: &mut MeshJobQueue,
118+
renderer: &mut Renderer,
119+
) -> Vec<ChunkPos> {
120+
let center = chunk_pos_of(camera_pos);
121+
let center_changed = Some(center) != self.last_center;
122+
if center_changed {
123+
self.last_center = Some(center);
124+
}
125+
126+
let mut requests = Vec::new();
127+
if center_changed {
128+
let r = self.render_distance;
129+
let desired: HashSet<ChunkPos> = (-r..=r)
130+
.flat_map(|dx| (-r..=r).map(move |dz| ChunkPos::new(center.x + dx, center.z + dz)))
131+
.collect();
132+
133+
for pos in desired.difference(&self.loaded).copied() {
134+
if self.requested.insert(pos) {
135+
requests.push(pos);
136+
}
137+
}
138+
}
139+
140+
// Remote 也要卸载远离视距的本地 world/mesh,否则长途移动会无限增长。
141+
let unload_r = self.render_distance + self.unload_buffer;
142+
let to_unload: Vec<ChunkPos> = self
143+
.loaded
144+
.iter()
145+
.copied()
146+
.filter(|p| chebyshev_distance(*p, center) > unload_r)
147+
.collect();
148+
for pos in to_unload {
149+
server.world.unload_chunk(pos);
150+
mesh_jobs.cancel(pos);
151+
renderer.drop_chunk_mesh(pos);
152+
self.loaded.remove(&pos);
153+
}
154+
155+
// 已经飞出缓冲范围但尚未返回的请求直接忘掉;若快照稍后到达,
156+
// 下一次 update_remote 会按当前位置把它清理出缓存。
157+
let stale_requests: Vec<ChunkPos> = self
158+
.requested
159+
.iter()
160+
.copied()
161+
.filter(|p| chebyshev_distance(*p, center) > unload_r)
162+
.collect();
163+
for pos in stale_requests {
164+
self.requested.remove(&pos);
165+
}
166+
167+
requests
168+
}
169+
170+
/// 标记一个 Remote chunk 已收到完整快照。
171+
pub fn mark_loaded(&mut self, pos: ChunkPos) {
172+
self.loaded.insert(pos);
173+
self.requested.remove(&pos);
174+
}
175+
176+
/// 标记一个正方形范围的 chunk 已经有快照在路上。
177+
///
178+
/// Host 当前仍会在玩家加入时主动推送出生点附近的 bootstrap 快照。
179+
/// Remote 端把这批 chunk 记为 in-flight,后续按视距请求时只补缺口,
180+
/// 避免初始阶段重复传同一批地图数据。
181+
pub fn mark_requested_square(&mut self, center: ChunkPos, radius: i32) {
182+
let r = radius.max(0);
183+
for dx in -r..=r {
184+
for dz in -r..=r {
185+
let pos = ChunkPos::new(center.x + dx, center.z + dz);
186+
if !self.loaded.contains(&pos) {
187+
self.requested.insert(pos);
188+
}
189+
}
190+
}
191+
}
105192
}
106193

107194
/// 世界坐标 → 所在 chunk 的 ChunkPos。

crates/client/src/lib.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use crate::app::{
3939
AppState, BASE_SENSITIVITY_RAD_PER_PIXEL, Game, GameMode, PreloadState, RemotePlayerState,
4040
};
4141
use crate::camera::CameraMode;
42-
use crate::chunk_loader::affected_chunks;
42+
use crate::chunk_loader::{affected_chunks, chunk_pos_of};
4343
use crate::input::InputState;
4444
use crate::mesh_jobs::{MeshPriority, MeshRunStats};
4545
use crate::prediction::{
@@ -1355,6 +1355,23 @@ fn render_connecting_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(
13551355
renderer,
13561356
);
13571357
drop(server_mut);
1358+
} else if game.entity_id != 0 {
1359+
let mut server_mut = game.server.borrow_mut();
1360+
let requests = game.chunk_loader.update_remote(
1361+
voxweb_server::DEFAULT_SPAWN,
1362+
&mut server_mut,
1363+
&mut game.mesh_jobs,
1364+
renderer,
1365+
);
1366+
drop(server_mut);
1367+
if !requests.is_empty() {
1368+
log::debug!("[remote] request {} spawn preload chunks", requests.len());
1369+
game.net.send_client_message(ClientMessage::ChunkRequest {
1370+
center: chunk_pos_of(voxweb_server::DEFAULT_SPAWN),
1371+
render_distance: game.chunk_loader.render_distance.max(0) as u32,
1372+
chunks: requests,
1373+
});
1374+
}
13581375
}
13591376

13601377
// 运行网格化(预载期间用 16ms 预算,比正常 4ms 更大)
@@ -1365,6 +1382,7 @@ fn render_connecting_frame(app: &Rc<RefCell<App>>, cw: u32, ch: u32) -> Result<(
13651382
// 统计已接收和已网格化的区块数
13661383
let spawn_center = crate::chunk_loader::chunk_pos_of(voxweb_server::DEFAULT_SPAWN);
13671384
let r = game.chunk_loader.render_distance;
1385+
preload.total = ((2 * r + 1) * (2 * r + 1)) as usize;
13681386
let mut received = 0usize;
13691387
let mut meshed = 0usize;
13701388
for dx in -r..=r {
@@ -1744,7 +1762,7 @@ fn apply_room_event(app: &Rc<RefCell<App>>, ev: RoomEvent) {
17441762
// 网络连接完成,启动区块预载(不再直接进 InGame)
17451763
if a.state == AppState::Connecting {
17461764
if let Some(ref game) = a.game {
1747-
let rd = game.settings.render_distance as i32;
1765+
let rd = game.chunk_loader.render_distance;
17481766
let total = ((2 * rd + 1) * (2 * rd + 1)) as usize;
17491767
a.preload_state = Some(PreloadState {
17501768
total,
@@ -2032,8 +2050,9 @@ fn render_game_frame(
20322050
)
20332051
};
20342052

2035-
// —— 5. ChunkLoader 滚动(仅 Local / Host;Remote 由 ChunkSnapshot / BlockUpdate 驱动) ——
2036-
if mode != GameMode::Remote {
2053+
// —— 5. ChunkLoader 滚动 ——
2054+
// Local/Host 直接生成;Remote 只请求缺失 chunk,由 Host 回 ChunkSnapshot。
2055+
{
20372056
let mut a = app.borrow_mut();
20382057
let App {
20392058
ref mut renderer,
@@ -2044,8 +2063,28 @@ fn render_game_frame(
20442063
return Ok(());
20452064
};
20462065
let mut server_mut = game.server.borrow_mut();
2047-
game.chunk_loader
2048-
.update(camera_pos, &mut server_mut, &mut game.mesh_jobs, renderer);
2066+
if mode == GameMode::Remote {
2067+
if game.entity_id != 0 {
2068+
let requests = game.chunk_loader.update_remote(
2069+
camera_pos,
2070+
&mut server_mut,
2071+
&mut game.mesh_jobs,
2072+
renderer,
2073+
);
2074+
drop(server_mut);
2075+
if !requests.is_empty() {
2076+
log::debug!("[remote] request {} chunks near camera", requests.len());
2077+
game.net.send_client_message(ClientMessage::ChunkRequest {
2078+
center: chunk_pos_of(camera_pos),
2079+
render_distance: game.chunk_loader.render_distance.max(0) as u32,
2080+
chunks: requests,
2081+
});
2082+
}
2083+
}
2084+
} else {
2085+
game.chunk_loader
2086+
.update(camera_pos, &mut server_mut, &mut game.mesh_jobs, renderer);
2087+
}
20492088
}
20502089

20512090
// —— 6. mesh_jobs run_until_budget ——
@@ -2659,19 +2698,37 @@ fn ingest_server_clock_sample(game: &mut Game, estimated_offset_ms: f64, alpha:
26592698
game.server_clock_offset_ms = game.server_clock_offset_ms * (1.0 - a) + estimated_offset_ms * a;
26602699
}
26612700

2701+
fn apply_host_render_distance(game: &mut Game, host_render_distance: u32) {
2702+
let capped = host_render_distance.max(1);
2703+
let before = game.chunk_loader.render_distance;
2704+
game.host_render_distance = capped;
2705+
game.apply_settings();
2706+
let after = game.chunk_loader.render_distance;
2707+
if game.mode == GameMode::Remote && before != after {
2708+
log::info!(
2709+
"[remote] effective render distance capped by host: requested={} host={} effective={}",
2710+
game.settings.render_distance,
2711+
capped,
2712+
after
2713+
);
2714+
}
2715+
}
2716+
26622717
fn apply_server_message(game: &mut Game, msg: ServerMessage) {
26632718
match msg {
26642719
ServerMessage::Welcome {
26652720
entity_id,
26662721
world_seed,
26672722
host_entity_id,
2723+
host_render_distance,
26682724
players,
26692725
..
26702726
} => {
26712727
game.entity_id = entity_id;
26722728
game.host_entity_id = host_entity_id;
2729+
apply_host_render_distance(game, host_render_distance);
26732730
log::info!(
2674-
"Welcome v2: entity_id={entity_id}, seed={world_seed}, host={host_entity_id}, roster_size={}",
2731+
"Welcome v3: entity_id={entity_id}, seed={world_seed}, host={host_entity_id}, host_rd={host_render_distance}, roster_size={}",
26752732
players.len()
26762733
);
26772734
// 写入 roster:除自己以外的玩家进入 remote_players
@@ -2693,6 +2750,9 @@ fn apply_server_message(game: &mut Game, msg: ServerMessage) {
26932750
game.server.borrow_mut().world.chunks.clear();
26942751
}
26952752
}
2753+
ServerMessage::HostSettings { render_distance } => {
2754+
apply_host_render_distance(game, render_distance);
2755+
}
26962756
ServerMessage::ChunkSnapshot {
26972757
pos,
26982758
frag_index,
@@ -2707,6 +2767,7 @@ fn apply_server_message(game: &mut Game, msg: ServerMessage) {
27072767
Ok(blocks) => {
27082768
let chunk = voxweb_core::chunk::Chunk { blocks };
27092769
game.server.borrow_mut().world.chunks.insert(pos, chunk);
2770+
game.chunk_loader.mark_loaded(pos);
27102771
// 自己 + 相邻 8 个 chunk 都重 mesh
27112772
for dz in -1..=1i32 {
27122773
for dx in -1..=1i32 {

0 commit comments

Comments
 (0)