把盾牌固定在玩家手上之後,武器也跟著鎖在同一個位置,結果實際揮刀時就看不出現在是攻擊哪個方向了。所以這篇的目標是補上一個攻擊瞄準框,讓玩家在移動時始終知道下一刀會朝哪個格子打,並且把戰鬥邏輯改成以瞄準框為準,而不是硬看移動向量。
主要想處理的重點:
AttackReticle
component 與相關常數,讓瞄準框有固定距離、Z 軸與預設方向。assets/reticle/reticle_aiming.png
,預設會在角色右手邊,也就是武器的右邊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.rs
的 spawn_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)
,確保新遊戲立即看得到提示。
核心邏輯放在 new_demo/src/systems/attack.rs
。update_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)維持原本架構。這樣畫面上的瞄準框就等同於攻擊中心,不會出現玩家看到在右邊卻打到上方的情況。
最後在 new_demo/src/plugins/attack.rs
把 update_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