SaberCore中的多线程与消息机制

经过这么多天艰难的摸索,SaberCore的多线程和消息机制终于初见雏形了。虽然实现得不是很高效,但丑媳妇总要见婆婆,还是在这里分享出来。

SaberCore中的多线程:

SaberCore是完全运行在时间轴上的引擎。我曾在这篇日志中介绍过时间轴动画系统的设计,其实动画时间轴是由普通的时间轴派生而来:

普通时间轴控制着调用频率,引擎中的各组件以回调函数的方式将自己注册进时间轴:

细心的人会发现我之前的截图中,窗口FPS永远都是62帧 : ),这是因为渲染模块所在的时间轴被限制了调用频率。

时间轴分为主轴和次轴,所有主轴运行在主线程上(即使调用频率不一样),所有次轴运行在分支线程上,并且任意两条次轴不会运行在同一个分支线程上。这样,SaberCore是一个天生的多线程引擎,但她也可以很容易地变为单线程的(只需要让所有的时间轴都跑在主线程上)。

SaberCore中的消息系统:

有点类似安卓,如果在其他线程上直接调用渲染线程的函数进行绘制,那肯定会出问题。因此,不同的时间轴之间通过传递消息来进行通信。消息的第一个字段是name,起到了id的作用。所有的消息统一由消息路由器负责分发管理,消息路由会保证任何两个不同类型的消息不会具有相同的name属性。

为了接受和发送消息,我们要做以下几件事:

  1. 创建一个消息队列用以接收消息,每个消息队列有一个独一无二的名字以作标识。
  2. 向消息路由中注册事件,其格式为(事件名称, 消息队列名称)。
  3. 向消息路由中put事件,路由会根据事件的名字将事件递送到指定的消息队列。
  4. 用户周期性地从自己的消息队列中fetch事件。

这是一个邮差模型,一条消息只有一个目的地,不支持广播。消息路由就像是邮差一样,将收集到的信件按照信件上的地址递送到不同的邮箱。其实,就游戏引擎内部而言,一条消息并不需要有多个接收者,所以这样的一个模型是够用的。

下面的图展示消息、消息队列和消息路由之间的关系。虽然画得有点龊,但应该够直白。

由于消息系统是为多线程设计的,因此每个消息队列都拥有自身的锁以防止不好的事情发生。云风的这篇博文中介绍了一种无锁的多线程消息队列,非常具有启发性。但要实现这样一个队列难度很大。首先它是用指针移动的原子操作来避免多线程下的竞争条件,但这个原子操作要怎么实现我还没想明白 – – 。再者就是我为了贪图方便,大量使用了stl中的容器以及boost中的智能指针。而这样的一个队列要想高效地实现,必须要使用原始的指针和自定义的数据结构。所以说,还是算了吧~


分享一下我在编码过程中遇到的一些小问题吧。

在EventRouter中,我将多个EventQueue放入一个std::map中管理,没想到出现了编译错误。我找了半天,终于发现原因出在boost::mutex上。我使用了boost::mutex作为多线程锁,boost::mutex是不可拷贝的,它继承自boost::noncopyable。当我把EventQueue放入std::map中时,会发生拷贝行为,于是就出现了编译错误。解决方法也很简单,就是放指针而不是放实例。

我之前在某本书上看到这么一句话“现代的C++程序中完全可以做到不出现delete”,结果一时头热在引擎中大量使用了boost::shared_ptr,包括消息。我的消息体并不是一个C struct而是一个类,不同类型的消息通过派生这个类来实现。我的消息队列中存放的不是消息体,而是消息的指针。当然,这个指针是经过shared_ptr包装的。
比如说我有一个叫做scTestEvent的消息是派生自scEvent的,所以我可以这样:

shared_ptr<scEvent> evt = shared_ptr<scEvent>(new scTestEvent());
scEventRouter::getSingleton().putEvent(evt);

那么当我将其从消息队列中取出来,并想转回scTestEvent类型,一开始我是这么做的:

shared_ptr<scTestEvent> e = shared_ptr<scTestEvent>(static_cast<scTestEvent>(evt.get()));

你们可以先无视这个蛋疼的语法。这里最大的问题在于,我将scTestEvent的原始指针从一个shared_ptr中取出来后,又拿去构造了另一个shared_ptr。这样,当其中一个指针先被销毁时,它会顺带销毁内部的原始指针。此时另一个智能指针内部的原始指针就变成了野指针。

下面是我随后的做法:

scTestEvent* e = static_cast<scTestEvent*>(evt.get());

这样就不会出问题了。

4.1 在二维中解决三维问题

本文是Game Engine Architecture的一部分
免责声明

