C# 托管 DirectX HexEngine - 第一部分






4.68/5 (19投票s)
2003年9月3日
8分钟阅读

187286

1666
一个使用 DirectX 的简单 Hex 引擎。
引言
我想写一个简单的策略游戏,并寻找一个2D等距引擎,但大多数都非常慢(主要是因为使用GDI+),所以我认为我将使用托管DirectX自己写一个。我在线上找到的唯一一个是一个VB等距引擎,但我想使用六边形。到目前为止,结果相当不错,通过使用DirectX,该程序提供了出色的性能,并允许高速度滚动、缩放等。背景
为什么要使用六边形?首先,棋盘游戏已经使用它们很长时间了,但打印机可以提供完美的质量。原因在于游戏性——六边形具有6个方向的优势,而无需处理等距瓦片可能出现的对角线问题;此外,与纯3D地形相比,六边形具有易于传达所有权,允许您为区域分配属性等的优势。
六边形的主要缺点是性能,但强调游戏性和策略的游戏通常性能不高。不过,使用我那台只支持软件顶点处理的破旧TNT32显卡,性能也相当不错。
在第二部分,我将发布如何使用六边形的高度,我还没有看到有人这样做,但它肯定是可以实现的,我们可以设置垂直悬崖,这是许多3D地形引擎无法处理的。
特点
该引擎支持以下功能。
- 拾取——通过点击一个六边形,程序可以定位被点击的六边形。
- 灯光——按L键可以切换场景的直接灯光。
- 六边形边框——按G键可以切换显示六边形轮廓。
- 居中——按C键将显示居中到地图中间。
- 放大和缩小,并进行边界检查,以确保用户不会穿过地图或离得太远。
- 滚动,带重复按键和边界检查,以快速移动地图。重复按键内置了加速器。
- 多种纹理
- 线框实体切换——使用W键在这些模式之间切换。
- 支持动态调整大小、Alt-Tab、全屏(Alt-Enter)、窗口模式以及后台CPU占用降低。
使用代码
在我们开始之前,我需要声明一句:我不是DirectX专家,所以任何建议都将受到赞赏。其次,这一切都相当仓促,所以请对语法、拼写和代码中糟糕的注释多加谅解。
代码相当容易使用,但需要一些工作才能集成到您的程序中,并且目前纹理是硬编码的。基本上,您创建六边形并将它们传递给HexGrid
,它会跟踪它们。您需要为云、移动光源(太阳)、道路、村庄等添加额外的渲染阶段。我可能会在后续部分添加其中一些。
代码需要安装Managed DirectX 9和.NET Framework。
使用的类
HexEngine
负责渲染。它基于微软的DirectX示例,因此已集成到窗体中。它具有处理Alt-Tab、全屏-窗口模式、调整大小和全局异常处理等功能的优势。它还控制键盘和鼠标移动。
Hexagon
这是基础单元,您可以直接修改此类或使用继承来创建您的游戏六边形。目前,引擎使用的唯一功能是纹理ID。
HexGrid
以数组的形式跟踪Hexagon
。此类计算顶点和索引缓冲区,并存储六边形的一些便捷属性。
关注点
当我开始时,我首先根据Thomas Jahn在Gamedev.net上于2002年2月28日发表的精彩文章“基于六边形瓦片地图的坐标”编写了HexGrid
。这是基本数学和坐标的一个良好起点。
第一个重大的3D问题是如何处理顶点、纹理坐标和索引。由于每个六边形的角都将与另外2个六边形相邻,而每个六边形都可以有不同的纹理。默认情况下,一个顶点只能包含一个纹理坐标。深入研究后,发现多个纹理坐标是可能的,但这些有一些限制。
- 我的显卡是最常见的显卡,只支持2个。对于2个顶点,计算哪个顶点需要渲染的额外复杂性不值得获得的好处。
- 为每个顶点添加3个纹理坐标会增加6个浮点数,使它们的大小增加一倍以上。
所以,我必须做出一个艰难的选择,但主要是由于显卡的原因,我决定每个六边形都有自己的顶点。这意味着顶点数量几乎增加了3倍!然而,这种顶点结构使得渲染更简单,节省了我购买新显卡(因为我的硬盘坏了,我需要新硬盘…),并且结果是更小的顶点(5或6个浮点数而不是11-12个),部分抵消了增加的数量。然后,我可以轻松地为每个地形计算索引缓冲区,而无需更改顶点缓冲区。还有一个小的好处是,可以实现垂直悬崖,如果我想做一些地形,六边形也可以被抬高。
计算顶点非常简单,如下所示,每个六边形有6个顶点。纹理坐标uv
和uw
与顶点的xy
匹配。yvalue
代表高度,目前保持为平面。
public void GenerateVertexBufferData(VertexBuffer source)
{
const float yValue = 0f;
VertexBuffer vb = source;
// Create a vertex buffer (100 customervertex) and lock it
CustomVertex.PositionNormalTextured[] verts =
(CustomVertex.PositionNormalTextured[])vb.Lock(0,0);
for (int y = 0; y < this.Y; y++)
for (int x = 0; x < this.X; x++)
{
//Hexagon hex = this.hexagons[x,y];
PointF topLeft= this.GetTopLeftPixelBoundingRectangle(x, y);
// vertex 0
verts[6*(x+ y*this.X)].SetPosition(new Vector3(topLeft.X, yValue,
topLeft.Y + this.H));
verts[6*(x+ y*this.X)].Tu = 0.0f;
verts[6*(x+ y*this.X)].Tv = (float) this.H / this.height;
// vertex 1 top
verts[6*(x+ y*this.X)+1].SetPosition(new Vector3(topLeft.X +
this.Radius, yValue, topLeft.Y));
verts[6*(x+ y*this.X)+1].Tu = 0.5f;
verts[6*(x+ y*this.X)+1].Tv = 0.0f;
// vertex 2
verts[6*(x+ y*this.X)+2].SetPosition(new Vector3( topLeft.X +
this.width, yValue, topLeft.Y + this.H ));
verts[6*(x+ y*this.X)+2].Tu = 1.0f;
verts[6*(x+ y*this.X)+2].Tv = (float) this.H/ this.height;
// vertex 3
verts[6*(x+ y*this.X)+3].SetPosition(new Vector3( topLeft.X +
this.width, yValue, topLeft.Y + this.H + this.side ));
verts[6*(x+ y*this.X)+3].Tu = 1.0f;
verts[6*(x+ y*this.X)+3].Tv = (float) (this.H + this.side) /
this.height;
// vertex 4 bottom
verts[6*(x+ y*this.X)+4].SetPosition(new Vector3( topLeft.X +
this.Radius, yValue, topLeft.Y + this.height ));
verts[6*(x+ y*this.X)+4].Tu = 0.5f;
verts[6*(x+ y*this.X)+4].Tv = 1.0f ;
// vertex 5
verts[6*(x+ y*this.X)+5].SetPosition(new Vector3( topLeft.X,
yValue, topLeft.Y + this.H + this.side ));
verts[6*(x+ y*this.X)+5].Tu = 0.0f;
verts[6*(x+ y*this.X)+5].Tv = (float) (this.H + this.side) /
this.height;
}
vb.Unlock();
}
然后我必须考虑如何索引顶点。起初,我使用了三角形扇形,因为大多数文档都说它们适用于像六边形这样的东西……这是一个大错误。三角形扇形适合一个六边形。如果要渲染2500个(50*50)六边形,将导致2500次设备调用……糟糕。而且,由于它们都是从一个点绘制的,所以无法实现单个扇形。要更改结构,我所要做的就是更新索引,所以我对所有选项进行了基准测试,在低六边形数量时,所有选项都差不多。在高六边形数量时,三角形带比三角形列表好20-50%。所以,我选择了三角形带。下一个问题是如何将多个六边形组合成一个带。答案是使用一个在每个六边形末端的退化三角形;这是通过重复最后一个顶点来实现的。这意味着当前六边形的最后一个三角形有两个相同的顶点(即没有面积,但您可以用默认的线框看到它们),以及下一个六边形。这允许我将一个地形类型的所有六边形发送到一个大的索引中,进行大块的调用肯定会更好。第二个问题是剔除;计算带的顺序,使其符合剔除要求,并且允许高效的带,我最终得到了一个每个六边形7个顶点的带,我认为这相当不错。
然后我决定了一个项目空间,并决定使用透视投影,这样我就可以放大和缩小,我想要一个俯视视角。这成了一个问题,因为每当我精确地对齐相机和观察点时,图像根本不会显示!我将其稍微偏移了一点,并希望这么小的差异的数学四舍五入不会给我带来麻烦。
经过大量的调整,显示看起来逐渐成形了。下一个障碍是拾取,例如点击屏幕的一部分并显示坐标。我尝试玩弄unproject,但不是很成功(主要是由于另一个部分的bug,但复杂性隐藏了它)。所以我采用了一个更简单的方法,我使用project来计算HexGrid
的左上角和右下角将投影到哪里,然后进行缩放。
最后一个需要实现的功能是滚动。我想使用重复按键,要实现这一点,唯一的方法很丑陋,如果其他人知道更简洁的方法,我将非常感谢。repeatkey
方法使用了char
转换,但我找不到表示箭头的char
,所以我会在调用之前进行钩子,并捕获箭头键,然后发送一个很少使用的ASCII序列。第二个问题是关于缩放。我认为加速器会很有用,这导致了一个稍微复杂的Scroll
方法,但现在它完成了,工作得相当好。
待解决的问题
- 大小限制在略高于50*50个六边形。这不会改变,因为您将看到索引缓冲区中的20,000个顶点。由于索引缓冲区是
short
类型,您在这里受到很大限制。要解决这个问题,请尝试以下方法:- 一种方法是渲染多个HexGrid。
- 当用户缩小视野时合并六边形。
HexGrid
可以(相对便宜地)在用户滚动时重新计算,每滚动10个六边形到新的中心。
- 注释——很少,而且做得不好。至少C#很容易阅读。
- 代码结构相当差。键盘事件应该移动到另一个类。纹理应该被传入等等。
- 代码检查——很少有。
- 六边形边框——我们正在使用一个技巧来渲染六边形边框,但这在边缘处很明显。
- 灯光显示六边形的边缘为白色。我尝试了几种组合,它们都导致边缘线消失,直到我找到一种突出边缘的组合。
- 鼠标滚动——滚动方法已经存在,所以很简单。只是时间不够……
历史
v00 发布于 2003/8/22