Flap Hero是一款完全用C ++编写的小型游戏,无需使用现有的游戏引擎。它的所有源代码都可以在GitHub上找到。我认为它可以作为新手和中级游戏开发人员学习的有趣资源。
在这篇文章中,我将说明Flap Hero的代码是如何组织的,与大型游戏项目的区别,为什么以这种方式编写以及可以做得更好的事情。
这篇文章中很少有信息专门针对C ++。如果Flap Hero是用另一种语言(如C#,Rust或纯C)编写的,那么大多数内容仍然有用。也就是说,如果您浏览(或构建)源代码,则需要在C ++中具有一定的流利性。学习C ++和学习OpenGL是适合初学者的两个重要资源。在大多数情况下,Flap Hero的源代码都遵循相当简单的C ++子集,但是您深入了解其低级模块(如运行时),则越会遇到诸如模板和SFINAE之类的高级C ++功能。
Flap Hero是使用Plywood开发的,Plywood是一种C ++框架,可帮助将代码组织到可重用的模块中。下图中的每个黄色框代表一个胶合板模块。蓝色箭头表示依赖关系。
平台运行时图像数学胶合板回购flapGame glfwFlap GameFlow.cpp GameState.cpp Collision.cpp Text.cpp FlapHero repo Public.h glfw非常高兴assimp Main.cpp iOS项目Android项目iOS项目iOS项目Windows,Linux& macOS Assets.cpp GLHelpers.cpp Flap Hero游戏代码的最大块位于flaveGame模块中,该模块包含大约6400条物理代码行。 flipGame模块中两个最重要的源文件是GameFlow.cpp和GameState.cpp。
单个游戏会话的所有状态都保存在单个GameState对象中。该对象又归GameFlow对象所有。 GameFlow实际上可以在给定时间拥有两个GameState对象,两个游戏会话同时进行更新。从一个游戏阶段到下一个游戏阶段的过渡期间,此功能可用于实现动画化的“分屏”效果。
GameState GameState GameFlow flipGame模块旨在合并到主项目中。在桌面操作系统上,主项目由glfwFlap模块实现。 glfwFlap模块负责初始化游戏的OpenGL上下文,并将输入和更新事件传递给flipGame。 (Android和iOS使用完全不同的主项目,但它们的用途与glfwFlap相同。)glfwFlap使用在单个文件Public.h中定义的API与flaveGame通信。此API中只有13个功能,并且并非在所有平台上都使用全部功能。
Flap Hero不是基于现有的游戏引擎。这只是一个C ++程序,当您运行它时,它会播放Flap Hero!因此,它不包含典型游戏引擎中的许多子系统。这是一个故意的选择。 Flap Hero是一个示例应用程序,我想使用尽可能少的代码来实现它。
我所说的“子系统”是什么意思?在以下各节中,我将给出一些示例。在某些情况下,没有子系统也是可以的。在其他情况下,这是不利的。我将对每个人做出裁决。
在典型的游戏引擎中,是3D(或2D)场景的中间表示,由一堆抽象对象组成。在Godot中,这些对象继承自Spatial;在虚幻引擎中,它们是AActor实例;在Unity中,它们是GameObject实例。所有这些对象都是抽象的,这意味着它们如何在屏幕上呈现的细节由子类或组件填充。
这种方法的一个好处是,它允许渲染器以通用方式执行视图视锥剔除。首先,渲染器确定哪些对象可见,然后有效地告诉这些对象自己绘制。最终,每个对象都会对基础图形API发出一系列绘图调用,无论是OpenGL,Metal,Direct3D,Vulkan还是其他对象。
Renderer Graphics API场景抽象对象Flap Hero没有此类场景表示。在Flap Hero中,渲染是使用一组专用功能直接执行OpenGL调用来执行的。大多数有趣的事情发生在renderGamePanel()函数中。此功能先绘制鸟类,然后绘制地板,然后绘制管道,然后绘制灌木,背景中的城市,天空,云彩,粒子效果,最后绘制UI层。而已。不涉及任何抽象对象。
渲染器图形API对于Flap Hero之类的游戏,屏幕内容在每一帧都是相似的,这种方法可以很好地工作。
显然,这种方法有其局限性。如果您要制作一个涉及探索的游戏,那么屏幕内容在一瞬间到下一瞬间可能会有很大差异,那么您将需要某种抽象的场景表示形式。根据游戏风格,您甚至可以采用混合方法。例如,要使Flap Hero绘制不同的障碍而不只是管道,可以将绘制管道的代码替换为绘制任意对象集合的代码。 renderGamePanel()函数的其余部分将保持不变。
自从最初的Xbox以来,我开发的每个游戏引擎都包含某种着色器管理器。着色器管理器允许游戏对象使用某种“着色器键”间接引用着色器程序。着色器键通常描述渲染每个网格所需的功能,无论是蒙皮,法线贴图,细节纹理还是其他。为了获得最大的灵活性,通常使用某种反射系统将着色器输入自动传递到图形API。
皮瓣英雄没有。它只有一组固定的着色器程序。 Shaders.cpp包含15个着色器的源代码,而Text.cpp还包含2个着色器。有一个专用的着色器用于管道,另一个用于烟云颗粒,仅在标题屏幕中使用。所有着色器均在启动时进行编译。
此外,在Flap Hero中,所有着色器参数都是手动管理的。这意味着什么?这意味着游戏使用为每个着色器手动编写的代码提取所有顶点属性和统一变量的位置。同样,当将着色器用于绘制时,游戏会传递统一变量并使用为每个着色器手动编写的代码配置顶点属性。
matShader-> vertPositionAttrib = GL_NO_CHECK(GetAttribLocation(matShader-> shader.id," vertPosition"))); PLY_ASSERT(matShader-> vertPositionAttrib> = 0); matShader-> vertNormalAttrib = GL_NO_CHECK(GetAttribLocation(matShader-> shader.id," vertNormal"))); PLY_ASSERT(matShader-> vertNormalAttrib> = 0); ...
我发现自己真的很想念我为自定义游戏引擎开发的着色器管理器,该管理器使用运行时反射来自动配置顶点属性并传递统一变量。换句话说,它会自动处理很多“胶水代码”。我没有在Flap Hero中使用类似系统的主要原因是因为Flap Hero是一个示例应用程序,并且我不想引入过多的额外设备。
顺便说一句,Flap Hero使用glUniform系列OpenGL函数将统一变量传递给着色器。这种方法是老式的,但易于实现,如果不小心通过了着色器所不希望的制服,则可以帮助捕获编程错误。产生较少驱动程序开销的更现代的方法是使用统一缓冲区对象。
许多游戏引擎都集成了物理引擎,例如Bullet,Havok或Box2D。这些物理引擎中的每一个都使用类似于上述“抽象场景表示”的方法。它们各自在一个称为物理学世界的集合中维护自己对物理学对象的表示。例如,在Bullet中,物理世界由btDiscreteDynamicsWorld对象表示,并包含btCollisionObjects的集合。在Box2D中,有一个b2World包含b2Body对象。
您几乎可以将物理世界视为游戏中的一个游戏。它或多或少是自给自足的,并且独立于包含它的游戏引擎。只要游戏不断调用物理引擎的step函数(例如,Box2D中的b2World :: Step),物理世界就会继续独立运行。游戏引擎在物理步骤的每一步之后对其进行检查,利用物理对象的状态来驱动位置和位置,从而利用物理世界的优势。自己游戏对象的方向。
物理世界物理对象场景游戏对象Flap Hero包含一些原始物理,但未使用物理引擎。 Flap Hero所需要做的只是检查那只鸟是否与某物相撞。为了碰撞,将鸟视为球形,将管道视为圆柱体。大部分工作由sphereCylinderCollisionTest()函数完成,该函数检测球体与圆柱体的碰撞。球体可以与圆柱体的三个部分碰撞:侧面,边缘或顶盖。
Side Edge Cap对于像Flap Hero这样的街机风格的游戏,只需要一些基本的碰撞检查,就足够了。物理引擎不是必需的,只会增加项目的复杂性。集成物理引擎所需的代码量可能会大于自己执行碰撞检查所需的代码量。
话虽如此,如果您正在使用已经具有集成物理引擎的游戏引擎,那么使用它通常很有意义。对于需要多个物体之间碰撞的游戏,例如第一人称射击游戏中的碎片或《愤怒的小鸟》中坍塌的结构,物理引擎绝对是必经之路。
我所说的“资产”是指游戏加载的数据文件:主要是纹理,网格,动画和声音。我在先前有关编写自己的游戏引擎的文章中谈到了资产管道。
Flap Hero没有资产管道,也没有游戏专用格式。它的每个资产都是从用于创建资产的格式加载的。游戏使用Assimp从FBX导入3D模型;使用stb_image从PNG解码纹理图像;加载TrueType字体并使用stb_truetype创建纹理图集;并使用stb_vorbis解码33秒的Ogg Vorbis音乐文件。所有这些都在游戏启动时发生。尽管处理量很大,游戏仍然可以快速加载。
如果Flap Hero拥有资产管道,那么大多数处理将使用离线工具(通常称为“炊具”)提前进行,并且游戏将更快地开始。但我并不担心。 Flap Hero只是一个示例项目,我不想介绍其他构建步骤。最后,我不得不承认,缺乏资产管道使某些事情变得更加困难。
如果您在Flap Hero中探索材质与3D网格关联的方式,您将会明白我的意思。 例如,用于绘制鸟类的材料具有多种属性:漫反射色,镜面反射色,边缘光色,镜面指数和边缘光衰减。 并非所有这些属性都可以FBX格式表示。 结果,我最终忽略了FBX材质属性,并完全在代码中定义了新材质。 有了资产管道,就没有必要了。 例如,在我的自定义游戏引擎中,我可以在Blender中定义任意材质属性,并将其直接导出为灵活的游戏格式。 每次导出网格时,即使游戏在移动设备上运行,游戏引擎也会即时对其进行重新加载。 这种方法非常适合迭代时间,但显然首先需要进行大量工作。