罗朝辉(飘飘白云)

关注嵌入式操作系统,移动平台,图形开发。-->加微博 ^_^

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  85 随笔 :: 0 文章 :: 169 评论 :: 0 Trackbacks

                                          Using LUA with C++

原文链接:http://www.spheregames.com/index.php?p=templates/pages/tutorials

作者:http://www.spheregames.com

翻译:飘飘白云(http://www.cppblog.com/kesalin

 

译注:翻译本文并未获得原作者的许可,翻译本文来仅供个人学习消遣,故谢却转载。

原文中的示例代码汲取了《游戏编程精粹 5》中Matthew Harmon写的 “Building LUA into Games”一文的想法与部分代码实现,我将“Building LUA into Games”一文的示例代码应用到最新的LUA版本,也打包贴在这里,供大家参考。

Using LUA with C++:示例代码下载

Building LUA into Games:示例代码下载

《游戏编程精粹 6》中有一篇有关LUA结合C++使用的文章,下回整理下也贴出来。

---------------------------------------------------------------------------------------------

到目前为止我知道有好几个教人们如何将LUA与C++结合起来进行游戏开发的教程,但是我注意到很多教程只不过是一个简要概述和一些不很容易让人明白的示例代码。我这个教程的目的是想描述SGE(Sphere Games Engine)中的LUA实现。这个C++实现可以让你很快也很容易地将其他类转变成脚本兼容类(译注:可以与Lua脚本交互的类)。

About the Demo

开发环境:MS Visual Studio 2003, DirectX SDK 2005 December版本,LUA 5.02

基础知识

在开始之前让我们做些下必要的准备工作:首先去http://www.lua.org下载LUA,我使用的是5.0.2版,同时去http://lua-users.org/wiki/SimplerCppBinding 拷贝和粘贴声称luna for Lua 5.0的代码,并保存到命名为luna.h的文件中,这样就很容易与C++绑定(这个文件已经包含在示例代码中了)。

下一步添加LUA文件到你的工程中。我不会详说这个,看看示例工程看是如何这些文件是如何添加的。值得一说的是你应该将LUA/include添加到你工程设置(project’s settings)的额外包含路径中(additional include directory)去,同时添加luna.h到工程中,我们很快就要使用它。

建立工程并添加了相关文件,而且编译也通过了,那我们就可以正式开始了。

 

这个例程使用很多宏来简化客户类与LUA的整合工作,第一次看上去这些宏有点让人头大,不过好消息是即使你不明白这些宏是如何运行的,你还是能够很容易地使用它们来。当然我相信学习这些宏是如何运作的是很有好处的,无论喜欢与否,它们迟早有用。

我们从创建一个名为sgLUA.h的文件开始,并添加通用预处理语句以及包含LUA与LUNA头文件:

 1#pragma once
 2
 3#ifndef SGLUA_H_
 4#define SGLUA_H_
 5
 6
 7extern "C" {
 8#include "lua.h"
 9#include "lauxlib.h"
10#include "lualib.h"
11}

12
13#include "./luna/luna.h"



我们最先看的这个宏DECLARE_SCRIPT_CLASS是你在声明一个类的时候使用的(头文件中),它添加两个静态成员变量到你的类中,一个是持有类名字的string,另一个是一个methods array,持有所有与LUA绑定的函数。下面是这个宏的代码实现:

1#define DECLARE_SCRIPT_CLASS(ClassName)                    \
2    public:                                                \
3    static const char className[];                        \
4    static Luna<ClassName>::RegType methods[];
5


接着的这一个宏是在你所声明的类的实现文件中使用的,在后面我们讲看到它如何被使用的,这个宏只是初始化类的名字。

1#define IMPLEMENT_SCRIPT_CLASS(ClassName)                \
2    const char ClassName::className[] = #ClassName;


接着的三个宏是连接在一起使用的,它们被用来指定哪些个成员函数将被当作LUA的绑定函数。

1#define DEFINE_SCRIPT_CLASS(ClassName)                        \
2    Luna<ClassName>::RegType ClassName::methods[] = {
3
4#define SCRIPT_METHOD(ClassName, MethodName) { #MethodName, &ClassName::MethodName }
5
6#define END_SCRIPT_CLASS };


如果这些宏让你看的头大,当你看到它们是如何被使用的时候就会轻松很多,它们只不过是初始化先前使用DECLARE_SCRIPT_CLASS宏声明的methods array。

1#define REGISTER_CLASS(ClassName)                    \
2    Luna<ClassName>::Register(m_pLuaState);


这个宏很重要,它通过luna将C++类绑定到LUA中,先看代码吧,在后面我们讲谈论如何使用它。

这就是我们所需要的所有宏,当然,有些人喜欢添加一个像下面一样的额外的宏:

#define SCRIPT_FUNCTION(FunctionName) int32 FunctionName(luaState* L);


他们使用这个宏来定义在LUA中调用的类成员函数,通过这种方法你可以很清晰的看明白哪些成员函数是被脚本使用的,哪些不是。如果你喜欢你也可以这么做,这只是个人喜好而已,我个人并不使用它,但是我会在后面讲述如何使用这个宏来满足下你的猎奇心。

 

脚本引擎

现在我们来讨论脚本是如何被装载,执行与移除。这里的代码是基于Matthew Harmon发表在《Game Programming Gems 5》上“Building LUA into Games”一文,为了清晰起见,我在这里做了一些简化工作,但我建议你看看Matthew那篇精华文章以及在这里被简化的特征。

              脚本引擎是一个单例(singleton)(参看附录1:单例实现),它被用来创建脚本,无论脚本在什么时候被创建,脚本都会被添加到链接列表(linked list)中,这样脚本引擎就可以通过这个链接列表更新每一帧。

              既然脚本引擎管理一个脚本链接表,我们将在脚本引擎上下文中讨论脚本。

脚本

              每个脚本在装载之后将创建它自己的LUA thread(译注:这里的thread与操作系统的进程概念不同,LUA中的thread是指携同作业吧。),装载了脚本文件之后,脚本就会被立即执行,直到脚本执行结束。通过使用循环脚本也可以被无限执行,在后面我们将看到这是如何被实现而不会让LUA虚拟机在无限循环中僵住。

              你可以通过下面的代码创建一个脚本:

    Script* pScript = GetScriptEngine().Create();
    pScript
->RunFile("scanner.lua");


              调用脚本引擎的创建函数,脚本引擎将返回一个脚本(这个脚本也被添加到脚本的引擎的脚本链接表中),脚本会创建一个LUA thread并开始执行。

              虽然脚本会持续执行,但有一种机制可以推迟脚本的执行,这需要在创建脚本引擎初始化LUA的时候添加一个库函数。在这个例子中你可以在LUASystem.h/cpp文件中找到这个库。

系统库

              我们稍稍绕开下先来解释下这个库以及它能做什么。目前的这个系统库只包含四个函数,它们都是用来绑定到LUA的静态函数。

// LUASystem.h

 1#pragma once
 2
 3#include "sgLUA.h"
 4
 5// This is the LUA system lib
 6
 7// this is the array where the functions are mapped to LUA
 8extern const luaL_reg systemLib[];
 9
10// this is used to initialize the lib
11int LUAOpenSystemLib(lua_State* L);
12
13// These are the actual functions that will be bound
14static int    LUAWaitframe(lua_State* l);
15static int    LUAWaittime    (lua_State* l);
16static int    LUAWait        (lua_State* l);
17static int    LUAGetTime    (lua_State* L);


// LUASystem.cpp

 1
 2     // I'm including windows.h just for timeGetTime, ideally you'd have
 3
 4// your own timing mechanism
 5#include <windows.h>
 6
 7#include "LUASystem.h"
 8#include "../Script.h"
 9
10// map the functions to their script equivalents
11static const luaL_reg systemLib[] = {
12    "waitframe", LUAWaitframe },
13    "waittime", LUAWaittime },
14    "wait", LUAWait },
15    "time", LUAGetTime },
16    { NULL, NULL }
17}
;
18
19// This is used by the script engine to load this library
20int LUAOpenSystemLib(lua_State* L)
21{
22    luaL_openlib(L, "system", systemLib, 0);
23    return 0;
24}

