发布者:
2011 年 5 月 16 日(最后更新:2014 年 12 月 24 日)

如何制作游戏

评分:4.2/5(1471 票)
*****
最近有人提醒我,很多人都在询问游戏开发的问题,但关于这个话题的文章却不多。我决定就游戏从头到尾的开发过程概览性地介绍一下。请记住,这主要是一个概述,并且:A. 不适用于所有项目。B. 不是一个完整的、一步一步的指导。您仍然需要自己弄清楚很多东西才能制作出一款游戏。



第一步:选择您的游戏库
除非您想自己编写所有琐碎的图形/声音编程库,否则您可能需要获取一个游戏库。市面上有许多游戏库,但它们都提供相同的基本功能

您希望您的库具备的功能
  • 加载和渲染图像的方式
  • 加载和播放音频的方式
  • 基本的图像处理(旋转等)
  • 基本绘图功能(圆形、线条、矩形、点等)
  • 渲染文本的能力
  • 跟踪时间和等待的能力
  • 创建和控制线程的能力(有益,但非必需)

一些游戏库包括




第二步:定义概念
所有游戏都从这里开始,仅仅是某人脑中的想法。
首先,想出一个游戏创意。一旦有了简单的想法,就扩展它。例如,如果是一款棋盘游戏,目标/获胜方式是什么?规则是什么样的?等等。如果您的游戏有角色或故事情节,就去创造它们。确保您对完成后的游戏有一个相当明确的概念。游戏越复杂,越应该在开始时就规划好,这样在编写代码时就不必过多地担心游戏本身。请记住,您的游戏在创建过程中会不断发展。





第三步:规划您的引擎
如果您制作的是棋盘游戏或基本的街机游戏,您可以完全跳过此步骤,直接编写您的游戏。然而,对于更复杂的游戏,您可能需要考虑使用预制引擎,或编写自己的“引擎”。您可能会问,什么是游戏引擎?虽然它们在结构和整体功能上差异很大,但您可以将游戏引擎视为一个超级强大的库,它提供更高级的功能,如物理、资源处理和游戏实体管理。无论您选择使用现有引擎还是创建自己的引擎,都取决于您,并且取决于您实际想做多少编程。使用预制引擎将大大简化您的工作,使您更多地专注于编写游戏玩法/事件的脚本。

为什么我说“规划”而不是“选择”?因为您很可能不会制作下一个《上古卷轴》,因此,您可以创建自己的“引擎”。请记住,您不会创建下一个虚幻引擎,并且您编写的大部分代码以及打算使其可重用的代码(这正是引擎的意义)最终将与您的游戏逻辑纠缠不清,以至于难以轻易重用。考虑到这一点,如果您的“引擎”的某些部分依赖于特定于游戏的 C++ 代码,不必担心,这很正常。与其专注于创建一个完全可重用、超级健壮的框架,不如专注于确保代码可读、有组织且功能齐全。先专注于制作游戏,然后再尝试创建可移植模块。如果您绝对必须编写一些有用且可重用的东西,资源管理器和其他各种实用类是很好的起点。





第四步:编写您的引擎(如果您打算自己写)

现在是时候开始编写您的引擎了,前提是您选择了这条路线。这不一定是指游戏本身,而是核心渲染、物理和文件处理;基本上是用于构建游戏的函数和类。简单的游戏实际上不需要太多框架,可以直接使用游戏库进行编程。大型游戏中最重要也是最被忽视的组件之一是资源管理器。资源管理器(据推测)是一个类,负责加载资源(如图形和声音),确保资源仅加载一次,并在不再需要时卸载资源。RAM 不是无限的,所以如果您的游戏为宇宙中的每一块草加载同一图像的单独副本,那您就麻烦了。下面是 Xander314 提供的一个优秀资源管理器示例。


