创建 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

公共属性

其中三个私有成员通过公共属性公开。 ScalePieceColor 属性可使应用程序分别指定块的色阶和颜色。 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() 方法来停止惯性。 然后它会将新的初始速度值分配为当前速度值(针对 sponge 行为进行调整),并将 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 类使用。

  1. 时间戳 – 获取要由操作处理器和惯性处理器使用的一个时间戳值。

  2. X – 获取或设置游戏块的 X 坐标。 设置时,会调整用于命中测试的界限,以及操作处理器的透视位置。

  3. 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

请参见

概念

在 XNA 应用程序中使用操作和惯性

创建 GamePieceCollection 类

创建 Game1 类

其他资源

操作和惯性