25
26// This forces the script to halt execution for the specified
27// number of frames
28static int LUAWaitframe(lua_State* L)
29{
30    Script*     pScript = 0;
31
32    pScript = GetScriptObject(L);
33
34    pScript->m_WaitFrame = (int)luaL_checknumber(L, 1);
35    pScript->m_State = Script::SS_WAITFRAME;
36
37    return(lua_yield(L, 1));
38}

39
40// This returns the current time in seconds to the script
41static int LUAGetTime(lua_State* L)
42{
43    float current_time = timeGetTime() / 1000.0f;
44    lua_pushnumber(L, current_time);
45    return 1;
46}

47


我在这里没有列出LUAWaittime与LUAWait这两个函数,因为它们和LUAWaitFrame非常相似,再在这里列出只会浪费纸张与看官你宝贵的精力。头文件非常直观,所以我不打算谈论它。有趣的是实现文件,我们从将脚本函数映射到静态C函数开始,这个映射过程是通过一个叫systemlib的函数名字数组实现的。脚本引擎将在后面调用LUAOpenSystemLib,解析这个数组并在LUA与我们的代码之间创建实际的映射。

              我在先前的类实现代码块中包含了处理不同事情的两个函数,它们相当有用。第一个函数:LUAWaitFrame,通过调用GetScriptObject得到当前正在执行的脚本,提取从脚本文件中传过来的第一个参数“waitframe”然后将它设置到我们的脚本对象中,然后改变脚本对象的状态为SS_WAITFRAME,这样这个脚本会在接着的指定帧数里暂停执行。在LUA中,可以通过像下面的代码使用:

