在遊戲開發中,準確的物理模擬和計分系統至關重要。本文利用 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;
}
}
內容解密:
- 時間步長的取得:
time.raw_delta_seconds()用於取得兩個系統執行之間的時間差,以秒為單位。這使得運動計算能夠根據真實的時間變化。 - X軸位置更新:
transform.translation.x += ball.velocity.x * time.raw_delta_seconds();直接根據球體的X軸速度和時間步長更新其X軸位置。 - Y軸位置更新:使用
transform.translation.y += (ball.velocity.y + time.raw_delta_seconds() * GRAVITY_ACCELERATION / 2.0) * time.raw_delta_seconds();來計算Y軸的新位置。這裡考慮了重力加速度對速度的影響,並採用了半步長修正來提高計算精確度。 - Y軸速度更新:
ball.velocity.y += time.raw_delta_seconds() * GRAVITY_ACCELERATION;根據重力加速度和時間步長更新球體的Y軸速度。
系統註冊
fn main() {
App::new()
// ... 其他系統和設定
.add_system(move_ball)
// ...
}
內容解密:
move_ball系統被註冊到Bevy應用中,負責處理所有帶有Ball和Transform元件的實體。- 該系統模擬了重力對球體的影響,使其能夠在遊戲場景中自然下落。
改善遊戲物理模擬:使用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;
}
}
內容解密:
time_difference代表每幀的時間間隔,假設遊戲以60 FPS執行。acceleration代表重力加速度,使球體向下加速。ball.velocity += acceleration * time_difference;更新球的速度。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,
) {
// 處理碰撞邏輯
}
}
}
}
內容解密:
bounce函式負責偵測球體是否碰撞到視窗邊緣或玩家。point_in_rect函式用於判斷球體是否在玩家的矩形範圍內。- 當球體碰撞到視窗邊緣時,反轉其對應軸的速度。
- 當球體碰撞到玩家時,除了反轉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此圖示說明瞭遊戲中球體運動和碰撞偵測的主要流程。
實作計分系統與顯示
在完成遊戲的基本玩法後,我們需要加入計分系統以提升遊戲的完整性。為了實作這一點,我們將建立一個新的系統來追蹤分數並將其顯示在螢幕上。
計分邏輯實作
我們的計分演算法需要滿足以下條件:
- 當球接觸到場地的底部邊界時(即球的中心低於地面一個半徑的距離),視為進球。
- 檢查球的x座標以確定它落在場地的哪一側。如果球落在右側,左邊的玩家得分,反之亦然。
- 將球重新定位到場地的中心,重置球的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; // 重置為自由落體
}
}
}
內容解密:
scoring函式會迭代所有帶有Ball和Transform元件的實體。- 當球的y座標小於其半徑時,表示球已接觸到地面,進而觸發計分邏輯。
- 根據球的x座標,判斷是哪位玩家得分,並更新
Score資源中的相應分數。 - 更新球的位置和速度,以準備下一輪的發球。
顯示計分板
為了在螢幕上顯示分數,我們需要建立一個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,
}),
));
}
內容解密:
initialize_scoreboard函式建立一個帶有ScoreBoard元件和TextBundle的實體,用於顯示分數。- 使用
AssetServer載入字型檔案,並設定文字樣式,包括字型大小、顏色和對齊方式。 - 設定計分板的絕對位置,使其顯示在螢幕的指定位置。
在 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
);
}
內容解密:
- 在
setup函式中呼叫initialize_scoreboard兩次,分別為左、右兩位玩家建立計分板。 - 設定計分板的水平位置,使其位於場地中央的兩側。
透過上述步驟,我們成功地實作了遊戲的計分系統,並將分數顯示在螢幕上。這不僅增強了遊戲的互動性,也提高了玩家的遊戲體驗。