DirectX棋盘游戏引擎(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/directx-board-game-engine-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 17 分钟阅读 - 8234 个词 阅读量 0DirectX棋盘游戏引擎(译文)
原文地址:https://www.codeproject.com/Articles/21337/DirectX-Board-Game-Engine
原文作者:Fredrik Bornander
译文由本站 robot-v1.0 翻译
前言
An article on how to create a generic engine for board games such as Checkers or Chess
有关如何为棋盘游戏或国际象棋等棋盘游戏创建通用引擎的文章
介绍(Introduction)
本文试图描述一种创建一组通用类的方法,这些类足以允许轻松实现几种棋盘游戏.只有实际的游戏逻辑(游戏规则)才需要在实现之间进行更改.(This article attempts to describe a way of creating a set of classes that are generic enough to allow several kinds of board games to be easily implemented. Only the actual game logic (the rules of the game) should have to change between implementations.)
更新(Updated)
本文已更新为两件事:(This article have been updated with two things:)
- API的常规清理,包括常规(General cleanup of the API, including a generic)
System.Windows.Forms.Form
实施可以进一步减少实施时间(implementation that further reduces implementation time) - 连接四的实现(An implementation of Connect Four) 本文将进一步讨论这两个添加项.(Both of these additions are discussed further in this article.)
要求(Requirements)
开始这个项目时,我满足了完成的代码需要满足的一组要求:(When starting this project, I settled for a set of requirements that the finished code needed to fulfill:)
- 该实现必须足够通用,以允许在不修改棋盘游戏实现的情况下创建Checkers和Chess的实现.(The implementation must be generic enough to allow an implementation of Checkers and an implementation of Chess to be created without modifying the board game implementation.)
- 使用DirectX必须以3D形式呈现游戏的视觉效果.(Visual representation of the game must be in 3D using DirectX.)
- 实施新的棋盘游戏时,实施人员不需要具有3D数学或Direct3D的任何知识.(When implementing a new board game, the person implementing does not need to have any knowledge of 3D mathematics or Direct3D.)
- 实施不需要"游戏循环",因为对于不习惯实施游戏的人来说,这通常是一个陌生的概念.(The implementation should not require a “game loop,” as this is often an unfamiliar concept to persons not used to implementing games.)
- 一种(A)
System.Window.Forms.Panel
应该使用组件来渲染游戏,以便可以像放置其他任何视觉组件一样将其放置在窗体上.(component should be used to render the game so that it can be placed onto a form like any other visual component.)
使用代码(Using the Code)
Visual Studio解决方案由两个项目组成:(The Visual Studio solution is made up of two projects:)
- 一个类库,其中包含适用于任何棋盘游戏的通用代码(我将其称为"框架")(A class library containing the generic code that should be applicable to any board game (I will refer to this as “the Framework”))
- Windows应用程序,通过Checkers的实现演示棋盘游戏库(A Windows application demonstrating the board game library using an implementation of Checkers)
游戏逻辑(在此示例中为Checkers实现)必须实现一个名为(The game logic (in this example, the Checkers implementation) must implement an interface called)
IBoardGameLogic
,其定义为:(, which is defined as:)
using System;
using System.Collections.Generic;
namespace Bornander.Games.BoardGame
{
public interface IBoardGameLogic
{
/// <summary>
/// Number of rows on the board.
/// </summary>
int Rows
{
get;
}
/// <summary>
/// Number of columns on the board.
/// </summary>
int Columns
{
get;
}
/// <summary>
/// Returns the state (the piece) at a specific row and column.
/// </summary>
/// The row on the board (0 based).
/// The column on the board (0 based).
/// <returns>A value indicating the
/// piece that occupies this row and column.
/// For empty squares zero should be returned.</returns />
int this[int row, int column]
{
get;
set;
}
/// <summary>
/// This method returns the same as int this[int row, int column].
/// </summary>
int this[Square square]
{
get;
set;
}
/// <summary>
/// Return the currently available moves.
/// </summary>
List<move /> GetAvailableMoves();
/// <summary>
/// Initializes the game to its start state.
/// </summary>
void Initialize();
/// <summary>
/// Used to determine if the game is over.
/// </summary>
/// A string describing the game
/// over state if the game is over, example "Player one lost!".
/// <returns>True if the game is over.</returns>
bool IsGameOver(out string gameOverStatement);
/// <summary>
/// Moves a piece on the board.
/// </summary>
/// From square.
/// To square.
/// <returns>The available moves after
/// the move was made.</returns>
List<move> Move(Square square, Square allowedSquare);
}
}
通过公开这几种方法,框架可以控制游戏流程并确保遵循游戏规则.但是,缺少一件事:框架无法确定要显示的内容.它可以推断出每个方块的状态,但是它没有有关应呈现给屏幕以可视化该状态的信息.通过为框架提供另一个接口的实例(称为(By exposing these few methods, the Framework can control the game flow and make sure that the rules of the game are followed. There is one thing missing, though: there is no way for the Framework to figure out what to display. It can deduce a state of each square, but it has no information about what should be rendered to the screen to visualize that state. This is solved by providing the Framework with an instance of another interface, called) IBordGameModelRepository
:(:)
using System;
using Bornander.Games.Direct3D;
namespace Bornander.Games.BoardGame
{
public interface IBoardGameModelRepository
{
/// <summary>
/// Initializes the Models.
/// </summary>
void Initialize(Microsoft.DirectX.Direct3D.Device device);
/// <summary>
/// Returns the visual representation for the board square
/// at the location given by the Square.
/// This is for example either a black box or a white box for
/// a chess implementation.
/// </summary>
Model GetBoardSquareModel(Square square);
/// <summary>
/// Returns the visual representation for a specific id.
/// This is for example the 3D model of a pawn in a chess
/// implementation.
/// </summary>
Model GetBoardPieceModel(int pieceId);
}
}
通过保留接口(By keeping the interfaces) IBoardGameLogic
和(and) IBoardGameModelRepository
单独地,我们允许将游戏逻辑与视觉表示完全分离.这很重要,因为我们可能想将此游戏移植到Windows Mobile设备上,例如在2D表示中要优先于3D表示.(separate, we allow the game logic to be completely decoupled from the visual representation. This is important because we might want to port this game to a Windows Mobile device, for example, where a 2D representation is preferred over a 3D one.)
现在,框架已经可以访问渲染游戏状态所需的所有信息,现在该考虑渲染实现了.几乎所有游戏渲染都由(Now that the Framework has access to all of the information it needs for rendering the state of the game, it is time to consider the render implementation. Almost all game rendering is handled by) VisualBoard
.此类查询(. This class queries) IBoardGameLogic
并使用返回的信息以及(and uses the information returned, together with the) Model
由传回的(s returned by) IBoardGameModelRepository
,同时渲染木板和碎片.(, to render both the board and the pieces.)
有一个元素没有被呈现(There is one element that is not rendered by) VisualBoard
就是当前选中的作品,即用户当前正在移动的作品.另一类叫做(and that is the currently selected piece, i.e., the piece the user is currently moving around. Another class called) GamePanel
,扩展了(, which extends) System.Windows.Forms.Panel
,处理输入以及在板上选择和移动棋子.这种实施方式似乎降低了内部的凝聚力(, handles input as well as selecting and moving pieces around on the board. This type of implementation might seem to lower the inner cohesion of the) GamePanel
课,但是我决定这样做,因为我想要(class, but I decided to do it this way because I want) VisualBoard
渲染棋盘游戏的状态.该状态对当前正在移动的物件一无所知.(to render the state of the board game. That state does not know anything about a piece currently being moved.)
课程概述(Classes Overview)
棋盘游戏类库(Board Game Class Library)
这些是类库中的类:(These are the classes in the class library:)
GamePanel
延伸(extends)System.Windows.Forms.Panel
并处理用户输入,DirectX设置和一些渲染(and handles user input, DirectX setup and some rendering)VisualBoard
负责渲染(is responsible for rendering)IBoardGameLogic
IBoardGameLogic
,一个界面,代表一个棋盘游戏的规则(, an interface, represents the rules of a board game)IBoardGameModelRepository
,一个界面,代表用于渲染的3D模型库(, an interface, represents a repository of 3D models used to render)IBoardGameLogic
Move
代表源自特定位置的所有可能动作(represents all possible moves originating from a specific)Square
Square
通过保存行和列信息来表示板正方形(represents a board square by holding row and column information)Camera
表示在3D渲染中使用的相机(represents a camera that is used when rendering in 3D)Model
组一个(groups a)Mesh
, 一种(, a)Material
和位置和方向一起方便使用(and a position and orientation together for convenience)
跳棋应用(Checkers Application)
这些是Checkers应用程序中的类:(These are the classes in the Checkers application:)
CheckersModelRepository
实施(implements)IBoardGameModelRepository
并负责返回与渲染Checkers板相关的模型(and is responsible for returning models related to rendering the Checkers board)CheckersLogic
是Checkers游戏逻辑的实现;它实现(is the Checkers game logic implementation; it implements)CheckersLogic
渲染游戏状态(Rendering the Game State)
渲染游戏状态非常简单:在所有棋盘方块上循环,渲染方块,然后渲染占据该方块的任何棋子.简单.但是,我们还需要向用户指示哪些动作有效.这是通过突出显示鼠标下方的木板正方形来实现的(如果可以从该正方形移动(或在"握住"一块时移至该正方形)是有效的).(Rendering the state of the game is pretty straightforward: loop over all board squares, render the square and then render any piece occupying that square. Simple. However, we also need to indicate to the user which moves are valid. This is done by highlighting the board square under the mouse if it is valid to move from that square (or to that square when “holding” a piece).)
值得一提的是,框架假设正方形的宽度为1个单位,深度为1个单位(高度由游戏开发人员决定).在创建游戏网格时必须考虑到这一点.为了解决这个问题,(It is important to mention that the Framework makes the assumption that the squares are 1 unit wide and 1 unit deep (height is up to the game developer to decide). This must be taken into account when creating the meshes for the game. To help out with this, the) Model
该类包含两种材料:一种是"正常"材料,另一种是"突出显示"材料.(class holds two materials: one “normal” material and one “highlighted” material.)
通过检查模型是否处于"(By checking whether the model is in state “) Selected
“,它会在渲染之前将其材质设置为正常或突出显示,如下所示:(,” it sets its material to either normal or highlighted just prior to rendering, like this:)
/// <summary>
/// Array containing two materials, at 0 the normal material
/// and at 1 the highlighted or selected material.
/// </summary>
class Model
{
private Material[] material;
...
public void Render(Device device)
{
device.Transform.World = this.World;
device.Material = material[selected ? 1 : 0];
mesh.DrawSubset(0);
}
}
VisualBoard
只需为每个板正方形模型设置选定状态,然后再将其渲染为(simply sets the selected state for each board square model before rendering it in its) Render
方法:(method:)
public class VisualBoard
{
...
public void Render(Device device)
{
for (int row = 0; row < gameLogic.Rows; ++row)
{
for (int column = 0; column < gameLogic.Columns; ++column)
{
Square currentSquare = new Square(row, column);
Model boardSquare =
boardGameModelRepository.GetBoardSquareModel
(currentSquare);
boardSquare.Position =
new Vector3((float)column, 0.0f, (float)row);
boardSquare.Selected = currentSquare.Equals(selectedSquare);
boardSquare.Render(device);
// Check that the current piece isn't grabbed by the mouse,
// because in that case we don't render it.
if (!currentPieceOrigin.Equals(currentSquare))
{
// Check which kind of model we need to render,
// move our "template" to the
// right position and render it there.
Model pieceModel =
boardGameModelRepository.GetBoardPieceModel
(gameLogic[currentSquare]);
if (pieceModel != null)
{
pieceModel.Position = new Vector3((float)column,
0.0f, (float)row);
pieceModel.Render(device);
}
}
}
}
}
}
找出实际选择的正方形是找到鼠标"下"的正方形的问题.在2D中,这是一个非常简单的操作,但在3D中会变得稍微复杂一些.我们需要获取屏幕上的鼠标坐标,然后使用"投影"和"视图"矩阵将屏幕坐标取消投影为3D坐标.然后,当我们将鼠标位置设为3D位置时,我们可以向所有板子正方形投射光线(Figuring out which square is actually selected is a matter of finding which square is “under” the mouse. In 2D, this is a really simple operation, but it gets slightly more complicated in 3D. We need to grab the mouse coordinates on the screen and, using the Projection and View matrices, un-project the screen coordinates to 3D coordinates. Then, when we have our mouse position as a 3D position, we can cast a ray towards all our board square) Model
看看我们是否有交叉路口.对于不熟悉3D数学的人来说,这样做的代码可能很难理解.这就是框架必须为我们处理的原因,这样我们(实施棋盘游戏的家伙)不必担心此类事情.一个函数在(s to see if we get an intersection. The code for this can be difficult to understand for someone not used to 3D mathematics. This is why the Framework must take care of it for us so that we (the guy or gal implementing a board game) don’t have to worry about such things. A function handles all this in the) VisualBoard
类:(class:)
/// <summary>
/// This method allows to check for "MouseOver" on Models in 3D space.
/// </summary>
public bool GetMouseOverBlockModel(Device device, int mouseX, int mouseY,
out Square square, List<square /> highlightIfHit)
{
selectedSquare = Square.Negative;
square = new Square();
bool foundMatch = false;
float closestMatch = int.MaxValue;
for (int row = 0; row < gameLogic.Rows; ++row)
{
for (int column = 0; column < gameLogic.Columns; ++column)
{
Square currentSquare = new Square(row, column);
Model boardSquare =
boardGameModelRepository.GetBoardSquareModel(currentSquare);
boardSquare.Position =
new Vector3((float)column, 0.0f, (float)row);
Vector3 near = new Vector3(mouseX, mouseY, 0.0f);
Vector3 far = new Vector3(mouseX, mouseY, 1.0f);
// Unproject a vector from the screen X,Y space into
// 3D space using the World matrix of the mesh we are checking.
near.Unproject(device.Viewport, device.Transform.Projection,
device.Transform.View, boardSquare.World);
far.Unproject(device.Viewport, device.Transform.Projection,
device.Transform.View, boardSquare.World);
far.Subtract(near);
// Find the closes match of all blocks, that is the one
// the mouse is over.
IntersectInformation closestIntersection;
if (boardSquare.Mesh.Intersect(near, far,
out closestIntersection)
&& closestIntersection.Dist < closestMatch)
{
closestMatch = closestIntersection.Dist;
square = new Square(row, column);
// If a list of squares is passed in we are over
// one of those squares we highlight that
// this is used to indicated valid moves to the user
if (highlightIfHit != null)
{
foreach (Square highlightSquare in highlightIfHit)
{
if (highlightSquare.Equals(square))
{
selectedSquare = new Square(row, column);
}
}
}
foundMatch = true;
}
}
}
return foundMatch;
}
处理输入(Handling Input)
显然,必须有一种在板上移动零件的方法.我认为最直观的方法是使用鼠标左键抓取并拖动片段.这实际上是简单棋盘游戏所需的全部输入,但是我还想允许用户从不同角度查看棋盘.这意味着将相机放置在不同的位置.我决定应该使用鼠标右键并拖动它,并且滚动鼠标滚轮应该放大和缩小.这意味着我必须处理(Obviously, there has to be a way of moving pieces around on the board. I decided that the most intuitive way of doing this is to grab and drag pieces using the left mouse button. That is really all the input a simple board game needs, but I also wanted to allow the user to view the board from different angles. This means positioning the camera at different places. I decided that using the right mouse button and dragging should be used for this, and that scrolling the mouse wheel should zoom in and out. This means I have to handle) MouseDown
,(,) MouseUp
,(,) MouseMove
和(and) MouseWheel
在里面(in the) GamePanel
类:(class:)
public void HandleMouseWheel(object sender, MouseEventArgs e)
{
// If the user scrolls the mouse wheel we zoom out or in
cameraDistanceFactor = Math.Max(0.0f, cameraDistanceFactor +
Math.Sign(e.Delta) / 5.0f);
SetCameraPosition();
Render();
}
private void GamePanel_MouseMove(object sender, MouseEventArgs e)
{
// Dragging using the right mousebutton moves the camera
// along the X and Y axis.
if (e.Button == MouseButtons.Right)
{
cameraAngle += (e.X - previousPoint.X) / 100.0f;
cameraElevation = Math.Max(0, cameraElevation +
(e.Y - previousPoint.Y) / 10.0f);
SetCameraPosition();
previousPoint = e.Location;
}
Square square;
if (e.Button == MouseButtons.Left)
{
if (ponderedMove != null)
{
if (board.GetMouseOverBlockModel
(device, e.X, e.Y, out square, ponderedMove.Destinations))
{
// Set the dragged pieces location to the current square
selectedPiecePosition.X = square.Column;
selectedPiecePosition.Z = square.Row;
}
}
}
else
{
board.GetMouseOverBlockModel(device, e.X, e.Y, out square,
GamePanel.GetSquaresFromMoves(availableMoves));
}
// Render since we might have moved the camera
Render();
}
private void GamePanel_MouseDown(object sender, MouseEventArgs e)
{
// The previous point has to be set here or the distance dragged
// can be too big.
previousPoint = e.Location;
// If the mouse is over a block (see GetMouseOverBlockModel)
// for details on how determining that
// and the left button is down, try to grab the piece
// (if there is one at the square and it has valid moves).
if (e.Button == MouseButtons.Left)
{
ponderedMove = null;
Square square;
if (board.GetMouseOverBlockModel(device, e.X, e.Y, out square, null))
{
foreach (Move move in availableMoves)
{
// We have a move and it is started
// from the square we're over, start dragging a piece
if (square.Equals(move.Origin))
{
selectedPieceModel = board.PickUpPiece(square);
selectedPiecePosition =
new Vector3(square.Column, 1.0f, square.Row);
ponderedMove = move;
break;
}
}
}
}
Render();
}
private void GamePanel_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Square square;
if (board.GetMouseOverBlockModel(device, e.X, e.Y, out square, null))
{
// ponderedMove keeps track of the current potential move
// that will take place
// if we drop the piece onto a valid square, if ponderedMove
// is not null that means
// we're currently dragging a piece.
if (ponderedMove != null)
{
foreach (Square allowedSquare in ponderedMove.Destinations)
{
// Was it drop on a square that's a legal move?
if (square.Equals(allowedSquare))
{
// Move the piece to the target square
availableMoves = gameLogic.Move
(ponderedMove.Origin, allowedSquare);
break;
}
}
}
}
board.DropPiece();
selectedPieceModel = null;
Render();
CheckForGameOver();
}
}
件控制(Piece Control)
鼠标方法检查是否由于鼠标左键按下而调用它们.如果是,则使用(The mouse methods check if they’re called as a result of a left mouse button press. If they are, they use the) VisualBoard.GetMouseOverBlockModel
确定光标在特定正方形上方时是否发生事件的方法.然后,这用于确定是否允许用户从当前方块中拾取一件物品或将其放到当前方块上.也,(method to determine whether the event occurred when the cursor was over a specific square. This is then used to figure out if the user is allowed to pick up a piece from or drop a piece onto the current square. Also,) VisualBoard.GetMouseOverBlockModel
内部自动处理方形突出显示.(internally handles square highlighting automatically.)
相机控制(Camera Control)
如果在拖动鼠标时按下鼠标右键,则可以找出两次更新之间的差异,并使用该信息来更新两个成员.滚动鼠标滚轮时,将更新第三个成员:(If the right mouse button is down when dragging the mouse, I can figure out the delta between two updates and use that information to update two members. A third member is updated when the mouse wheel is scrolled:)
private float cameraAngle = -((float)Math.PI / 2.0f);
private float cameraElevation = 7.0f;
private float cameraDistanceFactor = 1.5f;
另一种方法(Another method in) GamePanel
然后使用该信息计算摄像机的位置.此位置被限制为围绕木板的一个圆(缩放时会调整半径),并且摄像机也沿Y轴被限制为永远不会低于零:(then uses that information to calculate a position for the camera. This position is constrained to a circle around the board (the radius is adjusted when zooming) and the camera is also constrained along the Y-axis to never go below zero:)
private void SetCameraPosition()
{
// Calculate a camera position, this is a radius from the center
// of the board and then cameraElevation up.
float cameraX = gameLogic.Columns /
2.0f + (cameraDistanceFactor * gameLogic.Columns *
(float)Math.Cos(cameraAngle));
float cameraZ = gameLogic.Rows /
2.0f + (cameraDistanceFactor * gameLogic.Rows *
(float)Math.Sin(cameraAngle));
camera.Position = new Vector3(
cameraX, cameraElevation, cameraZ);
}
创建模型(Creating Models)
班级(The class) CheckersModelRepository
用于创建用于渲染(is used to create all of the models used to render the) Checkers
游戏.它实现(game. It implements) IBoardGameModelRepository
这样框架就可以使用一种通用的方式来访问模型(so that the Framework has a generic way of accessing the models using the) IBoardGameLogic
数据.(data.)
class CheckersModelRepository
{
...
public void Initialize(Microsoft.DirectX.Direct3D.Device device)
{
// Create a box to be used as a board square
// The .Clone call is used to get a Mesh that has vertices that
// contain both position, normal and color which I need to render
// them using flat shading.
Mesh blockMesh = Mesh.Box(device, 1.0f, 0.5f, 1.0f).Clone
(MeshFlags.Managed, VertexFormats.PositionNormal |
VertexFormats.Specular, device);
// Create some red and black material and their
// highlighted counterparts.
Material redMaterial = new Material();
redMaterial.Ambient = Color.Red;
redMaterial.Diffuse = Color.Red;
Material highlightedRedMaterial = new Material();
highlightedRedMaterial.Ambient = Color.LightSalmon;
highlightedRedMaterial.Diffuse = Color.LightSalmon;
Material squareBlackMaterial = new Material();
Color squareBlack = Color.FromArgb(0xFF, 0x30, 0x30, 0x30);
squareBlackMaterial.Ambient = squareBlack;
squareBlackMaterial.Diffuse = squareBlack;
Material blackMaterial = new Material();
blackMaterial.Ambient = Color.Black;
blackMaterial.Diffuse = Color.Black;
Material highlightedBlackMaterial = new Material();
highlightedBlackMaterial.Ambient = Color.DarkGray;
highlightedBlackMaterial.Diffuse = Color.DarkGray;
Material[] reds = new Material[]
{ redMaterial, highlightedRedMaterial };
Material[] blacks = new Material[]
{ blackMaterial, highlightedBlackMaterial };
blackSquare = new Model(blockMesh, new Material[]
{ squareBlackMaterial, highlightedBlackMaterial });
redSquare = new Model(blockMesh, reds);
blackSquare.PositionOffset = new Vector3(0.0f, -0.25f, 0.0f);
redSquare.PositionOffset = new Vector3(0.0f, -0.25f, 0.0f);
// Create meshes for the pieces.
Mesh pieceMesh = Mesh.Cylinder(device, 0.4f, 0.4f, 0.2f, 32, 1).Clone
(MeshFlags.Managed, VertexFormats.PositionNormal |
VertexFormats.Specular, device);
Mesh kingPieceMesh =
Mesh.Cylinder(device, 0.4f, 0.2f, 0.6f, 32, 1).Clone
(MeshFlags.Managed, VertexFormats.PositionNormal |
VertexFormats.Specular, device);
redPiece = new Model(pieceMesh, new Material[]
{ redMaterial, redMaterial });
blackPiece = new Model(pieceMesh, new Material[]
{ blackMaterial, blackMaterial });
redKingPiece = new Model(kingPieceMesh, new Material[]
{ redMaterial, redMaterial });
blackKingPiece = new Model(kingPieceMesh, new Material[]
{ blackMaterial, blackMaterial });
redPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f);
redKingPiece.PositionOffset = new Vector3(0.0f, 0.3f, 0.0f);
blackPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f);
blackKingPiece.PositionOffset = new Vector3(0.0f, 0.3f, 0.0f);
// The Mesh.Cylinder creates a cylinder that extends along the Z axis
// but I want it to extend along the Y axis, this is easily fixed by
// rotating it 90 degrees around the X axis.
// First create the rotation...
Quaternion rotation = Quaternion.RotationAxis
(new Vector3(1.0f, 0.0f, 0.0f), (float)Math.PI / 2.0f);
/// ... then apply it to all piece models
redPiece.Orientation = rotation;
blackPiece.Orientation = rotation;
redKingPiece.Orientation = rotation;
blackKingPiece.Orientation = rotation;
}
}
旋转和平移(Rotation and Translation)
我不会详细解释如何使用3D数学在3D空间中旋转和平移对象,但是我将解释示例实现中的操作.像这样的代码语句(I will not explain in detail how 3D math is used to rotate and translate objects in 3D space, but I will explain what is done in the example implementation. Code statements like) redPiece.PositionOffset = new Vector3(0.0f, 0.1f, 0.0f);
用于确保模型的"原点"偏移了(are used to make sure that the “origin” of the model is offset by) 0.1
沿Y轴.这样做是因为(along the Y-axis. This is done because) Mesh::Cylinder
创建一个圆柱体,其原点位于圆柱体的中心,我们需要将其位于圆柱体的边缘,以便将其正确放置在板上.同样,我们必须绕X轴旋转90度(PI/2弧度),因为它是沿着Z轴延伸的,而我们希望它沿着Y轴延伸.这就是为什么使用此代码的原因:(creates a cylinder with the origin in the center of the cylinder and we need it to be at the edge of the cylinder for it to be placed correctly on the board. Also, we have to rotate in 90 degrees (PI / 2 radians) around the X-axis because it is created extending along the Z-axis and we want it to extend along the Y-axis. This is why this code is used:)
...
// The Mesh.Cylinder creates a cylinder that extends along the Z axis
// but I want it to extend along the Y axis, this is easily fixed by
// rotating it 90 degrees around the X axis.
// First create the rotation...
Quaternion rotation = Quaternion.RotationAxis(new Vector3(1.0f, 0.0f, 0.0f),
(float)Math.PI / 2.0f);
/// ... then apply it to all piece models
redPiece.Orientation = rotation;
...
顶点格式(Vertex Format)
拥有一个(It is also important to have a) Mesh
具有适合我们目的的顶点格式. 3D模型中的顶点可以包含不同的信息,具体取决于如何使用它.至少必须包含位置数据.但是,如果模型要具有颜色,则还必须包括漫反射数据.在框架中,使用定向光来对场景进行着色以使其看起来更好.因此,还必须包括普通数据.(that has a vertex format that suits our purposes. A vertex in a 3D model can contain different information depending on how it is going to be used. At the very least, Position data must be included. However, if the model is to have a color, Diffuse data must also be included. In the Framework, a directional light is used to shade the scene to look nicer. Because of this, Normal data must also be included.)
的(The) Mesh
从返回(returned from the) static
方法(methods on) Mesh
用于创建不同的几何网格(例如盒子和圆柱体)不会返回(used to create different geometrical meshes (such as boxes and cylinders) does not return a) Mesh
我们想要的顶点格式.为了解决这个问题,我们克隆网格并在克隆时传递所需的顶点格式:(with the vertex format we want. To fix this, we clone the mesh and pass the desired vertex format when cloning:)
// Create a box to be used as a board square
// The .Clone call is used to get a Mesh that has vertices that
// contain both position, normal and color which I need to render them
// using flat shading.
Mesh blockMesh = Mesh.Box(device, 1.0f, 0.5f, 1.0f).Clone(MeshFlags.Managed,
VertexFormats.PositionNormal | VertexFormats.Specular, device);
代码清理(Code Clean-up)
(此部分在版本2中添加.)((This section added in version 2.))
当实施第二个游戏Connect Four时,我意识到可以重新使用整个表单设置,并决定为此在API类库中提供一个实现.这减少了实现游戏时所需的实际代码量.表单的创建和启动代码(When implementing a second game, Connect Four, I realized that the entire form setup could be reused and decided to provide an implementation in the API class library for this. This lessened the actual amount of code required when implementing a game. The form creation and startup code for the) Checkers
然后将游戏简化为:(game is then reduced to this:)
is
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new GameForm(
new CheckersLogic(),
new CheckersModelRepository(),
"Checkers",
"Checkers, a most excellent game!"));
}
这创建了一个新的(This creates a new) GameForm
对象和通行证(object and passes) IBoardGameLogic
,(,) IBoardGameModelRepository
,窗口标题和构造函数中的About文本.为了能够使(, the window title and the About text in the constructor. To be able to make the) GameForm
类可以显示哪些玩家转弯,这是我必须添加另一种方法(class able to display which players turn, it is that I had to add another method to) IBoardGameLogic
.我不想要(. I didn’t want) GameForm
必须轮询此信息的游戏逻辑,并决定使用通过委托实现的回调.这就需要一种新的附加方法(to have to poll the game logic for this information and decided to use a callback implemented with delegates. This required a new and additional method on the) interface
, 以及ASA(, as well as a) delegate
:(:)
public delegate void NextPlayerHandler(string playerIdentifier);
public interface IBoardGameLogic
{
...
void SetNextPlayerHandler(NextPlayerHandler nextPlayerHandler);
}
现在,表单实现可以将其方法之一添加为(Now the form implementation can add one of its methods as) NextPlayerHandler
游戏逻辑.指示玩家何时进行更改取决于游戏逻辑.超级简单!(to the game logic. It is up to the game logic to indicate when the player changes. Super simple!)
连接四实施(Connect Four Implementation)
(此部分在版本2中添加)((This section added in version 2))
为了显示实现另一款游戏有多么容易,我决定使用此API编写一个Connect Four游戏.我选择"连接四人"是因为它与(In order to show how easy it would be to implement another game, I decided to write a Connect Four game using this API. I chose Connect Four because it is fundamentally different from) Checkers
在某些方面.我想表明,不管这些差异如何,它不仅可能实现,而且实际上非常容易实现.(in some ways. I wanted to show that, regardless of these differences, it would be not only possible, but actually quite simple to implement.)
最大的区别在于,在"连接四人"中,您并没有从开发板上的所有内容开始.而是从一堆中挑选它们,然后将它们放在板上.通过使用比实际使用的更大的"逻辑"板,我创建了两个区域,可以从中提取无尽的零件.有了(The biggest difference is that in Connect Four, you do not start out with all the pieces on the board. Rather, you pick them from a pile and then place them on the board. By using a “logical” board that is larger that the board actually used, I created two areas from which an endless supply of pieces could be picked. By having) IBoardGameModelRepository
返回(return) null
对于不属于实际木板或"桩"区域的正方形,(for the squares that weren’t part of either the actual board or the “pile” areas,) GamePanel
可以忽略这些正方形的渲染.(could ignore rendering of these squares.)
上面显示的是使用以下方法的"连接四"实现(Shown above is the Connect Four implementation using)非常(very)看起来很棒的茶壶. “连接四人"游戏的实际游戏逻辑实现非常简单,花费了从伦敦到布莱顿的火车再返回的时间. :)(awesome-looking teapots as pieces. The actual game logic implementation for the Connect Four game is quite simple and took about the time it takes to take the train from London to Brighton and back again. :))
最后结果(Final Result)
那么,最终的实现又如何达到我要满足的要求呢?可悲的是,我不得不说我没有完全遵守要求#3,即在实施新的棋盘游戏时,实施人员不需要对3D数学或Direct3D有任何了解.对3D知识的需求可以在(So, how does the final implementation live up to the requirements I set out to fulfill? Sadly, I have to say that I failed to comply fully with requirement #3, that being that when implementing a new board game, the person implementing does not need to have any knowledge of 3D mathematics or Direct3D. The need for 3D knowledge can be seen in the) CheckersModelRepository
创建,转换和旋转网格的类(使用可怕的四元数进行旋转,不少!).这些东西至少需要初学者掌握3D数学.(class where meshes are created, translated and rotated (rotated using scary quaternions, no less!). This is stuff that requires at least a beginner’s knowledge of 3D mathematics.)
这还远远不是一个完整的框架,因为它目前不支持计算机播放器.此外,由于我决定不要求任何游戏循环,因此在四处移动棋子时没有流畅的动画.除此之外,我认为结果还不错.我花了不到一个小时的时间来实施(This is also quite far from being a complete Framework, as it does not currently support a computer player. Furthermore, since I decided that I should not require any game loop, there is no smooth animation when moving pieces around. Other than that, I think it turned out quite well. It took me less than an hour to implement the) Checkers
一旦框架完全实施,我认为这表明使用此框架实施棋盘游戏很容易.(game once the Framework was fully implemented and I think that indicates that it is easy to implement board games using this Framework.)
感谢您对代码和本文的任何评论.(I appreciate any comments, both on the code and on this article.)
历史(History)
- 2007/11/11:第一个版本(2007/11/11: First version)
- 2007/11/25:第二个版本,添加了Connect Four并清理了实现(2007/11/25: Second version, added Connect Four and cleaned up the implementation)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# C#2.0 Windows .NET .NET2.0 DirectX Visual-Studio Dev 新闻 翻译