iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0

經過這幾天的努力,我們已經擁有一個可以在地圖上自由走動的角色、基礎的世界場景,以及能夠流暢跟隨的相機系統。但是遊戲若只有「移動」還不夠,真正的冒險,必須要有戰鬥。

在所有勇者的遊戲中,總是會有拿起劍來攻擊敵人的畫面。像欣梅爾就算拔不出勇者之劍,但是還是會拿起他的其他劍來打魔王一樣。

沒錯,勇者一定要拿劍,這就是勇者的浪漫。所以我們要讓勇者真正「活」過來——第一次握起武器,揮出屬於自己的攻擊!

這篇文章會帶大家一步步實作一套基礎但完整的近戰揮劍系統

  • 角色轉身時,劍會自動站到正確的位置(左邊或右邊)。

  • 按下空白鍵會觸發揮劍動畫,帶有節奏感,不會被亂按打斷。

  • 程式架構依然保持模組化,方便未來加上敵人、判定、特效或連段攻擊。

在這個篇章之後,你的遊戲將不再只是「一個人在地圖上跑」,而是真正有了動作遊戲的靈魂

一、遊戲設計思路:一把「有手感」的劍

在開始之前,先想像一下玩家體驗。這不只是單純「劍揮出去了」,還要考慮:

  1. 劍的位置
  • 當角色往左轉時,劍應該自動換邊。

  • 當角色往右走時,劍回到右手。

  • 這樣的動作才能讓角色顯得自然,而不是劍「黏死在角色右邊」。

  1. 揮劍動畫的節奏
  • 攻擊動作需要有起手 → 出劍 → 收招的過程。

  • 如果空白鍵可以無限連按,動畫會亂跳,體驗會不好。

  • 因此需要用計時器(Timer)來限制,一次揮劍完整結束後,才能進入下一次攻擊。

  1. 架構的彈性
  • 我們會使用 ECS 的模組化方式,把攻擊流程拆成三個系統。

  • 這樣未來要加上「命中判定」、「武器種類」或「敵人 AI」時,不需要大改原本的程式,只要擴充就好。

所以我們的目標是: 玩家按下空白鍵時,角色不只是動了一下,而是「真的揮了一劍」。

二、準備資產:武器的左右版本

我們需要兩張劍的圖片,分別代表「右手」和「左手」:

  • assets/weapons/sword.png

  • assets/weapons/sword_left.png

這樣設計有兩個原因:

  1. 避免在 runtime 做圖片翻轉時造成像素邊緣模糊,因為有嘗試過只用一張圖來做,但效果不好,尤其是像素風格的圖片,看起來會很奇怪。

  2. 美術可以針對左右版本進行微調,但在這個簡易的版本中,只是幫圖片做翻轉而已。

左邊

右邊

三、建立父子實體:角色與武器綁在一起

在 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 把劍附加到玩家,之後移動玩家時,劍會自動跟著移動。

這種父子實體設計,在未來還可以用來擴充「盾牌」、「寵物」、「背包」等裝備。

四、三個攻擊系統的設計

我把攻擊流程拆成三個系統,避免一個超大函式塞滿邏輯。

  1. 觸發攻擊:attack_input_system
  • 檢查是否按下空白鍵。

  • 如果 Timer 還在跑,就忽略這次輸入。

  • 如果 Timer 結束,則重啟揮劍,設定揮擊角度(例如 -45° → +45°)。

這樣就能避免「動畫被亂按打斷」。

  1. 調整劍的位置與方向:update_weapon_offset_system
  • 讀取玩家的 PlayerFacing 元件。

  • 把面向分成八個象限(上下左右 + 四個斜向)。

  • 根據方向決定劍的座標偏移,以及要用左手還是右手貼圖。

例如:

  • 面向右 → 劍在角色右邊,使用 sword.png

  • 面向左 → 劍在角色左邊,使用 sword_left.png

這樣角色轉身時,劍會自然地跟著換邊。

  1. 播放揮擊動畫:update_weapon_swing_animation_system
  • 每幀更新 Timer,取得動畫進度。

  • 用線性插值(lerp_angle)從 from_angle 慢慢轉到 to_angle

  • 加上 base_angle,確保揮擊是基於角色面向方向。

  • 當 Timer 結束時,劍回到待機位置。

這裡也可以改成 easing 函式,例如「先快後慢」或「慢起手快揮出」,讓動畫更有重量感。

五、模組化整合:AttackPlugin

為了維持專案結構清晰,我們把所有攻擊相關的系統都放進 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


上一篇
相機跟隨系統(Camera Follow)
下一篇
隨機產生地圖
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言