在遊戲開發中,準確的物理模擬和計分系統至關重要。本文利用 Rust 和 Bevy 引擎,示範如何實作球體的運動、碰撞和計分功能。首先,我們使用 Velocity Verlet 積分法取代簡單的 Euler 積分法,提高物理模擬的精確度,減少時間步長變化造成的誤差。接著,我們加入碰撞偵測機制,讓球體可以與場地邊界和玩家發生碰撞並反彈,使遊戲更具互動性。最後,我們實作計分系統,根據球體落點位置判斷得分情況,並將分數顯示在螢幕上,提升遊戲的完整性和趣味性。

遊戲開發中的球體運動實作

在前一章節中,我們成功地將球體加入遊戲場景中,並賦予其初始速度。現在,我們需要實作一個新的系統來控制球體的運動。與玩家移動不同的是,球體的運動需要模擬重力加速度的影響。

球體運動系統的實作

首先,我們需要定義球體的初始速度和半徑,以及重力加速度的常數。這些常數將用於計算球體在每個時間步長中的位置和速度變化。

常數定義

const BALL_VELOCITY_X: f32 = 30.0;
const BALL_VELOCITY_Y: f32 = 0.0;
const BALL_RADIUS: f32 = 4.0;
pub const GRAVITY_ACCELERATION: f32 = -40.0;

球體結構定義

#[derive(Component)]
pub struct Ball {
    pub velocity: Vec2,
    pub radius: f32,
}

球體初始化函式

fn initialize_ball(
    commands: &mut Commands,
    asset_server: &Res<AssetServer>,
    atlas: Handle<TextureAtlas>,
    ball_sprite: usize,
) {
    commands.spawn((
        Ball {
            velocity: Vec2::new(BALL_VELOCITY_X, BALL_VELOCITY_Y),
            radius: BALL_RADIUS,
        },
        SpriteSheetBundle {
            sprite: TextureAtlasSprite::new(ball_sprite),
            texture_atlas: atlas,
            transform: Transform::from_xyz(
                ARENA_WIDTH / 2.0,
                ARENA_HEIGHT / 2.0, 
                0.0),
            ..default()
        },
    ));
}

球體運動系統

fn move_ball(
    time: Res<Time>,
    mut query: Query<(&mut Ball, &mut Transform)>
) {
    for (mut ball, mut transform) in query.iter_mut() {
        // 套用運動變化量
        transform.translation.x += ball.velocity.x * time.raw_delta_seconds();
        transform.translation.y += (ball.velocity.y + time.raw_delta_seconds() * GRAVITY_ACCELERATION / 2.0) * time.raw_delta_seconds();
        ball.velocity.y += time.raw_delta_seconds() * GRAVITY_ACCELERATION;
    }
}

內容解密:

  1. 時間步長的取得time.raw_delta_seconds()用於取得兩個系統執行之間的時間差,以秒為單位。這使得運動計算能夠根據真實的時間變化。
  2. X軸位置更新transform.translation.x += ball.velocity.x * time.raw_delta_seconds();直接根據球體的X軸速度和時間步長更新其X軸位置。
  3. Y軸位置更新:使用transform.translation.y += (ball.velocity.y + time.raw_delta_seconds() * GRAVITY_ACCELERATION / 2.0) * time.raw_delta_seconds();來計算Y軸的新位置。這裡考慮了重力加速度對速度的影響,並採用了半步長修正來提高計算精確度。
  4. Y軸速度更新ball.velocity.y += time.raw_delta_seconds() * GRAVITY_ACCELERATION;根據重力加速度和時間步長更新球體的Y軸速度。

系統註冊

fn main() {
    App::new()
    // ... 其他系統和設定
    .add_system(move_ball)
    // ...
}

內容解密:

  • move_ball系統被註冊到Bevy應用中,負責處理所有帶有BallTransform元件的實體。
  • 該系統模擬了重力對球體的影響,使其能夠在遊戲場景中自然下落。

改善遊戲物理模擬:使用Velocity Verlet積分法

在遊戲開發中,模擬真實世界的物理現象是非常重要的。在前面的章節中,我們使用了簡單的Euler積分法來模擬球的運動,但這種方法會引入一定的誤差,尤其是在時間差不穩定的情況下。為了提高模擬的準確性,我們將使用velocity Verlet積分法。

