Amethyst 引擎提供了一套完整的功能來處理玩家輸入和物理模擬。首先,透過設定輸入繫結,將鍵盤或滑鼠的輸入對映到遊戲中的動作。接著,建立系統來處理這些輸入,並更新遊戲物件的狀態。本文的範例中,玩家的左右移動便是透過讀取輸入軸的值來實作。此外,球體的運動模擬使用了速度 Verlet 積分,這是一種比簡單的尤拉方法更精確的數值積分方法,可以更真實地模擬球體在重力影響下的運動軌跡,並確保系統的穩定性。最後,透過碰撞偵測和彈跳處理,使球體與邊界和玩家互動,增加了遊戲的趣味性。
玩家輸入與移動系統實作
在遊戲開發中,處理玩家輸入是基本且重要的環節。本章節將介紹如何使用 Amethyst 遊戲引擎處理玩家輸入,並實作一個簡單的玩家移動系統。
設定輸入繫結
首先,需要定義輸入繫結(Input Bindings),即將特定的按鍵或按鈕與遊戲中的特定動作或軸(Axes)繫結。在 Amethyst 中,這通常是透過一個名為 bindings_config.ron 的檔案來完成的。
// resources/bindings_config.ron
(
axes: {
"left_player": Emulated(pos: Key(D), neg: Key(A)),
"right_player": Emulated(pos: Key(Right), neg: Key(Left)),
},
actions: {},
)
內容解密:
axes定義了兩個虛擬軸(“left_player” 和 “right_player”),分別對應左右玩家的移動控制。"left_player"使用D和A鍵進行控制,D鍵為正向(向右),A鍵為負向(向左)。"right_player"使用右箭頭和左箭頭鍵進行控制,右箭頭為正向(向右),左箭頭為負向(向左)。
載入輸入設定
接下來,需要將這個輸入設定檔載入到遊戲中。這是透過 InputBundle 完成的。
// src/main.rs
use amethyst::input::{InputBundle, StringBindings};
fn main() -> amethyst::Result<()> {
// ...
let binding_path = app_root.join("resources").join("bindings_config.ron");
let input_bundle = InputBundle::<StringBindings>::new().with_bindings_from_file(binding_path)?;
let game_data = GameDataBuilder::default()
// ... other bundles
.with_bundle(input_bundle)?;
// ...
}
內容解密:
InputBundle負責管理遊戲的輸入設定。with_bindings_from_file方法用於從指定的檔案中載入輸入繫結設定。- 將
input_bundle新增到GameDataBuilder中,使遊戲能夠識別和處理玩家輸入。
建立玩家移動系統
為了回應玩家的輸入,需要建立一個系統(System)。在 Amethyst 中,系統是用於封裝遊戲邏輯的基本單元。
// src/systems/player.rs
use amethyst::{
core::Transform,
core::SystemDesc,
derive::SystemDesc,
ecs::{Join, Read, ReadStorage, System, SystemData, World, WriteStorage},
input::{InputHandler, StringBindings},
};
use crate::catvolleyball::{Player, Side, ARENA_WIDTH, PLAYER_WIDTH};
#[derive(SystemDesc)]
pub struct PlayerSystem;
impl<'s> System<'s> for PlayerSystem {
type SystemData = (
WriteStorage<'s, Transform>,
ReadStorage<'s, Player>,
Read<'s, InputHandler<StringBindings>>,
);
fn run(&mut self, (mut transforms, players, input): Self::SystemData) {
for (player, transform) in (&players, &mut transforms).join() {
let movement = match player.side {
Side::Left => input.axis_value("left_player"),
Side::Right => input.axis_value("right_player"),
};
if let Some(mv_amount) = movement {
// 簡單地印出移動量,用於測試
let side_name = match player.side {
Side::Left => "left",
Side::Right => "right",
};
println!("Side {:?} moving {}", side_name, mv_amount);
}
}
}
}
內容解密:
PlayerSystem是一個用於處理玩家移動的系統。- 在
run方法中,遍歷所有具有Player和Transform元件的實體。 - 根據玩家的所屬方(左或右),讀取對應的輸入軸值。
- 如果有輸入,則印出移動量,用於測試目的。
實作玩家移動邏輯
最後,修改 PlayerSystem 以實作玩家的實際移動。
// src/systems/player.rs (修改後的版本)
const PLAYER_SPEED: f32 = 60.0;
impl<'s> System<'s> for PlayerSystem {
type SystemData = (
WriteStorage<'s, Transform>,
ReadStorage<'s, Player>,
Read<'s, Time>,
Read<'s, InputHandler<StringBindings>>,
);
fn run(&mut self, (mut transforms, players, time, input): Self::SystemData) {
for (player, transform) in (&players, &mut transforms).join() {
let movement = match player.side {
Side::Left => input.axis_value("left_player"),
Side::Right => input.axis_value("right_player"),
};
if let Some(mv_amount) = movement {
let scaled_amount = (PLAYER_SPEED * time.delta_seconds() * mv_amount) as f32;
let player_x = transform.translation().x;
let player_left_limit = match player.side {
Side::Left => 0.0,
Side::Right => ARENA_WIDTH / 2.0,
};
transform.set_translation_x(
(player_x + scaled_amount)
.max(player_left_limit + PLAYER_WIDTH / 2.0)
.min(player_left_limit + ARENA_WIDTH / 2.0 - PLAYER_WIDTH / 2.0),
);
}
}
}
}
內容解密:
- 新增了
Read<'s, Time>到SystemData以取得遊戲時間的差值,用於使移動速度與幀率無關。 - 計算出縮放後的移動量
scaled_amount,並根據玩家的限制調整其位置。 - 使用
transform.set_translation_x更新玩家的位置,同時確保玩家不會超出指定的移動範圍。
透過上述步驟,成功地在 Amethyst 遊戲引擎中實作了根據玩家輸入的移動系統。這不僅展示瞭如何處理遊戲中的輸入,還演示瞭如何使用系統來封裝和管理遊戲邏輯。
遊戲開發:移動球體與重力模擬
在前面的章節中,我們為遊戲增加了球體並賦予了初始速度。然而,球體卻像是在零重力環境中一樣直接飛出視窗外。為了使遊戲更真實,我們需要實作一個模擬重力的系統。
建立球體移動系統
首先,我們需要在 src/systems 目錄下建立一個新的檔案 move_balls.rs,並在其中新增以下程式碼:
use amethyst::{
core::timing::Time,
core::transform::Transform,
core::SystemDesc,
derive::SystemDesc,
ecs::prelude::{Join, Read, System, SystemData, World, WriteStorage}
};
use crate::catvolleyball::Ball;
#[derive(SystemDesc)]
pub struct MoveBallsSystem;
pub const GRAVITY_ACCELERATION: f32 = -40.0;
impl<'s> System<'s> for MoveBallsSystem {
type SystemData = (
WriteStorage<'s, Ball>,
WriteStorage<'s, Transform>,
Read<'s, Time>,
);
fn run(&mut self, (mut balls, mut locals, time): Self::SystemData) {
// 根據球體的速度和時間差移動每個球體
for (ball, local) in (&mut balls, &mut locals).join() {
local.prepend_translation_x(
ball.velocity[0] * time.delta_seconds()
);
local.prepend_translation_y(
(
ball.velocity[1] +
time.delta_seconds() *
GRAVITY_ACCELERATION / 2.0
) * time.delta_seconds(),
);
ball.velocity[1] = ball.velocity[1] +
time.delta_seconds() * GRAVITY_ACCELERATION;
}
}
}
程式碼解析:
- 引入必要的模組:我們引入了 Amethyst 引擎所需的模組,包括
Time、Transform、SystemDesc等。 - 定義
MoveBallsSystem結構體:這個結構體將實作System特徵,用於更新球體的位置。 - 定義重力加速度常數:
GRAVITY_ACCELERATION的值為-40.0,代表重力加速度的大小和方向。 run方法實作:在這個方法中,我們根據球體的速度和時間差更新其位置,並應用重力加速度。
重力模擬的數學原理
在物理學中,物體的運動可以用以下公式描述:
- $v_x(t) = \frac{dx}{dt}$
- $v_y(t) = \frac{dy}{dt}$
簡單的 Euler 積分法可能會引入誤差,尤其是在時間差不穩定的情況下。因此,我們採用了一種更穩定的更新方法:
local.prepend_translation_x(ball.velocity[0] * time.delta_seconds());local.prepend_translation_y((ball.velocity[1] + time.delta_seconds() * GRAVITY_ACCELERATION / 2.0) * time.delta_seconds());ball.velocity[1] = ball.velocity[1] + time.delta_seconds() * GRAVITY_ACCELERATION;
這種方法可以更準確地模擬重力對球體運動的影響。
實作彈跳球體的模擬
在開發遊戲時,模擬一個真實的物理環境對於提升遊戲體驗至關重要。本章節將著重於如何使用Amethyst遊戲引擎來實作一個簡單的球體彈跳系統。球體的運動和彈跳行為將會被詳細分析,並提供完整的程式碼實作。
改進球體運動模擬
在前面的章節中,我們已經實作了一個簡單的球體運動系統。然而,這個系統存在一些問題,例如球體的運動軌跡不夠真實。為了改進這一點,我們將採用一種更為精確的數值積分方法——速度Verlet積分。
速度Verlet積分
速度Verlet積分是一種用於模擬物體運動的數值方法,它能夠提供比簡單尤拉方法更為準確的結果。具體實作如下:
y = y + (velocity + time_difference * acceleration / 2) * time_difference
velocity = velocity + acceleration * time_difference
這種方法不僅能夠更準確地模擬球體在重力下的運動,還能夠確保系統的穩定性。
內容解密:
y = y + (velocity + time_difference * acceleration / 2) * time_difference:這行程式碼首先計算了球體在當前時間步內的位置變化。它考慮了初始速度和加速度對位置的影響。velocity = velocity + acceleration * time_difference:這行程式碼更新了球體的速度,考慮了加速度在時間步內的影響。
將球體運動系統整合到遊戲中
為了使球體運動系統生效,我們需要將其註冊到Amethyst的遊戲資料中。
// src/systems/mod.rs
mod move_balls;
pub use self::move_balls::MoveBallsSystem;
// src/main.rs
let game_data = GameDataBuilder::default()
.with(systems::MoveBallsSystem, "ball_system", &[])
// ...
內容解密:
mod move_balls;和pub use self::move_balls::MoveBallsSystem;:這兩行程式碼宣告並匯出了MoveBallsSystem,使其能夠被其他模組使用。.with(systems::MoveBallsSystem, "ball_system", &[]):這行程式碼將MoveBallsSystem註冊到遊戲資料中,使其能夠在遊戲迴圈中被執行。
實作球體彈跳
為了使遊戲更加有趣,我們需要實作球體與視窗邊界以及玩家之間的彈跳行為。
處理球體與視窗邊界的碰撞
當球體碰到視窗邊界時,我們需要反轉其對應軸上的速度,以實作彈跳效果。
// src/systems/bounce.rs
for (ball, transform) in (&mut balls, &transforms).join() {
let ball_x = transform.translation().x;
let ball_y = transform.translation().y;
// 碰撞偵測與彈跳處理
if ball_y <= ball.radius && ball.velocity[1] < 0.0 {
ball.velocity[1] = -ball.velocity[1];
} else if ball_y >= (ARENA_HEIGHT - ball.radius) && ball.velocity[1] > 0.0 {
ball.velocity[1] = -ball.velocity[1];
} else if ball_x <= (ball.radius) && ball.velocity[0] < 0.0 {
ball.velocity[0] = -ball.velocity[0];
} else if ball_x >= (ARENA_WIDTH - ball.radius) && ball.velocity[0] > 0.0 {
ball.velocity[0] = -ball.velocity[0];
}
}
內容解密:
for (ball, transform) in (&mut balls, &transforms).join():這行程式碼遍歷所有的球體及其變換元件。if陳述式:這些條件判斷用於偵測球體是否碰到了視窗邊界,如果是,則反轉對應軸上的速度。
處理球體與玩家的碰撞
為了簡化計算,我們假設玩家是一個矩形區域。當球體進入這個區域時,我們認為發生了碰撞,並據此更新球體的速度。
// src/systems/bounce.rs
fn point_in_rect(x: f32, y: f32, left: f32, bottom: f32, right: f32, top: f32) -> bool {
x >= left && x <= right && y >= bottom && y <= top
}
內容解密:
fn point_in_rect:這個函式用於判斷一個點是否在指定的矩形區域內。- 邏輯運算式:這個運算式確保點的x和y座標都在矩形區域的邊界之內。
整合彈跳系統
最後,我們需要將彈跳系統整合到遊戲中。
// src/main.rs
let game_data = GameDataBuilder::default()
.with(systems::BounceSystem, "collision_system", &["player_system", "ball_system"])
// ...
內容解密:
.with(systems::BounceSystem, "collision_system", &["player_system", "ball_system"]):這行程式碼註冊了BounceSystem,並指定了它依賴於player_system和ball_system。這確保了在執行碰撞偵測之前,玩家和球體的狀態已經被正確更新。