探索 C++ 在游戏开发中的应用
开发电子游戏时需要考虑哪些因素
要想制作一款属于自己的电子游戏,首先你需要一个有趣的游戏创意,而这正是游戏制作的一大难关。假设你已经有了游戏创意,并想把它变成现实。为此,你需要编写代码来实现所有功能,设计美术和音效来吸引玩家,并且竭尽所能确保游戏运行流畅,为用户带来愉悦的游戏体验。
确保游戏流畅运行是许多游戏开发者的首要任务,而选择合适的编程语言通常与此息息相关。C++ 以其“速度、灵活性以及直接与硬件通信的能力”而闻名。[1]在深入探讨 C++ 高效运行的原因之前,让我们先来了解一下其他一些用于游戏开发的语言,以及它们在哪些方面可能比 C++ 更具优势。
C++与其他游戏开发语言的比较
在讨论 C# 和 Java 之前,我想先重点介绍一下游戏开发中出于不同原因使用的其他几种语言。
JavaScript、Python 和 Lua
JavaScript 广泛应用于网页、服务器,甚至树莓派(Raspberry Pi)上,后者已成为制作便携式游戏系统的热门选择。一些游戏引擎支持 JavaScript 代码,例如 Impact.js、LÖVE 和 Crafty.js。[2] Python 和 Lua 在学习新的编程语言时更容易上手,并且都得到了许多开源游戏引擎的支持。这三种语言主要用于 2D 游戏。它们也可以用于 3D 游戏,但由于 JavaScript 对 3D 游戏的支持有限,而 Lua 则完全不支持,因此建议使用 Python 来开发 3D 游戏。我计划在以后的文章中花更多时间研究这些语言在游戏领域的应用。
C#(升号)
C# 得到了许多游戏引擎的支持,包括 Unity 和 MonoGame。C# 与 C++ 类似,据报道,“使用 Visual Studio 和 VS Code 作为集成开发环境 (IDE) 时,C# 的设置更简单、更便捷。” [2] C++ 和 C# 之间也存在许多差异,在选择编程语言时应考虑这些差异。我们将重点介绍其中几个差异[3]:
内存管理
在 C++ 中,程序员需要手动管理内存,而 C# 则有自动垃圾回收机制。C# 在这方面更方便,但 C++ 赋予程序员对内存使用方式的更多控制权,这有助于优化代码,使其运行速度更快,性能更流畅。如果项目规模较小,则可能不需要如此精细的内存控制。
指针
指针是一种引用内存地址的方式,我将在本文后面详细介绍。在 C++ 中,指针可以在程序的任何位置使用,而 C# 只能在不安全模式下使用指针。“不安全代码由于其固有的复杂语法以及潜在的内存相关错误(例如堆栈溢出、访问和覆盖系统内存),可能会导致稳定性和安全性问题。” [4]这意味着在 C# 中使用不安全模式时需要格外小心,也就是说,在编写项目代码时可能需要避免使用指针。
Java
Java 是一种面向对象的编程语言,类似于 C++,并且“Java 虚拟机 (JVM) 用于运行字节码,这使其几乎与任何平台兼容。” [2] Java 和 C++ 之间的区别与 C# 类似,让我们来探讨一下原因[5]:
内存管理
在 Java 中,内存管理由 Java 虚拟机 (JVM) 控制,而 C++ 是手动控制的。
平台依赖性
两种语言都可以在任何平台上运行。Java 使用 Java 虚拟机 (JVM) 来实现这一点。而 C++ 则需要正确的编译器来编译代码,使其适用于正确的平台。
指针
Java 可以使用指针,但对指针的支持有限。C++ 完全支持指针,甚至允许按引用调用值,而 Java 只能按指针的值进行调用。
选择 C++ 进行视频游戏开发
在开发电子游戏时,资源复用是自然而然的事情。设置障碍让玩家克服,并在之后再次遇到相同或类似的障碍,可以营造一种游戏进程感,展现玩家在游戏过程中的成长,或者让他们学会用不同的视角看待和克服障碍。这种结构听起来很像编程中的类,一种在整个应用程序中多次复用代码的方法。选择面向对象的编程语言对于更顺畅的开发过程至关重要。幸运的是,我提到的这些语言都支持基于类的编程技术。接下来,我们来看看为什么 C++ 经常被游戏项目选用。
就性能而言,使用 C++ 开发的视频游戏在运行速度和流畅度方面优于其他语言,尤其是在开发可扩展性强的游戏时。C++ 通过赋予程序员对内存管理的直接控制权来实现这一点。其他语言则通过垃圾回收器自动处理这些任务。“理解指针、内存分配和内存泄漏可以帮助你的游戏流畅运行,避免浪费宝贵的资源。” [1]我们将通过一些代码示例来详细讲解这三点。
指针
指针是一种变量,它将内存地址存储为其值。由于指针存储的是地址,因此我们可以进行按引用调用。指针可以“创建和操作动态数据结构” [6],并可用于遍历这些数据结构。
要使用指针,需要定义一个与要引用的数据类型相匹配的指针变量。数据类型必须与指针关联,以便指针“知道数据存储在多少字节中”。[6]使用一元运算符 & 指定要存储在先前创建的指针上的变量地址。要访问存储在某个地址上的值,请使用一元运算符 * 指定指针。
#include <bits/stdc++.h>
using namespace std;
void pointers() {
int var = 20;
// Declare pointer variable
// Note that the data type of ptr and var must be the same
int* ptr;
// Declare pointer of a pointer variable
int** ptr2;
// Assign the address of a variable to a pointer
ptr = &var;
// Assign the address of a pointer to another pointer
ptr2 = &ptr;
// ptr holds the address of var
cout << "Value at ptr = " << ptr << endl;
// var holds the value of 20
cout << "Value at var = " << var << endl;
// * dereferences ptr to give the value of 20
// located at the address assigned to ptr
cout << "Value at *ptr = " << *ptr << endl;
// ptr2 holds the address of ptr
// Even pointers have addresses of their own
cout << "Value at ptr2 = " << ptr2 << endl;
// Dereferencing ptr2 once reveals that
// ptr2 references the same address as ptr
cout << "Value at *ptr2 = " << *ptr2 << endl;
// Dereferencing ptr2 twice reveals 20,
// the value you receive when dereferencing ptr once
cout << "Value at **ptr2 = " << **ptr2 << endl;
}
int main() {
pointers();
/*
* Value at ptr = 0x6caebffc54
* Value at var = 20
* Value at *ptr = 20
* Value at ptr2 = 0x6caebffc48
* Value at *ptr2 = 0x6caebffc54
* Value at **ptr2 = 20
*/
return 0;
}
由于我们存储了其他数据类型的地址,因此可以模拟按引用调用。我们可以在函数内部修改任何数据类型,并在代码后续部分重用更新后的数据。
动态内存分配
如果我们预期输入数据量达到某个最大值,我们可以预先为程序预留足够的内存来应对最坏情况。但随着项目规模的不断扩大,这可能会成为一个问题。如果我们能够只在确定所需内存量后才分配内存呢?这样可以避免不必要的内存使用,并使代码运行更加高效。这就是动态内存分配的核心原则:只分配完成当前任务所需的内存空间,并在任务完成后立即释放剩余空间。
请查看以下代码[7],其中添加了注释,以帮助您了解我们如何实现动态内存分配:
#include <iostream>
#include <new>
using namespace std;
int main() {
// i => Used for looping
int i;
// n => Used for capturing the input for the first question
int n;
// ptr => Pointer used to reference memory that is allocated
int* ptr;
// n is assigned the input from the user
cout << "How many numbers would you like to type? ";
cin >> n;
// new => Allocates space match the input for n
ptr = new (nothrow) int[n];
// Check to make sure ptr points to a valid object
if (ptr == nullptr) {
cout << "Memory allocation error!" << endl;
}
else {
for (i=0; i<n; i++) {
// Ask for a number n times
cout << "Enter number: ";
// Store the number entered in memory
cin >> ptr[i];
}
// Reveal to the user the choices they made
cout << "You have entered: ";
for (i=0; i<n; i++) {
// Pull each number from the allocated memory
cout << ptr[i] << ", ";
}
// After we finish with the task,
// we free up the space taken using delete[]
delete[] ptr;
}
return 0;
}
在 C++ 编程中,使用 delete 关键字至关重要。由于程序员需要手动清理内存,因此养成在内存使用完毕后及时清理的良好习惯,可以减少挫败感和 bug,并避免可怕的内存泄漏。
内存泄漏
在 C++ 中,没有自动垃圾回收机制,这意味着程序员在程序生命周期内动态分配的任何内存,都需要在使用完毕后手动释放。如果程序员忘记释放这些内存,它们就会一直占用空间,直到程序停止运行,其他进程也无法访问。通常,一个函数可能需要被多次调用,如果每次调用都分配了新的内存而没有及时释放,就会积累大量的未使用内存。这种不必要的内存占用被称为内存泄漏。内存泄漏会显著降低程序的运行速度,但为了提高代码的运行速度,这是可以避免的。[8]
关于 C++ 在游戏开发中的应用的最后思考
完全掌控内存使用是一大优势,或者说是“锦上添花”。我们创建指针来存储新创建的数据结构的地址。存储的内存使用完毕后,我们会将数据从内存中移除,以便在程序后续执行时释放空间,防止内存泄漏。C++ 的面向对象编程语言特性也体现了代码重用的重要性。
C++并非游戏开发语言的唯一选择。Java具有良好的可移植性,可以跨平台使用,但在制作3D游戏方面存在局限性。C#(夏普)比C++更简单,但除非使用不安全模式,否则它对指针的支持几乎为零。不安全模式不建议使用,除非您在该模式下编程时格外小心。
由于 C++ 允许程序员直接控制内存,因此可以利用指针和动态内存分配实现极快的运行速度。速度是设计大型视频游戏时需要考虑的重要因素,因为响应迟缓或无响应的游戏体验会让用户感到不满。
祝您编程愉快,
Tyler Meyer
来源
资料来源:
[1] https://www.geeksforgeeks.org/cpp-for-game-development/
[2] https://www.orientsoftware.com/blog/gaming-programming-language/
[3] https://www.geeksforgeeks.org/c-vs-c-sharp/
[4] https://www.c-sharpcorner.com/UploadFile/f0b2ed/understanding-unsafe-code-in-C-Sharp/
[5] https://www.geeksforgeeks.org/cpp-vs-java/
[6] https://www.geeksforgeeks.org/cpp-pointers/
[7] https://cplusplus.com/doc/tutorial/dynamic/
[8] https://www.geeksforgeeks.org/memory-leak-in-c-and-how-to-avoid-it/
文章来源:https://dev.to/tymey/why-are-people-choosing-c-for-game-development-29fp