下一步的一些小计划

前一段时间思绪还是很混乱的关键是不知道自己以后能干啥。之前一路走来做的项目大多与游戏有关,但是后来慢慢地觉得一来老做游戏没意义,二来网易又不要我,所以开始考虑“转行”事宜。

我算是个比较纠结的人。我喜欢华丽的东西,对东西的外观又很看重,以至于大部分项目中我都客串了一把美工。悲剧在于,全世界的前端招的都是学Java,Js一类的人。对于java,我是挺反感的。对于js,我觉得这玩意过于简单。现在是个程序员都会帮自己整个主页,写几段htmlcssjs不在话下。让我拿这个全世界都会的东西养家糊口,实在是很虚。怎么办呢……最熟悉的语言是C++,可要用到这货的大概是在写操作系统,图像引擎,以及处理海量数据吧。鉴于国内没几个公司在做前两者,我只好硬着头皮去看看所谓的后台。

后台也真够蛋疼的,大部分情况是考验你对linux和unix的熟悉程度而不是编程水平。我看了几天的套接字,也自己写了写小验证程序,发现实在是看不下去。然而在一个很偶然的机会,我看到了一个叫做云风的人的博客。这家伙算是国内元老级的游戏开发者了,他的博客上有许多关于网游服务器端实现的讨论。看着看着我还是有很大感触的,原来游戏的服务器端这么复杂,而且和普通的后台服务端有很多的共性。我突然觉得,游戏也是软件的一种,而且还是一种非常复杂且坑爹的软件。也许我不应该放弃对游戏这块的追求,我可以将我从中所学到的知识运用到其他软件的开发中。

前天上网下载了开源的网游服务器端实现Mangos,然而由于一直在看云风的博客所以还没来得及研究。不过在看完云风关于游戏开发的博客后,我反而不想去研究Mangos了。原因很简单,缺乏环境。既然我找不到那种大型的数千个玩家同时接入服务器的环境,那么无论我如何研究都是在纸上谈兵。但是一些思想,比如进程间通信,比如负载均衡,这些都是共通的基础知识,需要从书中找到答案。

所以接下来我应该会干这么些事:
1. 重新把Ogre给安了,好好研究下其中的实现,这应该是目前来说对我帮助最大的。
2. 好好看看Unix编程艺术,Unix网络编程和操作系统,恶补一下自己对操作系统和进程的疏漏。
3. 做做编程珠玑和算法之美里面的题,这个是为了应付万恶的笔试。
4. 无聊的时候阅读下代码大全和程序员修炼之道。

要看很多书

今天是被电话吵醒的,申通快递的人嚷嚷着让我去拿快递,说我刚才给你发短信了你怎么这么晚还不来拿。我对申通一向无甚好感,回了一句你什么j8态度然后就挂电话继续睡。再次醒来发现已经是十二点半,突然有点后悔干嘛不早点起床去拿快递,其实是书,Unix编程艺术,我又要等多一天才有书看了。

这学期我心态变化还是蛮大的。一月份时感觉自己无所不能,意气风发,有种“没有我一个晚上解决不了的问题”的感觉。后来去腾讯面试,被虐了个七荤八素,才知道自己的底子有多么的薄弱,实在不足以支持自己骄傲的资本。再后来的各种笔试面试,弄得我身心俱崔。看着自己手里的小公司offer,再看看身边的人都去了网易腾讯,心中很是酸楚,感觉就要这样被拉开距离了。

幸好认识了亮哥,他说,你要看很多很多很多的书。亮哥算是良师益友这个级别的了,说的很多话都非常有道理。我这个人生性浮躁,最讨厌看书。不过被各种笔试面试虐了那么久,倒也发现了很多东西确实在书里面才有。再看看亮哥,书多得可以把自己埋起来= =。于是在亮哥的“怂恿”下,花了百多块钱买了基本书开始读了起来,没想到这样却发现了读书的乐趣。写代码是灰常累的工作,当你不熟的时候,你感觉写代码就像是在查字典;当你熟悉的时候,写代码又像是在练打字。相比起来看书就要轻松多了,一支笔一杯咖啡。我甚至习惯于在走廊上站着边吹风边看书。也许是之前各种项目的积累,我看书的速度非常的快。书中的知识和曾经的经验一相验证,理解起来非常舒服。

