iT邦幫忙

2025 iThome 鐵人賽

DAY 21
1
Rust

Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記系列 第 21

攻擊瞄準框與方向鎖定

  • 分享至 

  • xImage
  •  

把盾牌固定在玩家手上之後,武器也跟著鎖在同一個位置,結果實際揮刀時就看不出現在是攻擊哪個方向了。所以這篇的目標是補上一個攻擊瞄準框,讓玩家在移動時始終知道下一刀會朝哪個格子打,並且把戰鬥邏輯改成以瞄準框為準,而不是硬看移動向量。

主要想處理的重點:

  • 新增 AttackReticle component 與相關常數,讓瞄準框有固定距離、Z 軸與預設方向。
  • 玩家出場時自動帶出瞄準框 sprite,這個瞄準框也是圖片素材,是 assets/reticle/reticle_aiming.png,預設會在角色右手邊,也就是武器的右邊
  • 透過新的 Update system 追蹤玩家最後一次的移動方向,並把瞄準框貼在最近的上下左右格子。
  • 近戰系統改用瞄準框位置作為攻擊中心,確保攻擊判定與畫面同步。

AttackReticle 與常數定義

new_demo/src/components/player.rs 加了一個很單純的 component:

#[derive(Component)]
pub struct AttackReticle {
    pub last_direction: Vec2,
}

impl AttackReticle {
    pub fn new() -> Self {
        Self {
            last_direction: Vec2::new(1.0, 0.0),
        }
    }
}

同時在 new_demo/src/constants.rs 集中放置瞄準框用到的常數,包含圖片路徑、與玩家的距離以及 Z 軸微調:

pub const ATTACK_RETICLE_SPRITE_PATH: &str = "reticle/reticle_aiming.png";
pub const ATTACK_RETICLE_DISTANCE: f32 = ROOM_TILE_SIZE * PLAYER_SCALE;
pub const ATTACK_RETICLE_Z_OFFSET: f32 = -1.0;

距離直接用一個房間格子的長度(ROOM_TILE_SIZE * PLAYER_SCALE),這樣瞄準框就會完美對齊地圖的格子。


玩家出場時一併顯示瞄準框

new_demo/src/systems/setup.rsspawn_player 系統,把瞄準框作為獨立 entity 生成。由於它不需要跟著玩家骨架擺動,所以沒有掛成子節點,而是每禎自行追蹤位置:

commands.spawn((
    AttackReticle::new(),
    Sprite::from_image(asset_server.load(ATTACK_RETICLE_SPRITE_PATH)),
    Transform::from_translation(Vec3::new(
        spawn_position.x + ATTACK_RETICLE_DISTANCE,
        spawn_position.y,
        spawn_position.z + ATTACK_RETICLE_Z_OFFSET,
    ))
    .with_scale(Vec3::splat(PLAYER_SCALE)),
    Name::new("AttackReticle"),
));

這段會在玩家右側產生一個與角色同尺度的瞄準框。一開始的朝向沿用 component 預設的 (1, 0),確保新遊戲立即看得到提示。


update_attack_reticle_system 鎖定最後移動方向

核心邏輯放在 new_demo/src/systems/attack.rsupdate_attack_reticle_system 每禎讀取玩家目前的 PlayerFacing,判斷哪一個軸向比較明顯,並且只保留上下左右四個方向:

let raw_direction = facing.direction;
if raw_direction.length_squared() > INPUT_DEADZONE * INPUT_DEADZONE {
    let x_abs = raw_direction.x.abs();
    let y_abs = raw_direction.y.abs();

    let axis_direction = if x_abs >= y_abs {
        Vec2::new(raw_direction.x.signum(), 0.0)
    } else {
        Vec2::new(0.0, raw_direction.y.signum())
    };

    if axis_direction != Vec2::ZERO {
        reticle.last_direction = axis_direction;
    }
}

let offset = reticle.last_direction * ATTACK_RETICLE_DISTANCE;
transform.translation.x = player_position.x + offset.x;
transform.translation.y = player_position.y + offset.y;

這個系統使用了 Without<Player> / Without<AttackReticle> 把玩家與瞄準框分成兩個 disjoint 的 Query,確保同一個系統不會同時以可變、不可變方式讀寫同一批 component。


近戰判定改用瞄準框中心

player_melee_attack_system 也在同一檔案調整。過去是用玩家位置加上一段距離來推算攻擊中心,現在改成直接讀瞄準框的座標:

let Some((reticle_transform, reticle)) = reticle_query.iter().next() else {
    return;
};

let facing_direction = reticle.last_direction.normalize_or_zero();
if facing_direction == Vec2::ZERO {
    return;
}

let attack_center = reticle_transform.translation.truncate();

剩下的流程(計算 PLAYER_ATTACK_RADIUS、扣血、Log)維持原本架構。這樣畫面上的瞄準框就等同於攻擊中心,不會出現玩家看到在右邊卻打到上方的情況。


Plugin 排程與測試心得

最後在 new_demo/src/plugins/attack.rsupdate_attack_reticle_system 安排在 movement_system 之後、近戰處理之前:

app.add_systems(
    Update,
    (
        attack_input_system,
        update_attack_reticle_system.after(movement_system),
        player_melee_attack_system
            .after(attack_input_system)
            .after(update_attack_reticle_system),
        update_weapon_offset_system,
        update_weapon_swing_animation_system,
    ),
);

在這個排程下,玩家移動 → 瞄準框更新 → 按下攻擊鍵,就會依序執行,整體節奏非常直覺。移動角色時瞄準框會貼著最近的格子,按空白鍵攻擊就能看到敵人血量從那個格子開始扣。

實際操作流程

實際操作流程


小結

瞄準框其實只是個小小的 sprite,但讓格子制戰鬥瞬間清楚許多。現在玩家可以:

  • 在走位時提前確認下一刀會指向哪裡,透過瞄準框掌握攻擊距離。
  • 配合原本的音效事件,揮空或命中的資訊一眼就懂。
  • 開箱也是按照這個瞄準框來決定要開哪一個寶箱,可以更精準的開啟。

接下來如果要做遠程武器或是特殊技能,也可以沿用 AttackReticle 做延伸,甚至調整 ATTACK_RETICLE_DISTANCE 來支援不同行為。這一步算是把視覺與邏輯的落差補齊,也讓戰鬥體驗更扎實。

今日程式碼同步至 repo


上一篇
事件驅動的音效系統
下一篇
世界邊界與環境整理
系列文
Bevy Rogue-lite 勇者冒險篇 × Rust 遊戲開發筆記24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言