在Rust/Bevy/Wasm环境下实现游戏排行榜的异步网络请求问题
在Bevy Wasm环境中实现异步排行榜数据交互
针对你在Bevy Wasm游戏中实现排行榜的异步数据交互问题,核心解决方案是放弃同步返回值的思路,改用事件驱动+异步任务,结合Bevy的ECS(实体组件系统)特性来处理数据流动,完全适配Wasm的非阻塞运行要求。
核心思路
Wasm在浏览器主线程运行时无法阻塞,因此必须通过异步任务处理网络请求,再通过Bevy的事件系统将结果传回主线程的ECS系统,避免类型系统冲突和生命周期问题。
具体实现步骤
1. 定义事件与资源
先通过Bevy的Event和Resource定义数据流转所需的载体:
use bevy::prelude::*; use serde::Deserialize; // 存储高分数据的全局资源 #[derive(Resource, Default)] struct HighScores { scores: Vec<u32>, is_loading: bool, } // 触发高分获取的事件 #[derive(Event)] struct FetchHighScores; // 高分获取成功的事件(携带数据) #[derive(Event)] struct HighScoresFetched(Vec<u32>); // 提交玩家分数的事件 #[derive(Event)] struct SubmitScore(u32);
2. 异步获取高分数据
使用wasm-bindgen-futures的spawn_local启动异步网络请求,完成后通过事件将结果传回Bevy主线程:
use wasm_bindgen_futures::spawn_local; use reqwest::Client; fn handle_fetch_high_scores( mut events: EventReader<FetchHighScores>, mut high_scores: ResMut<HighScores>, mut commands: Commands, ) { // 避免重复发起请求 if events.iter().next().is_some() && !high_scores.is_loading { high_scores.is_loading = true; let client = Client::new(); let url = "https://your-server.com/highscores".to_string(); // 克隆事件写入器,用于异步任务完成后发送结果 let mut event_writer = commands.get_or_spawn_event_writer::<HighScoresFetched>(); // 启动浏览器微任务队列中的异步任务 spawn_local(async move { match client.get(&url).send().await { Ok(response) if response.status().is_success() => { match response.json::<Vec<u32>>().await { Ok(scores) => { event_writer.send(HighScoresFetched(scores)); } Err(e) => eprintln!("解析高分失败: {}", e), } } Ok(response) => eprintln!("请求失败,状态码: {}", response.status()), Err(e) => eprintln!("获取高分失败: {}", e), } }); } }
3. 处理高分获取结果
在主线程系统中监听HighScoresFetched事件,更新全局资源并处理后续逻辑:
fn handle_high_scores_fetched( mut events: EventReader<HighScoresFetched>, mut high_scores: ResMut<HighScores>, mut submit_writer: EventWriter<SubmitScore>, // 假设你有存储玩家当前分数的资源 player_score: Res<PlayerScore>, ) { if let Some(HighScoresFetched(scores)) = events.iter().last() { high_scores.scores = scores.clone(); high_scores.is_loading = false; // 对比玩家分数,若更高则触发提交 let top_score = scores.iter().max().unwrap_or(&0); if player_score.value > *top_score { submit_writer.send(SubmitScore(player_score.value)); } } }
4. 异步提交玩家分数
提交逻辑与获取类似,完成后可重新拉取最新榜单:
fn handle_submit_score( mut events: EventReader<SubmitScore>, mut commands: Commands, ) { for SubmitScore(score) in events.iter() { let client = Client::new(); let url = "https://your-server.com/submit-score".to_string(); let score = *score; let mut fetch_writer = commands.get_or_spawn_event_writer::<FetchHighScores>(); spawn_local(async move { let payload = serde_json::json!({ "score": score }); match client.post(&url).json(&payload).send().await { Ok(response) if response.status().is_success() => { println!("分数提交成功!"); // 提交成功后刷新榜单 fetch_writer.send(FetchHighScores); } Ok(response) => eprintln!("提交失败,状态码: {}", response.status()), Err(e) => eprintln!("提交分数失败: {}", e), } }); } }
5. 注册系统到Bevy App
将上述系统添加到游戏主逻辑中,并在游戏结束时触发高分获取:
// 假设游戏结束时会触发GameOver事件 #[derive(Event)] struct GameOver; fn trigger_fetch_on_game_over( mut game_over_events: EventReader<GameOver>, mut fetch_writer: EventWriter<FetchHighScores>, ) { if game_over_events.iter().next().is_some() { fetch_writer.send(FetchHighScores); } } fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::<HighScores>() .init_resource::<PlayerScore>(|| PlayerScore { value: 0 }) .add_event::<FetchHighScores>() .add_event::<HighScoresFetched>() .add_event::<SubmitScore>() .add_event::<GameOver>() .add_system(handle_fetch_high_scores) .add_system(handle_high_scores_fetched) .add_system(handle_submit_score) .add_system(trigger_fetch_on_game_over) .run(); }
关键说明
- 为什么不用
block_on:Wasm运行在浏览器主线程,阻塞会导致页面卡死,必须使用非阻塞的spawn_local将任务放入浏览器微任务队列。 - 事件驱动的优势:完全适配Bevy的ECS设计,避免捕获栈变量的生命周期问题,通过资源和事件安全传递数据。
- 资源更新规则:Bevy的全局资源必须在主线程系统中修改,因此异步任务完成后通过事件通知主线程处理更新。
备选方案:通过JavaScript处理请求
如果需要用JS完成网络请求,可通过wasm-bindgen定义JS交互函数,再通过回调发送事件到Bevy:
use wasm_bindgen::prelude::*; use js_sys::Function; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_name = fetchHighScores)] fn js_fetch_high_scores(callback: &Function); } fn fetch_via_js(mut commands: Commands) { let callback = Closure::wrap(Box::new(move |scores: JsValue| { let scores: Vec<u32> = scores.into_serde().unwrap(); let mut event_writer = commands.get_or_spawn_event_writer::<HighScoresFetched>(); event_writer.send(HighScoresFetched(scores)); }) as Box<dyn Fn(JsValue)>); js_fetch_high_scores(callback.as_ref()); callback.forget(); // 避免Closure被提前释放 }
对应的JS实现:
function fetchHighScores(callback) { fetch('https://your-server.com/highscores') .then(res => res.json()) .then(scores => callback(scores)) .catch(err => console.error(err)); }
内容的提问来源于stack exchange,提问作者shakesbeare
相关产品推荐
相关产品推荐

