亂數在遊戲中也是個舉足輕重的腳角,少了亂數的遊戲就像沒加珍珠的奶茶(?,讓玩家食之無味;這章我們會加上亂數,以及產生小行星的 system。
亂數直接用 rust 官方的 rand 模組,為了把它嵌入 ECS 裡面,可以觀察一下亂數模組會有什麼特性:大家使用一個亂數模組而不是大家都有一份,符合這個特性的就是 resource 了。
在 resources.rs 加上一個新的 struct:
pub struct RandomGen;
impl RandomGen {
pub fn next_f32(&self) -> f32 {
use rand::Rng;
rand::thread_rng().gen::<f32>()
}
}
它把 rand 模組給包起來,在內部呼叫 thread_rng().gen 產生 f32 的亂數,在載入資源的時候我們一併插入這個 resource。
AsteroidRes 請仿造 ShipRes 跟 BulletRes 寫一份,這裡應該可以再把三個資源共用的部分抽出來,不過因為是範例 project 我就沒這麼做:
ShipRes::initialize(world);
BulletRes::initialize(world);
AsteroidRes::initialize(world);
world.insert(RandomGen);
ECS 的道理,要加上新的行為就是寫一個新的系統,我們把和生成小行星有關的設定都放在這個系統裡面(雷射槍的冷卻時間和太空船是綁在一起的,因此放在 Ship component 裡面):
#[derive(SystemDesc)]
pub struct SpawnAsteroidSystem {
pub time_to_spawn: f32,
pub max_velocity: f32,
pub max_rotation: f32,
pub distance_to_ship: f32,
pub average_spawn_time: f32,
}
實作上:
- entities、LazyUpdate 是產生新物件必備
- Ship、Transform 用來取得船的位置,免得小行星直接出現在太空船的旁邊
- AsteroidRes、RandomGen 則是需要的資源:
impl<'s> System<'s> for SpawnAsteroidSystem {
type SystemData = (
Entities<'s>,
ReadStorage<'s, Ship>,
ReadStorage<'s, Transform>,
ReadExpect<'s, AsteroidRes>,
ReadExpect<'s, RandomGen>,
Read<'s, LazyUpdate>,
Read<'s, Time>,
);
fn run(&mut self,
(entities,
ships,
transforms,
asteroidres,
rand,
lazy,
time): Self::SystemData) {
let delta = time.delta_seconds();
self.time_to_spawn -= delta;
if self.time_to_spawn <= 0.0f32 {
// other code
}
一開始就跟 ShipControlSystem 一樣,從 time.delta_seconds
取得秒數,去扣掉 system 的 time_to_spawn
,一但低於零就會生成一顆小行星,並將 time_to_spawn
設定回 average_spawn_time
。
let mut create_point: Vector3<f32> = zero();
// generate creation point
loop {
create_point.x = rand.next_f32() * ARENA_WIDTH;
create_point.y = rand.next_f32() * ARENA_HEIGHT;
if (ship_translation-create_point).norm() > self.distance_to_ship {
break;
}
}
transform.set_translation_x(create_point.x);
transform.set_translation_y(create_point.y);
生成點的位置我用了偷懶的方式,一般在 system 內最好不要有這種執行時間不確定的東西,如果一不小心 distance_to_ship
設太大,可能會讓 system 進到無窮迴圈把整個遊戲卡住,比較好的做法應該是從 distance_to_ship
,亂數產生距離跟方位角就可以了。
let e = entities.create();
lazy.insert(e, Asteroid {} );
lazy.insert(e, transform);
lazy.insert(e, physical);
lazy.insert(e, asteroidres.sprite_render());
self.time_to_spawn = self.average_spawn_time + rand.next_f32();
所有的 component 都準備好之後,一樣透過 LazyUpdate 把 component 塞進 entity 裡面即可,
另外也要記得重設 time_to_spawn
的值。
現在我們的遊戲應該有個樣子,平均每兩秒生成一顆小行星,按空白可以射出雷射。
我們還沒實作碰撞,所以自然還沒有任何打爆小行星的行為,雷射打到小行星也只是無視的飛過去,下一章我們就進到一般遊戲除了亂數之外另一個必備成員:碰撞。