因為搬家(詳見上一篇 )的關係好久沒有更新了;現在搬完家終於可以回來繼續寫 amethyst 的文章啦, 現在寫文章插 code 根本是一種享受,跟 blogger 那種鳥編輯器完全不一樣。
到目前為止,我們討論的範圍都不脫 ECS 的範疇,這兩章我們會實作更多有關 state 的部分, 我覺得寫到這邊我比較有感受到 amethyst 整體的架構是怎麼實作的,但也只是個人的感覺就是。

我們這章先來實作一個小小的功能,就是按下 esc 鍵的時候,能讓遊戲暫停下來, 對應 amethyst 的文件是第十章的 controlling system execution

文中建議了三個不同的寫法:

這裡面 Custom GameData 我試過但都沒有成功,Pausable Systems 沒有試,用 State-specific Dispatcher 成功實作,所以這裡就教這個作法。

還記得我們上次提到 state 是在第一章 剛設定狀態的時候, 那時候就是宣告一個 struct 作為 state,所以這裡的第一步是把 Play State 跟 Pause State 分開,Pause State 比較簡單先看它:

use amethyst::{
  input::{VirtualKeyCode, is_key_down},
  prelude::*,
};

pub struct StatePause;
impl SimpleState for StatePause {
  fn handle_event(&mut self,
      _data: StateData<'_, GameData<'_, '_>>,
      event: StateEvent) -> SimpleTrans {
    if let StateEvent::Window(event) = event {
      if is_key_down(&event, VirtualKeyCode::Escape) {
        return Trans::Pop;
      }
    }

    Trans::None
  }
}

我們先宣告暫停用的 StatePause,所有的 State 可以分到獨立的檔案裡,會比較清楚,然後對 StatePause 實作 SimpleStateSimpleState 可以實作的函式可見參考文件 , 這裡只需要處理鍵盤事件,實作 handle_event 即可。
後面就如上面所示,使用 is_key_down 跟 VirtualKeyCode 偵測 Escape 被按下,

按下時回傳一個 Trans::Pop 的 SimpleTrans,我們提過 amethyst 裡是用 stack 在儲存我們的設計遊戲進行時會處在 StatePlay, 按下 Escape 會將 StatePause 推入 stack 中,解除 Pause 只要把 StatePause Pop 掉就可以了。
SimpleTrans 是 amethyst 提供 Trans 的簡化版本, 使用預設的 StateData 跟 StateEvent,Trans 裡面有各種不同的 stack 操作,稍後實作更多 state 會用到其他操作,這裡只用到 None, Push 跟 Pop。

下一步來實作 StatePlay,因為要加 dispatcher 的關係,現在宣告要加上生命周期 (就是這個會讓 pretty-print 自動上色功能大混亂,讓我狠下心決定搬離 blogger XDD):

#[derive(Default)]
pub struct StatePlay<'a, 'b> {
  pub dispatcher: Option<Dispatcher<'a, 'b>>,
}

後面我們要把所有在 StatePlay 會用到的 System 從 main 生成 game_data 那邊搬過來,本來的 main 大概是這樣:

let game_data = GameDataBuilder::default()
  .with_bundle(RenderingBundle::<DefaultBackend>::new()
  .with_bundle(TransformBundle::new())?
  .with_bundle(input_bundle)?
  .with_bundle(UiBundle::<StringBindings>::new())?
  // .with many system

let mut game = Application::new(assets_dir, StatePlay, game_data)?;
game.run();

現在我們要把上面註解的 system 都搬到 StatePlayon_start 裡面,留在 game_data 裡的就只剩下連接輸入、繪圖、UI 等 bundle:

use amethyst::{
  core::ArcThreadPool,
  input::{VirtualKeyCode, is_key_down},
  prelude::*,
  shred::{Dispatcher, DispatcherBuilder},
};

impl<'a, 'b> SimpleState for StatePlay<'a, 'b> {
  fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>)
    let world = data.world;

    // initialize Resource
    // register Component
    // create dispatcher
    let mut dispatcher = DispatcherBuilder::new()
      .with(ShipControlSystem, "ship_control_system", &[])
      .with(PhysicalSystem, "physical_system", &["ship_control_system"])
      .with(BoundarySystem, "boundary_system", &["physical_system"])
      .with(SpawnAsteroidSystem::new(), "spawn_system", &[])
      .with(CollisionSystem, "collision_system", &[])
      .with(ExplosionSystem, "explosion_system", &[])
      .with_pool((*world.read_resource::<ArcThreadPool>()).clone())
      .build();
    dispatcher.setup(world);

    self.dispatcher = Some(dispatcher);
  }

在新版的 on_start 裡面,我們除了要初始化資源、加入物件之外,多出一步生成一個 dispatcher,然後把本來在 game_data 那邊的 system 一股腦全塞進去。
with_pool 可有可無,預設 dispatcher 會產生一套自己的 thread pool,同時 amethyst 的主執行緒也會產生一個 thread pool , 使用 with_pool 可以重用主執行緒的 thread pool (注意它也是一個 resource ArcThreadPool)可以節省資源。

fn update(&mut self, data: &mut StateData<GameData>) -> SimpleTrans {
  if let Some(dispatcher) = self.dispatcher.as_mut() {
    dispatcher.dispatch(&data.world);
  }
  Trans::None
}

有了 dispatcher 之後,再 SimpleState 的 update 函式裡面,呼叫 dispatch 函式,就會照著我們註冊系統的順序在每次更新時呼叫各 system 了; 而相對的 StatePause 裡面沒有註冊任何 system,因此一進入暫停狀態,所有的東西都會停止運作。

最後 StatePlay 也需要偵測是否按下 Escape 鍵,event_handle 的實作跟 StatePause 的很像,但按下 escape 時要回傳 Trans::Push ,並把 StatePause 推入狀態的堆疊中:

fn handle_event(&mut self,
    _data: StateData<'_, GameData<'_, '_>>,
    event: StateEvent) -> SimpleTrans {
  if let StateEvent::Window(event) = event {
    if is_key_down(&event, VirtualKeyCode::Escape) {
      return Trans::Push(Box::new(StatePause));
    }
  }
  Trans::None
}

這樣我們就完成了 Play 跟 Pause 兩個狀態的實作,在遊戲中按下 escape,跳入暫停模式,會看到畫面上所有東西都停止運作;再按一下繼續遊戲。