• About
    • Resume
A Game Developer also plays some guitar

Tag Archives: C++

Game Framework的两种实现方式

April 24, 2011 2:11 pm / Leave a Comment / Benny Chen

一年多前,曾经写过一篇关于Game Engine Framework的文章,当时基本上是为了巩固并加深对framework的理解。最近又做了一些关于framework的工作,对于framework的实现方式又有了些新的认识。虽然我现在做的已经完全不是game了,不过方式对于game也同样适用。

这篇文章主要希望通过一些示例性的C++代码介绍game framework的两种实现方式。首先,我还是搬出一年多前的那篇文章里的game流程图,以下的一些代码也主要基于这张图实现。对于图的细节在这里不再赘述,可以再去翻看之前的那篇文章。

FoC of Game

1.通过继承

这是一种最传统的方式了,之前我一直使用这种方式。基本上是提供一个基类,基类封装并决定了整个程序控制流,同时基于该控制流,基类提供了一系列的接口(在C++里是虚函数),以供继承类override,并实现定制化的行为。就像下面的这个Game类,run函数决定了整个程序的执行逻辑,同时initialize,update,render等非纯虚函数则提供了接口以供用户定制并实现具体Game想要的行为。

// 通过继承来实现的framework
class Game
{
public:
	void run()
	{
		m_isQuit = false;
		initialize();
		while ( !m_isQuit ) 
		{
			handleInput();
			update();
			render();
		}
		destroy();
	}
	
protected:
	virtual void initialize() {}
	virtual void update() {}
	virtual void render() {}
	virtual void destroy() {}
	virtual void onMouse( Mouse mouse ) {}
	virtual void onKeyboard( Keyboard keyboard ) {}
	
private:
	void handleInput()
	{
		Event e;
		while ( popEvent( &e )  )
		{
			switch ( e.type ) 
			{
			case EVENT_QUIT:
				m_isQuit = true;
				break;
			case EVENT_MOUSE;
				onMouse( e.mouse );
				break;
			case EVENT_KEYBOARD:
				onKeyboard( e.keyboard );
				break;
			default:
				break;
			}
		}
	}
	
private:
	bool m_isQuit;
};

在上面的代码中,我把实现全部写在了类的定义头文件中,在这里只是为了省事,在现实中你最好还是分开在.h和.cpp文件中。而且在这里,Game类还可以做的更多,我这里只是为了说明实现方式,所以依照前面的流程图而尽量让它简单。另外,不要纠结这段代码中的Event,Mouse,Keyboard等几个类和popEvent方法,它们只是我为了将故事说得更圆满一点而假象出来的几个类,我想你应该能猜到它们是用来干嘛的。

基于此framework,一个具体的游戏只需要重写这些虚函数就可以了,像下面的这些代码。

class ConcreteGame : public Game
{
private:
	void initialize()
	{
		// do some real initialization
	}
	
	void update()
	{
		// do some real update
	}
	
	void render()
	{
		// do some real rendering
	}

	void destroy()
	{
		// do some real destroy
	}
	
	void onMouse( Mouse mouse )
	{
		// handle mouse event
	}
	
	void onKeyboard( Keyboard keyboard )
	{
		// handle keyboard event
	}
};

int main()
{
	ConcreteGame mygame;
	mygame.run();
	return 0;
}

2.通过Delegation模式

前一种方法有一个缺点,就是将程序的控制流和具体行为紧耦合在了一起,而且还必须使用继承,不易于扩展。现代软件设计的一些方法告诉我们,要尽量使用接口,且尽量使用组合而非继承。Delegation模式就可以帮我们达到这一目的。

何为Delegation模式,wiki上的解释一语中的:

Delegation is the simple yet powerful concept of handing a task over to another part of the program.

Delegation将一些task委托给程序的另外一部分来处理,以达到了行为使用者和具体行为的松耦合。

以下是通过Delegation模式重新实现的framework。

// 通过Delegation模式来实现的framework
class GameDelegation
{
public:
	virtual void initialize() {}
	virtual void update() {}
	virtual void render() {}
	virtual void destroy() {}
	virtual void onMouse( Mouse mouse ) {}
	virtual void onKeyboard( Keyboard keyboard ) {}
};

class Game
{
public:
	Game( GameDelegation *gameDelegation )
	{
		m_gameDelegation = gameDelegation;
	}
	
