65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 托管 DirectX HexEngine - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (19投票s)

2003年9月3日

8分钟阅读

viewsIcon

187286

downloadIcon

1666

一个使用 DirectX 的简单 Hex 引擎。

Sample Image - hexengine.jpg

引言

我想写一个简单的策略游戏,并寻找一个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个顶点。纹理坐标uvuw与顶点的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

© . All rights reserved.