小伙伴关心的问题:CSD链路(CSDlogo设计),本文通过数据整理汇集了CSD链路(CSDlogo设计)相关信息,下面一起看看。

CSD链路(CSDlogo设计)

第九章 Tilemap地图

这章重新回到绘制相关的部分,来完成一个很重要的部分——地图。

一、Tilemap地图

在2D游戏里Tilemap算是最常见的地图了,中文有翻译成瓦片地图也有翻译成瓷砖地图的。至于什么是Tilemap,可以随便联想一下能想到的各种经典游戏,比如早期的马里奥、宝可梦、勇者斗恶龙等,地图都是由相同大小的地图块拼接而成,每个地图块一般都有自己的贴图。如果以前用过一些rpgmaker之类的2D游戏引擎的话应该很熟悉,只需要有一堆地图素材,然后就能像拼图一样拼出一张地图。

图 9.1.1 试着用现成的RPG Maker MZ随便画了一下

最最普通的就是每个地图块都是正方形的Tilemap,其次就是菱形的地图,能以一个斜的45度来展示更多细节,一般称为isometric。也有正六边形的Tilemap,通常在战棋策略游戏里能见到。我们的目标还是最基础的正方形Tilemap,希望能用代码实现出与图9.1.1类似的效果。

二、MapComponent组件

地图组件肯定是要有绘制功能的,还记得在第六章里我们实现过SpriteComponent精灵组件,可以单一地绘制出一张图片,不过只用精灵组件想要绘制出一整张Tilemap不知道得需要多少个用来挂载的物体才能实现,显然是不现实的。但可以通过继承SpriteComponent类的方式来创建一个新的MapComponent类,重写里面的Draw()函数来完成地图的绘制。同时也能继承到SpriteComponent类里面的mDrawOrder属性,方便让精灵绘制和地图绘制分层。

地图在程序内部的数据结构可以简单地用一个二维数组来表示。这里用一个简单的例子,假如只有两张贴图,一张可以看作是地板,一张可以看作是墙壁,那么就可以为每张贴图挑一个编号。我们让地板为0,墙壁为1,那么重复利用这两张贴图就能拼出一张地图,同时也能很简单地写出其所对应的二维数组,如下图9.2.1所示。

图 9.2.1 二维数组与Tilemap地图

有更多的贴图的话就可以继续为所有贴图标号,然后把这些数字填进二维数组里,就能随心所欲地创作出想要的地图了。如果有宝可梦地图素材的话,那用这个数组来还原出一张宝可梦地图也不是难事。

目前来说,可以先用固定的数组作为地图数据,之后如果想做Roguelike类游戏来随机生成地图的话,本质就是随机生成出这个数组。

可以先来看看MapComponent类的内部了:

class MapComponent : public SpriteComponent { public: //! 构造函数 MapComponent(class GameObject* gameObject); //! 析构函数 ~MapComponent(); //! 绘制(重写) void Draw(SDL_Renderer* renderer) override; //! 设置贴图 void SetTexture(SDL_Texture* texture, int number); private: int* mMapArray; //!< 地图数组 std::unordered_map<int, SDL_Texture*> mTextures; //!< 贴图存储 };

成员变量里可以看到我们上面说的地图数组mMapArray,为了更灵活应对不同大小的地图,使用了动态数组。

显然这个地图组件中绝大多数情况下都需要不止一张贴图(当然也有把利用到的小贴图都结合到一张tile set里的做法),这里我们就用2张不同的贴图,因此创建了存储贴图用的哈希表mTextures。这里用std::vector之类的也完全可以,只不过哈希表可以更自由地对地图贴图标号。

来看看各个函数的具体实现。

MapComponent::MapComponent(GameObject* gameObject): SpriteComponent(gameObject, 0) { // 宽20高15的地图 mMapArray = new int[20 * 15]; // 随便进行一下初始化 for (int i = 0; i < 15; i++) { for (int j = 0; j < 20; j++) { if (i < 2 || j < 2 || i>12 || j>17) { mMapArray[i * 20 + j] = 1; } else { mMapArray[i * 20 + j] = 0; } } } }

MapComponent继承了SpriteComponent类,所以构造函数里先要设置一下绘制顺序的变量drawOrder,之前我们把Player类的精灵组件的drawOrder设置成了100,而排在靠前位置的精灵会先进行绘制,后绘制的精灵会覆盖在先绘制的精灵上面,因此让地图的绘制顺序小于100即可。考虑到Player和地图之间还可能存在别的东西,留有一些余裕,就给地图设置成0好了。

之后就是创建并初始化数组mMapArray。先要明确我们用的单位地图块是32*32像素的,目前的窗口大小是640*480像素的,所以现在只需要一个宽20高15大小的数组。这里就直接new出了一个一维数组(和二维数组没什么本质差别,索引的时候用二维的方式就行了)。我用双重循环随便给这个数组赋了一下值,就是边界两圈都为1,中心部分为0。想要更复杂的数组时可以直接把整个数组写出来赋值,当然也可以事先写到文件里,然后通过读文件的方式来读取地图数组。

MapComponent::~MapComponent() { delete mMapArray; }

析构函数里注意要释放掉构造函数里面new出来的这个数组。

void MapComponent::SetTexture(SDL_Texture* texture, int number) { mTextures.emplace(number, texture); }

这个设置贴图的函数也没什么可说的,把贴图添加到存储结构里就行了。

void MapComponent::Draw(SDL_Renderer* renderer) { SDL_Rect dstRect{ 0, 0, 32, 32 }; for (int i = 0; i < 15; i++) { for (int j = 0; j < 20; j++) { // 实际在窗口中的位置 dstRect.x = j * 32; dstRect.y = i * 32; // 该地图块的值 int massNumber = mMapArray[i * 20 + j]; // 通过地图块的值去索引贴图并绘制 SDL_RenderCopy(renderer, mTextures[massNumber], nullptr, &dstRect); } } }

最后是需要重写的Draw()函数,其实就是按顺序把所有地图块画出来而已。在这期间,绘制图片的目标位置dstRect是要不断改变的,我们就不断改变这个临时变量dstRect中x和y的值就可以了。w和h的值保持32不变,因为所有地图块宽高都是32像素。每组x和y都是目标地图块的左上角的位置,同时要注意数组下标和实际位置之间的对应关系,需要乘上32才行。

接下来就要检查数组中当前元素的值是多少,根据这个值去mTextures中索引取出对应的贴图,再进行最后的绘制。

三、Dungeon类

不把MapComponent组件挂载到一个GameObject上的话是发挥不了作用的,类似于之前的Player类,再创建一个Dungeon类来作为地图组件的主人。

Dungeon::Dungeon(Game* game): GameObject(game) { MapComponent* mapComponent = new MapComponent(this); mapComponent->SetTexture(game->GetTexture("ground"), 0); mapComponent->SetTexture(game->GetTexture("wall"), 1); }

目前需要写的只有它的构造函数。在构造函数中,挂载上地图组件,然后再设置一下贴图并为每个贴图标号就OK了,标号要和地图数组里的对应上。

最后别忘了在Game类的初始化中实例化Dungeon对象,以及在加载数据的函数中读取地面贴图和墙壁贴图。至于这两个贴图素材我直接附到工程文件里了,可以在最下面的工程链接里面下载。

如果一切顺利的话会得到图9.3.1的运行效果。

图 9.3.1 绘制了地图后本章的最终运行结果

可以感受到,目前我们的角色移动还是比较自由的,但如果回忆一下以前的那些经典RPG,比如宝可梦、勇者斗恶龙之类的游戏,玩家的每一次移动指令是让角色移动一整格,角色就像是在棋盘上一样,绝对不会停留在格子以外的位置。下一章将会改进MoveComponent来实现上述的效果。

本章完整工程:

https://github.com/as *** ee0/Cplus-Game-Tutorial/tree/main/Chapter9

更多CSD链路(CSDlogo设计)相关信息请关注本站,本文仅仅做为展示!