	void run()
	{
		m_isQuit = false;
		if ( m_gameDelegation == NULL )
		{
			return;
		}
		
		m_gameDelegation->initialize();
		while ( !m_isQuit ) 
		{
			handleInput();
			m_gameDelegation->update();
			m_gameDelegation->render();
		}
	}
	
private:
	void handleInput()
	{
		Event e;
		while ( popEvent( &e ) )
		{
			switch ( e.type ) 
			{
			case EVENT_QUIT:
				m_isQuit = true;
				break;
			case EVENT_MOUSE;
				m_gameDelegation->onMouse( e.mouse );
				break;
			case EVENT_KEYBOARD:
				m_gameDelegation->onKeyboard( e.keyboard );
				break;
			default:
				break;
			}
		}
	}
	
private:
	bool m_isQuit;
	GameDelegation *m_gameDelegation;
};

基于此framework,当需要具体实现一个游戏的时候,只需要实现GameDelegation接口即可,然后将Game类的GameDelegation设置为你所实现的具体的ConcreteGameDelegation类,代码如下。

class ConcreteGameDelegation : public GameDelegation
{
public:
	void initialize()
	{
		// do some real initialization
	}
	
	void update()
	{
		// do some real update
	}
	
	void render()
	{
		// do some real rendering
	}

	void destroy()
	{
		// do some real destroy
	}
	
	void onMouse( Mouse mouse )
	{
		// handle mouse event
	}
	
	void onKeyboard( Keyboard keyboard )
	{
		// handle keyboard event
	}
};

int main()
{
	ConcreteGameDelegation myGameDelegation;
	Game mygame( &myGameDelegation );
	mygame.run();
	return 0;
}

最近我正好做了一些iOS上开发的研究,发现Delegation在iOS框架中被普遍使用。比如,iOS中的每个应用程序对应的是一个UIApplication类,为每一个UIApplication,开发人员必须要实现一个特定的UIApplicationDelegate,并将它指定给当前的应用程序(在main函数中通过UIApplicationMain函数指定,或者是在nib文件中绑定)。在这个UIApplicationDelegate类中,开发人员就需要重写诸如didFinishLaunchingWithOptions,applicationWillTerminate这样的方法,就类似与上面game framework中的initialize,destroy等方法。当然iOS框架要复杂很多,它还用到其它一系列的设计模式,有空研究些这样设计是非常有趣的。

图片来自于这篇苹果官方关于iOS中delegation的介绍:http://developer.apple.com/library/ios/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html

Posted in: C++, Game Programming, iOS / Tagged: C++, delegation, framework, game, 实现, 继承

Lua相关问题整理(2)- 如何在C中为Lua提供同步调用接口

November 7, 2010 10:25 pm / Leave a Comment / Benny Chen

这个问题的具体描述是——C注册给Lua一个函数,但Lua调用该C函数并不能立即获得结果(比如需要访问远程服务器获取值),如何能让Lua停止并等待,直到获取到结果后,才继续执行接下来的脚本。

举个例子,比如说有一个C函数login,我们试图通过调用该函数以执行用户的登录操作并获取验证结果。首先看下面这段C代码:

// C code
int login( lua_State *L )
{
	string user = luaL_checkstring( L, 1 );
	string password = luaL_checkstring( L, 2 );

	// 该函数将user和password发送到服务器后立即返回,
	// 绝不要在此处阻塞,这将严重影响效率
	sendAuthenticationInfo( user, password );

	return 0;
}
// 注册给Lua
lua_register( L, "login", login );

可以看到,因为该函数需要访问远程的登录服务器,在系统中一般都采取异步操作(让系统阻塞等待结果是不可接受的)。但是这样Lua开发人员调用login函数时,也只能异步等待结果,下面是Lua中处理登录操作的代码。

-- Lua code
-- 当用户点击“登陆”按钮后,执行该函数
function onClickLoginBtn()
	local user = ...
	local password = ...
	login( user, password ) -- 只是发送login命令
end

--当获取login结果后的回调函数
function onGetLoginResult( bPass )
	if bPass == true then
		print( "Authentication succeeded." )
		...omitted code...
	else
		print( "Authentication failed." )
		...omitted code...
	end
end

