其實這章才是真的讓這堆教學跟官方 pong 不一樣的地方,在遊戲內生成 entity;官方的 pong 就是生出兩塊板子一顆球,球跑到場外就計分然後把球放回場中間,完全不會新增/刪除 entity。
在這之前我們先做一點 refactor,之前我們在 states.rs 裡寫了一個函式:load_sprite_sheet
用來載入 sprite 資源,但這個資源沒向 world 註冊,在 system 裡面會無法使用。
先來改善這點,把 Handle<SpriteSheet>
包進 struct,放到另一個 textures.rs 檔案裡:
pub struct SpriteStore {
handle: Handle<SpriteSheet>
}
這個 struct 提供兩個函式:
from_path
:利用load_sprite_sheet
取得Handle<SpriteSheet>
,建構 SpriteStoresprite_render
:給定一個frame id
,從 handle 建構出SpriteRender
。
另外一個新檔案則是 resources.rs:
pub struct ShipRes {
pub sprite_store: SpriteStore,
}
impl ShipRes {
pub fn initialize(world: &mut World) {
let sprite_store = SpriteStore::from_path(world, "ship");
world.insert(
ShipRes { sprite_store : sprite_store }
);
}
pub fn sprite_render(&self) -> SpriteRender {
self.sprite_store.sprite_render(0)
}
}
用一個 struct 再把 sprite_store
包起來,我在名字後綴 Res 表示 Resource,用來跟 component 的 ship 做區隔,不然到處都是 Ship 很麻煩。
這個 struct initialize 拿到 SpriteStore 後,會呼叫 world.insert()
把自己這個資源註冊到 world 裡面,使用同樣的結構,我們可以另外生成資源 BulletRes 用來生成子彈。
記得在產生 game data 的時候,呼叫資源 struct 的 initialize 函式。
現在我們可以擴充我們的 ShipControlSystem 了:
impl<'s> System<'s> for ShipControlSystem {
type SystemData = (
WriteStorage<'s, Physical>,
WriteStorage<'s, Ship>,
ReadStorage<'s, Transform>,
ReadExpect<'s, BulletRes>,
Entities<'s>,
Read<'s, LazyUpdate>,
Read<'s, InputHandler::<StringBindings>>,
Read<'s, Time>,
);
擴充後的 SystemData 大幅增加取用的類別:
- BulletRes 是 Resource,因為沒有 default 我們使用 ReadExpect 來讀取。
- Entities<’s> 新增/刪除 entity 必要的
- LazyUpdate
是 amethyst 提供的一個方式
如果一條更新會動到很多的資源,可以先用 LazyUpdate 的方式記下來之後一次處理。 - Time 跟 Ship 的寫入權限是要處理冷卻時間用的。
下面是在 ShipControlSystem 和射出子彈有關的原始碼:
fn run(&mut self,
(mut physicals,
mut ships,
transforms,
bullet_resources,
entities,
lazy,
input,
time): Self::SystemData) {
for (physical, ship, transform) in (&mut physicals, &mut ships, &transforms).join() {
let shoot = input.action_is_down("shoot").unwrap_or(false);
// handle shoot
if ship.reload_timer <= 0.0f32 {
if shoot {
ship.reload_timer = ship.time_to_reload;
let bullet_transform = transform.clone();
let velocity = transform.rotation() * Vector3::y() * 150f32;
let velocity = physical.velocity + Vector2::new(velocity.x, velocity.y);
let bullet_physical = Physical {
velocity: velocity,
max_velocity: 200f32,
rotation: 0f32,
};
let e = entities.create();
lazy.insert(e, Bullet {} );
lazy.insert(e, bullet_transform);
lazy.insert(e, bullet_physical);
lazy.insert(e, bullet_resources.sprite_render());
}
} else {
ship.reload_timer = (ship.reload_timer - delta).max(0.0f32);
}
}
}
首先,在遊戲裡面會重複的只有 component,所以需要用 for loop 配 join 解開的也只有用 ReadStorage/WriteStorage 拿進來的 component,其他都不用。
接著我們會去檢查 ship component 儲存的 reload_timer
,依照 Time delta 減少,變為 0 就可以射擊,一但射擊了就會把 reload_timer
加一個冷卻時間。
後面會用 ship 的 transform 算出子彈速度,產生子彈用的 physical component。
產生 entity 其實很簡單,呼叫 entities.create()
,再用 LazyUpdate insert,往這個 entity 裡面塞 component。
bullet_transform
跟 physical 都是我們剛產生的;resource 一定要先註冊過之後,才能像這樣用 ReadExpect 拿出來用,我們直接呼叫 sprite_render
函式,拿到可顯示的 SpriteRender component,一樣塞進去就行了。
用 system 產生 entity 就介紹到這邊,其實沒有很難;大家應該更能感受到 ECS 系統的精神,Entity - 我們在螢幕上面看到的船 - 其實就是個空殼。
- 它為什麼可以顯示東西?我們幫他加上一個 SpriteRender component。
- 它為什麼有速度可以旋轉?它有 physical component。
- 要變化位置?加上 transform component ,讀取 physical 來更新它。
- 要設定加速度的值?把加速度存在 ship component 裡。
沒錯,所有的東西都是 component 達成的,entity 一點用也沒有,隨之而來的就是彈性。
還要加上碰撞?在需要碰撞的三個東西:船、小行星、雷射加上一個新的 Collider component ,再寫新的系統處理它就可以了,舊有的程式碼完全不需要改動,這大概是 ECS 系統帶來的最大好處了。