簡單一點的土砲法,是用 for loop 把小行星跟電射砲的座標收集起來,太近的兩者把 entity 刪掉就行了;但我們畢竟身為專業的遊戲設計(才怪),用土砲法就太遜了,這裡我們用同樣是 rust 寫的 ncollide2d 套件來實作碰撞偵測。
因為 amethyst 內部包了一層 nalgebra 的關係,我們的 ncollide2d 用的版本必須是 0.21 版,我覺得這個問題挺…麻煩的,要自己去搜 amethyst 相依的 nalgebra 到底是哪個版本,然後對應回 ncollide2d 對應的版本。
為了識別每一個物件的屬性,我們在各個 entity 上面都加上一個新的 component:Collider,內含一個 enum 屬性;在每個 entity 上面都要附上這個 component 作為識別。
pub struct Collider { pub typ: ColliderType } pub enum ColliderType { Ship, Bullet, Asteroid, }從我們之前的教學文學到的 rule of thumb:要改變行為,就是加一個系統。
use ncollide2d::{ bounding_volume, broad_phase::{DBVTBroadPhase, BroadPhase, BroadPhaseInterferenceHandler} };引入 ncollide2d 相關的模組。
#[derive(SystemDesc)] pub struct CollisionSystem; impl<'s> System<'s> for CollisionSystem { type SystemData = ( Entities<'s>, ReadStorage<'s, Collider>, ReadStorage<'s, Ship>, ReadStorage<'s, Transform>, );這段就只是宣告一下 system,因為有生命周期上色很麻煩所以獨立出一段來。
fn run(&mut self, (entities, colliders, ships, transforms): Self::SystemData) { // collect collider let mut broad_phase = DBVTBroadPhase::new(0f32); let mut handler = BulletAsteroidHandler::new(); for (e, collider, _, transform) in (&entities, &colliders, !&ships, &transforms).join() { let pos = transform.translation(); let pos = Isometry2::new(Vector2::new(pos.x, pos.y), zero()); let vol = bounding_volume::bounding_sphere( &Ball::new(5.0), &pos ); broad_phase.create_proxy(vol, (collider.typ, e)); } broad_phase.update(&mut handler);新增一個 CollisionSystem,要動到有 Collider component 的 entity,讀位置需要 Transform。
上面顯示了 ncollide2d 提供的 DBVTBroadPhase 的碰撞偵測,它會把輸入的資料依照空間位置分到樹狀的結構上,更有效率的去偵測碰撞。
用 for loop 把所有非 ship 的 collider 取出來,從 transform 建立物體位置,再建立 ncollide2d 提供圓形 bounding_volume,這是 DBVTBroadPhase 偵測用的 key。
我們用 (ColliderType, Entity) 作為 value,ColliderType 是用來識別碰撞的物體型別,我們只希望偵測子彈跟小行星的碰撞,忽略其他像小行星自己的碰撞;Entity 則是在碰撞發生的時候,能夠追溯到是哪個 entity 發生碰撞。
最後只要呼叫 DBVTBroadPhase 的 update 並代入實作 BroadPhaseInterferenceHandler 的 struct 即可。下面就來實作 handler:
struct BulletAsteroidHandler { collide_entity: Vec<Entity>, } impl BulletAsteroidHandler { pub fn new() -> Self { Self { collide_entity: vec![], } } } type ColliderEntity = (ColliderType, Entity);定義一下 type alias 寫起來比較方便:
impl BroadPhaseInterferenceHandler<ColliderEntity> for BulletAsteroidHandler { fn is_interference_allowed(&mut self, a: &ColliderEntity, b: &ColliderEntity) -> bool { a.0 != b.0 } fn interference_started(&mut self, a: &ColliderEntity, b: &ColliderEntity) { self.collide_entity.push(a.1); self.collide_entity.push(b.1); } fn interference_stopped(&mut self, _a: &ColliderEntity, _b: &ColliderEntity) {} }is_interference_allowed 用來判斷這兩個物體能不能發生碰撞,這裡要求它們的 ColliderType 要不一樣;interference_started 則是碰撞發生時的處理,把兩個 entity 存起來;interference_stopped 處理碰撞結束的行為,留空就好
上面呼叫完 broad_phase.update(&mut handler) 之後,從 handler.collide_entity 就能拿到碰撞的 bullet 跟 asteroid,直接刪掉 entity 即可。
for e in handler.collide_entity { if let Err(e) = entities.delete(e) { error!("Failed to destroy collide entity: {}", e) } }這篇其實非常偷懶了,目前至少有下面兩點可以改進:
- 不用在 system 裡面產生新的 DBVTBroadPhase 並重新插入 proxy,應該把碰撞偵測當成一個 resource,每次只要更新 DBVTBroadPhase 內記錄的位置,速度應該會比我們這樣從頭打造一個快。
- 將 bounding_volume 存在 Collider 裡面,而不是每個東西都是單一尺寸的圓形,這樣也不用每次都重新生成新的 bounding_volume,從 Collider 裡面拿就可以了。