一開始讓我們在 component.rs 裡面新增一個 component physical:
use amethyst::{ core::math::Vector2, }; pub struct Physical { // velocity, [vx, vy] pub velocity: Vector2<f32>, // maximum velocity (units / s) pub max_velocity: f32, // rotation (rad / s) pub rotation: f32, } impl Component for Physical { type Storage = DenseVecStorage<Self>; }
直覺上來想,可能會覺得我們之前產生的 Ship component 裡面應該要有 velocity 屬性,system 會讀取 velocity 去更新太空船的位置 - 也就是 transform。
但是不對,因為畫面上會動的東西不止有 Ship,之後會新增的子彈跟小行星都是會動的,而實作<會動>都是共同的,寫在 Ship 內的性質無法共用,所以我們直接新增一個 physical component 記錄速度和旋轉量,想要動的東西只要加上 physical component 就行了,程式碼共用的部分可以很漂亮的抽出來。
這裡我們直接使用 nalgebra 的 vector2 來代表速度,amethyst 裡面有包一包 nalgebra 並且重命名為 core::math,版本是 0.19.0,這個在我們後面引入 ncollide2d 的時候會帶來麻煩,不過這裡就先用吧。
state.rs 裡面,在 initialize_ship 函式幫產生的 entity 加上 Physical component:
在讀取和寫入有不同的方式,可以參考文件 system 章節,大致整理起來是:
從 SystemData 拿到的,會是這個遊戲裡「所有有這個 component 的資料」,這裡會收集到所有有 Physical component 的 entity;所以我們用 for loop ,搭配 join() 把資料展開來。
只要是 component 展開這步幾乎是必要的,我個人是建議從 system data 裡解出來的資料,一律加 s 用複數,用 for 解出來再變單數,變數名詞選同一個,比較不會去考慮哪個是哪個。
再來就是位移跟旋轉的實作,從 Time 這個 resource 裡面,我們可以拿到和上一個 frame 之間的時間差,配合 physical 裡面記錄的速度和角速度算出位移量,並更新到 transform 就行了。
上一篇的 ShipControlSystem 也要修改,它會從 ship 定義的加速度算出速度的變化值,修改到 physical 速度內,這裡揭示了 for loop 配 join 的用法,從 ReadStorage<'s, Physical> 拿到的,是所有「有 physical component」,只想要改 ship,只要把 physicals、ships 一起放進 join 裡面,就會只拿出同時有 physical 和 ship component 的 entity 了:
state.rs 裡面,在 initialize_ship 函式幫產生的 entity 加上 Physical component:
use crate::components::{Ship, Physical}; world .create_entity() .with(transform) .with(sprite_render.clone()) .with(Ship::new()) .with(Physical { velocity: zero(), max_velocity: 10.0, rotation: 0.0 }) .build();現在來修改 system,先新增一個操作 Physical component 的 system:
#[derive(SystemDesc)] pub struct PhysicalSystem; impl<'s> System<'s> for PhysicalSystem { type SystemData = ( ReadStorage<'s, Physical>, WriteStorage<'s, Transform>, Read<'s, Time>, ); fn run(&mut self, (physicals, mut transforms, time): Self::SystemData) { let delta = time.delta_seconds(); for (physical, transform) in (&physicals, &mut transforms).join() { let movement = physical.velocity * delta; let rotation = physical.rotation * delta; transform.prepend_translation(Vector3::new(movement.x, movement.y, 0.0)); transform.rotate_2d(rotation); } } }每個系統都要定義 SystemData,也就是它需要存取哪些資源:無非就是讀寫 component、新增/刪除 entity 還有讀取 world 內存的 resource,PhysicalSystem 會需要去讀 Physical component、系統提供的 Time resource、寫入 transform component。
在讀取和寫入有不同的方式,可以參考文件 system 章節,大致整理起來是:
- Read<'s, Resource>、Write<'s, Resource>:取得唯讀/可讀寫的 Resource,這個是保證不會失敗的取得資源,如果失敗的話會直接給你一個 Default::default() 的版本。
- ReadExpect<'s, Resource>、WriteExpect<'s, Resource>:同上,但這個適用在沒有實作 Default::default() 的資源上。
- ReadStorage<'s, Component>、WriteStorage<'s, Component>:取得唯讀/可讀寫的 Component 參考。
- Entities<'s>:創造或刪除 entity 用。
fn run(&mut self, data: Self::SystemData) { let (physicals, mut transforms, time) = data; // ... }兩種對編譯器來說應該是一樣的,所以選一個喜歡的就可以了,但記得一個原則,一定要把每一行都分開寫,不要擠在一行:
let (physicals, mut transforms, time) = data;這是因為我們的 system 是會長大的,哪天要加一個新的 component,直接加一行會比在一行裡面找到正確的位置還要簡單。
從 SystemData 拿到的,會是這個遊戲裡「所有有這個 component 的資料」,這裡會收集到所有有 Physical component 的 entity;所以我們用 for loop ,搭配 join() 把資料展開來。
只要是 component 展開這步幾乎是必要的,我個人是建議從 system data 裡解出來的資料,一律加 s 用複數,用 for 解出來再變單數,變數名詞選同一個,比較不會去考慮哪個是哪個。
再來就是位移跟旋轉的實作,從 Time 這個 resource 裡面,我們可以拿到和上一個 frame 之間的時間差,配合 physical 裡面記錄的速度和角速度算出位移量,並更新到 transform 就行了。
上一篇的 ShipControlSystem 也要修改,它會從 ship 定義的加速度算出速度的變化值,修改到 physical 速度內,這裡揭示了 for loop 配 join 的用法,從 ReadStorage<'s, Physical> 拿到的,是所有「有 physical component」,只想要改 ship,只要把 physicals、ships 一起放進 join 裡面,就會只拿出同時有 physical 和 ship component 的 entity 了:
let delta = time.delta_seconds(); for (physical, ship, transform) in (&mut physicals, &ships, &transforms).join() { let acceleration = input.axis_value("accelerate"); let rotate = input.axis_value("rotate"); // handle acceleration -> velocity let acc = acceleration.unwrap_or_default(); let added = Vector3::y() * delta * acc * ship.acceleration; let added = transform.rotation() * added; physical.velocity += Vector2::new(added.x, added.y); let magnitude = physical.velocity.magnitude(); if magnitude > physical.max_velocity { physical.velocity *= physical.max_velocity / magnitude; }最後一步,和上一篇一樣把我們的 system 註冊到 game data 裡面
use crate::system::{ShipControlSystem, PhysicalSystem}; let game_data = GameDataBuilder::default() // Render, transform bundle here ... .with(ShipControlSystem, "ship_control_system", &["input_system"]) .with(PhysicalSystem, "physical_system", &["ship_control_system"]);註冊 system 的 with 是可以寫明相依關係的,三個參數分別是 system struct,system name 和 dependencies list,我們這裡的寫法 input_system(這個名字應是預設的)、ShipControlSystem、PhysicalSystem 會依序執行。