繼續遊戲的調整,要做的是玩家出手時會有擊中閃光,敵人被打會閃爍,並且做出死亡粒子(Death Particle Effect)的效果,鏡頭也會跟著震動。所有效果都走事件 + Timer
,讓動畫可以跟著 Bevy 的 ECS 節奏同步。
這回的重點:
EnemyHitEvent
成為攻擊系統與視覺效果的橋樑,集中傳遞傷害量、座標與剩餘血量。HitSpark
、EnemyHitFlash
、DeathParticle
元件,利用 Timer
控制亮度/縮放/壽命。CameraShake
資源負責震動參數,EffectsPlugin
與 CameraPlugin
串接鏡頭偏移。rand
做死亡粒子扇形噴發,強化擊殺瞬間的回饋。new_demo/src/systems/enemy.rs:17
增加了 EnemyHitEvent
,在玩家近戰命中敵人時會發出事件 (new_demo/src/systems/attack.rs:97
):
hit_events.write(EnemyHitEvent {
entity: enemy_entity,
position: enemy_transform.translation,
damage,
remaining_health: health.current,
});
事件包含敵人實體、命中座標、造成的傷害與剩餘血量,後續的視覺特效都只需要讀取同一份資料即可。AttackPlugin
也同步註冊事件 (new_demo/src/plugins/attack.rs:8
),確保效果插件能夠在同一個 Update 週期接到訊息。
new_demo/src/components/effects.rs:3
新增了三個特效元件,其中 HitSpark
與 EnemyHitFlash
分別處理擊中閃光與受擊閃爍。系統放在 new_demo/src/systems/effects.rs
:
pub fn spawn_hit_spark_system(
mut commands: Commands,
mut events: EventReader<EnemyHitEvent>,
) {
for event in events.read() {
if event.damage <= 0 {
continue;
}
commands.spawn((
HitSpark::new(0.18, 1.8),
Sprite {
color: Color::srgba(1.0, 0.86, 0.48, 0.95),
custom_size: Some(Vec2::new(18.0, 18.0)),
..Default::default()
},
Transform::from_translation(event.position + Vec3::Z * 20.0),
Name::new("HitSpark"),
));
}
}
update_hit_spark_system
使用 Timer
的進度來計算縮放與透明度 (new_demo/src/systems/effects.rs:31
),讓閃光在 0.18 秒內逐漸擴散並淡出。敵人本體則由 apply_enemy_hit_flash_system
將 EnemyHitFlash
掛到實體上,update_enemy_hit_flash_system
每 0.04 秒切換一次顏色 (new_demo/src/systems/effects.rs:58
),再由 Timer
決定何時還原成白色。實作上只需要操作 Sprite.color
,不用顧慮 UV 或 shader。
所謂的死亡粒子(Death Particle Effect)就是當角色死亡時,畫面上散開的粒子效果,用來強化死亡的瞬間感與衝擊感。
在擊殺敵人時,spawn_enemy_death_particles_system
會以事件的命中座標為中心,產生 9 個帶有速度向量的 DeathParticle
(new_demo/src/systems/effects.rs:106
):
let angle = rng.gen_range(0.0..TAU);
let speed = rng.gen_range(140.0..220.0);
let velocity = Vec2::new(angle.cos(), angle.sin()) * speed;
commands.spawn((
DeathParticle::new(velocity, 0.6, Vec3::splat(scale_factor)),
Sprite {
color,
custom_size: Some(Vec2::splat(10.0)),
..Default::default()
},
Transform::from_translation(event.position + Vec3::new(0.0, 0.0, 16.0)),
Name::new("EnemyDeathParticle"),
));
這些粒子在 update_death_particles_system
內依照 Timer
的經過時間調整位置、縮放與透明度 (new_demo/src/systems/effects.rs:129
)。每幀使用 delta_secs()
推進計算,保證粒子壽命與遊戲幀率無關。一旦 Timer
結束,就會自動 despawn。
鏡頭震動透過 CameraShake
資源管理 (new_demo/src/resources/camera_shake.rs:1
)。這個資源紀錄 Timer
、振幅、頻率與當前偏移量,並提供 trigger
/ update
API:
impl CameraShake {
pub fn trigger(&mut self, amplitude: f32, duration: f32) {
self.timer = Timer::from_seconds(duration, TimerMode::Once);
self.timer.reset();
self.amplitude = amplitude;
self.phase = 0.0;
self.active = true;
}
pub fn update(&mut self, delta: f32) {
if !self.active {
self.offset = Vec2::ZERO;
return;
}
self.timer.tick(Duration::from_secs_f32(delta));
let progress = (self.timer.elapsed_secs() / self.timer.duration().as_secs_f32()).clamp(0.0, 1.0);
let damping = 1.0 - progress;
self.phase += delta * self.frequency;
self.offset = Vec2::new(
(self.phase * TAU).sin() * self.amplitude * damping,
((self.phase * TAU) + FRAC_PI_2).sin() * self.amplitude * damping * 0.6,
);
}
}
trigger_camera_shake_on_enemy_hit
根據事件判斷是擊中或擊殺 (new_demo/src/systems/effects.rs:148
),分別給予 0.22 秒與 0.35 秒的震動。CameraPlugin
在 Update
中先執行 camera_follow_system
,再呼叫 apply_camera_shake_system
把偏移量加到鏡頭位置 (new_demo/src/systems/camera.rs:24
)。這樣可以保留原來的跟隨邏輯,又能在特效階段加上動態震動。
所有打擊特效系統集中在新的 EffectsPlugin
(new_demo/src/plugins/effects.rs:3
),掛在 AttackPlugin
後方加入 App。EffectsPlugin
只需要宣告一次,未來要增加新的動畫也能放在同一個模組:
app.add_systems(
Update,
(
spawn_hit_spark_system.after(player_melee_attack_system),
update_hit_spark_system,
apply_enemy_hit_flash_system.after(player_melee_attack_system),
update_enemy_hit_flash_system,
spawn_enemy_death_particles_system.after(player_melee_attack_system),
update_death_particles_system,
trigger_camera_shake_on_enemy_hit.after(player_melee_attack_system),
),
);
現在每次攻擊都會有閃爍的效果,而且畫面看起來會震動,最後如果角色死亡的話,會呈現成粒子四處散開
的效果,在打擊上呈現出良好的手感。
今日程式碼同步至 repo