XNA-二維空間物體的移動
相信大家在國中的時候就學過幾何數學了,當時或許覺得有趣,也可能認為不知所云!!
不過現在可能需要稍微回想一下了!
在二維空間中物體的移動會有速度,而速度有方向性,在xna中可以用Vector2記錄,Vector2支援許多向量的運算。
假設有一顆紅色的球往右上角移動,他的速度是V,此速度可以分解成x和y的分量,分別是Vx以及Vy,如下圖:
此時我們可以用Vector2中的X與Y來記錄Vx和Vy。
若我們要讓物體往定點移動,假設由A點到B點速率為v(速率v是純量),速度可以由下面的算式得出:
而當物體在向著目的地移動時,BA向量與速度的內積會是正數(如下圖左上),當物體到達目的地時,BA向量與速度的內積會是零,
當物體超過目的地時,BA向量與速度的內積會是負數,(如下圖右下):
因此我們可以由此關係知道是否已經到達目的地,該停止移動了!
知道以上簡單的幾何數學後,就可以開始設計我們的程式了。
首先設計一個球物件,他是我們的主角,如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace WindowsGame1 {
public class Ball : IUpdateable {
private Texture2D image;
public Texture2D Image {
get { return image; }
}
//速度
private Vector2 velocity;
public Vector2 Velocity {
get { return velocity; }
}
//目前位置
private Vector2 position;
public Vector2 Position {
get { return position; }
set { position = value; }
}
//目標位置
private Vector2 destination;
public Vector2 Destination {
get { return destination; }
}
private bool enabled;
private int updateOrder;
public Ball(Texture2D image) {
this.image = image;
velocity = Vector2.Zero;
position = Vector2.Zero;
enabled = true;
updateOrder = 0;
}
/// <summary>
/// 計算前往目標位置所需要的速度
/// </summary>
/// <param name="destination">目標位置</param>
/// <param name="speed">速率</param>
public void MoveTo(Vector2 destination, float speed) {
this.destination = destination;
Vector2 vector = Vector2.Subtract(destination, Position);
float length = vector.Length();
if (length == 0) {
enabled = false;
} else {
float percent = speed / length;
velocity = Vector2.Multiply(vector, percent);
velocity /= 1000f;
enabled = true;
}
}
/// <summary>
/// 是否已經到達
/// </summary>
/// <returns>到達回傳true,否則回傳false</returns>
private bool isArrive() {
if (Vector2.Dot(Vector2.Subtract(destination, position), velocity) <= 0) {
return true;
} else return false;
}
#region IUpdateable 成員
public bool Enabled {
get { return enabled; }
}
public event EventHandler EnabledChanged;
public void Update(GameTime gameTime) {
if (!enabled) return;
if (isArrive()) {
enabled = false;
} else {
position += velocity * (float)gameTime.ElapsedGameTime.TotalMilliseconds;
}
}
public int UpdateOrder {
get { return updateOrder; }
}
public event EventHandler UpdateOrderChanged;
#endregion
}
}
其中IUpdateable介面是讓物件擁有Update函式,這裡只用到Update和Enabled。
讓我稍微介紹一下MoveTo函式,此函式的第一個參數是要到達的目標位置,第二個參數是速率。
在寫之前,應該先決定「單位」,
一般生活中的速率單位是公尺每秒,但我想螢幕可以用公尺算的人應該不多!
這裡我們就用像素每秒來當速率的單位!表示每一秒物體移動多少像素。
因此MoveTo的內容就是一些簡單的幾何數數學了~先求出我與目標的向量,在乘以縮放比率,
最後除以1000是因為我們輸入的速率是以秒為單位,
但遊戲時間是以毫秒為單位,所以除以一千,這樣算出來的速度就是像素每毫秒。
接著來看看isArrive函式,他會先計算目前位置與目標位置的向量然後和速度向量做內積運算,
若為正表示方向相同,那麼我們離目標越來越近!
若為負,表示方向已經相反,也就是說我們已經超過目的位置了,所以不用再動囉。
最後看看Update函式,他會在每次呼叫時計算目前位置(如果需要移動的話),
時間乘上速度就是距離了,所以我們把目前位置加上距離得到新的位置。
GameTime.ElapsedGameTime.TotalMilliseconds表示遊戲應該呼叫Update的時間,單位是毫秒,為什麼會說「應該」呢?
因為遊戲若是計算來不及,並不會如此準時的呼叫Update,
若想要用正確的時間可以用GameTime.ElapsedGameTime.TotalMilliseconds,就是上次呼叫Update距離這次多久時間,
計算位置要用這兩個時間哪一個比較好呢?我想應該用ElapsedGameTime或許會好一點,
舉例來說,遊戲都使用ElapsedGameTime來當基準時間,若是遊戲的計算來不及,本來應該每16毫秒呼叫一次,現在變成每25毫秒呼叫一次,
但是我們在程式裡還是認為只過了16毫秒,則出現的情形是慢動作!
若使用ElapsedGameTime來當基準時間,則會出現跳格,一般來說慢動作會比跳格爽一點吧!
寫好這顆球,我們來說說主要的game1程式。
首先在Game1的建構子內加上this.IsMouseVisible = true; 表示我們要顯示滑鼠。
然後在Update裡面加上呼叫Ball的Update,程式碼如下:
protected override void Update(GameTime gameTime) {
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
MouseState mouseState = Mouse.GetState();
if (preMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released) {
ball.MoveTo(new Vector2(mouseState.X, mouseState.Y), speed);
}
ball.Update(gameTime);
preMouseState = mouseState;
base.Update(gameTime);
}
這邊的滑鼠狀態的用法,和上一篇XNA取得鍵盤輸入。用法相同,這裡就不多說了!只不過我們取得滑鼠的座標當做球的目的座標。
然後將球畫出來:
protected override void Draw(GameTime gameTime) {
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(ball.Image, ball.Position, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
執行這個程式,一開始球會在左上角,當你滑鼠點畫面時,球會移動到滑鼠點擊的位置,不過這裡會發現幾個比較嚴重的問題,
一是你點畫面外,遊戲竟然也會收到滑鼠訊息讓小球亂跑,我的作法是在Update內加上if (!this.IsActive) return;
表示當遊戲不是焦點的話就不執行,因為你點別的地方一定會讓遊戲失去焦點。
再來的問題就是若將速度設大一點,小球就很容易跑過頭。這也好解決,將Update改成:
public void Update(GameTime gameTime) {
if (!enabled) return;
position += velocity * (float)gameTime.ElapsedGameTime.TotalMilliseconds;
if (isArrive()) {
position = destination;
enabled = false;
}
}
這樣寫和之前的寫法插變就是檢查的時間,之前是會等到下一次的Update才檢查是否到達目的地,這次就馬上檢查,若到達了,就讓目前位置等於目的位置!
範例程式下載:WindowsGame1.rar