这段Lua代码很好理解,首先onClickLoginBtn是一个按钮事件处理函数,当用户点击了”登录“按钮后,会触发该函数。该函数首先从界面上获取用户输入的user和password,然后调用了上面C中所定义的login函数。正如前面的C代码所写的,login函数不会马上得到认证结果,所以执行后马上退出。另一个函数是onGetLoginResult,这个Lua函数需要当C系统中获取到认证结果后被回调执行,以真正执行login的后续操作。所以,我们还需要在C中添加回调的代码。

// C code
// 当服务器端返回认证结果后
BOOL loginResult = ...
lua_getglobal( L, "onGetLoginResult" );
lua_pushboolean( L, loginResult );
lua_call( L, 1, 0 );

在这里,我们在C中hardcode了回调onGetLoginResult的代码,这很丑陋,不过可以避免,比如可以给前面注册给Lua的login函数增加一个参数callbackFunctionName,以让Lua显式的告诉系统当获取登录结果后的回调函数名称。

再次回到本文一开始所提出的问题,尽管在系统中的异步调用不可避免,但我们希望在Lua中能够有同步机制,即Lua脚本在得到验证结果后才允许被继续执行,如何才能做到这一点呢。

Lua有一个非常棒的coroutine机制,在Lua代码中可以通过协同程序来进行多线程,可以使用coroutine.yield和coroutine.resume来对协同程序进行挂起和恢复。需要达到上面所提出的目标,只需在系统中使用与coroutine.yield和coroutine.resume相对应的Lua C API——lua_yield和lua_resume即可。如下所示,login函数有些小改变:

// C code
int login( lua_State *L )
{
	string user = luaL_checkstring( L, 1 );
	string password = luaL_checkstring( L, 2 );
	sendAuthenticationInfo( user, password );
	return lua_yield( L, 0 );
}
// 注册给Lua
lua_register( L, "login", login );

可以看到,与前面的区别只有login函数的最后一句,调用了lua_yield后再返回,而不是return 0,这样就可以达到阻塞Lua脚本的目的。事实是,lua_yield是一个比较特殊的函数,它只能作为注册的C函数的返回值使用,否则调用失败。

当系统获得服务器端的登陆验证结果后,通过lua_resume即可恢复之前被阻塞的Lua。

// C code
// 当服务器端返回认证结果后
BOOL loginResult = ...
lua_pushboolean( L, loginResult );
lua_resume( L, 1 );

上面的代码是当你的系统中只有一个Lua虚拟机的情形,如果使用了多个Lua虚拟机,事情稍微有一点点复杂,login函数还需要将当前调用的lua_State存储下来,以便lua_resume的时候,可以知道恢复的是哪一个被阻塞的虚拟机。

OK,当C中有这样的实现后,Lua程序人员将会为此而高兴,因为Lua代码变得如此简洁。

-- Lua code
-- 当用户点击“登陆”按钮后,执行该函数
function onClickLoginBtn()
	local user = ...
	local password = ...
	if login( user, password ) == true then
		print( "Authentication succeeded." )
		...omitted code...
	else
		print( "Authentication failed." )
		...omitted code...
	end
end

系统or平台开发人员应当尽可能的为用户考虑,正如上面第二种解决方法所追求的那样。

Posted in: C++, Game Programming, Lua / Tagged: C++, coroutine, login, lua, resume, yield, 同步调用

[译]Google的C++风格参考指南(1)– 头文件

March 28, 2010 8:15 pm / Leave a Comment / Benny Chen

译者的话:

在一个开发团队中,很重要的一点是要遵循一个统一的风格,这样才能便于相互之间在代码上进行快速的交流和理解。反之,如果团队中的每个人只是按照自己的个性去写属于个人风格的代码,这必然会造成项目代码的五花八门,互相之间难以沟通,整个项目的代码质量低下,BUG率攀高,软件的可维护率也极差。长此以往,国将不国,团队将不团队,最终软件极有可能以一种极其混乱的状态收尾,这真是一个悲剧。

每一个C++程序员都有自己的编程风格,虽然大家的风格迥异,但也一定有一些共通的地方。我希望能找到一个对于C++风格的归纳和总结,这样在以后我的工作中,对我自己,甚至对我工作的团队,当然也对于正在阅读的你,能够有一个帮助和参考。所以,我决定翻译Google的这篇C++风格参考指南。当然,真正促使我去翻译这篇文章的原因是,我是一个C++技术和简洁代码风格的狂热追捧者。

注意,这只是一篇参考,它是Google的C++风格,你并不一定严格的遵循所有的规则,永远记住很重要的一点,风格永远跟着你的团队走。