Euler積分法的限制

Euler積分法是一種簡單的數值積分方法,它透過以下公式來更新物體的位置和速度:

y = y + velocity * time_difference
velocity = velocity + acceleration * time_difference

然而,這種方法會引入一定的誤差,尤其是在時間差不穩定的情況下。如果幀率不同,球的軌跡也會略有不同。

Velocity Verlet積分法的優勢

velocity Verlet積分法是一種更準確的數值積分方法,它透過以下公式來更新物體的位置和速度:

y = y + (velocity + time_difference * acceleration / 2) * time_difference
velocity = velocity + acceleration * time_difference

這種方法可以更好地模擬物體的運動,減少誤差。

程式碼實作

fn move_ball(mut ball_query: Query<(&mut Ball, &mut Transform)>) {
    for (mut ball, mut transform) in ball_query.iter_mut() {
        let time_difference = 1.0 / 60.0; // 假設幀率為60 FPS
        let acceleration = Vec3::new(0.0, -9.8, 0.0); // 重力加速度
        ball.velocity += acceleration * time_difference;
        transform.translation += ball.velocity * time_difference;
    }
}

內容解密:

  1. time_difference代表每幀的時間間隔,假設遊戲以60 FPS執行。
  2. acceleration代表重力加速度,使球體向下加速。
  3. ball.velocity += acceleration * time_difference;更新球的速度。
  4. transform.translation += ball.velocity * time_difference;更新球的位置。

新增碰撞偵測與反彈機制

為了使遊戲更加真實,我們需要新增碰撞偵測與反彈機制。當球體碰到視窗邊緣或玩家時,應該反彈。

碰撞偵測邏輯

fn bounce(
    mut ball_query: Query<(&mut Ball, &Transform)>,
    player_query: Query<(&Player, &Transform)>,
) {
    for (mut ball, ball_transform) in ball_query.iter_mut() {
        let ball_x = ball_transform.translation.x;
        let ball_y = ball_transform.translation.y;

        // 碰撞視窗邊緣的偵測與處理
        if ball_y <= ball.radius && ball.velocity.y < 0.0 {
            ball.velocity.y = -ball.velocity.y;
        } else if ball_y >= (ARENA_HEIGHT - ball.radius) && ball.velocity.y > 0.0 {
            ball.velocity.y = -ball.velocity.y;
        }

        // 碰撞玩家的偵測與處理
        for (player, player_trans) in player_query.iter() {
            let player_x = player_trans.translation.x;
            let player_y = player_trans.translation.y;

            if point_in_rect(
                ball_x,
                ball_y,
                player_x - PLAYER_WIDTH / 2.0 - ball.radius,
                player_y - PLAYER_HEIGHT / 2.0 - ball.radius,
                player_x + PLAYER_WIDTH / 2.0 + ball.radius,
                player_y + PLAYER_HEIGHT / 2.0 + ball.radius,
            ) {
                // 處理碰撞邏輯
            }
        }
    }
}

內容解密:

  1. bounce函式負責偵測球體是否碰撞到視窗邊緣或玩家。
  2. point_in_rect函式用於判斷球體是否在玩家的矩形範圍內。
  3. 當球體碰撞到視窗邊緣時,反轉其對應軸的速度。
  4. 當球體碰撞到玩家時,除了反轉Y軸速度外,還會根據玩家的位置調整X軸速度,使球體朝對手方向移動。

圖表說明

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 遊戲球體運動與碰撞計分系統實作

package "圖論網路分析" {
    package "節點層" {
        component [節點 A] as nodeA
        component [節點 B] as nodeB
        component [節點 C] as nodeC
        component [節點 D] as nodeD
    }

    package "中心性指標" {
        component [度中心性
Degree Centrality] as degree
        component [特徵向量中心性
Eigenvector Centrality] as eigen
        component [介數中心性
Betweenness Centrality] as between
        component [接近中心性
Closeness Centrality] as close
    }
}

nodeA -- nodeB
nodeA -- nodeC
nodeB -- nodeD
nodeC -- nodeD

nodeA --> degree : 計算連接數
nodeA --> eigen : 計算影響力
nodeB --> between : 計算橋接度
nodeC --> close : 計算距離