妖都最近风大,在走廊上真的非常舒服。

突然发现我已经一个月没回家了,昨天爸打电话给我,问我五一要不要一起去泡温泉。真是一件非常幸福的事情,泡着温泉看着书,当然是要去的。再过几天我生日,不知道会不会下很大的雨。去年下了很大的雨,和某小朋友一起过了个颇有意义的生日,挺值得怀恋。

Python反转字符串的方法碉堡了

有个同学在网易面试时被问到:在python中如何反转一个字符串。我同学当时想都没想就说:“不是有个函数叫做reverse吗?”
事实上,对于str类型,python并没有reverse函数。然而,通过反向步进切片,我们可以高效地反转一串字符串。

>>> string = 'abcde'
>>> string[::-1]
'edcba'

STL中deque和list的效率比较

我在做项目的时候经常会遇到一些情况,是需要用到队列的。比如,我要维护消息队列,或者动画队列。STL中有两种容器胜任此项工作,它们分别是list和deque。list是基于链表实现的,而deque是基于动态数组实现的。《C++标准程序库》一书中介绍了两者的差别,但并没有比较两者的效率。由于今后我会经常地使用到这两种结构,因此我对两者的效率进行了一下测试。

测试的类型fuck定义如下:

struct fuck
{
int a,b,c;
}

测试平台为win7,环境为vc2010,stl版本为P.J. Plauger。

测试1:容器大小为十万个fuck,循环进行pop_back()和push_front()操作

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i {
d.push_front(d.back());
d.pop_back();
}
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i {
l.push_front(l.back());
l.pop_back();
}
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   312    308    307    312
list        1046  1044  1043  1044