在以下章节中,我们将学到许多在二维和三维中都能使用的数学运算。这是个很好的消息,因为这意味着有时候你可以通过在2D中的想象和画图来解决一个三维的向量问题(这是相当容易做到的!)。可悲的是,这种二维和三维之间的等效性并不一直成立。一些运算,比如交叉乘积(向量积),仅在三维中有定义。尽管如此,先使用一个简化的二维版本来考虑眼前的问题几乎总是无害的。一旦你理解了二维中的方法,你就能想到这个问题是如何扩展到三维中去的。在某些方面,你会惊喜的发现在二维中做出的结果在三维中同样可用。而在其他方面,你总能找到一个坐标系将三维中的问题化作二维。在这本书中,我们总会采用二维图示。

4 游戏中的3D数学

本文是Game Engine Architecture的一部分
免责声明

游戏是一种数学模型,它以某种方式对虚拟世界进行实时仿真。因此,数学覆盖了我们在游戏业界所做的所有工作。事实上游戏程序员几乎利用了所有的数学分支,从三角函数到代数到统计再到微积分。然而到目前为止,作为一个程序员你所最常用到的数学知识是三维向量和矩阵数学(如: 三维线性代数)

然而这样的一个数学分支依旧博大精深,因此我们不能期望在但单一的章节里覆盖它很多的知识。相反,我会试着对游戏程序员所需要的数学工具进行一个概述。与之同时,我会给出一些技巧和窍门,它们会帮助你记忆所有复杂的概念和规则。关于这个主题,我强烈推荐埃里克 ·阿帕的[28],书中对游戏3D数学进行了精彩而深入的讨论。

今天初步完成了OIS输入模块的整合(InputManager)

这两天在SaberCore中整合OIS输入模块,到今天算是初步完成了。目前我的设计中,InputManager会将某种输入事件(鼠标移动,键盘按下之类的)与某个lua脚本中的某个回调函数绑定起来。这样,用户就可以在lua脚本中处理输入事件。当然了,绑定的工作还是需要用户在C++中完成的。在将来我会考虑把绑定的操作也放在脚本中实现。

以下是对应的lua脚本

我在将C++中的枚举类型导出给lua时遇到了一些小问题,那就是luabind官方文档中只提供了导出类中的枚举常量的演示,所以我并不知道如何导出普通的枚举常量。在寻找方法未果后,我小小地偷了下懒,将普通枚举常量当做类枚举常量导出,这个luabind是允许的。所以如果在C++中的枚举变量叫做KC_F,那么到了lua中就变成了KeyEvent.KC_F,因为我将它附在KeyEvent类中导出了。

有了新工具,生产力就是不一样

今天部分完成了在lua脚本中定义场景。还是验证了那句老话,科学技术是第一生产力啊!有了新工具,我瞬间就为Ogre和SaberCore生成了一堆一堆的包装。只要我愿意,我甚至可以用lua把Ogre完全包一遍……不过那毫无意义。

上图中我使用lua脚本在场景中放置了一个摄像机,一个Ogre头以及三个视口。代码截图:

和python一样方便!

AutoWrapLuaApi v0.3

今天顺手把自动为lua包装C++的脚本给完善了下,总体来说v0.3版本有以下改进:

  1. 仅导出public区域的api
  2. 支持成员函数重载
  3. 支持运算符重载

还存在许多问题:

其一是依旧未实现导出类的static函数。

其二是成员函数重载有一些问题,但我觉得是luabind的问题。我的导出格式是没有问题的,但有一些重载函数无法通过C++的编译。例如以下这两条函数:

.def("ToAngleAxis", (void (Matrix3::*)(Ogre::Vector3 &,  Ogre::Radian &))&Matrix3::ToAngleAxis)
.def("ToAngleAxis", (void (Matrix3::*)(Ogre::Vector3 &,  Ogre::Degree &))&Matrix3::ToAngleAxis)

貌似luabind无法区分Radian和Degree两个类型,再比如下面两条函数

.def("ToAxes", (void (Quaternion::*)(Ogre::Vector3 *))&Quaternion::ToAxes)
.def("ToAxes", (void (Quaternion::*)(Ogre::Vector3 &,  Ogre::Vector3 &,  Ogre::Vector3 &))&Quaternion::ToAxes)

貌似在luabind眼中,一个Vector3 * 和三个Vector3 &是同一种东西?但下面这条加了const的又可以通过编译

.def("FromAxes", (void (Quaternion::*)(const Ogre::Vector3 *))&Quaternion::FromAxes)
.def("FromAxes", (void (Quaternion::*)(const Ogre::Vector3 &,  const Ogre::Vector3 &,  const Ogre::Vector3 &))&Quaternion::FromAxes)

这些依旧需要在C++中手工编辑一下

其三是luabind仅支持有限的几个运算符重载,分别是:
+   –   *   /   ==   <   <=
其余的运算符重载均无法导出。注意,以上的运算符均为二元运算符。

