建立 GamePiece 類別
GamePiece 類別會封裝執行下列作業所需的全部功能:載入 Microsoft XNA 遊戲片段影像、追蹤滑鼠相對於遊戲片段的狀態、擷取滑鼠、提供操作和慣性處理,以及提供當遊戲片段碰到檢視埠的界限時的彈回功能。
私用成員
GamePiece 類別的頂端宣告了數個私用成員。
#region PrivateMembers
// The sprite batch used for drawing the game piece.
private SpriteBatch spriteBatch;
// The position of the game piece.
private Vector2 position;
// The origin used for rendering the game piece.
// Gets set to be the center of the piece.
private Vector2 origin;
// The texture for the piece.
private Texture2D texture;
// The bounds of the game piece. Used for hit testing.
private Rectangle bounds;
// The rotation of the game piece, in radians.
private float rotation;
// The scale, in percentage of the actual image size. 1.0 = 100%.
private float scale;
// The view port, used to detect when to bounce.
private Viewport viewport;
// The manipulation processor for this game piece.
private ManipulationProcessor2D manipulationProcessor;
// The inertia processor for this game piece.
private InertiaProcessor2D inertiaProcessor;
// Flag to indicate that inertia processing should start or continue.
private bool processInertia;
// Flag to indicate whether this piece has captured the mouse.
private bool isMouseCaptured;
// Used during manipulation to indicate where the drag is occurring.
private System.Windows.Point dragPoint;
// The color of the game piece.
private Color pieceColor;
// Represents how spongy the walls act when the piece bounces.
// Must be <= 1.0 (if greater than 1.0, the piece will accelerate on bounce)
// 1.0 = no slowdown during a bounce.
// 0.0 (or less) = won't bounce.
private float spongeFactor = 0.925f;
#endregion
公用屬性
私用成員中有三個是透過公用屬性公開。 Scale 和 PieceColor 屬性分別讓應用程式得以指定遊戲片段的縮放大小和色彩。 Bounds 屬性之所以公開,則是為了讓一個遊戲片段得以利用另一個遊戲片段的界限來呈現自己,例如當某個遊戲片段應該覆疊在另一個遊戲片段上時。 下列程式碼會顯示這些公用屬性的宣告。
#region PublicProperties
public float Scale
{
get { return scale; }
set
{
scale = value;
bounds.Width = (int)(texture.Width * value);
bounds.Height = (int)(texture.Height * value);
// Setting X and Y (private properties) causes
// bounds.X and bounds.Y to adjust to the scale factor.
X = X;
Y = Y;
}
}
public Color PieceColor
{
get { return pieceColor; }
set { pieceColor = value; }
}
public Rectangle Bounds
{
get { return bounds; }
}
#endregion
類別建構函式
GamePiece 類別的建構函式接受下列參數:
SpriteBatch 型別。 這裡傳遞的參考已指派給私用成員 spriteBatch,並且會在遊戲片段呈現時用於存取 SpriteBatch.Draw 方法。 此外,GraphicsDevice 屬性會用於建立與遊戲片段相關聯的 Texture 物件,以及取得檢視埠的大小,以便讓遊戲片段在碰到視窗界限時能夠彈回。
字串,指定要讓遊戲片段使用的影像檔案名稱。
建構函式也會建立 ManipulationProcessor2D 物件和 InertiaProcessor2D 物件,並且建立其事件的事件處理常式。
下列程式碼會顯示 GamePiece 類別的建構函式。
#region Constructor
public GamePiece(SpriteBatch spriteBatch, string fileName)
{
// For brevity, omitting checking of null parameters.
this.spriteBatch = spriteBatch;
// Get the texture from the specified file.
texture = Texture2D.FromFile(spriteBatch.GraphicsDevice, fileName);
// Initial position set to 0,0.
position = new Vector2(0);
// Set the origin to be the center of the texture.
origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
// Set bounds. bounds.X and bounds.Y are set as the position or scale changes.
bounds = new Rectangle(0, 0, texture.Width, texture.Height);
// Create manipulation processor.
Manipulations2D enabledManipulations =
Manipulations2D.Translate | Manipulations2D.Rotate;
manipulationProcessor = new ManipulationProcessor2D(enabledManipulations);
manipulationProcessor.Pivot = new ManipulationPivot2D();
manipulationProcessor.Pivot.Radius = texture.Width / 2;
manipulationProcessor.MinimumScaleRotateRadius = 10.0f;
manipulationProcessor.Started += OnManipulationStarted;
manipulationProcessor.Delta += OnManipulationDelta;
manipulationProcessor.Completed += OnManipulationCompleted;
// Create inertia processor.
inertiaProcessor = new InertiaProcessor2D();
inertiaProcessor.Delta += OnInertiaDelta;
inertiaProcessor.Completed += OnInertiaCompleted;
inertiaProcessor.TranslationBehavior.DesiredDeceleration = 0.0001F;
inertiaProcessor.RotationBehavior.DesiredDeceleration = 1e-6F;
inertiaProcessor.ExpansionBehavior.DesiredDeceleration = 0.0001F;
// Save the view port. Used to detect when the piece needs to bounce.
viewport = spriteBatch.GraphicsDevice.Viewport;
// Set the piece in a random location.
Random random = new Random((int)Timestamp);
X = random.Next(viewport.Width);
Y = random.Next(viewport.Height);
// Set a random orientation.
rotation = (float)(random.NextDouble() * Math.PI * 2.0);
dragPoint = new System.Windows.Point(double.NaN, double.NaN);
pieceColor = Color.White;
// Set scale to normal (100%)
Scale = 1.0f;
}
#endregion
擷取滑鼠輸入
UpdateFromMouse 方法負責偵測當滑鼠在遊戲片段界限內時的滑鼠按鈕按下動作,以及滑鼠按鈕放開動作。
按下滑鼠左鍵 (當滑鼠在遊戲片段界限內) 時,這個方法會設定旗標以表示這個遊戲片段已經擷取到滑鼠,然後開始進行操作處理。
操作處理開始時,會先建立 Manipulator2D 物件的陣列,並將這些物件傳遞給 ManipulationProcessor2D 物件。 這會讓操作處理器評估操作工具 (在這個案例中是單一操作工具) 並引發操作事件。
此外,還會儲存拖曳發生點。 稍後會在 Delta 事件期間使用這個點來調整平移差異值,讓遊戲片段擺盪到拖曳點後的那條線上。
最後,這個方法會傳回滑鼠擷取狀態。 這讓 GamePieceCollection 物件得以在有多個遊戲片段的情況下管理擷取動作。
下列程式碼範例會顯示 UpdateFromMouse 方法。
#region UpdateFromMouse
public bool UpdateFromMouse(MouseState mouseState)
{
if (mouseState.LeftButton == ButtonState.Released)
{
if (isMouseCaptured)
{
manipulationProcessor.CompleteManipulation(Timestamp);
}
isMouseCaptured = false;
}
if (isMouseCaptured ||
(mouseState.LeftButton == ButtonState.Pressed &&
bounds.Contains(mouseState.X, mouseState.Y)))
{
isMouseCaptured = true;
Manipulator2D[] manipulators = new Manipulator2D[]
{
new Manipulator2D(0, mouseState.X, mouseState.Y)
};
dragPoint.X = mouseState.X;
dragPoint.Y = mouseState.Y;
manipulationProcessor.ProcessManipulators(Timestamp, manipulators);
}
// If the right button is pressed, stop the piece and move it to the center.
if (mouseState.RightButton == ButtonState.Pressed)
{
processInertia = false;
X = viewport.Width / 2;
Y = viewport.Height / 2;
rotation = 0;
}
return isMouseCaptured;
}
#endregion
處理操作
開始操作時,會引發 Started 事件。 這個事件的處理常式會停止正在進行的慣性處理,並將 processInertia 旗標設定為 false。
#region OnManipulationStarted
private void OnManipulationStarted(object sender, Manipulation2DStartedEventArgs e)
{
if (inertiaProcessor.IsRunning)
{
inertiaProcessor.Complete(Timestamp);
}
processInertia = false;
}
#endregion
與操作相關聯的值變更時,會引發 Delta 事件。 這個事件的處理常式會使用事件引數中傳遞的差異值來變更遊戲片段的位置和旋轉值。
#region OnManipulationDelta
private void OnManipulationDelta(object sender, Manipulation2DDeltaEventArgs e)
{
//// Adjust the position and rotation of the game piece.
float deltaX = e.Delta.TranslationX;
float deltaY = e.Delta.TranslationY;
if (dragPoint.X != double.NaN || dragPoint.Y != double.NaN)
{
// Single-manipulator-drag-rotate mode. Adjust for drag / rotation
System.Windows.Point center = new System.Windows.Point(position.X, position.Y);
System.Windows.Vector toCenter = center - dragPoint;
double sin = Math.Sin(e.Delta.Rotation);
double cos = Math.Cos(e.Delta.Rotation);
System.Windows.Vector rotatedToCenter =
new System.Windows.Vector(
toCenter.X * cos - toCenter.Y * sin,
toCenter.X * sin + toCenter.Y * cos);
System.Windows.Vector shift = rotatedToCenter - toCenter;
deltaX += (float)shift.X;
deltaY += (float)shift.Y;
}
X += deltaX;
Y += deltaY;
rotation += e.Delta.Rotation;
}
#endregion
當與某個操作相關聯的所有操作工具 (在這個案例中是單一操作工具) 都移除時,操作處理器會引發 Completed 事件。 這個事件的處理常式會經由將慣性處理器的初始速度設定為事件引數所報告的速度,並將 processInertia 旗標設定為 true,來開始進行慣性處理。
#region OnManipulationCompleted
private void OnManipulationCompleted(object sender, Manipulation2DCompletedEventArgs e)
{
inertiaProcessor.TranslationBehavior.InitialVelocityX = e.Velocities.LinearVelocityX;
inertiaProcessor.TranslationBehavior.InitialVelocityY = e.Velocities.LinearVelocityY;
inertiaProcessor.RotationBehavior.InitialVelocity = e.Velocities.AngularVelocity;
processInertia = true;
}
#endregion
處理慣性
當慣性處理外插補新的角度、線性速度、位置 (平移) 座標和旋轉等值時,會引發 Delta 事件。 這個事件的處理常式會使用事件引數中傳遞的差異值來修改遊戲片段的位置和旋轉值。
如果新的座標導致遊戲片段移至超出檢視埠界限的位置,即會反轉慣性處理的速度。 這會讓遊戲片段在碰到檢視埠界限時反彈。
當 InertiaProcessor2D 物件正在執行外插補時,無法變更該物件的屬性。 因此,事件處理常式在反轉 X 或 Y 速度時,會先呼叫 Complete() 方法來停止慣性。 然後便會指派新的慣性速度做為目前的速度值 (已調整為展現海綿般的效果),並將 processInertia 旗標設定為 true。
下列程式碼會顯示 Delta 事件的事件處理常式。
#region OnInertiaDelta
private void OnInertiaDelta(object sender, Manipulation2DDeltaEventArgs e)
{
// Adjust the position of the game piece.
X += e.Delta.TranslationX;
Y += e.Delta.TranslationY;
rotation += e.Delta.Rotation;
// Check to see if the piece has hit the edge of the view port.
bool reverseX = false;
bool reverseY = false;
if (X > viewport.Width)
{
reverseX = true;
X = viewport.Width;
}
else if (X < viewport.X)
{
reverseX = true;
X = viewport.X;
}
if (Y > viewport.Height)
{
reverseY = true;
Y = viewport.Height;
}
else if (Y < viewport.Y)
{
reverseY = true;
Y = viewport.Y;
}
if (reverseX || reverseY)
{
// Get the current velocities, reversing as needed.
// If reversing, apply sponge factor to slow the piece slightly.
float velocityX = e.Velocities.LinearVelocityX * ((reverseX) ? -spongeFactor : 1.0f);
float velocityY = e.Velocities.LinearVelocityY * ((reverseY) ? -spongeFactor : 1.0f);
// Must stop inertia processing before changing parameters.
if (inertiaProcessor.IsRunning)
{
inertiaProcessor.Complete(Timestamp);
}
// Assign the new velocities.
inertiaProcessor.TranslationBehavior.InitialVelocityX = velocityX;
inertiaProcessor.TranslationBehavior.InitialVelocityY = velocityY;
// Set flag so that inertia processing will continue.
processInertia = true;
}
}
#endregion
慣性處理完成時,慣性處理器會引發 Completed 事件。 這個事件的處理常式會將 processInertia 旗標設定為 false。
#region OnInertiaCompleted
private void OnInertiaCompleted(object sender, Manipulation2DCompletedEventArgs e)
{
processInertia = false;
}
#endregion
目前為止所介紹的邏輯中,都不會真的引發外插補動作。 這項作業是在 ProcessInertia 方法中完成。 這個由遊戲更新迴圈 (Game.Update 方法) 重複呼叫的方法,會檢查 processInertia 旗標是否已設定為 true,如果是的話,就會呼叫 Process() 方法。 呼叫這個方法會引發外插補動作,並且引發 Delta 事件。
#region ProcessInertia
public void ProcessInertia()
{
if (processInertia)
{
inertiaProcessor.Process(Timestamp);
}
}
#endregion
遊戲片段必須直到呼叫 Draw 方法多載時,才會真的呈現。 這個方法的第一個多載會由遊戲繪製迴圈 (Game.Draw 方法) 重複呼叫。 這樣遊戲片段就會以目前的位置、旋轉和縮放比例呈現。
#region Draw
public void Draw()
{
spriteBatch.Draw(
texture, position,
null, pieceColor, rotation,
origin, scale,
SpriteEffects.None, 1.0f);
}
public void Draw(Rectangle bounds)
{
spriteBatch.Draw(texture, bounds, pieceColor);
}
#endregion
其他屬性
GamePiece 類別會使用三個私用屬性。
Timestamp - 取得由操作處理器和慣性處理器使用的時間戳記值。
X - 取得或設定遊戲片段的 X 座標。 設定時,會調整用於點擊測試的界限,並調整操作處理器的樞紐位置。
Y - 取得或設定遊戲片段的 Y 座標。 設定時,會調整用於點擊測試的界限,並調整操作處理器的樞紐位置。
#region PrivateProperties
private long Timestamp
{
get
{
// Get timestamp in 100-nanosecond units.
double nanosecondsPerTick = 1000000000.0 / System.Diagnostics.Stopwatch.Frequency;
return (long)(System.Diagnostics.Stopwatch.GetTimestamp() / nanosecondsPerTick / 100.0);
}
}
private float X
{
get { return position.X; }
set
{
position.X = value;
manipulationProcessor.Pivot.X = value;
bounds.X = (int)(position.X - (origin.X * scale));
}
}
private float Y
{
get { return position.Y; }
set
{
position.Y = value;
manipulationProcessor.Pivot.Y = value;
bounds.Y = (int)(position.Y - (origin.Y * scale));
}
}
#endregion