测试2:容器大小为十万个fuck,先使用pop_back清空,然后使用push_front插入十万个新的fuck

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i d.pop_back();
for (int i=0; i d.push_front(fuck());
clock_t end = clock();

begin = clock();
for (int i=0; i l.pop_back();
for (int i=0; i l.push_front(fuck());
end = clock();

return 0;
}
测试结果:(毫秒)
deque   97         96  
list        11756   11250 

这不科学……我也懒得再进行下去了

测试3:容器为空,然后使用push_front插入十万个新的fuck

int main()
{
int len = 100000;
list<fuck> l;
deque<fuck> d;

clock_t begin = clock();
for (int i=0; i<len; i++)
d.push_front(fuck());
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i<len; i++)
l.push_front(fuck());
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   628    632    625    618
list         383    377    375    379

测试4:容器大小为十万个fuck,使用pop_back清空

int main()
{
int len = 100000;
list<fuck> l(len);
deque<fuck> d(len);

clock_t begin = clock();
for (int i=0; i<len; i++)
d.pop_back();
clock_t end = clock();
cout << end – begin << endl;

begin = clock();
for (int i=0; i<len; i++)
l.pop_back();
end = clock();
cout << end – begin << endl;
system(“pause”);
return 0;
}
测试结果:(毫秒)
deque   34         34  
list        10241   10326

事实证明,大规模的pop操作会使list慢得像吃屎,这让我很不解…… 当参照测试1的结果,我就更加不解了。希望有高手能够帮忙解答。

C++的默认拷贝构造函数(Copy Constructor)

何时我们该使用C++编译器提供的默认拷贝构造函数,何时该自己重写?

有一个Class X如下:

class X
{
public:
string _str;
X(string str) : _str(str) { } 
}

它的拷贝构造函数应该是长成这样子的:

X::X(const X& x);

可是这里我们并没有手动帮X类写出拷贝构造函数,所以编译器会在认为有必要的时候以inline的形式创建一份编译器版本的拷贝构造函数。它的样子应该是这样的:

X::X(const X& x)
{
this.str = x.str;
// 如果x类还有更多的成员
// 那么拷贝函数会依次拷贝它们
// 像这样:
// this.a = x.a;
// this.b = x.b;
// ...
}

并且,由于我们并没有手动重载赋值号(=),所以编译器会自动生成一份=的重载,实际上就是调用copy constructor。下面分情况研究一下。

void main()
{
X *a = new X("a");
X *b = a;
X c("c");
X d = c;
}

由于a和b都是指针,所以把a的值赋予b并不会调用X的copy constructor,没有任何复制操作发生,b现在仅仅把指针指向了a所指的位置。
但是对于c和d来说,就发生了复制操作。如果我们分别打印出&(c._str)和&(d._str),会发现他们确实指向了不同的位置。

但是考虑一下的一种情况,让我们修改一下X类:

class X
{
public:
string *_str;
X(string str) : _str(&string(str)) { } 
}

注意到X类中的成员已经变成了指针。那么当我们这样做时,会发生什么事情呢?

void main()
{
X *a = new X("a");
X *b = a;
}

答案是,复制过程确实发生了,但是由于类成员是指针,所以仅仅发生了指针的复制。也就是说,如果我们分别打印出&(c._str)和&(d._str),会发现他们指向了不同的位置。但当打印出a._str和b._str时,会发现它们其实是一样的。

分别是&(c._str),&(d._str),a._str,b._str

此时,问题的答案已经很明显了。当类中存在指针变量,而我们又想对这些指针所指的地方进行复制时,默认拷贝构造函数是无能为力的,可以考虑自己重写。

OGRE中的单例模式实现

单例模式(Singleton)是最基本的设计模式之一,许多的项目中都会见到它的身影。然而,虽然单例模式概念简单,但要真正实现得好就不那么容易了。

以下是我自己实现的一个非常简单的Singleton类

class Singleton
{
private:
// 以下四句显示声明了Singleton类的
// 构造函数,析构函数,复制函数和赋值语句
// 如果不这么做,那么编译器就会帮我们自动生成它们public版本
// 这会让我们的单例模式毫无意义……
Singleton();
~Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
// 声明为static,表示在整个程序中仅存在这一个实例
static Singleton* instance;
public:
static Singleton* GetInstancePtr()
{
if (!instance)
instance = new Singleton();
return instance;
}
};
// 在此初始化实例的指针为null
// 如果不这么做,编译器会出现LinkError
Singleton* Singleton::instance = 0;

但这样的一个类是无法被其它类继承的,因为当你调用GetInstancePtr函数时,总会返回Singleton类型的指针。因此我们不得不为每个想要实现单例的类重写一遍以上代码,这有违重用的原则。因此,我们要将Singleton类改造成模板类,这样其它的类只要继承自Singleton类,就可以实现单例。

以下是Ogre中Singleton.h的源码

/** Template class for creating single-instance global classes.
*/
template class Singleton
{
private:
/** \brief Explicit private copy constructor.
 This is a forbidden operation.*/
Singleton(const Singleton &);

/** \brief Private operator= . This is a forbidden operation. */
Singleton& operator=(const Singleton &);

protected:

static T* ms_Singleton;

public:
Singleton( void )
{
assert( !ms_Singleton );
#if defined( _MSC_VER ) && _MSC_VER < 1200
int offset = (int)(T*)1 - (int)(Singleton *)(T*)1;
ms_Singleton = (T*)((int)this + offset);
#else
ms_Singleton = static_cast< T* >( this );
#endif
}
~Singleton( void )
{ assert( ms_Singleton ); ms_Singleton = 0; }
static T& getSingleton( void )
{ assert( ms_Singleton ); return ( *ms_Singleton ); }
static T* getSingletonPtr( void )
{ return ms_Singleton; }
};

第一次看见ogre的这个版本我觉得十分奇怪,因为它的构造函数居然是public的。但仔细观察会发现它的构造函数中有一个断言,规定了在调用构造函数的时候实例不能存在。我上面写的那个Singleton类是在GetInstancePtr函数中判断是否存在实例,如果没有就new一个,这种做法叫做 Lazy Initialize。而ogre的Singleton类则规定了,如果想要调用GetSingleton,必须先要使用构造函数来初始化类。这种做法看起来多此一举,但却是非常有意义的。

考虑一种情况,类A想要实现单例,于是继承了Singleton类。然而类A的构造函数需要传入两个参数x和y。如果使用Lazy Initialize,那么就需要单独为类A再写一个形如Initialize(x, y)的函数,并规定在调用GetSingleton之前必须先要Initialize,这反而变得更麻烦。而使用Ogre的方式,虽然也要手动调用一遍构造函数,但Singleton内部已经规定了构造函数只能被调用一次。

以下的例子显示了类Fuck该如何正确继承Singleton类

class Fuck : public Singleton
{
public:
	Fuck() {};
	~Fuck() {};
	// 防止Link Error
	static Fuck& getSingleton()
	{
		assert(ms_Singleton);
		return *ms_Singleton;
	}
	// 防止Link Error
	static Fuck* getSingletonPtr()
	{
		return ms_Singleton;
	}
};
// 如果不初始化,编译器会报Link Error
template<> Fuck* Singleton::ms_Singleton = 0;

为什么我要在Fuck类中重载一遍getSingleton函数和getSingletonPtr函数呢?这个是因为嘛……其实我也不太清楚。不过Ogre给出的解释是这样的:

/** Override standard Singleton retrieval.
        @remarks
        Why do we do this? Well, it's because the Singleton
        implementation is in a .h file, which means it gets compiled
        into anybody who includes it. This is needed for the
        Singleton template to work, but we actually only want it
        compiled into the implementation of the class based on the
        Singleton, not all of them. If we don't change this, we get
        link errors when trying to use the Singleton-based class from
        an outside dll.
        @par
        This method just delegates to the template version anyway,
        but the implementation stays in this single compilation unit,
        preventing link errors.
        */

但说实话我没看懂他想说什么……

另外值得一提的是,ogre版本的Singleton类提供了两个获取instance的函数,一个返回指针(getSingletonPtr),一个返回引用(getSingleton)。一开始我十分不解为什么还要返回指针,我觉得两者都一样~。~但是后来发现,getSingleton中有断言而getSingletonPtr中没有,也就是说getSingletonPtr不提供实例已初始化的检测。所以我们可以这样:

if (!Fuck::getSingletonPtr())
{
Fuck f = new Fuck();
}

另外今天上网看到一篇讲关于单例的文章,介绍如何在单例模式中实现多个实例。上面说他的单例中instance不止有一个,而是像这样
std::vector instance;
他给出的解释是:“因为我的系统中有多个实例,但是是有限个,而且已知数量”
我个人觉得既然是这样干嘛还要用单例,应该直接在这些实例上面再包一层XXManager类,简直是误人子弟。

Clover 项目总结

很遗憾clover未能进入微软ImagineCup的中国区半决赛,这个项目也因此而结项了。虽然因为不能再继续前进而感到很可惜,但有一点值得高兴的是终于可以不用那么忙了~。~按照惯例,还是要总结一下自己做过的东西,免得以后忘记了。

Clover演示视频:http://v.youku.com/v_show/id_XMzc0MTAyMDg0.html

clover最初是在2012年1月6日开始的,到2012年3月29日结束。开发人员四人,分别是KidETNeil HsuFish。大部分的开发时间是在图书馆七楼的小黑屋度过的。

clover在开发过程中由于为了提高开发效率一共换了三套SDK,最初是C++/Ogre,然后是Python/Py-Ogre,到最后是C#/WPF。每当换一个SDK,我们都要为搭环境和学习新语言痛苦得死去活来。不过这个过程极大地提升了我们的学习能力和对新语言的适应能力。前两天为了弄chidongxi.me,又非常蛋疼地啃php和sql,结果就是我现在已经基本上忘记C++要怎么写了。接下来的时间要静下心来好好看书,把母语C++给重新捡起来。

说一下我觉得clover里面比较好的一些设计。

首先,受Ogre的影响,clover中大量使用了单例的设计模式(甚至我觉得已经过量了)。使用单例的好处就是实例不会被重复创建,在节省了空间开销的同时还方便了不同模块间的调用。如果是在C++中,我们肯定会专门写一个Singleton的模板类,然后再让需要使用单例的类派生自Singleton。然而在C#中并没有模板这个概念,虽然我们知道C#有种东西叫做泛型(Generics),但是我们谁也没打算深入研究C#,所以我们使用了一种非常愚蠢的做法,就是为每个需要使用单例的类都写了一次单例……

另外我们还使用了观察者模式。在C#中实现观察者倒是比在C++中轻松了许多,因为C#有种东西叫做Delegate(托管?)。C#在这方面已经帮我们做了许多东西。

clover中我们使用了分层的形式,各层之间的耦合还是比较松散的。其中我主要负责的部分是蓝色和红色,也就是UI层和渲染层。

渲染层的工作主要是根据上层传下来的数据对mesh中的一部分进行更新。我忘记我是从哪里看到的一句话,大意是越接近底层的东西就越要简单,因此我在设计的时候非常注意渲染层模块的复杂度。最后事实证明,渲染层在绝绝大多数情况都运行良好,非常可靠。

在UI层,wpf已经提供了非常强大的界面功能,基本上关于界面的所有东西都可以通过编写XAML来完成。然而,非界面的视觉效果还是需要自己来实现的。比如,当进入折叠模式的时候,会显示非常多的视觉提示信息。这些视觉效果相互独立,且每个都有动画效果。对于视觉效果,我写了个叫做Visual的基类,在该类中规定了视觉效果的基本行为。比如,所有视觉效果都会有淡出和淡入的动画。这样,在后期当我想要加某种特定的视觉效果,我只需要派生自Visual就可以了,要另外写得代码非常少。另外我还写了一个类叫做VisualManager,该类中通过一个List来管理各种视觉效果,保证它们能以正常的方式显示和销毁。

模块间如果出现循环调用,将会是非常悲剧的事情,它可能会导致死循环或无限递归。我们写了一个controller类来负责调用其他各类,形成了一个星形结构,避免了循环调用。

由于折纸算法涉及到了大量的数学计算,因此我们将一些常用的数学计算函数(如计算空间中两线段交点)写成静态函数放在Utility模块中。这极大地提高了我们的开发效率。

由于UI层处于2D空间,逻辑层和渲染层处于3D空间,因此少不了2D空间坐标和3D空间坐标的相互转换运算。如果每当需要进行3D-2D转换时都重新乘一遍所有的矩阵(模型到空间矩阵,空间到摄像机矩阵,摄像机投影矩阵,投影到屏幕矩阵)将会非常耗时。首先我通过固定摄像机在000点,仅移动模型的做法减少了矩阵的运算量。另外每当模型发生旋转或位置改变时,我都会预先运算好矩阵并储存在一个静态变量中。这样,当需要将一个点从3D变换到2D,仅需要用自己去乘这个矩阵就行了。

 

最后再做一下自我的总结吧。为了比赛而做项目是一件非常辛苦的事情,首先有个deadline压着你,另外你无法完全按照自己的意愿去做想做的事。在clover的开发中我学到了很多新知识,但也逐渐发现了自己的缺点。我是个喜欢通过项目来学习东西的家伙,这样导致了我所学的知识很杂而不精深,基础也很薄弱。

看书和做项目是个挺矛盾的事情,因为时间有限,一旦进行其中一项另一项就难以进行。我觉得学习的过程就像是软件开发,也是需要迭代的。也许看书多了,就需要写写代码来消化下知识;当发现写代码出现瓶颈以后,就要通过多读书来汲取更多的知识。我一直相信一句话:“问题不在于你懂得多少知识,而在于当你需要知识时,你应该知道在何处找到它们。”但如果不读书的话,我无法知道该在何处寻找答案。

接下来的时间会比较清闲,是时候好好静下心来看看书。