本人能力有限,所以不一定每个地方都翻译的准确,另外一些不太好翻译的专业名词首次出现时,我会标注英文原文。

原文:Google C++ Style Guide

背景 (Background)

C++是很多Google开源项目所使用的开发语言。正如每一个C++程序员所熟知的,C++有很多强大的功 能,但功能强大也同时意味着复杂性(complexity),使得代码bug率更高且难以阅读和维护。

这篇参考指南 (guide)的目的在于,通过详细描述应该做的(dos)和不应该做的(don’ts),来控制我们撰写C++代码时的复杂性。这些规则令程序员在高效利用C++语言特性的同时,还能让手工写出的所有代码(code base)易于管理。

风格(style),或者可 读性(readability),我们把它称作为“用来管理我们C++代码的常用惯例(convention)”。不过用“风格”这个术语有些不当,因为这些惯例并不只是指代码源文件的格式。

让代码可管理,一种方式是加强代码的一致性(consistency)。令任何一个程序员能够快速的理解另一个程序员所写的代码是非常重要的。如果代码保持统一的风格并且遵循惯例,我们可以更加轻松的使用“模式匹配”(pattern- matching)推断出,不同的符号分别代表什么。创造这种代码公共性(common)需要习语(idiom)和模式(pattern),使得代码易于理解。在某些情况下,也许有好的理由来改变某些风格规定,但为了获得一致性,不管怎样我们还是得保持风格。

这篇参考指南所关注的另一点是C++的功能膨胀问题。C++是一个有很多高级特性的庞大语言。在某些情况下,我们限制甚至禁止使用一些特性。这样做是为了保持代码的简洁,并避免因使用这些特性而带来各种常见的错误和问题。指南里会列出这些特性,并解释为何限制使用它们。

Google 所开发的开源项目遵守这篇参考指南里所列出的规定。

注意,这篇指南不是C++教程,我们认为所有正在阅读的同志们都是熟悉C++的。

头文件(Header Files)

一般而言,每个.cc文件都有一个关联.h文件。但也有一些例外,比如用于单元测试(unit test)的.cc文件或者只包括一个main()函数的小型.cc文件。

正确的使用头文件可以让代码的可读性、规模(scale)和性能(performance)得到很大的改观。

接下来介绍的一些规则将会引导你越过使用头文件时的各种陷阱。

#define防护(The #define Guard)

所有的头文件都应该使用#define防护来防止多重包含(multiple inclusion),格式是<PROJECT>_<PATH>_<FILE>_H_

为了保证唯一性(uniqueness),该格式应该基于源文件在整个项目中的路径。比如说,项目foo中的头文件foo/src/bar/baz.h应该用如下的格式:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

…

#endif // FOO_BAR_BAZ_H_

头文件依赖(Header File Dependencies)

如果使用前置声明(forward declaration)就可以解决依赖,就不要用#include。

当你#include一个头文件的时候,你同时也引入了依赖关系————每当头文件有任何的修改,包含该头文件的代码也要重新编译。如果你的头文件还包含了其他的头文件,那任何对于这些文件的修改会导致任何包含了你的头文件的代码重新编译。因此,我们倾向于尽量减少使用#include,尤其避免在一个头文件中包含另一个头文件。

使用前置声明,可以显著的减少在你的头文件中#include其他头文件的次数。比如说,如果你的头文件要用到class File,但你不需要访问class File的详细定义,那你的头文件只需前置声明class File即可,而不是#include “file/base/file.h”。

那在哪些情况下,我们是只使用一个类(比如 class Foo),而无需访问它的定义呢?

  • 声明类数据成员类型为 Foo* 或者 Foo&
  • 声明(但不定义)参数或返回值为Foo的函数
  • 声明静态数据成员类型为Foo。这是因为静态数据成员在class之外定义

另 一方面,如果你的类继承自Foo或者你有一个类数据成员是Foo,那就必须包含Foo的头文件。

有些时候,使用指针 (scoped_ptr 更佳)成员而不是对象成员(object member)是明智的。然而这却降低了代码的可读性,并且导致性能损失。所以,如果这么做的目的只是减少头文件中的#include,那就别这么做。

当 然,.cc文件必须需要它所使用的类的定义,所以它会#include比较多的头文件。

内联函数(Inline Functions)

只当函数很小,比如只有10行或更少时,才定义它为内联函数。