–- This script will halt executing at this point for 5 frames

system.waitframe(5)

 

              现在我要解释的下一个函数是LUAGetTime,这个函数实际上返回一个值到LUA脚本中,在这里它返回系统时间(以秒为单位),实际上它可以返回任何你需要的任何值或对象。下面是LUA中的使用示例:

-- the t variable will contain the system time in seconds

local t = system.time()

 

              我发现通过将绑定静态函数到LUA中从而在LUA和脚本引擎之间使用库的方式工作得相当好。我将不会在游戏真实代码中这样使用,我更愿意使用一个类,就像在这个教程开始时讨论的那样。

脚本引擎

              脚本引擎不是一个复杂的系统,简言之,它只是让你可以从链接列表中取得一个脚本对象,并且每一帧都通过迭代器遍历这个链接列表,调用脚本对象的更新函数。

              脚本引擎创建之后就立即装载基本的LUA库。在这里我装载了全部的LUA库,实际上你可能不需要全部的库,并想减少内存使用,那你可以移除那些不需要的库。

 1// Add any LUA libraries that need to be loaded here, this
 2// may include user created libs
 3static const luaL_reg lualibs[] =
 4{
 5    {"math",        luaopen_math},
 6    {"str",            luaopen_string},
 7    {"io",            luaopen_io},
 8    {"tab",            luaopen_table},
 9    {"db",            luaopen_debug},
10    {"base",        luaopen_base},    
11    {"system",        LUAOpenSystemLib},    
12    { NULL,        NULL }
13}
;
14
15// This opens all the LUA libraries declared above
16void OpenLUALibs(lua_State *l)
17{
18    const luaL_reg *lib;
19
20    for (lib = lualibs; lib->func != NULL; lib++)
21    {
22        lib->func(l);
23        lua_settop(l, 0);
24    }

25}

26


              任何你想要装载的LUA库必须在追加到这个数组里面来,比如最后一个入口:“system”,那是我们的系统库,LUAOpenSystemLib是我们在LUASystem.cpp中追加的用来装载system library的函数。

              在装载了所有库之后,我们调用RegisterScriptClasses,这非常重要,以为在这里我们通过宏REGISTER_CLASS将我们的LUA脚本兼容类注册到(或绑定到)LUA中。因为这个函数非常非常的重要,为了避免在大堆代码中迷失方向,我把相关代码都列在它自己的CPP文件LUARegistry.cpp中。完整代码参看下面:

 1#include "../ScriptEngine.h"
 2#include "../sgLUA.h"
 3
 4// Any classes that are exposed to LUA must be registered in this file.
 5// Failure to register a class will result in a LUA error such as this one:
 6//
 7// attempt to call global `ClassName' (a nil value)
 8//
 9
10#include "../Entity.h"
11
12void ScriptEngine::RegisterScriptClasses()
13{
14    REGISTER_CLASS(Entity);
15}


              确保任何与LUA脚本兼容的类都会在这个文件中通过REGSITER_CLASS注册到LUA中是相当重要的。比如:要用LUA创建一个叫Vehicle的类,那么你应当在LUARegistry.cpp文件中为Vehicle类添加头文件并在最后一个REGISTER_CLASS之后追加语句REGISTER_CLASS(Vehicle),从而将Vehicle注册到LUA中去。

              很容易忘记注册你的类,因此如果你得到脚本错误信息,就像在注释中描述的那样,那你的检查下看你是否注册了你的类。

