返回文章列表

Amethyst遊戲引擎玩家輸入與球體移動

本文介紹如何使用 Amethyst 遊戲引擎處理玩家輸入,實作玩家移動和球體運動,包含碰撞偵測與彈跳機制,並運用速度 Verlet 積分提升物理模擬的精確度。文章涵蓋輸入繫結設定、系統設計、重力模擬、碰撞處理以及程式碼範例,適合 Amethyst 引擎的初學者。

遊戲開發 Rust

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" 使用 DA 鍵進行控制,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 方法中,遍歷所有具有 PlayerTransform 元件的實體。
  • 根據玩家的所屬方(左或右),讀取對應的輸入軸值。
  • 如果有輸入,則印出移動量,用於測試目的。

實作玩家移動邏輯

最後,修改 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;
        }
    }
}

程式碼解析:

  1. 引入必要的模組:我們引入了 Amethyst 引擎所需的模組,包括 TimeTransformSystemDesc 等。
  2. 定義 MoveBallsSystem 結構體:這個結構體將實作 System 特徵,用於更新球體的位置。
  3. 定義重力加速度常數GRAVITY_ACCELERATION 的值為 -40.0,代表重力加速度的大小和方向。
  4. 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

這種方法不僅能夠更準確地模擬球體在重力下的運動,還能夠確保系統的穩定性。

內容解密:

  1. y = y + (velocity + time_difference * acceleration / 2) * time_difference:這行程式碼首先計算了球體在當前時間步內的位置變化。它考慮了初始速度和加速度對位置的影響。
  2. 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", &[])
    // ...

內容解密:

  1. mod move_balls;pub use self::move_balls::MoveBallsSystem;:這兩行程式碼宣告並匯出了MoveBallsSystem,使其能夠被其他模組使用。
  2. .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];
    }
}

內容解密:

  1. for (ball, transform) in (&mut balls, &transforms).join():這行程式碼遍歷所有的球體及其變換元件。
  2. 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
}

內容解密:

  1. fn point_in_rect:這個函式用於判斷一個點是否在指定的矩形區域內。
  2. 邏輯運算式:這個運算式確保點的x和y座標都在矩形區域的邊界之內。

整合彈跳系統

最後,我們需要將彈跳系統整合到遊戲中。

// src/main.rs
let game_data = GameDataBuilder::default()
    .with(systems::BounceSystem, "collision_system", &["player_system", "ball_system"])
    // ...

內容解密:

  1. .with(systems::BounceSystem, "collision_system", &["player_system", "ball_system"]):這行程式碼註冊了BounceSystem,並指定了它依賴於player_systemball_system。這確保了在執行碰撞偵測之前,玩家和球體的狀態已經被正確更新。