經過這幾天的努力,我們已經擁有一個可以在地圖上自由走動的角色、基礎的世界場景,以及能夠流暢跟隨的相機系統。但是遊戲若只有「移動」還不夠,真正的冒險,必須要有戰鬥。
在所有勇者的遊戲中,總是會有拿起劍來攻擊敵人的畫面。像欣梅爾就算拔不出勇者之劍,但是還是會拿起他的其他劍來打魔王一樣。
沒錯,勇者一定要拿劍,這就是勇者的浪漫。所以我們要讓勇者真正「活」過來——第一次握起武器,揮出屬於自己的攻擊!
這篇文章會帶大家一步步實作一套基礎但完整的近戰揮劍系統:
角色轉身時,劍會自動站到正確的位置(左邊或右邊)。
按下空白鍵會觸發揮劍動畫,帶有節奏感,不會被亂按打斷。
程式架構依然保持模組化,方便未來加上敵人、判定、特效或連段攻擊。
在這個篇章之後,你的遊戲將不再只是「一個人在地圖上跑」,而是真正有了動作遊戲的靈魂。
在開始之前,先想像一下玩家體驗。這不只是單純「劍揮出去了」,還要考慮:
當角色往左轉時,劍應該自動換邊。
當角色往右走時,劍回到右手。
這樣的動作才能讓角色顯得自然,而不是劍「黏死在角色右邊」。
攻擊動作需要有起手 → 出劍 → 收招的過程。
如果空白鍵可以無限連按,動畫會亂跳,體驗會不好。
因此需要用計時器(Timer)來限制,一次揮劍完整結束後,才能進入下一次攻擊。
我們會使用 ECS 的模組化方式,把攻擊流程拆成三個系統。
這樣未來要加上「命中判定」、「武器種類」或「敵人 AI」時,不需要大改原本的程式,只要擴充就好。
所以我們的目標是: 玩家按下空白鍵時,角色不只是動了一下,而是「真的揮了一劍」。
我們需要兩張劍的圖片,分別代表「右手」和「左手」:
assets/weapons/sword.png
assets/weapons/sword_left.png
這樣設計有兩個原因:
避免在 runtime 做圖片翻轉時造成像素邊緣模糊,因為有嘗試過只用一張圖來做,但效果不好,尤其是像素風格的圖片,看起來會很奇怪。
美術可以針對左右版本進行微調,但在這個簡易的版本中,只是幫圖片做翻轉而已。
在 Bevy 中,我們可以把「劍」作為「玩家的子實體」。
這樣劍的座標會自動跟隨玩家,不需要在系統裡手動更新。
程式碼如下:
let player_entity = commands.spawn((
Player,
Sprite::from_image(asset_server.load("characters/knight_lv1.png")),
Transform::from_translation(Vec3::ZERO).with_scale(Vec3::splat(PLAYER_SCALE)),
PlayerFacing::new(),
Health::new(100),
Velocity::zero(),
)).id();
let weapon_entity = commands.spawn((
Weapon,
Sprite::from_image(asset_server.load("weapons/sword.png")),
Transform::from_translation(Vec3::new(8.0, 2.0, 1.0))
.with_scale(Vec3::splat(WEAPON_SCALE)),
WeaponSprites {
right_sprite: asset_server.load("weapons/sword.png"),
left_sprite: asset_server.load("weapons/sword_left.png"),
},
WeaponOffset {
base_angle: 0.0,
position: Vec2::new(8.0, 2.0),
},
WeaponSwing {
timer: Timer::from_seconds(0.5, TimerMode::Once),
from_angle: 0.0,
to_angle: 0.0,
},
)).id();
commands.entity(player_entity).add_children(&[weapon_entity]);
講解一下這裡的重點:
劍的座標 (8.0, 2.0, 1.0)
代表它會在角色右邊稍微偏上,Z=1 確保渲染時蓋在角色前面。
Timer::from_seconds(0.5, TimerMode::Once)
確保揮劍動畫跑完 0.5 秒才算完成。
使用 add_children
把劍附加到玩家,之後移動玩家時,劍會自動跟著移動。
這種父子實體設計,在未來還可以用來擴充「盾牌」、「寵物」、「背包」等裝備。
我把攻擊流程拆成三個系統,避免一個超大函式塞滿邏輯。
attack_input_system
檢查是否按下空白鍵。
如果 Timer 還在跑,就忽略這次輸入。
如果 Timer 結束,則重啟揮劍,設定揮擊角度(例如 -45° → +45°)。
這樣就能避免「動畫被亂按打斷」。
update_weapon_offset_system
讀取玩家的 PlayerFacing
元件。
把面向分成八個象限(上下左右 + 四個斜向)。
根據方向決定劍的座標偏移,以及要用左手還是右手貼圖。
例如:
面向右 → 劍在角色右邊,使用 sword.png
。
面向左 → 劍在角色左邊,使用 sword_left.png
。
這樣角色轉身時,劍會自然地跟著換邊。
update_weapon_swing_animation_system
每幀更新 Timer,取得動畫進度。
用線性插值(lerp_angle)從 from_angle
慢慢轉到 to_angle
。
加上 base_angle
,確保揮擊是基於角色面向方向。
當 Timer 結束時,劍回到待機位置。
這裡也可以改成 easing
函式,例如「先快後慢」或「慢起手快揮出」,讓動畫更有重量感。
為了維持專案結構清晰,我們把所有攻擊相關的系統都放進 AttackPlugin
:
pub struct AttackPlugin;
impl Plugin for AttackPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (
attack_input_system,
update_weapon_offset_system,
update_weapon_swing_animation_system,
));
}
}
然後在 main.rs
裡加入:
.add_plugins((WorldPlugin, PlayerPlugin, CameraPlugin, AttackPlugin))
看一下攻擊的樣子:
我們完成了勇者的第一把劍,並且建立了一個模組化的攻擊系統。
實作這個功能比原本預期的要困難,因為一開始沒想到有父子實體的設計。原本是想直接做出角色拿武器跟不拿武器的切換,但是這樣做不出武器的揮砍手感。後來也是詢問有做過遊戲的朋友,才知道可以這樣做。
雖然做出來還是滿陽春的,但是有達到我想要的效果了。之後也可以再加強攻擊的特效,或是加上攻擊的聲音,還有很多有趣的想法可以做。
今天的程式碼分享在 repo