XNA-二維空間物體的移動

XNA-二維空間物體的移動

相信大家在國中的時候就學過幾何數學了,當時或許覺得有趣,也可能認為不知所云!!

不過現在可能需要稍微回想一下了!

在二維空間中物體的移動會有速度,而速度有方向性,在xna中可以用Vector2記錄,Vector2支援許多向量的運算。

假設有一顆紅色的球往右上角移動,他的速度是V,此速度可以分解成x和y的分量,分別是Vx以及Vy,如下圖:

球的速度

此時我們可以用Vector2中的X與Y來記錄Vx和Vy。

若我們要讓物體往定點移動,假設由A點到B點速率為v(速率v是純量),速度可以由下面的算式得出:

算式

而當物體在向著目的地移動時,BA向量與速度的內積會是正數(如下圖左上),當物體到達目的地時,BA向量與速度的內積會是零,

當物體超過目的地時,BA向量與速度的內積會是負數,(如下圖右下):

BAV

因此我們可以由此關係知道是否已經到達目的地,該停止移動了!

知道以上簡單的幾何數學後,就可以開始設計我們的程式了。

首先設計一個球物件,他是我們的主角,如下:


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