解释:你可以以这种方式定义函数,该方式允许编译器将函数内联展开,而不是以传统的函数调用机制去调用它们。

优点:内联函数可以产生更加高效的目标代码(object code),但前提是内联函数要足够小。你可以随意的将这些函数设置为内联函数:访问函数(accessor),设置函数(mutator),以及其他的短小的并与性能紧密相关的函数。

缺点:过度使用内联函数会导致程序变慢。内联一个函数时,根据该函数的大小,内联会增加或降低代码的规模。内联一个很小的访问函数通常会降低代码规模,而内联一个非常庞大的函数则会大大增加代码的规模。在现代的处理器 (processor)上,因为使用指令缓存(instruction cache)的缘故,短小的代码通常跑的更快。

结论:

一个不错的经验法则是,不要内联超过10行的函数。要小心析构函数,它们通常要比表面上看到的要长很多,因为它们会隐式的调用成员或者基类的析构函数。

另一个有用的经验是,内联带有循环或者switch语句的函数是低效的(除非循环或switch语句从未被执行过)。

还有重要的一点是,就算函数被声明为内联,但它不一定就被内联。比如说,虚函数或者递归函数通常都不被内联。一般我们也不应该声明递归函数为内联函数。而通常我们把一个虚函数声明为内联函数的主要原因是,将它的定义放在类定义内,这样是为了图方便或者将它文档化(document)的行为,比如说在访问函数和设置函数上我们就是这么干的。

-inl.h文件(The -inl.h Files)

当你需要使用复杂的内联函数时,你可以使用文件名 后缀为-inl.h的文件来定义它。

内联函数的定义要放在头文件里,这样编译器在函数调用点才能访问到定义用于内联。然而,函数的实现代码通常放在.cc中,而且我 们也不喜欢把太多的实际代码放在.h文件中,除非这么做有利于可读性或性能。

如果一个内联函数的定义很短, 里面几乎没有任何逻辑,你应该把这样的定义代码放到.h文件中。比如说,访问函数和设置函数应毫无疑问地放在类定义中。稍复杂一些的内联函数定义也可能放 在.h文件中,这样能方便实现者(implementer)和调用者(caller)。尽管这样做可能使得.h文件有些笨重,但你可以把代码放到另一个 -inl.h文件中。这将实现(implementation)从类定义中分离,而且使得实现能够在需要的地方被包含。

-inl.h 文件还可以用于函数模板的定义,这使得你的模板定义易于阅读。

当然不要忘了,像其他的头文件一样,-inl.h文件 也需要#define防护。

函数参数顺序(Function Parameter Ordering)

当定义一个函数时,参数的顺序应该是:先输入参数,再输出参数。

C/C++函数的参数可以是函数的输入,函数输出,或者两者兼有。输入参数通常是值(values)或者常量引用(const reference),而输出参数和输入/输出参数是非常量指针(non-const pointer)。对于函数参数的排序,将纯输入参数放在任何输出参数之前。尤其不要因为一个参数是新添加的,就把它放在函数参数的最后,要将纯输入参数放在输出参数之前。

这不是个硬性规则。那种既是输入又是输出的参数(通常是class或者struct)会让情况变 得更复杂,还有一点,就像前面所说的一样,为了与相关函数保持一致性,也会需要违背这一规则。

文件包含的名字和顺序(Names and Order of Includes)

请使用以下标准顺序以保证可读性,并且避免隐藏的依赖:C库,C++库,其他库的.h,你的项目的.h。

所有项目的头文件应作为项目源文件目录的后代列出,不要使用UNIX中的目录快捷方式 . (当前目录)和 .. (父目录)。

举个例子,google-awesome-project/src/base/logging.h应该以#include “base/logging.h”的格式包含。

在 dir/foo.cc中,该文件的主要目的是实现或者测试在dir2/foo2.h中的东西,文件包含应按照如下排序:

  1. dir2/foo2.h (首选位置(preferred location) — 请见下面的详细说明).
  2. C system files.
  3. C++ system files.
  4. Other libraries’ .h files.
  5. Your project’s .h files.

首选的排序(preferred ordering,译者注:在以上例子中为dir2/foo2.h )降低了隐藏的依赖。我们希望每个头文件能够自我编译。最简单的方式就是确保它们中的每一个都在某个.cc文件中是第一个被#include的。

dir/foo.cc and dir2/foo2.h通常在同一目录下(比如base/basictypes_unittest.cc and base/basictypes.h),但也可能在不同的目录下。