将所有东西整合起来

              如果你像我一样的话,你大概会在看到上面说的这些是如何被使用之后更容易理解一些。因此我将解释我是如何使用LUA来控制例程中非常简单的实例。

这是例程的目标:

-允许在LUA脚本中创建实体

-可以在LUA脚本中实现实体的移动,缩放,旋转

-从实体发送信息到执行中的脚本

              这些都是非常适当的目标,它实现了创建可从LUA中访问的更加复杂的类的所需功能。

              为了实现第一个目标,我们需要一个entity管理器,这个管理器将包含一个entity列表,并持续更新它们。当新实体被创建之后,新实体会自动被entity管理器处理,知道实体被销毁。下面是一个简单的entity管理器的头文件:

 1#pragma once
 2
 3// This class detects when an entity is spawned and adds it to
 4// a list of entities. The list of entities is processed each 
 5// frame and each entity is updated and rendered.
 6// Entities get added into the manager upon creation, so you 
 7// don't need to manually do this.
 8
 9#include <vector>
10#include "Singleton.h"
11#include "Common/dxstdafx.h"
12
13class Entity;
14
15class EntityManager : public Singleton<EntityManager>
16{
17    std::vector<Entity*> m_Entities;
18
19    IDirect3DDevice9* m_pDevice;
20
21public:
22    EntityManager();
23    ~EntityManager();
24
25    void AddEntity(Entity* pEntity);
26
27    void UpdateAll();
28
29}
;
30
31extern EntityManager* gEntityMngr;
32extern EntityManager& GetEntityManager();
33extern void ReleaseEntityManager();


       这个entity管理器是一个单例实现,它所做的事情就是检测实例的创建并追加到实例列表中,然后每一帧都遍历所有的实例,调用相应的更新函数。

              AddEntity被entity的构造函数所调用,这样就保证所有的被创建实例都会被entity管理器所管理。

              下面让我们看看实体类的声明:

 1#pragma once
 2
 3#include "Common/dxstdafx.h"
 4#include "../sgLUA.h"
 5
 6class Entity
 7{
 8    DECLARE_SCRIPT_CLASS(Entity)
 9
10    CDXUTMeshFile* m_pMesh;
11    
12    D3DXVECTOR3 m_vRotationAxis;
13    float        m_fAngle;
14
15    D3DXVECTOR3 m_vPosition;
16    D3DXVECTOR3 m_vScale;
17
18    DWORD m_dwNumMaterials;
19
20    // We use this flag to determine if an entity was not instanced
21    // through LUA, in this case we need to call delete for this entity
22    bool m_bDelete;
23
24public:
25    
26    Entity();
27    Entity(lua_State* L);
28    virtual ~Entity(void);
29
30    bool MustDelete() const return m_bDelete; }
31
32    HRESULT Spawn(const char* pFilename);
33
34    void SetPosition(const D3DXVECTOR3& pos);
35    void SetScale(const D3DXVECTOR3& pos);
36
37    void Update(float fElapsed = 0.0f);
38    void Render();
39    
40    // Script Functions
41    int Spawn(lua_State* L);
42    int SetPosition(lua_State* L);
43    int SetScale(lua_State* L);
44    int Rotate(lua_State* L);
45    int GetAngle(lua_State* L);
46    // End Script Functions
47
48private:
49
50    HRESULT LoadMesh(WCHAR* strFileName);
51
52}
;
53


  OK,这个类有一点有趣的地方,值得一看的地方是:我们声明这个类被当作一个脚本类:

DECLARE_SCRIPT_CLASS(Entity)

 

这意味着我们需要一个供LUA使用的构造函数:

Entity(lua_State* L);

 

最后,我们声明一些脚本函数:(译注:脚本函数就是从LUA脚本中获得输入参数,然后将LUA数值类型转换成C++数值类型,再传到LUA脚本中去的函数)

1    // Script Functions
2    int Spawn(lua_State* L);
3    int SetPosition(lua_State* L);
4    int SetScale(lua_State* L);
5    int Rotate(lua_State* L);
6    int GetAngle(lua_State* L);
7    // End Script Functions

 