Xander314 的资源管理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/*
 ResourceManagerB.hpp - Generic template resource manager				
									
 (C) Alexander Thorne (SFML Coder) 2011	
 <a href="http://sfmlcoder.wordpress.com/">http://sfmlcoder.wordpress.com/</a>	
		
 Manages loading and unloading of a resource type specified by a
 template argument.

****************************************************************/

#include <map>
#include <string>
#include <exception>

typedef const std::string URI;

// exceptions
namespace Exceptions {

	// thrown if user requests a resource URI not present in the manager's list
	class URINotFound : public std::runtime_error 
	{ 
	public: 
		URINotFound(const std::string& Message = "The specified URI was not found in the resource index.")
			: runtime_error(Message) { } 
	};

	// thrown if a resource allocation fails
	class BadResourceAllocation : public std::runtime_error {
	public: 
		BadResourceAllocation(const std::string& Message = "Failed to allocate memory for resource.")
			: runtime_error(Message) {}
	};
}

template <class Resource> class ResourceManagerB {
	typedef std::pair<URI, Resource*> ResourcePair;
	typedef std::map<URI, Resource*> ResourceList;

	// the list of the manager's resources
	ResourceList Resources;
public:
	~ResourceManagerB() { UnloadAll(); }

	// Load a resource with the specified URI
	// the URI could represent, e.g, a filename
	URI& Load(URI& Uri);
	// unload a resource with the specified URI
	void Unload(URI& Uri);
	// unload all resources
	void UnloadAll();

	// get a pointer to a resource
	Resource* GetPtr(URI& Uri);
	// get a reference to a resource
	Resource& Get(URI& Uri);
};

template <class Resource>
URI& ResourceManagerB<Resource>::Load(URI& Uri)
{
	// check if resource URI is already in list
	// and if it is, we do no more
	if (Resources.find(Uri) == Resources.end())
	{
		// try to allocate the resource
		// NB: if the Resource template argument does not have a
		// constructor accepting a const std::std::string, then this
		// line will cause a compiler error
		Resource* temp = new (std::nothrow) Resource(Uri);
		// check if the resource failed to be allocated
		// std::nothrow means that if allocation failed
		// temp will be 0
		if (!temp)
			throw Exceptions::BadResourceAllocation();
		// add the resource and it's URI to the manager's list
		Resources.insert(ResourcePair(Uri, temp));
	}
	return Uri;
}

template <class Resource>
void ResourceManagerB<Resource>::Unload(URI& Uri)
{
	// try to find the specified URI in the list
	ResourceList::const_iterator itr = Resources.find(Uri);
	// if it is found...
	if (itr != Resources.end())
	{
		// ... deallocate it
		delete itr->second;
		// then remove it from the list
		Resources.erase(Uri);
	}
}

template <class Resource>
void ResourceManagerB<Resource>::UnloadAll()
{
	// iterate through every element of the resource list
	ResourceList::iterator itr;
	for (itr = Resources.begin(); itr != Resources.end(); itr++)
		// delete each resource
		delete itr->second;
	// finally, clear the list
	Resources.clear();
}

template <class Resource>
Resource* ResourceManagerB<Resource>::GetPtr(URI& Uri)
{
	// find the specified URI in the list
	ResourceList::const_iterator itr;
	// if it is there...
	if ((itr = Resources.find(Uri)) != Resources.end())
		// ... return a pointer to the corresponding resource
		return itr->second;
	// ... else return 0
	return 0;
}

template <class Resource>
Resource& ResourceManagerB<Resource>::Get(URI& Uri)
{
	// get a pointer to the resource
	Resource* temp = GetPtr(Uri);
	// if the resource was found...
	if (temp)
		// ... dereference the pointer to return a reference
		// to the resource
		return *temp;
	else
		// ... else throw an exception to notify the caller that
		// the resource was not found
		throw Exceptions::URINotFound();
}



