上一篇把隨機產生房間搞定後,角色雖然能在不同類型的房間出現,但是這個邏輯不太正確。應該會是角色在房間外的位置,然後需要經由門的出入才是正確的互動過程。
所以這一篇主要做的是,當隨機產生房間時,角色比較在外面,而且房間的門必須是可開關,如果是關起來的狀態,就不能進出,必須要開啟的門才能進出。
這次處理的是一個「跨系統耦合」的問題。門的開關、玩家的移動、房間的傳送都屬於不同的系統,任何一個判斷錯誤就會讓流程結束。
所以我預想需求會拆成四個階段:
DoorInteractionEvent
,否則就觸發攻擊事件。InputVector
判斷方向,但角色按下空白鍵時輸入系統會把向量清成 Vec2::ZERO
,造成碰撞檢查失效。所以我在系統中加入 Velocity
備援,必要時用速度來推估目前移動方向並重新正規化。ROOM_TILE_SIZE * PLAYER_SCALE * 1.5
,跟著瓷磚大小變動,門內外緩衝距離就會自動調整。整體流程回事:按空白鍵 → 門就緒 → 朝門走到觸發範圍 → 在移動方向與門位置條件成立時進行傳送。
pub fn input_system(
keyboard_input: Res<ButtonInput<KeyCode>>,
player_query: Query<&Transform, With<Player>>,
door_query: Query<(&Door, &Transform), (With<RoomTile>, Without<Player>)>,
mut door_events: EventWriter<DoorInteractionEvent>,
mut attack_events: EventWriter<AttackInputEvent>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
let player_transform = match player_query.single() {
Ok(transform) => transform,
Err(_) => return,
};
let interaction_distance = ROOM_TILE_SIZE * PLAYER_SCALE * 10.0;
let mut near_door = false;
for (_door, door_transform) in &door_query {
if player_transform
.translation
.distance(door_transform.translation)
<= interaction_distance
{
near_door = true;
break;
}
}
if near_door {
door_events.write(DoorInteractionEvent);
info!("門交互事件已發送!");
} else {
attack_events.write(AttackInputEvent);
}
}
}
這段邏輯可以確保點擊空白鍵可以處理門,也不會讓攻擊系統沒作用。玩家就可以不需要記兩個按鍵,使用體驗上可以更直觀。
pub fn room_transition_system(
door_query: Query<(&Door, &Transform), Without<Player>>,
mut player_query: Query<(&mut Transform, &Velocity, &InputVector), With<Player>>,
mut transition_cooldown: ResMut<TransitionCooldown>,
time: Res<Time>,
) {
transition_cooldown.timer.tick(time.delta());
if !transition_cooldown.timer.finished() {
return;
}
let (mut player_transform, velocity, input_vector) = match player_query.single_mut() {
Ok(result) => result,
Err(_) => return,
};
let tile_size = ROOM_TILE_SIZE * PLAYER_SCALE;
let trigger_distance = tile_size * 3.0;
let teleport_offset = tile_size * 1.5;
let mut movement = input_vector.0;
if movement.length_squared() <= f32::EPSILON {
movement = Vec2::new(velocity.x, velocity.y);
if movement.length_squared() > 0.0 {
movement = movement.normalize();
}
}
for (door, door_transform) in &door_query {
let door_pos = door_transform.translation.truncate();
let player_pos = player_transform.translation.truncate();
if door.is_open && door_pos.distance(player_pos) < trigger_distance {
let door_to_player = player_pos - door_pos;
let moving_up = movement.y > 0.1;
let moving_down = movement.y < -0.1;
if door_to_player.y < -20.0 && moving_up {
let new_position = door_pos + Vec2::new(0.0, teleport_offset);
player_transform.translation.x = new_position.x;
player_transform.translation.y = new_position.y;
transition_cooldown.timer.reset();
info!("✅ 玩家進入房間!從 {:?} 傳送到 {:?}", player_pos, new_position);
} else if door_to_player.y > 20.0 && moving_down {
let new_position = door_pos + Vec2::new(0.0, -teleport_offset);
player_transform.translation.x = new_position.x;
player_transform.translation.y = new_position.y;
transition_cooldown.timer.reset();
info!("✅ 玩家離開房間!從 {:?} 傳送到 {:?}", player_pos, new_position);
}
}
}
}
這裡特別講一下兩個地方:
InputVector
因為按下互動鍵而歸零時,會立刻改用速度算方向,避免系統誤判玩家沒有在移動。teleport_offset
按照地板大小計算,未來如果調整 PLAYER_SCALE
的時侯也不用再去動這段程式。if should_generate_door {
if let Some(door_pos) = door_world_position {
let outdoor_depth_tiles = 5;
let outdoor_extra_width = 4;
let spawn_offset_tiles = 2;
let half_width = (width as i32 + outdoor_extra_width) / 2;
for depth in 1..=outdoor_depth_tiles {
let exterior_y = door_pos.y - tile_size * depth as f32;
for offset in -half_width..=half_width {
let exterior_x = door_pos.x + offset as f32 * tile_size;
commands.spawn((
Sprite::from_image(room_assets.floor_outdoor.clone()),
Transform::from_translation(Vec3::new(exterior_x, exterior_y, Z_LAYER_GRID))
.with_scale(Vec3::splat(PLAYER_SCALE)),
RoomTile {
tile_type: RoomTileType::FloorOutdoor,
},
));
}
}
let spawn_y = door_pos.y - tile_size * spawn_offset_tiles as f32;
commands.insert_resource(EntranceLocation::new(Vec3::new(door_pos.x, spawn_y, 10.0)));
}
}
pub fn spawn_player(
mut commands: Commands,
asset_server: Res<AssetServer>,
entrance_location: Option<Res<EntranceLocation>>,
) {
let spawn_position = entrance_location
.map(|location| location.position)
.unwrap_or_else(|| Vec3::new(0.0, -ROOM_TILE_SIZE * PLAYER_SCALE * 3.0, 10.0));
commands
.spawn((
Player,
Sprite::from_image(asset_server.load("characters/knight_lv1.png")),
Transform::from_translation(spawn_position)
.with_scale(Vec3::splat(PLAYER_SCALE)),
/* ... */
));
// 省略武器子節點邏輯
}
EntranceLocation
紀錄門外的座標,PostStartup
階段的 spawn_player
從中取值,確保房間建立完畢後才產生角色。室外地板則根據房間寬度向左右延伸,比門寬多出兩格視覺緩衝,深度則向下鋪五格,形成一個暫時的戶外落腳區。
實際測試流程:
目前的系統還有蠻多需要加強的,像是在進出空間時,會有一種跳躍的感覺,或許可以考慮加上一個過場的畫面來解決這個問題。不過以功能面來說算是達到需求了,後續進入打磨階段再來決定怎麼做比較好。
今天的程式碼分享在 repo