note right of degree
  直接連接數量
  衡量局部影響力
end note

note right of eigen
  考慮鄰居重要性
  衡量全局影響力
end note

@enduml

此圖示說明瞭遊戲中球體運動和碰撞偵測的主要流程。

實作計分系統與顯示

在完成遊戲的基本玩法後,我們需要加入計分系統以提升遊戲的完整性。為了實作這一點,我們將建立一個新的系統來追蹤分數並將其顯示在螢幕上。

計分邏輯實作

我們的計分演算法需要滿足以下條件:

  1. 當球接觸到場地的底部邊界時(即球的中心低於地面一個半徑的距離),視為進球。
  2. 檢查球的x座標以確定它落在場地的哪一側。如果球落在右側,左邊的玩家得分,反之亦然。
  3. 將球重新定位到場地的中心,重置球的y軸速度,並反轉球的x軸速度,以模擬發球權的轉換。

程式碼實作

fn scoring(
    mut query: Query<(&mut Ball, &mut Transform)>,
    mut score: ResMut<Score>,
) {
    for (mut ball, mut transform) in query.iter_mut() {
        let ball_x = transform.translation.x;
        let ball_y = transform.translation.y;
        if ball_y < ball.radius {
            // 球接觸到地面
            if ball_x <= ARENA_WIDTH / 2.0 {
                score.right += 1;
                // 改變方向
                ball.velocity.x = ball.velocity.x.abs();
            } else {
                score.left += 1;
                // 改變方向
                ball.velocity.x = -ball.velocity.x.abs();
            }
            // 重置球到場地中心
            transform.translation.x = ARENA_WIDTH / 2.0;
            transform.translation.y = ARENA_HEIGHT / 2.0;
            ball.velocity.y = 0.0; // 重置為自由落體
        }
    }
}

內容解密:

  1. scoring函式會迭代所有帶有BallTransform元件的實體。
  2. 當球的y座標小於其半徑時,表示球已接觸到地面,進而觸發計分邏輯。
  3. 根據球的x座標,判斷是哪位玩家得分,並更新Score資源中的相應分數。
  4. 更新球的位置和速度,以準備下一輪的發球。

顯示計分板

為了在螢幕上顯示分數,我們需要建立一個ScoreBoard元件和一個初始化函式來生成計分板實體。

定義 ScoreBoard 元件

#[derive(Component)]
struct ScoreBoard {
    side: Side
}

初始化計分板

fn initialize_scoreboard(
    commands: &mut Commands,
    asset_server: &Res<AssetServer>,
    side: Side,
    x: f32,
) {
    commands.spawn((
        ScoreBoard { side },
        TextBundle::from_sections([
            TextSection::from_style(TextStyle {
                font_size: SCORE_FONT_SIZE,
                color: Color::WHITE,
                font: asset_server.load("fonts/square.ttf"),
            })])
        .with_style(Style {
            position_type: PositionType::Absolute,
            position: UiRect {
                top: Val::Px(25.0),
                left: Val::Px(x),
                ..default()
            },
            ..default()
        })
        .with_text_alignment(match side {
            Side::Left => TextAlignment::Left,
            Side::Right => TextAlignment::Right,
        }),
    ));
}

內容解密:

  1. initialize_scoreboard函式建立一個帶有ScoreBoard元件和TextBundle的實體,用於顯示分數。
  2. 使用AssetServer載入字型檔案,並設定文字樣式,包括字型大小、顏色和對齊方式。
  3. 設定計分板的絕對位置,使其顯示在螢幕的指定位置。

在 Setup 中初始化計分板

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    // ...
    initialize_scoreboard(
        &mut commands,
        &asset_server,
        Side::Left, ARENA_WIDTH / 2.0 - 25.0
    );
    initialize_scoreboard(
        &mut commands,
        &asset_server,
        Side::Right,
        ARENA_WIDTH / 2.0 + 25.0
    );
}

內容解密:

  1. setup函式中呼叫initialize_scoreboard兩次,分別為左、右兩位玩家建立計分板。
  2. 設定計分板的水平位置,使其位於場地中央的兩側。

透過上述步驟,我們成功地實作了遊戲的計分系統,並將分數顯示在螢幕上。這不僅增強了遊戲的互動性,也提高了玩家的遊戲體驗。