Latest commit 8c81e81
use bevy::{ app::{AppExit, ScheduleRunnerPlugin, ScheduleRunnerSettings}, ecs::schedule::ReportExecutionOrderAmbiguities, log::LogPlugin, prelude::*, utils::Duration, }; use rand::random; /// 这是Bevy实体组件系统(ECS)的介绍 /// 所有Bevy应用程序的逻辑都是用ECS模式构建的,所以一定要注意! /// /// 为什么用ECS? /// 略 /// /// ECS定义: /// /// 组件:Rust数据类型 /// /// 实体:组件的集合就是实体(本质是一个唯一的ID) /// 例如,Entity1{ Name("Alice"), Position(0, 0) },Entity2{ Name("Bil"), Position(10, 5) } /// /// 资源:全局数据 /// 例如,asset_storage,events,system state等 /// /// 系统:在实体、组件和资源上运行的逻辑 /// // 组件 // // 游戏里有很多“玩家”,每个玩家都有“名字” #[derive(Component)] struct Player { name: String, } // 每个玩家都有“分数” #[derive(Component)] struct Score { value: usize, } // 资源 // // 储存游戏信息 #[derive(Default)] struct GameState { // 当前回合 current_round: usize, // 玩家总数 total_players: usize, // 胜利的玩家 winning_player: Option<String>, } // 储存游戏规则 struct GameRules { // 胜利所需分数 winning_score: usize, // 最大回合 max_rounds: usize, // 最大玩家数量 max_players: usize, } // 系统 // // 这是最简单的一种系统 fn print_message_system() { println!("这游戏真好玩!"); } // 系统也可以读取和修改资源。此系统每次更新都会开始新的“一轮”游戏: // 提示:“mut”表示资源是“可变的” // Res<GameRules>是只读的,ResMut<GameState>可以修改资源 fn new_round_system(game_rules: Res<GameRules>, mut game_state: ResMut<GameState>) { game_state.current_round += 1; println!( "Begin round {} of {}", game_state.current_round, game_rules.max_rounds ); } // 访问带有“Player”和“Score”组件的实体,更新实体的分数 fn score_system(mut query: Query<(&Player, &mut Score)>) { for (player, mut score) in query.iter_mut() { let scored_a_point = random::<bool>(); if scored_a_point { score.value += 1; println!("{} 得分!他们的分数是:{}", player.name, score.value); } else { println!("{}没有得分!他们的分数是:{}", player.name, score.value); } } } // 访问所有带有“Player”和“Score”组件的实体,也会访问“GameRules”资源,以确定玩家是否已经获胜 fn score_check_system( game_rules: Res<GameRules>, mut game_state: ResMut<GameState>, query: Query<(&Player, &Score)>, ) { for (player, score) in query.iter() { if score.value == game_rules.winning_score { game_state.winning_player = Some(player.name.clone()); } } } // 如果满足条件,结束游戏,然后触发一个AppExit事件,App退出(查看“event.rs”示例) fn game_over_system( game_rules: Res<GameRules>, game_state: Res<GameState>, mut app_exit_events: EventWriter<AppExit>, ) { if let Some(ref player) = game_state.winning_player { println!("{} 赢得游戏!", player); app_exit_events.send(AppExit); } else if game_state.current_round == game_rules.max_rounds { println!("回合结束。平局!"); app_exit_events.send(AppExit); } println!(); } // 这个系统在app启动时运行一次 // 只运行一次的系统一般用来创建游戏的初始状态(state),“启动”系统和“普通”系统的区别是它们的注册方式不同: // “启动”系统:app.add_startup_system(startup_system) // “普通”系统:app.add_system(normal_system) fn startup_system(mut commands: Commands, mut game_state: ResMut<GameState>) { // 创建游戏规则资源 commands.insert_resource(GameRules { max_rounds: 10, winning_score: 4, max_players: 4, }); // 把一些玩家添加到“World”,玩家默认有0分 commands.spawn_batch(vec![ ( Player { name: "Alice".to_string(), }, Score { value: 0 }, ), ( Player { name: "Bob".to_string(), }, Score { value: 0 }, ), ]); // 设置最大玩家数量为“2” game_state.total_players = 2; } // 这个系统用命令缓冲区来(潜在地)在每次迭代时向游戏增加一个新玩家(也就是用command来修改world) // 普通系统不能安全地直接访问World实例,因为实例是并行运行的 // World包含所有组件,所以在并行中修改它的任意部分都是不安全的 // 命令缓冲区可以把所有“修改”排列起来修改World,从而不用直接访问World fn new_player_system( mut commands: Commands, game_rules: Res<GameRules>, mut game_state: ResMut<GameState>, ) { // 随机添加一个新玩家 let add_new_player = random::<bool>(); if add_new_player && game_state.total_players < game_rules.max_players { game_state.total_players += 1; commands.spawn_bundle(( Player { name: format!("Player {}", game_state.total_players), }, Score { value: 0 }, )); println!("Player {} joined the game!", game_state.total_players); } } // 如果要对World或Resource进行完全的、即时的读/写访问,可以用“独占系统” // 警告:本地线程会阻止其他系统的并行执行,直到线程完成,如果需要多线程性能,一般避免使用本地线程 #[allow(dead_code)] fn exclusive_player_system(world: &mut World) { // 和“new_player_system”的功能一样 let total_players = world.get_resource_mut::<GameState>().unwrap().total_players; let should_add_player = { let game_rules = world.get_resource::<GameState>().unwrap(); let add_new_player = random::<bool>(); add_new_player && total_players < game_rules.max_players }; // 随机添加一个新玩家 if should_add_player { world.spawn().insert_bundle(( Player { name: format!("Player {}", total_players), }, Score { value: 0 }, )); let mut game_state = world.get_resource_mut::<GameState>().unwrap(); game_state.total_players += 1; } } // 有时系统需要单独的“本地”状态,Bevy的ECS为此提供了Local<T>资源 // Local<T>对系统来说是唯一的,并自动初始化(如果它们还不存在) // 如果拥有某个系统的ID,也可以用`Resources::get_local()`直接从Resources集合中访问本地资源 // 仅在以下情况适用: // 1.拥有同某个系统的多个实例,并且这些系统都需要单独的状态 // 2.已经拥有某个资源的全局版本,不想在当前的系统覆盖它 // 3.不想把系统要使用的资源注册成全局资源 #[derive(Default)] struct State { counter: usize, } // 注意:这段代码的作用是为了说明问题所在,和这个游戏没有任何关系 #[allow(dead_code)] fn local_state_system(mut state: Local<State>, query: Query<&Player, &Score>) { for (player, score) in query.iter() { println!("Processed: {} {}", player.name, score.value); } println!("this system ran {} times", state.counter); state.counter += 1; } #[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] enum MyStage { BeforeRound, AfterRound, } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] enum MyLabels { ScoreCheck, } // Bevy应用程序的入口 fn main() { // Bevy的App是用建造者模式来创建的,可以把系统、资源和插件添加到App App::new() // 可以这样把资源添加到app .insert_resource(State { counter: 0 }) // 把“系统设置”作为资源,用来配置系统 .insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs(5))) // 插件只是一组app构造器的调用 // 下面的插件会每5秒(上面已经设置为5秒)运行一次app的“system schedule” .add_plugin(ScheduleRunnerPlugin::default()) // 实现了Default或FromResources特型的资源可以这样添加 .init_resource::<GameState>() // startup系统会先于其他系统运行一次,一般包含用来初始化的代码(例如添加实体或资源等) .add_startup_system(startup_system) // system的调用会把Rust函数转换成ECS系统 .add_system(print_message_system) // 系统执行顺序 // // 系统属于阶段,阶段控制着每一tick内所有系统的执行策略和大致顺序 // “startup阶段”(startup系统注册在这个阶段)总会在普通阶段开始之前完成,并且阶段内的所有系统都完成后才能进入下一阶段 // 一旦每一个阶段都结束,就完成一个循环(main),然后开始新的循环 // // 默认情况下,所有系统都是并行运行的,除非这个系统要对某个数据进行可变访问 // 并行运行很有效率,但有时需要考虑顺序 // 例如,“游戏结束”系统不应该与其他系统并行运行,而是在其他系统之后运行 // // 与其把每个系统分割成不同阶段,不如给相关系统打上`.label`标签,然后用`.before`或`.after`方法来明确排序 // 被排序的系统会形成“排序依赖”关系,然后按顺序执行 // // 大多数情况下,打上排序标签比拆分系统带来更好的性能,因为这样调度算法有更多并行运行的机会 // 但是,阶段仍然是必须的:阶段的结束是一个硬同步点(意味着没有系统在运行),在这里处理系统发出的`Commands` // 这样做的原因是,`Commands`可能会执行一些与运行中的系统不兼容的操作,例如生成或删除实体,添加或删除资源等 // // `add_system(system)`默认把系统添加到UPDATE阶段,也可以手动指定阶段 // 下面的代码相当于:`add_system(score_system)` .add_system_to_stage(CoreStage::Update, score_system) // 也可以创建新的阶段e,下面是这个游戏的阶段顺序 // "before_round": new_player_system, new_round_system // "update": print_message_system, game_over_system // "after_round": score_check_system, game_over_system .add_stage_before( CoreStage::Update, MyStage::BeforeRound, SystemStage::parallel(), ) .add_stage_after( CoreStage::Update, MyStage::AfterRound, SystemStage::parallel(), ) .add_system_to_stage(MyStage::BeforeRound, new_round_system) .add_system_to_stage(MyStage::BeforeRound, new_player_system) .add_system_to_stage( MyStage::BeforeRound, exclusive_player_system.exclusive_system(), ) // 用显式排序的约束来确保game_over系统运行在score_check_system后面。 // 首先,用`.label`标记将要引用的系统,... .add_system_to_stage( MyStage::AfterRound, score_check_system.label(MyLabels::ScoreCheck), ) // 然后用`.before`或`.after`来表明顺序 .add_system_to_stage( MyStage::AfterRound, game_over_system.after(MyLabels::ScoreCheck), ) // 检查系统是否存在执行顺序歧义:用LogPlugin检查控制台产生的输出,并把下面的资源添加到app // 请注意,检查器的报告不全是潜在问题,需要自行判断 .add_plugin(LogPlugin::default()) .insert_resource(ReportExecutionOrderAmbiguities) // 开始运行app .run(); }