您的引擎/框架的另一个重要方面是接口。当您编写游戏逻辑本身时,您不应该花费 4 个小时来编写主游戏循环,因为您要搜索数百个更新函数,试图找出您实际需要哪些。保持简单明了。如果您能通过一两个函数调用更新所有游戏逻辑,并通过一两个函数调用渲染场景,那么您就走在正确的道路上了。利用面向对象原则,如继承和纯虚基类(想想“接口”)是创建具有良好结构的框架的好方法。

例如,所有游戏对象的基类可以这样定义:
1
2
3
4
5
6
7
8
9
10
11
class GameObject
{
public:
    virtual ~GameObject()=0;

    virtual Vector2f getPosition();

    virtual bool interact(Object* o);

    virtual void draw(); //highly library dependent
};


现在所有子类都遵循这个接口,可以有一个持有实体,它能够轻松地存储和管理您定义的任何和所有对象,而不管该对象实际上是什么。随着您对您选择的语言的更多学习和编程,您会发现更多利用其各种功能来发挥优势的方法。





第五步:媒体(音频和图形)
到目前为止,您可能至少已经考虑过游戏的外观,并且可能已经有一套媒体资源。然而,如果您和我一样,您可能会因为自己想出的“漂亮设计”而感到兴奋和投入,以至于到测试阶段却没有任何图像可以在屏幕上舞动。现在是开始获取所需资源的好时机。如果您有艺术天赋,那太棒了。如果您没有,别担心,希望仍在。通过谷歌搜索可以找到大量的免费图形和音效。Audacity 和 GIMP 是编辑您获取或创建的任何内容的必备工具。





第六步:编写您的游戏
一旦您选择了引擎或拥有了自己的框架,您就可以着手编写游戏逻辑本身了。理想情况下,您在投入无数个小时来创建一个“引擎”而该引擎的功能越界到几乎无法使用但又不足以独立运行之前,已经完整地阅读了本文至少一次。您的框架应该提供一个基础,用于构建对象交互(但不一定定义它),并处理所有渲染和其他低级细节,如物理。游戏逻辑本身将定义对象交互(例如,通过定义 GameObject 的子类)、游戏规则(例如,什么构成输赢)以及游戏的初始状态(例如,先加载哪个地图,您开始时拥有什么物品等),并且将包含“主游戏循环”。

到底什么是主游戏循环?简单来说:它是一个循环,主循环。想想游戏运行时不断重复的事情,这些就是包含在这个神秘循环中的内容。例如,每次迭代,游戏都应该更新所有对象,然后将它们全部绘制到屏幕上。除了更新和绘制之外,主循环还可能负责计时。更新过多的游戏会显得速度极快,对用户来说很可能太难了。想想光速的乒乓球。理想情况下,这个循环将使用您之前创建的框架,并且本身会非常简单。请参阅下面的示例:

游戏循环
1
2
3
4
5
6
7
8
9
10
while (!Game.playerLost())
{
    world.update(); //assume this world object owns all of the GameObjects and updates them as well

    screen.clear();
    world.draw(screen);
    screen.display();

    ensureProperFPS(); //just a placeholder, put the actual timing logic right here in the loop
}






第七步:从中吸取教训
我谈论将框架与游戏逻辑分开创建的主要原因是为了让您学习编写可重用代码。然后我告诉您不要担心使其真正可重用,而是专注于编写游戏。我坚持这一点,初学者放弃项目的一个主要原因是他们花费大量时间和精力来为他们的游戏“编写引擎”,但他们还不知道一个好的引擎应该包含什么,或者一个真正可行的结构/接口。在浪费了所有这些时间之后,他们一无所获,然后因此感到气馁并放弃。通过先专注于编写游戏,然后是可重用代码,您最终应该会得到一些可以看到的东西。这是您辛勤付出的有形回报,也是您继续努力工作的原因。

现在您已经有了一款满意的可玩游戏,您可以尝试将代码模块化为可移植的。您是否编写了一个很棒的资源管理器或处理键盘输入的绝佳类?尝试使其完全可移植,这样您就可以轻松地将源文件复制到其他项目中,“开箱即用”。如果您想在下一个项目中完全从头开始,那也没关系。您不必从项目中实际提取代码才能从中获得东西。只要您在这个过程中学到了东西,一切都是值得的。




