上一篇我們做了兩個 state:遊戲進行和暫停的 state,理論上整個遊戲還需要更多的 state:像是遊戲結束、遊戲選單等等,但在這之前我們要先介紹 amethyst 的 event channel。
先前我們在 state 間切換是透過按下鍵盤的事件,但遊戲進行間可能有些場景的轉換是由遊戲內的事件發動的,
例如我們的太空船被隕石砸到,要從遊戲狀態切到遊戲結束,這時候就要透過 event channel 來發送事件了;
同時,用了 EventChannel 可以讓我們改寫一些之前的程式碼,把不同功用的程式分到不用的 system 裡,不用全塞在一起。
這章的內容多半來自同類型,也是 amethyst 展示箱的遊戲 Space Shooter , 畢竟Event Channel 的文件 裡面, 沒有多加描述該如何正確使用 Event Channel,只能翻 code 學怎麼用。
使用 Event Channel 切換狀態
就從我們的 CollisionSystem 開始,請參考舊文,使用 DBVTBroadPhase 偵測是否有兩個物體相碰撞,只要下列幾個 step 即可:
引入需要的模組,前面兩個分別是遊戲核心,要控制狀態轉換,還有 event channel ;後面我們要切換到 Pause State,實作的這個 state 也要引入:
use amethyst::prelude::{Trans, TransEvent, GameData, StateEvent},
use amethyst::shrev::{EventChannel};
use crate::states::{AsteroidGamePause};
EventChannel 可以廣播事件到整個遊戲裡,只要實作了 Send + Sync + 'static
的東西皆可為事件,
CollisionSystem 的 SystemData 中,我們加上這個長得很可怕的東西:
impl<'s> System<'s> for CollisionSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Collider>,
ReadStorage<'s, Transform>,
WriteExpect<'s, ScoreRes>,
WriteStorage<'s, UiText>,
Write<'s, EventChannel<TransEvent<GameData<'static, 'static>, StateEvent>>>,
ReadExpect<'s, ExplosionRes>,
Read<'s, LazyUpdate>,
);
https://docs.rs/amethyst/0.9.0/amethyst/enum.StateEvent.html 執行的部分,一旦我們偵測到太空船和隕石的碰撞,就會往 event channel 送入一個 Transition 的事件,透過 lambda 跟 Box 包裝起來,用 single_write 寫入這個事件:
fn run(&mut self,
(//other component
mut trans_events): Self::SystemData) {
// call DBVTBroadPhase to detect collision
if handler.ship_hit {
let trans = Box::new(move || Trans::Switch(Box::new(AsteroidGamePause)));
trans_events.single_write(trans);
}
很神奇的,我們這樣就完成了 state 的轉換,在太空船相撞的時候就會跳進 Pause State 裡。
使用 Event Channel 在 System 間溝通
Event channel 當然不止這個用途,Event Channel 其實是整個 amethyst 的基礎之一,在各系統間都可以用 Event Channel 串接。
例如我們之前實作的 Collision System
,把偵測碰撞、處理碰撞都寫在一起了,
這樣混雜當然不是好事,我們要加上新的行為時就會愈來愈難改動,
可以利用 Event Channel 將兩者分開,偵測碰撞把碰撞的 Entity 往 Event Channel 裡面塞;
處理碰撞的 system 從 Event Channel 裡面拿出 entity,執行對應的動作。
第一步要先製作傳送的內容物,我們稱作 CollisionEvent,需要傳送的東西只有 entity 一個:
pub struct CollisionEvent {
pub entity: Entity,
}
先看傳送的部分,比較簡單,新的 CollisionSystem 修改一下,component 新增一個寫入 CollisionEvent 的 EventChannel,有東西碰撞往裡面塞就是了:
impl<'s> System<'s> for CollisionSystem {
type SystemData = (
// ...
Write<'s, EventChannel<CollisionEvent>>,
);
fn run(&mut self,
(entities,
//...
mut collision_channel): Self::SystemData) {
// call DBVTBroadPhase to detect collision
for e in handler.collide_entity {
collision_channel.single_write(CollisionEvent::new(e));
}
}
新的系統命名為 DeletionSystem
,這個 system 會複雜一點,在讀取 Event Channel 需要下面幾個步驟:
- 建立了 event channel
- reader 向它註冊拿到一個
ReaderId
- 讀取的時候送進這個
ReaderId
,用來追蹤你上次讀到哪裡
要注意的是 Event Channel 在所有註冊的 Reader 都讀取完之後,才會移除寫入的 Event,
所以只要有一個 Reader 沒有固定更新,Event Channel 就會記錄愈來愈多的 event,最後把記憶體吃乾抹淨就像 chrome 一樣。
#[derive(Default)]
pub struct DeletionSystem {
event_reader: Option<ReaderId<CollisionEvent>>,
}
我們要在哪裡初始化這個 event_reader
呢(注意它是 Option,因為還沒註冊時當然還沒有 ReaderId)?需要多實作一個函式 setup。
impl<'s> System<'s> for DeletionSystem {
fn setup(&mut self, world: &mut World) {
Self::SystemData::setup(world);
self.event_reader = Some(
world
.fetch_mut::<EventChannel<CollisionEvent>>()
.register_reader()
)
}
}
setup 會在把系統加到 dispatcher 上時被呼叫,這時就會透過 register_reader
取得 ReaderId。
(在這裡我真的很想吐槽一下…setup 這個函式會被呼叫這件事文件提都沒有提,到底寫文件的都在幹嘛)
在系統內就可以用這個 ReaderId 去跟 EventChannel 拿出 entity 了。
impl<'s> System<'s> for DeletionSystem {
type SystemData = (
Read<'s, EventChannel<CollisionEvent>>,
);
fn run(&mut self,
(collision_channel): Self::SystemData) {
for event in collision_channel.read(self.event_reader.as_mut().unwrap()) {
// do the things to entity
}
}
}
在 amethyst 裡面 entity 只是一個標記,用來抓出相關的 component ,完全沒有任何資料在裡面,所以我們可以像這樣用 event channel 在 system 間傳送 entity; 如果是內含資料的物件,在 rust 嚴格的記憶體管理下,實作 event channel 很容易就變成惡夢一場。
本文展示使用 EventChannel 來控制狀態的轉換,以及用 EventChannel 將不同功能的程式碼分到不同的 System 裡面, 下一章我們來多加幾個狀態,就能完成一款超級陽春的遊戲,也就準備迎來 amethyst 系列文的尾聲啦lol。