| 越's profile三无之徒PhotosBlogLists | Help |
|
|
April 27 关于《游戏创造》文章的错误近日读〈游戏创造〉2006.3期中“设计模式与游戏开发”一文,文章主要讲的是设计模式在游戏开发中的应用,并举了3个实例,其中第三个关于“游戏对象访问控制”中作者提出使用 Proxy 模式来解决对象访问中的“野指针”问题,并给出相应的代码。当我看到这里时,我产生了疑问:
1. 对于句柄的使用,文中说效率会有影响,值得商榷。但据我测试,如果使用 stl hash map 来存储的话,队列中有 1 亿个对象,每次访问 1 万个不同对象总计时间不会超过 0.0008s(PIV 3.0G),更何况游戏中的队列几乎不会有这么大的数量,所以使用 hash map 从效率来说还是可以满足要求的。
2. 文中提出用 Proxy 模式解决问题,但是 Proxy 模式的提出并不是为了解决访问控制的问题的,而是屏蔽掉对象细节,比如两种不同的数据库 API 我们希望能做到通过统一接口去访问,而不是针对每种 API 来写不同的实现方式,这时就可以使用 Proxy 模式。所以在使用的模式方面,我对作者的观点产生了怀疑。我反复仔细看了作者附上了实现代码,总的来说就是对象和 Proxy 都保存彼此的指针,并且都指向自己,而且还保存引用其他对象的 Proxy 指针列表,这样当某个对象销毁时,会通知在其内部的代理对象,其他引用该对象的对象再进行访问时,首先访问其 Proxy 指针,如果 Proxy 指针内部的对象指针为空,则对象不能访问。但是问题在于,Proxy 中只保存唯一的一个被代理对象的指针(这是关键,因为如果存在多个指针的话,还是会存在野指针的问题),也就决定了对象的 Proxy 只能被一个对象所引用,这样当出现多个对象引用同一对象时,依然会出现 野指针的问题。如果我说得不清楚的话,可以通过下面的代码进行测试,为了说明问题,我简化并去掉了原文代码中不相关的部分。
template < typename T >
class Proxy { public: T* m_p; Proxy( T* p ) : m_p(p) { if ( p ) p->SetProxy( this ); } void Reset() { m_p = NULL; } }; template <typename T>
class Proxiable { Proxy<T>* m_proxy; public: Proxiable() : m_proxy(NULL){} virtual ~Proxiable(){ if ( m_proxy ) m_proxy->Reset(); } void SetProxy( Proxy<T>* proxy ){ m_proxy = proxy; } }; class Player : public Proxiable<Player>
{ char* m_name; Proxy<Player>* m_player; public: Player(char* name) : m_name(name),m_player(NULL){} void AddRef( Player* p){ m_player = new Proxy<Player>(p); } char* GetName(){ return m_name; } void UseRef(){ if ( m_player->m_p ) printf(m_player->m_p->GetName());} }; int _tmain(int argc, _TCHAR* argv[])
{ Player* p1 = new Player("p1\n"); Player* p2 = new Player("p2\n"); Player* p3 = new Player("p3\n"); p2->AddRef( p1 ); p3->AddRef( p1 ); delete p1; p2->UseRef(); p3->UseRef(); return 0; } 程序会 crash 在红色标注处。当然也有改进方法,就是在每个对象中加入一个 Proxy 列表,这样当对象销毁时,迭代列表每一项,实际上就是 mvc 的通知方式。不过这样不仅编写代码麻烦,而且每个需要这样功能的类要从 Proxyable 继承,拖泥带水,很是丑陋。更难以忍受的是 每个对象要多出 n*4 个空间来存储 Proxy 指针,并没有达到作者“轻量”的目的。所以综合考虑,句柄方式来实现对象访问控制是目前比较好的选择,而且 hash 算法也保证了查找效率,这也是 windows 操作系统内部标准的对象访问控制机制。 |
|
|