第八步:打包和分发
经过所有这些工作,您可能希望人们实际玩您的游戏!将所有必需的文件打包到一个 zip 文件、压缩存档或可执行安装程序中,然后发送给所有人!





技巧
我从制作游戏中学习了很多东西,有些是艰难的。以下是一些您应该做的事情:
  • 首先,保持条理!您应该有一个良好的组织系统来处理一切;您的代码、媒体、文档等。文件夹有其存在的理由,请使用它们!

  • 另外,尽量保持代码干净、易读。给函数起有意义的名字,并尽可能简化一切。

  • 记录!我在文章中没有真正谈到它,但请记录所有内容!记录所有数据文件的格式,并记录所有函数和类的作用。您根本不知道这能节省多少时间以及可以避免多少头痛,直到您真正去做。

  • 从小处着手。不要一开始就尝试制作下一个《宝可梦》游戏。从小型、可管理的开始,并根据您的技能扩展您的目标。尝试承担一个超出您能力范围的项目只会让您气馁。

  • 着眼于目标!我太多项目失败的最大原因之一是我过分纠结于细节而忽略了更大的图景。是的,我的云、雨和雷声、脚印和雾效都很漂亮,但我最终没有完成游戏。您可以稍后使其变得漂亮,先制作游戏!

  • 享受乐趣!游戏的意义在于乐趣,制作游戏也可以很有趣。如果您喜欢工作,完成任务总是更容易的。当然,有时您会感到沮丧,但如果您发现自己过于生气,那就休息一下!散步并思考其他事情通常是解决问题的最佳方法。回来后,您将有一个新的开始,这可以帮助您找到一个在之前的思路中可能想不到的解决方案。

chrisname 的一些入门建议
您不必如此辛苦。您需要做的是,去学习编程教程(例如此网站上的那个)。不要一天做太多,否则您会感到厌倦和失去动力。不要设定基于时间的 C++ 目标,那样没用。如果您在中途停止学习,您会忘记很多东西。按照此网站上的教程进行学习(https://cplusplus.net.cn/doc/tutorial/)。目标是每天完成两课。不要在中途停止(除非是为了短暂休息,那是好事),并且不要一次学太多,否则您根本记不住。我建议阅读并复制每一个示例(不是复制粘贴;自己输入,这将帮助您理解您正在做什么),编译它,看看运行它时会发生什么,然后修改内容以查看会发生什么变化。我还建议您查看别人的代码(帮助我的一件事是获取别人损坏的代码并尝试修复它,尽管一开始不要太纠结于此,因为阅读别人的代码很难)。阅读时,请尝试重新表述:“如果你不能简单地解释它,你就没有足够好地理解它”(阿尔伯特·爱因斯坦)。

一旦您完成了这个教程,以及也许还有一些其他的教程(我读了大约三个不同的教程,因为用不同的方式解释东西很有用——我发现用两种不同的方式解释东西对于理解和记住它很有帮助),您可以阅读 SFML 的教程(http://sfml-dev.org/tutorials/1.6/)。学习 SFML 将教会您制作 2D 游戏。我也推荐学习 SDL(http://lazyfoo.net/SDL_tutorials/index.php),因为很多游戏都使用它,您很可能会在某个时候遇到它。

在那之后,如果您想制作 3D 游戏,您应该开始学习 OpenGL 编程。SFML 使这一点变得非常容易,SFML 教程中包含了使用 OpenGL 的教程。对于 OpenGL,也许这里有人可以推荐您一本书或一个教程。



在所有这些过程中,您应该记住,掌握节奏很重要。不要试图一次性吸收太多东西,否则您会忘记很多。而且,如果您后天有考试,就不要熬到凌晨 3 点……