在每个部分(section)中,根据字母顺序排列是不错的方式。

举个例子,google-awesome-project/src/foo/internal/fooserver.cc中的文件包含顺 序可能是这样:

#include “foo/public/fooserver.h” // 首选位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include “base/basictypes.h”
#include “base/commandlineflags.h”
#include “foo/public/bar.h”

未完待续…

Posted in: C++ / Tagged: C++, Google, 参考指南, 头文件, 风格

日积月累: Uncopyable & Uninheritable in C++

November 13, 2009 4:45 pm / 7 Comments / Benny Chen

如何在C++中阻止复制和继承呢,我做了个整理。

Uncopyable
有些类需要禁止被复制或者赋值,这需要显示的禁掉它们的拷贝构造和赋值函数。
摘自《Effective C++》Item 6: Explicitly disallow the use of compiler-generated functions you do not want

class Uncopyable 
{
protected:                                   // allow construction
    Uncopyable() {}                       // and destruction of
    ~Uncopyable() {}                     // derived objects...

private:
    Uncopyable(const Uncopyable&);             // ...but prevent copying
    Uncopyable& operator=(const Uncopyable&);
};

class UncopyableExample: private Uncopyable 
{ 
    // THIS CLASS IS UNCOPYABLE
}; 

Uninheritable
Java提供了final关键字用来阻止被继承,而在C++中没有这个关键字,但阻止继承的类在C++中是同样可能的。
摘自:http://www.csse.monash.edu.au/~damian/Idioms/Topics/04.SB.NoInherit/html/text.html

template <class OnlyFriend>
class Uninheritable
{
    friend class OnlyFriend;
    Uninheritable(void) {}
};

class NotABase : virtual public Uninheritable< NotABase >
{
    // WHATEVER
};

class NotADerived: public NotABase
{
    // THIS CLASS GENERATES A COMPILER ERROR
    NotADerived(){}
};
Posted in: C++, Talking in Code / Tagged: C++, 禁止复制, 禁止继承

LinkedIn

Milan Petrovic

Categories

  • In My Life (25)
    • A Day in the Life (8)
    • English Learning (2)
    • Learn a Word (7)
    • Something In The Way (8)
  • Music Heaven (8)
    • Guitar (1)
    • In Concert (1)
    • Lyrics (3)
  • OK Computer (54)
    • 3D (3)
    • C++ (10)
    • Computer Graphics (15)
    • Game Programming (23)
    • iOS (6)
    • Linux (1)
    • Lua (9)
    • My Projects (3)
    • Some Experiences (9)
    • Talking in Code (2)
    • Unity (2)
  • Quotations (2)
  • Uncategorized (1)
  • Visca Barça (24)
    • FCB BJ (5)

Recent Posts

  • [译]优化你的手机游戏(没有延迟的,才是健康的)- 一篇给游戏美术设计师读的文章
  • 新浪微博API for MOAI
  • 稍后继续
  • Unity Developer ++
  • Another Thread @ Moai Forum
  • 1st Day of Golden Week
  • 为SyntaxHighlighter添加新语言
  • 基于Lua的State Pattern
  • Class Diagram of Pacman
  • 基于Moai的Pacman

Recent Comments

  • 约修亚_RK on 为SyntaxHighlighter添加新语言
  • 爱装的小男孩 on 小心DLL链接静态库时的内存错误
  • happyfire on Game Loop的几种实现方式
  • William on 新浪微博API for MOAI
  • Benny Chen on 新浪微博API for MOAI
  • your man on 新浪微博API for MOAI
  • 你家男人 on 稍后继续
  • 逍遥 on 关于对std::vector的遍历
  • papa on Unity Developer ++
  • T客网 ︱ Techpot » Blog Archive » iOS开发与OpenGL ES相关问题整理(1) on iOS开发与OpenGL ES相关问题整理(1)

Tags

2d 3D 3dsmax 3ds max air Apply architecture Asia tour barca Beijing bilbao binary search blocked bob boost bruce springsteen C++ capo CGContextDrawImage Champions League Change DLL DX10 eval exporter flash framework frustum culling game game engine iniesta ios linux lua Moai opengles pacman plug-in plugin 北京 导出插件 崩溃 巴萨 游戏引擎 踢球
© Copyright 2026 - A Game Developer
Infinity Theme by DesignCoral / WordPress