苏格拉底:“我知道我一无所知”
为了谁: 对于那些吐槽所有开发者并想玩他们的游戏的 IT 人来说!
关于什么: 关于如何开始用 C/C++ 编写游戏,如果您突然需要它!
为什么你应该读这篇文章: 应用程序开发不是我的工作专长,但我每周都会尝试编码。 因为我爱游戏!
你好我的名字是
电脑游戏产业规模巨大,有传言甚至比当今的电影产业还要大。 自计算机诞生以来,游戏就已经被编写出来,按照现代标准,使用复杂而基本的开发方法。 随着时间的推移,具有已编程图形、物理和声音的游戏引擎开始出现。 它们使您可以专注于开发游戏本身,而不必担心其基础。 但随着它们和引擎的出现,开发人员“失明”并退化。 游戏的制作本身就被放到了传送带上。 产品的数量开始超过其质量。
同时,在玩别人的游戏时,我们不断受到别人想出的地点、情节、人物、游戏机制的限制。 所以我意识到...
……是时候创造你们自己的世界了,只受我管辖。 我是圣父、圣子和圣灵的世界!
我真诚地相信,通过编写自己的游戏引擎并在上面玩,您将能够脱掉鞋子,擦窗户并升级您的小屋,成为一名更有经验和更全面的程序员。
在这篇文章中,我将尝试告诉你我是如何开始用 C/C++ 编写小游戏的,开发过程是怎样的,以及我在繁忙的环境中如何找到时间来培养爱好。 它是主观的,描述了个人开始的过程。 关于无知和信仰的材料,关于我个人对当前世界的看法。 换句话说,“政府不对你们的个人大脑负责!”。
实践
孔子“知而不行无益,行而不知则殆”
我的笔记本就是我的生命!
所以,在实践中,我可以说对我来说一切都是从记事本开始的。 我不仅在那里写下我的日常任务,我还画图、编程、设计流程图并解决问题,包括数学问题。 始终使用记事本并仅用铅笔书写。 恕我直言,它干净、方便、可靠。
我的(已经写满了)笔记本。 这就是他的样子。 它包含日常任务、想法、绘图、图表、解决方案、黑簿、代码等
在这个阶段,我成功完成了三个项目(这是我对“完整性”的理解,因为任何产品都可以相对无休止地开发)。
- 项目0: 这是使用 Unity 游戏引擎用 C# 编写的 Architect 演示 3D 场景。 适用于 macOS 和 Windows 平台。
- 游戏一 适用于 Windows 的控制台游戏《Simple Snake》(大家都称为“Snake”)。 用C写成。
- 游戏一 控制台游戏 Crazy Tanks(众所周知的“坦克”),已经用 C++(使用类)编写,并且也在 Windows 下编写。
项目 0 架构师演示
- 平台: Windows(Windows 7、10)、Mac OS(OS X El Capitan v.10.11.6)
- 语言: C#
- 游戏引擎:
Unity - 灵感:
达林·莱尔 - 存储库:
GitHub上
3D 场景架构师演示
第一个项目不是用 C/C++ 实现的,而是使用 Unity 游戏引擎用 C# 实现的。 该引擎对硬件的要求并不像
对我来说,Unity 的目标不是开发某种游戏。 我想创建一个带有某种角色的 3D 场景。 他,或者更确切地说,她(我模仿了我所爱的女孩=)必须移动并与外界互动。 重要的是要了解 Unity 是什么、开发过程是什么以及创建某些东西需要付出多少努力。 这就是 Architect Demo 项目的诞生(这个名字几乎是从废话中发明的)。 编程、建模、动画、纹理花了我大约两个月的日常工作。
我从 YouTube 上的教程视频开始,了解如何在中创建 3D 模型
对锁骨进行建模,添加骨杠杆,使动画看起来更自然。 经过这样的课程,你会意识到动画电影的创作者为了制作30秒的视频而付出了多么巨大的努力。 但 3D 电影要持续几个小时! 然后我们走出电影院,说这样的话:“Ta,一部糟糕的卡通/电影! 他们本来可以做得更好……”傻瓜!
关于这个项目中的编程还有一件事。 事实证明,对我来说最有趣的部分是数学部分。 如果您运行场景(链接到项目描述中的存储库),您会注意到相机在球体中围绕女孩角色旋转。 为了对这样的相机旋转进行编程,我必须首先计算圆(2D)上的位置点的坐标,然后计算球体(3D)上的位置点的坐标。 有趣的是,我在学校讨厌数学,而且知道它是减号。 部分原因可能是因为在学校里他们根本不会向你解释这些数学到底是如何应用在生活中的。 但当你痴迷于你的目标、梦想时,你的头脑就会变得清晰、显露出来! 你开始将复杂的任务视为一次令人兴奋的冒险。 然后你会想:“好吧,为什么*亲爱的*数学家通常不能告诉我们这些公式可以用在哪里?”。
计算圆上和球上点坐标的公式的计算(来自我的笔记本)
游戏 1. 简单的贪吃蛇
简单的贪吃蛇游戏
3D 场景不是游戏。 此外,对 3D 对象(尤其是角色)进行建模和动画制作既耗时又困难。 在使用 Unity 后,我意识到我需要继续,或者更确切地说,从基础开始。 简单而快速,但同时具有全局性的东西,以了解游戏的结构。
那么我们有什么简单快速的方法呢? 没错,控制台和 2D。 更准确地说,甚至是控制台和符号。 我再次开始在互联网上寻找灵感(总的来说,我认为互联网是XNUMX世纪最具革命性和危险性的发明)。 我挖出了一个制作控制台俄罗斯方块的程序员的视频。 正如他的游戏一样,他决定砍掉“蛇”。 从视频中,我了解了两个基本知识 - 游戏循环(具有三个基本功能/部分)和输出到缓冲区。
游戏循环可能看起来像这样:
int main()
{
Setup();
// a game loop
while (!quit)
{
Input();
Logic();
Draw();
Sleep(gameSpeed); // game timing
}
return 0;
}
该代码立即呈现了整个 main() 函数。 游戏周期在适当的评论后开始。 循环中有三个基本函数:Input()、Logic()、Draw()。 首先输入数据Input(主要是按键控制),然后对输入的数据进行逻辑处理,然后输出到屏幕上——Draw。 每一帧都是如此。 这就是动画的创建方式。 就像动画片里那样。 通常,处理输入的数据需要花费最多的时间,并且据我所知,决定了游戏的帧速率。 但这里 Logic() 函数执行得非常快。 因此,你必须使用Sleep()函数和gameSpeed参数来控制帧速率,它决定了这个速度。
游戏周期。 记事本中的蛇编程
如果您正在开发符号控制台游戏,那么使用通常的流输出“cout”在屏幕上显示数据将不起作用 - 它非常慢。 因此,输出必须在屏幕缓冲区中进行。 速度快得多,游戏也不会出现任何故障。 老实说,我不太明白屏幕缓冲区是什么以及它是如何工作的。 但我会在这里给出一个代码示例,也许评论中的人能够澄清情况。
获取屏幕缓冲区(如果我可以这么说的话):
// create screen buffer for drawings
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0,
NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
DWORD dwBytesWritten = 0;
SetConsoleActiveScreenBuffer(hConsole);
直接输出到屏幕的某一行scoreLine(显示分数的行):
// draw the score
WriteConsoleOutputCharacter(hConsole, scoreLine, GAME_WIDTH, {2,3}, &dwBytesWritten);
从理论上讲,这个游戏没有什么复杂的;我认为这是一个很好的入门级例子。 该代码编写在一个文件中,并格式化为多个函数。 没有类,没有继承。 您可以访问 GitHub 上的存储库,亲自查看游戏源代码中的所有内容。
游戏2.疯狂坦克
- 平台: Windows(在 Windows 7、10 上测试)
- 语言: C + +中
- 游戏引擎: Windows 控制台
- 灵感: книга
通过游戏编程开始 C++ - 存储库:
GitHub上
游戏疯狂坦克
将角色打印到控制台可能是您可以将其变成游戏的最简单的事情。 但是这样就出现了一个问题:字符的高度和宽度不同(高度大于宽度)。 因此,一切都会看起来不成比例,向下或向上移动看起来比向左或向右移动要快得多。 这种效果在《贪吃蛇》(游戏1)中非常明显。 “坦克”(游戏 2)没有这样的缺点,因为那里的输出是通过用不同颜色绘制屏幕像素来组织的。 你可以说我写了一个渲染器。 确实,这已经有点复杂了,尽管更有趣。
对于这个游戏,描述我在屏幕上显示像素的系统就足够了。 我认为这是游戏的主要部分。 你可以自己想出其他一切。
因此,您在屏幕上看到的只是一组移动的彩色矩形。
一组矩形
每个矩形由一个填充数字的矩阵表示。 顺便说一句,我可以强调一个有趣的细微差别 - 游戏中的所有矩阵都被编程为一维数组。 不是二维的,是一维的! 一维数组使用起来更加容易和快捷。
游戏坦克矩阵示例
将游戏坦克矩阵表示为一维数组
将矩阵表示为一维数组的更直观示例
但是对数组元素的访问发生在双循环中,就好像它不是一维数组,而是二维数组一样。 这样做是因为我们仍在处理矩阵。
在双循环中遍历一维数组。 Y - 行标识符,X - 列标识符
请注意:我使用标识符 x 和 y,而不是通常的矩阵标识符 i、j。 在我看来,这样看起来更赏心悦目,也更容易被大脑理解。 此外,这种表示法使得可以方便地将所使用的矩阵投影到二维图像的坐标轴上。
现在关于像素、颜色和屏幕输出。 StretchDIBits 函数用于输出(头文件:windows.h;库:gdi32.lib)。 除其他外,该函数接收以下信息:显示图像的设备(在我的例子中,它是 Windows 控制台)、图像显示的起始坐标、其宽度/高度以及图像本身位图的形式,由字节数组表示。 位图作为字节数组!
StretchDIBits() 函数的实际应用:
// screen output for game field
StretchDIBits(
deviceContext,
OFFSET_LEFT, OFFSET_TOP,
PMATRIX_WIDTH, PMATRIX_HEIGHT,
0, 0,
PMATRIX_WIDTH, PMATRIX_HEIGHT,
m_p_bitmapMemory, &bitmapInfo,
DIB_RGB_COLORS,
SRCCOPY
);
使用 VirtualAlloc() 函数预先为此位图分配内存。 也就是说,保留所需的字节数来存储所有像素的信息,然后将这些信息显示在屏幕上。
创建 m_p_bitmapMemory 位图:
// create bitmap
int bitmapMemorySize = (PMATRIX_WIDTH * PMATRIX_HEIGHT) * BYTES_PER_PIXEL;
void* m_p_bitmapMemory = VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);
粗略地说,位图由一组像素组成。 数组中的每四个字节就是一个 RGB 像素。 每个红色值 XNUMX 个字节,每个绿色值 (G) XNUMX 个字节,每个蓝色值 (B) XNUMX 个字节。 另外,每个缩进有一个字节。 这三种颜色 - 红/绿/蓝 (RGB) - 以不同的比例相互混合 - 并获得最终的像素颜色。
现在,每个矩形或游戏对象都由数字矩阵表示。 所有这些游戏对象都放置在一个集合中。 然后将它们放在比赛场地上,形成一个大的数值矩阵。 我将矩阵中的每个数字与特定的颜色相关联。 例如,数字8对应蓝色,数字9对应黄色,数字10对应深灰色,等等。 因此,我们可以说我们有一个比赛场地矩阵,其中每个数字都是一种颜色。
因此,我们一方面有整个比赛场地的数字矩阵,另一方面有用于显示图像的位图。 到目前为止,位图是“空的”——它还没有有关所需颜色的像素的信息。 这意味着最后一步将根据比赛场地的数字矩阵用有关每个像素的信息填充位图。 下图是这种转换的一个说明性示例。
使用基于比赛场地数字矩阵的信息填充位图(像素矩阵)的示例(颜色索引与游戏中的索引不匹配)
我还将展示游戏中的一段真实代码。 循环每次迭代时的变量 colorIndex 都会被分配一个来自游戏场数字矩阵 (mainDigitalMatrix) 的值(颜色索引)。 然后根据索引将颜色本身写入颜色变量。 此外,所得颜色被分为红、绿、蓝(RGB)的比例。 并且与缩进(pixelPadding)一起,该信息被一遍又一遍地写入像素,在位图中形成彩色图像。
该代码使用指针和按位运算,这可能很难理解。 因此,我建议您单独阅读此类结构的工作原理。
根据比赛场地的数字矩阵填充位图信息:
// set pixel map variables
int colorIndex;
COLORREF color;
int pitch;
uint8_t* p_row;
// arrange pixels for game field
pitch = PMATRIX_WIDTH * BYTES_PER_PIXEL; // row size in bytes
p_row = (uint8_t*)m_p_bitmapMemory; //cast to uint8 for valid pointer arithmetic
(to add by 1 byte (8 bits) at a time)
for (int y = 0; y < PMATRIX_HEIGHT; ++y)
{
uint32_t* p_pixel = (uint32_t*)p_row;
for (int x = 0; x < PMATRIX_WIDTH; ++x)
{
colorIndex = mainDigitalMatrix[y * PMATRIX_WIDTH + x];
color = Utils::GetColor(colorIndex);
uint8_t blue = GetBValue(color);
uint8_t green = GetGValue(color);
uint8_t red = GetRValue(color);
uint8_t pixelPadding = 0;
*p_pixel = ((pixelPadding << 24) | (red << 16) | (green << 8) | blue);
++p_pixel;
}
p_row += pitch;
}
根据上述方法,在《疯狂坦克》游戏中,在Draw()函数中形成了一张图片(帧)并显示在屏幕上。 在Input()函数中注册击键并在Logic()函数中进行后续处理后,形成一个新的图片(帧)。 确实,游戏对象可能已经在比赛场地上具有不同的位置,因此,被绘制在不同的位置。 这就是动画(运动)的发生方式。
理论上(如果我没有忘记任何事情的话),理解第一个游戏(“贪吃蛇”)的游戏循环和第二个游戏(“坦克”)在屏幕上显示像素的系统就是你编写任何游戏所需的全部内容。 Windows 下的 2D 游戏。 无声无息! 😉 其余部分只是幻想而已。
当然,《坦克》这款游戏的设计比《贪吃蛇》要复杂得多。 我已经使用了C++语言,即我用类来描述不同的游戏对象。 我创建了自己的集合 - 您可以在 headers/Box.h 中看到代码。 顺便说一句,该集合很可能存在内存泄漏。 使用的指针。 凭记忆工作。 我必须说这本书对我帮助很大。 通过游戏编程开始 C++。 对于 C++ 初学者来说,这是一个很好的开始。 它很小,很有趣,而且组织得很好。
开发这款游戏大约花了六个月的时间。 我主要在工作时的午餐和小吃期间写作。 他坐在办公室的厨房里,一边吃食物一边写代码。 或者在家吃晚饭。 于是我就有了这样的“厨房大战”。 一如既往,我积极使用笔记本,所有概念性的东西都诞生在其中。
在实践部分结束时,我会拿出一些笔记本的扫描件。 为了展示我究竟在写下、绘画、计数、设计……
坦克形象设计。 以及每个坦克在屏幕上应占据多少像素的定义
罐体绕轴旋转的算法和公式的计算
我的收藏图(最有可能发生内存泄漏的)。 该集合被创建为链接列表
这些都是将人工智能融入游戏的徒劳尝试
Теория
“行万里路,始于足下”(中国古代智慧)
让我们从实践转向理论! 你如何找到时间从事你的爱好?
- 确定你真正想要什么(唉,这是最困难的)。
- 设定优先事项。
- 为了更高的优先级而牺牲所有“多余的”。
- 每天朝着你的目标前进。
- 不要指望会有两三个小时的空闲时间来从事一项爱好。
一方面,您需要确定自己想要什么并确定优先顺序。 另一方面,可以放弃一些活动/项目以支持这些优先事项。 换句话说,你将不得不牺牲“额外”的一切。 我在哪里听说过,生活中最多应该有三项主要活动。 然后您将能够以最高的质量完成它们。 额外的项目/方向将开始超载。 但这可能都是主观的和个人的。
有一条黄金法则:永远不要有 0% 的一天! 我在一位独立开发者的文章中了解到了这一点。 如果你正在做一个项目,每天就做一些事情。 你做了多少并不重要。 写一个字或一行代码,观看一个教程视频或在板上钉一颗钉子 - 只是做某事。 最难的是开始。 一旦开始,你最终可能会做得比你想要的多一点。 这样你就会不断地朝着你的目标前进,相信我,很快。 毕竟,所有事情的主要障碍就是拖延。
重要的是要记住,你不应该低估和忽视5、10、15分钟的免费“木屑”,等待一些持续一两个小时的大“日志”。 你在排队吗? 为你的项目考虑一些事情。 你要上自动扶梯吗? 在笔记本上写下一些东西。 你在公交车上吃饭吗? 好吧,读一些文章。 利用一切机会。 别再在 YouTube 上看猫和狗了! 别乱动你的大脑!
最后一件事。 如果在阅读本文后,您喜欢不使用游戏引擎来创建游戏的想法,那么请记住 Casey Muratori 这个名字。 这家伙有
凯西还解释说,通过开发自己的游戏引擎,您将对任何现有引擎有更好的了解。 在每个人都试图实现自动化的框架世界中,您学习的是创建而不是使用。 您了解计算机的本质。 而且你也将成为一名更加聪明和成熟的程序员——一名专业人士。
祝你在选择的道路上好运! 让我们让世界变得更加专业。
作者: 格兰金·安德烈 、开发运营
来源: habr.com