还记得我前面说有些人喜欢用宏来声明脚本函数么?就是在这里使用那些宏,就像如下:

SCRIPT_FUNCTION(Spawn);

SCRIPT_FUNCTION(SetPosition);

SCRIPT_FUNCTION(SetScale);

SCRIPT_FUNCTION(Rotate);

 

你可以根据你的喜好来决定使用那种方式声明,只不过是要记得保持前后一致。

              当你阅读这个类的时候也许你会有疑问m_bDelete这个标志是做什么的呢?当我创建这个类的时候,我决定我要从脚本中创建entity对象们,然而,它们有时候可能会在(C/C++)代码被创建。这里会有点小问题,因为在脚本中创建的实体可能会被LUA的垃圾回收机制给销毁掉,而在代码中创建的实体则不会。因此我需要一个标志来标识哪个实体不是由脚本创建的,从而我可以在实体管理器销毁的时候删除这些实体。

              让我们来看一下entity的构造函数:

 1// LUA will use this constructor to instance an object of this class
 2Entity::Entity(lua_State* L)
 3: m_pMesh(0)
 4, m_bDelete(false)
 5, m_fAngle(0.0f)
 6{
 7    // Add this entity into the entity manager
 8    GetEntityManager().AddEntity(this);
 9
10    m_vPosition = D3DXVECTOR3(0.0f0.0f0.0f);
11    m_vScale = D3DXVECTOR3(1.0f1.0f1.0f);
12}


              这是一个LUA构造函数,m_bDelete被设置成false,因为这个实例对象会被LUA的垃圾回收机制自动回收。在这个构造函数里,首先我们将“this”添加到entity管理器中,这样就保证这个实体每帧都会被更新与描绘。在LUA脚本中是这样来创建实体对象的:

local tiger = Entity()

 

              在这里创建并调用实体类的构造函数,就像我们看到的那样,将会把创建的实体添加到实体管理器中。

              下面让我们来看一下我们的第一个脚本函数Spawn:

 1// Spawn receives a .X file name to load a mesh
 2//
 3// Ex: tiger:Spawn("tiger.x")
 4//
 5int Entity::Spawn(lua_State* L)
 6{
 7    const char* pFilename = lua_tostring(L, 1);
 8
 9    HRESULT hr = Spawn( pFilename );
10
11    if ( FAILED(hr) )
12        return -1;
13
14    return 0;
15}

 

Spawn将会装载实体的模型与纹理,值得注意的是我们如何从LUA中取得函数参数,来看第一行:

1    const char* pFilename = lua_tostring(L, 1);


lua_tostring(L, 1)将把在spawn函数里传递的第一个参数当作string返回,请查看下LUA的reference manual了解更多有关参数传递的信息。

这样,在脚本中你可以像下面这样地来使用