工具github地址:https://github.com/kidsang/LuabindAutoWrap

自动为C++类生成luabind api

在我的上一篇日志中提到使用luabind来导出C++类会很方便,但是通常来说,一个类中都会有几十个函数需要被导出,而我们手头上往往有几十个这样的类。如果要人工去一个一个为它们写导出API,那将会是一件十分令人崩溃的事情!对于boost::python来说,有一个现成的工具(具体叫什么名字请询问亮哥…)。但luabind就没有这般幸运了~所以我决定自己写一个。

我最初的做法是完全使用正则表达式来匹配各种类和函数,花了一整天时间做出来一个有着许多错误,但经过人手动修改修改也勉勉强强能用的版本。在这个过程中我深深地了解到了从“还不错”到“完美”之间存在着多大的鸿沟。这让我下定决心要用词法分析的做法重新写一个。但是C++的词法何其复杂,如果要自己手写一个那就实在是太浪费时间了!经过亮哥指点,我使用clang作为词法分析器,并使用其python包装,完成了为C++自动生成luabind api的第一个版本。

工具github地址:https://github.com/kidsang/LuabindAutoWrap
因为我已经把clang的dll也放在了仓库里,所以理论上来说,只要下载下来就可以用了。这是个使用python写成的工具,所以为了使用它你首先需要下载安装python2.7

源码主要有三个文件,其中parse.py负责调用clang的函数对文件进行分析,construct.py负责输出格式化后的api代码,autowrap.py是一个调用工具进行api包装的例子。

目前版本为0.1,主要的功能是为C++的类导出luabind api。其中包含了以下缺陷:

  1. 不支持静态成员函数,事实上,它会把它们当做非静态的函数导出,需要手动去删……
  2. 不支持运算符重载,并且不会进行导出。
  3. 不支持成员函数重载,会导出多个重复的api,需要手动去删……

这些缺陷我会慢慢地改进。之所以是“慢慢地”,主要原因是这个工具主要是我写给自己用,为了辅助SaberCore开发的。目前的程度已经可以为我节省大量的时间了,在向其中投入时间会使回报不成比例。不过我会抽空完善。

使用的注意事项:如果你想为一个文件file1.h生成api,而file1.h中有#include “file2.h”,那么你必须保证他们之间的路径正确,否则clang会因为找不到file2.h而解析失败。

下面是为OgreMatrix4生成api的例子。

导出结果的截图:

使用luabind,告别lua原生C API

我在上一篇日志中提到,由于多线程的原因,弃用python而改用lua。绝大部分的游戏都会在启动的时候从外部读取某种地图文件来加载场景,而不是直接把场景写死在C++中。在不考虑加密性和文件大小的情况下,我决定直接使用lua文件作为地图文件。这样,我可以先在C++中写好函数,封好接口,再由lua来调用这些函数,来完成场景的创建工作。

整个流程大概是这样的:

创建新的场景实例(C++) -> 场景实例调用特定的lua地图文件(C++调用lua) -> lua地图文件中调用C++中的函数完成场景创建(lua调用C++) -> 场景创建完成,开始游戏(C++)

抽象起来,大概有以下几点

  1. C++中有个叫做Scene的类,class Scene。
  2. 类Scene自己有许多写好的场景创建函数,例如createBuilding(),createVehicle()等等。为方便起见我们就用一个createObject()来代指这些函数吧。这些函数都是非静态成员函数,它们要被导出为lua可使用的函数,并被lua所调用。
  3. 类Scene中有个非静态成员方法叫做loadMap(),该方法负责调用lua脚本。
  4. lua脚本中在通过调用Scene类中的成员函数来创建场景。

如果要使用lua原生的API来完成这些事情,那简直是复杂得难以想象。不过幸好,我们有luabind,它的使用方式与boost::python十分相似,十分方便。这里是luabind的使用文档,十分详细。如果不想看英文的话,这里有中文版的。

为了演示luabind的用法,我首先在C++中写一个名为Scene的类

class Scene
{
public:
	Scene()
	{}

	void createObject(int obj);

	void printObjects();

	void loadMap(std::string fileName);

private:
	std::vector<int> mObjects;
};

在这里,我用一个int型的数组来模拟场景中的物体,createObject()方法就是向数组中添加数字,printObjects()会把数组中的东西一起过打印出来。

void Scene::createObject( int obj )
{
	mObjects.push_back(obj);
}

void Scene::printObjects()
{
	for (auto iter = mObjects.begin(); iter != mObjects.end(); ++iter)
		std::cout << (*iter) << std::endl;
}

在loadMap()方法中,我们打开一个lua虚拟机,将Scene中的成员函数导出,并调用lua脚本中的createScene()函数来创建场景。

