ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Amethyst 自定义状态事件实战:基于 events_custom_state_event 示例扩展 StateEvent 事件系统

Amethyst 自定义状态事件实战:基于 events_custom_state_event 示例扩展 StateEvent 事件系统 【免费下载链接】amethystData-oriented and>项目地址https://gitcode.com/gh_mirrors/ame/amethyst点击查看免费下载事件系统是游戏引擎中连接系统产出与状态消费的关键通道。Amethyst 默认只向State分发窗口事件与输入事件当你的游戏逻辑需要状态层感知自定义信号例如难度提升、关卡切换、成就达成时就必须扩展这一事件类型。本文以仓库自带的 events_custom_state_event 示例 为主线从自定义事件枚举、EventReader派生宏、事件生产/消费两端到CoreApplication泛型装配完整讲解如何在 Amethyst 中构建一套自定义状态事件管线。读完本文你将掌握扩展StateEvent、用EventChannel在系统与状态间传递自定义事件的标准做法。示例概览与预期运行结果示例的官方说明只有一句话它演示了如何用自定义状态事件扩展事件系统Demonstrates how to extend the event system with a custom state event.。运行该示例后控制台会按帧持续输出如下日志Event received, game difficulty is now 1 Event received, game difficulty is now 2 Event received, game difficulty is now 3 ...这段输出的含义是一个名为DifficultySystem的 ECS 系统在每一帧向EventChannelGameEvent写入GameEvent::IncreaseDifficulty事件而GameplayState通过handle_event收到该事件后将自身的game_difficulty递增 1 并打印。整个 demo 无窗口渲染依赖Cargo.toml 中仅启用optionalfeature是理解 Amethyst 事件分发机制的理想最小样例。为什么需要自定义状态事件Amethyst 内置的StateEvent定义在 src/state_event.rs它是一个通过EventReader派生宏生成的聚合枚举#[derive(Clone, Debug, EventReader)] #[reader(StateEventReader)] pub enum StateEvent { /// Events sent by the winit window. Window(Eventstatic, ()), /// Events sent by the ui system. #[cfg(feature ui)] Ui(UiEvent), /// Events sent by the input system. Input(InputEvent), }它把 winit 窗口事件、UI 事件、输入事件统一成一种可枚举类型State的handle_event正是以该类型为入参。问题在于游戏自己的业务事件怪被打死了难度提升了并不在其中。若想在不改引擎源码的前提下让状态层收到自定义事件就需要复刻StateEvent的模式——定义自己的事件枚举并派生自己的 Reader。这套机制之所以可行根源在于 Amethyst 应用入口CoreApplication对事件类型完全泛型化。在 src/app.rs 中pub struct CoreApplicationa, T, E StateEvent, R StateEventReader三个泛型参数分别对应State数据、事件类型E、事件读取器R。默认的别名ApplicationT只是CoreApplicationT, StateEvent, StateEventReader的缩写因此只要提供自定义的E与R就能让整个状态机运行在自定义事件之上。第一步定义扩展事件枚举示例的 examples/events_custom_state_event/event.rs 完整复刻了引擎内StateEvent的写法use amethyst::{ core::{ shrev::{EventChannel, ReaderId}, EventReader, }, derive::EventReader, ecs::Resources, input::InputEvent, winit::event::Event, }; /// Heres a copy of the original StateEvent with our own type added #[derive(Clone, Debug, EventReader)] #[reader(MyExtendedStateEventReader)] pub enum MyExtendedStateEvent { /// Events sent by the winit window. Window(Eventstatic, ()), /// Events sent by the input system. Input(InputEvent), /// Our own events for our own game logic Game(GameEvent), } #[derive(Clone, Debug, PartialEq)] pub enum GameEvent { IncreaseDifficulty, }关键点有三#[derive(EventReader)]#[reader(MyExtendedStateEventReader)]这是来自 amethyst_derive/src/event_reader.rs 的过程宏。它要求被派生的类型必须是枚举且每个变体内部携带一种事件类型collect_field_types会取每个变体的第一个字段类型否则编译期panic!(Event enum variant does not contain an inner event type)。#[reader(...)]属性指定要生成的 Reader 结构体名称缺失时会编译报错并给出示例用法。聚合既有事件把Window、Input原样保留确保自定义后窗口关闭检测、输入处理等既有能力不受影响。从派生宏生成的代码可以看出Reader 会为每个变体维护一个独立的ReaderId对应事件类型并在setup阶段对每个EventChannel调用register_reader()。业务事件独立成枚举GameEvent单独定义并derive(PartialEq)MyExtendedStateEvent::Game(GameEvent)作为桥接变体。这样GameEvent可以独立在系统间流转只有到达状态层时才被包装成上层事件。派生宏实际生成的内容amethyst_derive/src/event_reader.rs 的quote!部分等价于#[derive(Default)] pub struct MyExtendedStateEventReader( OptionReaderIdEventstatic, (), OptionReaderIdInputEvent, OptionReaderIdGameEvent, ); impl EventReader for MyExtendedStateEventReader { type Event MyExtendedStateEvent; fn read(mut self, resources: mut Resources, events: mut VecMyExtendedStateEvent) { // 依次从每个 EventChannel 读取并映射为对应变体追加到 events } fn setup(mut self, resources: mut Resources) { // 对每个 EventChannel get_mut_or_default 并 register_reader } }EventReadertrait 本身定义于 amethyst_core/src/event.rsread负责把底层通道中的事件克隆并追加到输出 Vecsetup负责注册 ReaderId可缺省为空实现。第二步事件生产端——SystemBundle 与 EventChannel示例中自定义事件的来源不是外部输入而是一个 ECS 系统。事件的生产与注入集中在 examples/events_custom_state_event/system.rsuse amethyst::{ core::shrev::EventChannel, ecs::{systems::ParallelRunnable, *}, Error, }; use crate::event::GameEvent; #[derive(Debug)] pub(crate) struct MyBundle; impla, b SystemBundle for MyBundle { fn load( mut self, _world: mut World, resources: mut Resources, builder: mut DispatcherBuilder, ) - Result(), Error { let chan EventChannel::GameEvent::default(); resources.insert(chan); builder.add_system(DifficultySystem); Ok(()) } } /// Signals the state when its time to increase the game difficulty struct DifficultySystem; impl System for DifficultySystem { fn build(self) - Boxdyn ParallelRunnable { Box::new( SystemBuilder::new(DifficultySystem) .write_resource::EventChannelGameEvent() .build(|_, _, my_event_channel, _| { my_event_channel.single_write(GameEvent::IncreaseDifficulty); }), ) } }两点值得展开SystemBundle是资源与系统的装配单元load在应用构建期被调用这里创建EventChannelGameEvent并insert进Resources同时把DifficultySystem注册进DispatcherBuilder。事件通道必须在这个阶段就存在因为事件 Reader 的setup会调用get_mut_or_default::EventChannel#ty()——虽然get_mut_or_default能兜底创建但显式插入更清晰。single_write写入DifficultySystem通过SystemBuilder声明对EventChannelGameEvent的写访问每帧执行一次single_write(GameEvent::IncreaseDifficulty)。这正是示例日志中难度逐帧 1 的直接原因。在真实游戏中这里应替换为真实的业务触发条件血量归零、计时器到期、连击达成等事件内容也可从GameEvent::IncreaseDifficulty扩展为携带数据的变体。第三步事件消费端——State 的 handle_event状态层消费事件的逻辑在 examples/events_custom_state_event/state.rsuse amethyst::prelude::*; use crate::event::{GameEvent, MyExtendedStateEvent}; pub(crate) struct GameplayState { game_difficulty: i32, } impl Default for GameplayState { fn default() - Self { GameplayState { game_difficulty: 0 } } } impla, b StateGameData, MyExtendedStateEvent for GameplayState { fn handle_event( mut self, _data: StateData_, GameData, event: MyExtendedStateEvent, ) - TransGameData, MyExtendedStateEvent { if let MyExtendedStateEvent::Game(GameEvent::IncreaseDifficulty) event { self.game_difficulty 1; println!( Event received, game difficulty is now {}, self.game_difficulty ); } Trans::None } fn update(mut self, data: StateData_, GameData) - TransGameData, MyExtendedStateEvent { data.data.update(data.world, data.resources); Trans::None } }核心要点StateGameData, MyExtendedStateEventStatetrait 的第二个泛型参数src/state.rs必须与应用装配的事件类型一致。handle_event在每一帧、状态更新之前被调用src/state.rs 的 trait 文档注明 Executed on every frame before updating, for use in reacting to events。模式匹配分发if let MyExtendedStateEvent::Game(GameEvent::IncreaseDifficulty)是推荐的消费姿势——先剥掉外层变体再匹配业务事件。不匹配的事件如Window、Input在此被忽略若有窗口关闭需求可在此分支调用is_close_requested返回Trans::Quit。返回Transhandle_event返回TransGameData, MyExtendedStateEvent默认返回Trans::None。Trans枚举src/state.rs支持None、Pop、Push、Switch、Replace、NewStack、Sequence、Quit等转移事件处理中触发状态切换正是 Amethyst 的常规做法。update中的data.data.update手动驱动GameData内注册的 Dispatcher这是当前版本使用optionalfeature 的无渲染配置下状态驱动系统运行的标准写法。第四步组装应用——泛型装配与帧率限制examples/events_custom_state_event/main.rs 将前三步串起来use amethyst::{ core::frame_limiter::FrameRateLimitStrategy, prelude::*, utils::application_root_dir, }; use crate::{ event::{MyExtendedStateEvent, MyExtendedStateEventReader}, state::GameplayState, }; mod event; mod state; mod system; fn main() - amethyst::Result() { amethyst::start_logger(Default::default()); let assets_dir application_root_dir()?.join(assets); let mut game_data DispatcherBuilder::default(); game_data.add_bundle(system::MyBundle); let game CoreApplication::_, MyExtendedStateEvent, MyExtendedStateEventReader::build( assets_dir, GameplayState::default(), )? .with_frame_limit(FrameRateLimitStrategy::Sleep, 1) .build(game_data)?; game.run(); Ok(()) }装配要点显式指定泛型CoreApplication::_, MyExtendedStateEvent, MyExtendedStateEventReader中_是GameData由DispatcherBuilder自动推导后两个泛型则是自定义的事件类型与 Reader。这一步把整个状态机的handle_event入参类型替换为MyExtendedStateEvent是自定义事件生效的开关。build的两级调用CoreApplication::build(path, initial_state)返回ApplicationBuilder再.build(game_data)完成最终构建。在 src/app.rs 的build实现中引擎会对 Reader 执行X::default()与reader.setup(mut self.resources)——这正是派生宏生成的setup被调用的时刻它会为Window、Input、Game三类EventChannel各注册一个ReaderId。with_frame_limit(FrameRateLimitStrategy::Sleep, 1)以 Sleep 策略限制为 1 FPSsrc/app.rs 中该方法会插入FrameLimiter::new(strategy, max_fps)。这是示例特意放慢节奏的手段便于肉眼观察逐帧递增的日志实际游戏中可按需配置更高帧率或改用其他策略。底层原理事件从产生到消费的完整链路结合 src/app.rs 的advance_framesrc/app.rs自定义事件的每一帧旅程如下系统写入DifficultySystem执行single_write将GameEvent::IncreaseDifficulty追加到EventChannelGameEvent环形缓冲区聚合读取主循环中self.reader.read(resources, mut self.events)src/app.rsMyExtendedStateEventReader从三个通道分别读取新事件映射为MyExtendedStateEvent::Window/Input/Game变体后集中放入eventsVec逐条分发for e in self.events.drain(..) { states.handle_event(..., e) }src/app.rsStateMachine::handle_eventsrc/state.rs把事件交给栈顶活跃状态的handle_event并根据其返回的Trans执行状态转移驱动更新随后states.fixed_update与states.update依次执行示例中update再驱动GameData内的 Dispatcher让DifficultySystem在下一帧继续产出事件形成循环。值得注意的分发语义Reader 按ReaderId只消费自己注册后新写入的事件环形通道的游标语义因此每帧IncreaseDifficulty恰好被消费一次日志编号严格递增——这也解释了 README 中...省略号所暗示的持续输出。实践建议与扩展方向保持变体对齐自定义事件枚举应保留Window、Input等原生变体否则StateMachine默认的窗口关闭检测is_close_requested见 src/state.rs 中EmptyState/SimpleState的实现需要你在handle_event里自行处理否则窗口将无法正常关闭。用EventChannel::single_write做信号量GameEvent这类无负载、仅表示发生了一次的枚举与single_write是天然搭配需要携带数据时如GameEvent::ScoreChanged(u32)通道内的clone分发机制同样适用。多个 Reader 可共存一个EventChannel支持多个ReaderId独立游标这意味着同一事件既可被状态层消费也可被其他系统消费互不干扰——这是EventChannel作为引擎级事件总线的核心优势参见 amethyst_core/src/event.rs 中多 Reader 的测试用例。更简化的默认路径如果不需要自定义事件直接用Application::build(...)即CoreApplicationT, StateEvent, StateEventReader别名即可SimpleState/EmptyState已内置窗口关闭处理src/state.rs。小结events_custom_state_event示例虽短却完整呈现了 Amethyst 事件系统的扩展闭环以#[derive(EventReader)]定义聚合事件枚举、以SystemBundleEventChannel生产事件、以State::handle_event消费事件、以CoreApplication的三个泛型参数完成装配。这一模式可无痛推广到任意业务事件——从难度系统、计分系统到成就与剧情触发只要遵循事件枚举派生 Reader、通道写入、状态匹配三步走就能让游戏状态层始终与业务逻辑保持解耦而畅通的通信。赞分享【免费下载链接】amethystData-oriented and>项目地址https://gitcode.com/gh_mirrors/ame/amethyst点击查看免费下载相关推荐UI-Router状态事件系统事件监听与自定义事件实现UI Router状态事件系统事件监听与自定义事件实现 在单页应用SPA开发中页面切换的流畅性和状态管理的准确性直接影响用户体验。你是否还在为Angul前端路由tui.editor自定义事件扩展编辑器事件系统tui.editor自定义事件扩展编辑器事件系统 在现代富文本编辑开发中事件系统是连接用户操作与应用逻辑的核心枢纽。tui.editorTOAST UI前端UI组件老款 Mac 免费续命指南OpenCore Legacy Patcher 根补丁三步走完整实战老款 Mac 免费续命指南OpenCore Legacy Patcher 根补丁三步走完整实战 那台 2012 年的 MacBook Pro是不是正躺在抽屉操作系统固件驱动开发上一篇Fizeau 开源项目教程下一篇推荐文章rbndr——简单高效的DNS重绑定测试服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表