tiger:Spawn("tiger\\tiger.x)

 

              这样就创建了实体,并保证实体会被追加到实体管理器中,而实体管理器将会在每一帧更新与描绘所有实体。万事OK了,你现在就可以创建被LUA脚本使用的脚本兼容类了!


              在这个截图中你可以看到scanner(扫描器)的臂绕悬挂在中央的调速器(speeder)旋转,调速器同时也在自转,并且有轻微的上下移动来模拟震动。所有的这些,包括所有的模型的产生都是在这两个脚本中实现的:scanner.lua和speeder.lua。

              Scanner.lua-构建房子并使臂绕Y轴旋转,你可以打开这个脚本,放开被注释掉的两行,这样就可以使整个房间缓慢地旋转,值得一试!

              Speeder.lua-构建调速器,并轻微地缩放,然后使它旋转,同时轻微的上下移动,就像它在摇摆一样。

              玩转脚本吧,你就可以看到只要向脚本展露很少一些函数你就可以做完成很多事情!

              附录1- Singleton

              在这个附录里,我将讲述一点如何来使用singleton类。我只会讲述如何来使用这个类而不会讲述太多的内部实现过程,因为有太多的出色的设计模式资源在讲解singleton。这个类相当直观:

 1///////////////////////////////////////////////////////////
 2//
 3// File..: sgSingleton.h
 4// Author: Luis Sempe
 5// Created on..: Sunday, July 13, 2003
 6//
 7// Sphere Games Copyright (C) 2003
 8// All rights reserved.
 9//
10//                C O N F I D E N T I A L
11//
12///////////////////////////////////////////////////////////
13#ifndef SGSINGLETON_H_
14    #define SGSINGLETON_H_
15
16    #include <cassert>
17
18/*
19Example usage:
20
21class StateManager : public Singleton <StateManager>
22{
23// 
24State* GetState() { return &State; }
25}
26
27State = StateManager::Get()->GetState();
28
29Also, it's possible to do:
30
31#define STATEMANAGER (StateManager::Get())
32
33and then reference the statemanager like this:
34
35STATEMANAGER->GetState();
36
37Finally, do not forget to create the StateManager as a static global or with new. Either way
38you do it, it has to be done before using StateManager::Get() otherwise, bad things occur.
39*/

40    #pragma warning(push)
41    #pragma warning(disable : 4311// 'variable' : pointer truncation from 'type' to 'type'
42    #pragma warning(disable : 4312// 'variable' : conversion from 'type' to 'type' of greater size
43template<typename T>
44class Singleton {
45    static T*  m_pInstance;
46public:
47    Singleton() {
48        assert(!m_pInstance);
49        int offset = reinterpret_cast<int> (reinterpret_cast < T * > (0x1)) - reinterpret_cast<int> (reinterpret_cast < Singleton<T> * > (reinterpret_cast < T * > (0x1)));
50        m_pInstance = reinterpret_cast < T * > (reinterpret_cast<int> (this+ offset);
51    }

52    virtual~Singleton()
53    {
54        assert(m_pInstance);
55        m_pInstance = 0;
56    }

57
58    static T& Get()
59    {
60        assert(m_pInstance);
61        return *m_pInstance;
62    }

63
64    static T* GetPtr()
65    {
66        assert(m_pInstance);
67        return m_pInstance;
68    }

69
70private:
71    Singleton(const Singleton& )
72    {
73    }

74
75    Singleton &operator=(const Singleton& )
76    {
77    }

78}
;
79
80template<typename T>
81*Singleton<T>::m_pInstance = 0;
82
83    #pragma warning(pop)
84#endif
85


              如果你要将某个类当成单一实例类,那就必须让它继承自Singleton,并且将那个类设置成Singleton的模板参数,像下面一样:

class MySingleton : public Singleton<MySingleton>

{

// . . .

};

 

              因为这个类只允许有一个实例,于是我通过创建一个指向这个类的全局指针来实现。你必须留心你如何使用这个单例实现,因为就像你看到的那样它没有太多的错误检测。只用你始终按照它应该被使用的方式来使用它,那你就不会有任何问题。

              在头文件的类声明之后,我添加了如下代码:

extern MySingleton* gMySingleton;

extern MySingleton& GetMySingleton();

extern void ReleaseMySingleton();

 

gMySingleton是一个指向类实例的全局指针,它唯一的真实目的就是提供初始化实例的途径。GetMySingleton要注意检查gMySingleton在返回之前是否已经被初始化了,如果没有,它将会创建实例(唯一一次),然后返回这个实例的引用。

MySingleton* gMySingleton = 0;

MySingleton& GetMySingleton()

{

if ( !gMySingleton )

gMySingleton = new MySingleton();

return MySingleton::Get();

}

void ReleaseMySingleton()

{

if ( gMySingleton )

{

delete gMySingleton;

gMySingleton = 0;

}

}

 

              最后,在关闭游戏之前不要忘记调用ReleaseMySingleton()。使用单一实例类有时很棘手,因为你必须确保在使用之前它被创建,并且不能在别处还在使用的场合下释放它。

              这个单一实例类的实现一点也不完美,有很多简单与复杂的途径来实现单一实例类。总之,我对我这份单一实例类的实现相当满意,因为只要你一致地使用,就不会跑出问题来。

引用

Matthew Harmon, “Building LUA into Games” , Game Programming Gems 5

Andrei Alexandrescu, “Implementing Singletons”, Modern C++ Design

http://www.spheregames.com

Donations

If you found this tutorial helpful, and would like to see more of them, please donate, even a small contribution helps a lot!

 

 

posted on 2008-06-17 12:38 罗朝辉 阅读(5167) 评论(0)  编辑 收藏 引用 所属分类: C/C++脚本语言

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理