void Scene::loadMap( std::string fileName )
{
	try
	{
		using namespace luabind;

		lua_State* L = lua_open();
		luaL_openlibs(L);
		luabind::open(L);
		// 导出类Scene
		module(L)
			[
				// 导出的类名字不必与C++中的一样
				// 方法也是这样
				// 但是为了看着方便
				// 我让它们名称都一样了
				class_<Scene>("Scene")	
				.def(luabind::constructor<>())	
				.def("createObject", &Scene::createObject)	
				.def("printObject", &Scene::printObjects)	
				// 注意到我并没有导出loadMap()方法
			];
		// 加载lua文件
		luaL_dofile(L, fileName.c_str());
		// 调用lua文件中的createScene方法
		luabind::call_function<void>(L, "createScene", this);
	}
	catch (luabind::error& e)
	{
		std::cout << e.what() << std::endl;
	}
}

在lua中的脚本是这样的:

-- map.lua
function createScene( scene )
	for i=0, 10 do
		scene:createObject(i)
	end
end

现在,我在main函数中调用Scene的loadMap函数:

void main()
{
	Scene s;
	s.loadMap("test.lua");
	s.printObjects();
	system("pause");
}

大功告成!

游戏引擎中多线程和网络的一些实践,近日一些小总结

自己整个游戏引擎真心不容易,即使是最简陋的,也需要花费很多的心机。我感觉我的思维就在不断地跳来跳去,今天还在考虑时间轴动画,第二天就发现有些地方需要和多线程装载一起考虑,第三天又跳去考虑网络。果然我的经验还是太浅了,感觉就是把大学没有掌握的知识都过了一遍。

一开始我准备使用python来作为游戏引擎的脚本,主要原因是boost::python已经很好地封装了C++和python的接口,使用起来非常方便。然而在多线程中这就出现问题了。因为Python的虚拟机使用GIL,这个玩意的存在使得Python中基本不可能存在真正的多线程,无法发挥多核的优势。不得已之下我只能转投Lua。一开始使用Lua我尝试这自己封装Lua和C++交互的接口,我希望可以在C++中调用Lua,然后又从Lua中调用回C++某个类的非静态成员函数。我发现要完成这样的工作,封装实在是太蛋疼了,我一度徘徊在崩溃的边缘……幸好我在网上找到了luabind,它使用了与boost::python类似的方法来封装C++和Lua的接口。谢天谢地!生活瞬间变得简单很多。

另外就是网络。我希望我的游戏引擎可以支持小型的P2P局域网对战,也就是类似CS和魔兽的那种。我遇到的第一个问题就是这样的一个引擎如何实现单机游戏。我的想法是,把单机看作是多人游戏的一个特殊版本(受GEA启发)。一开始尝试让服务器和客户端同时运行在一个进程的两个不同线程中,之间使用socket进行通讯。事实证明这样做……是不行的。程序运行起来会出“端口已经被打开”的错误。看来socket只能提供两个进程之间的通信,而在一个进程的两个线程中,由于使用的是同一个socket文件描述符,因此无法打开两次。

但毫无疑问魔兽争霸是一个单进程单线程的游戏。因为即使在游戏最卡的时候,魔兽CPU占用率都不会超过50%(我的是双核)。并且,在开启游戏时,我只能看见一个魔兽进程。我猜测CS就更是如此了,毕竟在那个年代,多核CPU是闻所未闻的概念。突然我觉得我陷入了思维的死路,在单机游戏时(或者说是作为多人游戏的主机时),服务端和客户端可以在两条不同的线程上,但它们完全没必要使用socket通信啊……

网络游戏的流程大概是这样的:1.服务端计算->2.服务端将计算结果包装为游戏事件(或之类的东西)->3.服务端将事件封成数据包,并将数据包发给客户端->4.客户端接包,解封成游戏事件->5.客户端处理游戏事件。如果是本机连本机的情况,步骤3和4是可以去除的,服务端和客户端之间可以直接使用一个双缓冲队列来完成数据交换。应该只需要一个if..else语句,就可以很好地区分是本机还是网络,如果是网络的话还是加入步骤3和4。


昨天是第一天去公司上班实习。不知道为什么,上班真的很累,晚上回到宿舍什么都不想干了。尽管由于我是第一天,一整天基本上是无所事事。我猜可能是因为换了陌生的环境以后人变得比较紧张,精神压力也大的原因吧。大概过几天就可以适应了。公司的环境还不错,饭堂伙食也很好。而且我环顾四周,发现貌似我们组的显示器比其他组的都要大一圈。CPU是i7四核3.5GHz;内存8G;显示器不知道是二十几寸,但我两个笔记本拼在一起应该都没它大。我猜开发组由于经常编译的原因,机器要好一点才行吧。

但……唯一让我蛋疼的是……我们组居然是在……用Java……噢……