You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 23:26:20