|
Nebula2的脚本系统实现了一个面向C++的脚本接口, 它把脚本命令直接映射到了C++方法. 从技术角度来说, 这是一个简捷的思路, 但是对于需要把游戏逻辑和行为脚本化的关卡设计师来说, Nebula2的脚本系统太底层和透明了. 关卡逻辑脚本一般来说构架于比C++接口更高级的层次上, 直接把脚本命令映射到C++方法会把脚本层次弄得错综复杂. Bug甚至会比同样的C++代码更多, 因为脚本语言一般缺少强类型检查和”编译时”的错误检测, 所以在本应在C++编译时发现的Bug会在脚本运行时才发现(这对于不同的脚本语言有所不同). 这是我们从Project Nomads中得出的经验, 它就是用Nebula2的脚本系统驱动的. 所以教训就是: 把你的脚本架构在一个正确的抽象层上, 并且: 把你的C++接口映射到一种脚本语言是没有意义的, 因为那样你不如从一开始直接用C++来做这些东西. 相应的, 新的Nebula3脚本哲学为关卡设计师提供一些在”正确的抽象层”的(大多是限于特定应用)积木. 当然, “正解的抽象层” 很难来定义, 因为这要在灵活性跟易用性之间找到一个平衡( 例如, 一个”Pickup” 命令是不是应该把角色移动到拾取范围内呢? ) 除了太底层以外, Nebula2的脚本系统也有一些其它的缺点: - C++方法必须遵循可以转化为脚本的原则( 只有简单数据类型才可以做为参数 )
- 给程序员带来麻烦. 每个C++方法都需要额外的脚本接口代码( 每个方法几行 )
- 只有派生自nRoot的类可以脚本化
- 对象关联到脚本系统( 思路简单, 但是增加的依赖性会使重构非常困难 )
下面是Nebual3的底层脚本的大概: - 脚本系统的基础是Script::Command类
- Script::Command是一个完全脚本语言无关的, 它包含了一个命令名称, 一些输入参数的集合还有一些输出参数的集合.
- 一个新的脚本命令通过派生Script::Comand类来创建, 脚本的C++功能代码可以写入子类的OnExecute()方法
- ScriptServer类是脚本系统中仅有一个脚本语言相关的类, 它会把Command对象注册成新的脚本命令, 并且把命令参数在脚本语言和C-API之间做翻译.
这个观念比Nebula2更为简单, 最重要的是, 它不会跟Nebula3的其它部分交织在一起. 甚至可以通过改变一个#define来编译一个没有脚本支持的Nebula3. 当然, 书写脚本命令的C++代码跟Nebula2一样烦人, 这是NIDL的由来. NIDL的是全称是”Nebula Interface Definition Language”. 基本思想是通过为脚本命令定义一个简单的XML schema并把XML描述编译成派生了Script::Command的C++代码, 来尽量减少书写脚本命令的重复性工作. 对于一个脚本命令必不可少的信息有: - 命令的名称
- 输入参数的类型和名称
- 输出参数的类型和名称
- 对应的C++代码( 通常只有一行 )
还有一些非必须, 但是可以带来便利性的信息: - 关于命令的作用和每个参数的意义的描述, 这可以作为运行时的帮助系统
- 一个唯一的FourCC(四字符码), 可以更快的通过二进制通道传输
大部分的脚本命令翻译成了大约7行的XML-NIDL代码. 这些XML文件再用”nidlc”NIDL编译器工具编译为C++代码. 这个预处理是VisualStudio完全集成的, 所以使用NIDL文件不会为程序员代来任何困难. 为了减少乱七八糟的文件(编译生成的), 相关的脚本命令被组织到一个叫作库的集合中. 一个库由一个单独的NIDL-XML文件表示, 并且它只会被翻译一个C++头文件和一个C++源代码文件. 脚本库可以在程序启动时注册到ScriptServer, 所以如果你的应用程序不需要脚本访问文件的话, 仅仅不注册IO脚本库就可以了. 这会减小可执行文件的体积, 因为连接器会把没有用到的脚本库丢弃掉. 最后, Nebula3放弃了TCL作为标准的脚本语言, 而采用了运行时代码更加小巧的LUA. LUA已经成为游戏脚本的准规范, 这也使得寻找熟练的LUA关卡设计师更加容易.
N3的场景管理最为核心的一个类是GrphicsServer, 它包含一些"stage"和"View". Stage把图形实体(模型, 摄像机, 灯光)进行分类渲染. 它的主要工作是在连接的图形实体间加速可见性查询. 不同的可见性查询由不同的Stage子类来实现. N3会提供了一些不同用途的Stage子类, 但你也可以根据程序需要自己来实现可见性查询机制. 可见性查询适用于这些实体: - Camera->Light: 查找对于指定摄像机可见的所有灯光
- Camera->Model: 查找对于指定摄像机可见的所有模型
- Light->MOdel: 查找被指定光源照射到的所有模型
这些可见性查询在图形实体间建立了一些所谓的"可见性链接", 再利用低级的渲染子系统来加速渲染. 要渲染一个Stage的内容, 需要至少一个View对象. 一个View对象通过绑定一个摄像机实体把Stage渲染到一个render target. 可以并存任意数目的View, 也可能都被绑定到任意Stage. 此外, View对象之间可能存在依赖关系(结果就是一个View对象会在渲染自身时首先请求它所依赖的View对象). 图形实体表示了可以被连接到Stage的一个最小图形对象, 它分为以下三种: - ModelEntity: 一个可见的模型实例
- LightEntity: 一个光源
- CameraEntity: 一个摄像机
可见性查询使图形实体间形成一种双向的链接关系. 一个CameraEntity链接到所有对于这个摄像机来说可见的ModelEntity和LightEntity. 因为可见性链接是双向的, 所以ModelEntity和LightEntity也知道它们对于哪个摄像机可见. LightEntity有它们影响到的ModelEntity的链接, ModelEntity也知道它们被哪个光源照亮. ========================================================== N3 画个东西真简单, 想画个模型, 创建出来设置一下位置扔给Stage就好了 - this->model = ModelEntity::Create();
- this->model->SetTransform(matrix44::translation(0.0f, 3.0f, 0.0f));
- this->model->SetResourceId(ResourceId("mdl:examples/eagle.n2"));
- this->stage->AttachEntity(this->model.upcast<GraphicsEntity>());
模型是黑的? 再往场景里扔个灯就好了: - // attach a light entity
- matrix44 lightTransform = matrix44::multiply(matrix44::scaling(100.0f, 100.0f, 100.0f), matrix44::lookatrh(point(20.0f, 20.0f, 20.0f), point::origin(), vector::upvec()));
- this->lightEntity = SpotLightEntity::Create();
- this->lightEntity->SetCastShadows(true);
- this->lightEntity->SetTransform(lightTransform);
- this->lightEntity->SetColor(float4(4.0f, 2.0f, 1.0f, 1.0f));
- this->stage->AttachEntity(this->lightEntity.upcast<GraphicsEntity>());
想控制的话, 再扔个摄像机进去就OK了....... - GraphicsServer* gfxServer = GraphicsServer::Instance();
-
- // setup the camera util object
- this->mayaCameraUtil.Setup(point(0.0f, 0.0f, 0.0f), point(0.0f, 0.0f, 10.0f), vector(0.0f, 1.0f, 0.0f));
-
- // setup a stage
- this->stage = gfxServer->CreateStage(StringAtom("DefaultStage"), SimpleStageBuilder::Create());
-
- // attach a camera to the stage
- this->cameraEntity = CameraEntity::Create();
- cameraEntity->SetTransform(this->mayaCameraUtil.GetCameraTransform());
- this->stage->AttachEntity(cameraEntity.upcast<GraphicsEntity>());
-
- // setup a default view
- this->view = gfxServer->CreateView(View::RTTI, StringAtom("DefaultView"), true);
- this->view->SetStage(this->stage);
- this->view->SetFrameShader(FrameServer::Instance()->GetFrameShaderByName(ResourceId(DEFAULT_FRAMESHADER_NAME)));
- this->view->SetCameraEntity(cameraEntity);
别忘了处理输入事件: 可以参考ViewerApplication::OnProcessInput().
相对于其他的子系统来说, 输入系统是比较简单的. 很多游戏根本就没有对这一块进行封装, 而直接采用了Win32的消息机制. 不过经过封装的输入系统使用起来很方便, 呵呵. N3中有三种输入设备, 键盘, 鼠标, 手柄. 分别是基于Win32消息, DirectInput, XInput实现的. 这里有一个继承图能够很好的说明输入系统的组织结构: 基本的消息处理机制是这样的一个流程: InputServer里有默认的一个键盘, 一个鼠标, 一个手柄的"handler", 在每帧开始时InputServer会检测当前的输入消息, 得到一个InputEvent, 由相应的InputHandler来处理. 各个InputHandler都保存着当前帧各种输入状态的缓存(如鼠标左键是否按下), 因此, 在程序运行过程中, 我们只要在绘制结束前检测各个InputHandler的状态就相当于知道当前用户是怎样输入的了. 一般只需要关心这么几个函数就够了: - ////////////////////// Mouse////////////////////////////
-
- /// return true if button is currently pressed
- bool ButtonPressed(Input::MouseButton::Code btn) const;
- /// return true if button was down at least once in current frame
- bool ButtonDown(Input::MouseButton::Code btn) const;
- /// return true if button was up at least once in current frame
- bool ButtonUp(Input::MouseButton::Code btn) const;
- /// return true if a button has been double clicked
- bool ButtonDoubleClicked(Input::MouseButton::Code btn) const;
- /// return true if mouse wheel rotated forward
- bool WheelForward() const;
- /// return true if mouse wheel rotated backward
- bool WheelBackward() const;
- /// get current absolute mouse position (in pixels)
- const Math::float2& GetPixelPosition() const;
- /// get current screen space mouse position (0.0 .. 1.0)
- const Math::float2& GetScreenPosition() const;
- /// get mouse movement
- const Math::float2& GetMovement() const;
- //////////////////////Keyboard//////////////////////
-
- /// return true if a key is currently pressed
- bool KeyPressed(Input::Key::Code keyCode) const;
- /// return true if key was down at least once in current frame
- bool KeyDown(Input::Key::Code keyCode) const;
- /// return true if key was up at least once in current frame
- bool KeyUp(Input::Key::Code keyCode) const;
- /// get character input in current frame
- const Util::String& GetCharInput() const;
GamePad先略过, 原理相同 测试例子, 在上一次的代码中添加一段: - void OnRenderFrame()
- {
- if (this->inputServer->GetDefaultMouse()->ButtonDown(MouseButton::LeftButton))
- {
- MessageBoxA(this->displayDevice->GetHwnd(), "Left Button Down", NULL, 0);
- }
- //...//
- }
效果:
概述 - 一些为了兼容Nebula2的代码所做的修改, 主要是一些宏的名字受到影响(DeclareClass -> __DeclareClass, ImplementSingleton -> __ImplementSingleton etc...)
- 着手删除#ifndef/#define/#endif 这些防止重复include的宏, 因为几乎所有的编译器(VStudio, GCC, Codewarrior) 都支持#pragma once
- 把同的样Win32 和Xbox360 代码移动到一个共同的Win360 命名空间来消除代码冗余
- 加入了一个新的Toolkit层, 它包含了一些导出工具和辅助类
编译系统 - 重新组织了 VStudio解决方案的结构, 让所有的依赖工程都在一个解决方案中, 这样就不用再同时打开多个VStudio了
- 现在可以通过.epk编译脚本来导入VStudio工程(对于不在Nebula3 SDK目录下的工程很有用)
- 新的"projectinfo.xml" 文件为一些有用的导出工具定义了工程和平台特有的属性
- 把 export.zip 档案文件分割到一个独立的平台无关文件和几个特定平台的文件 (export.zip 包含所有平台无关的文件, export_win32.zip, export_xbox360.zip, export_wii.zip 包含特定平台的文件)
- 加入一个统一的多平台支持到 asset-pipeline (如 "msbuild /p:Platform=xbox360" 来生成XBOX360的东西)
- 一个新的命令行生成工具 (有代码):
- audiobatcher3.exe (包装了音频导出)
- texturebatcher3.exe (包装了纹理导出)
- shaderbatcher3.exe (包装了 shader 编译)
- buildresdict.exe (生成资源词典文件)
- 这些工具大部分只是调用其它的生成工具(像xactbld3.exe, nvdxt.exe, 还有其它命令下的生成工具)
- 注意公开的N3-SDK因为法律原因只包含Win32平台的支持
基础层 - 修正Core::RefCounted 和Util::Proxy 引用计数线程不安全的BUG
- 加入 WeakPtr<> 类用于更好地处理环形引用
- 在 Ptr<>中加入类型转换的方法
- 简化System::ByteOrder 类接口
- 加入平台相关的面向任务的"virtual CPU core id" (如 MainThreadCode, RenderThreadCore, 等等...)
- 加入一个 System::SystemInfo 类
- 加入 Threading::ThreadId 类型和 Threading::Thread::GetMyThreadId()静态方法
- 现在可以在VStudio调试器和其它的高度工具中看到线程的固有名称了
- SetThreadIdealProcessor() 现在用于在Win32平台上把线程分配给可用CPU核心
- 新的线程子系统的HTTP 调试页面(现在只列出Nebula3的活动线程)
- MiniDump支持: 崩溃, n_assert()和 n_error() 现在在Win32平台上会生成 MiniDump 文件
- 新的 Debug 子系统用于代码分析:
- 提供 DebugTimer 和 DebugCounter 对象
- HTTP 调试页面允许在运行时检查DebugTimers和 DebugCounters
- 新的Memory::MemoryPool 类来分配同样大小的内存块(加快分配速度和减少内存碎片)
- Math::matrix44在中的一些新的和改名的方法
- Http 子系统现在运行在它自己的线程里
- 把 SVG 支持加入到 Http 子系统(Http::SvgPageWriter 和Http::SvgLineChartWriter) (xoyojank:难道是Scalable Vector Graphics?这样的话可以输出图表了)
- 加入 IO::ExcelXMLReader 流读取类, 允许读取XML模式的MS Excel电子表格文件
- 在Messaging::AsyncPort加入行为方式, 定义了处理线程怎样去等待新的消息:
- WaitForMessage: 在消息到达前一直阻塞
- WaitForMessageOrTimeOut: 在消息到达或超时前一直阻塞
- DoNotWait: 不等待消息
- 加入 Remote 子系统, 允许通过TCP/IP连接远程控制N3应用程序
渲染层 - 把渲染移动了它自己的线程 (InternalGraphics子系统在渲染线程这边, Graphics 前端子系统在主线程这边)
- 加入了 CoreAnimation 和 Animation 子系统 (构造中)
- 为简单的用户界面加入了UI子系统 (构造中) (xoyojank: 这个不错^_^)
- 加入CoreAudio和 Audio 子系统(构造中):
- CoreAudio 是后台的, 运行在自己的线程里
- Audio 是前台的"客户端", 运行在主线程里 (或者其它任何线程)
- 围绕XACT的概念设计
- 提供 XACT 的包装实现
- 加入 CoreGraphics::TextRenderer 和 CoreGraphics::ShapeRenderer 类, 打算用于渲染调试信息
- 加入调试渲染子系统(现在在Debug命名空间下)
- Frame 子系统: FramePostEffect 现也也许会包含 FrameBatch
- Input 子系统: 断开 XInput 游戏手柄接口现在对于连接中的设备每隔0.5秒才检测一次
- Resources 子系统: 加入 ResourceAllocator/ResourceLump 系统为Console平台真正的资源流做准备
应用层和插件: - 删除了 CoreFeature (这东西不得不进入GameApplication类来阻止鸡生蛋问题)
- 加入 NetworkFeature (构造中)
- 加入 UIFeature (构造中)
- 加入 CoreNetwork 和 Multiplayer 插件(RakNet的包装)
可能是还在开发当中的缘故, 我感觉Nebula3中的lua脚本系统不是很完善. 所有的调用都是封装成Command来执行的, 并不像LuaBind那样直接绑定到C++类对象; 而且, 对于C++调用脚本的接口也不是很方便, 只有一个Eval()来执行一个字符串. 如果要实际进行应用的话, 我想最好是自己扩展一下, 这里有一篇不错的文章: Integrating Lua into C++. 当然, 对于需求更高的用户来说, 可以选择使用LuaBind等第三方库来整合脚本系统. Command(命令) 可以这么说, 脚本中调用的, 都是一个个的Command. 一个新的Command定义了一个脚本语言独立的新的脚本命令, 你可以通过派生一个Command的子类并注册到脚本服务器来实现. 也就是说, 新的命令不依赖于你具体使用的脚本系统, 可以是lua, 也可以是python等等. view plaincopy to clipboardprint? - class Print : public Scripting::Command
- {
- DeclareClass(Print);
- public:
- virtual void OnRegister();
- virtual bool OnExecute();
- virtual Util::String GetHelp() const;
- private:
- void Callback(const Util::String& str);
- };<PRE></PRE>
class Print : public Scripting::Command
{
DeclareClass(Print);
public:
virtual void OnRegister();
virtual bool OnExecute();
virtual Util::String GetHelp() const;
private:
void Callback(const Util::String& str);
};
ScriptServer(脚本服务器) ScriptServer是语言无双的, 也就是说你可以自己派生一个相应语言的子来来支持一种脚本言. Nebula3里已经实现了一个LuaServer, 不过个感觉没有LuaBind方便. 所有的脚本执行都是通过LuaServer::Eval(const String& str)来完成的. 脚本要调用C++代码的话, 需要封装一个Command, 然后用LuaServer::RegisterCommand()来注册就可以用了. 具体可以参考Command命名空间里的相关代码.
view plaincopy to clipboardprint?
- scriptServer->RegisterCommand("print", Print::Create());<PRE></PRE>
scriptServer->RegisterCommand("print", Print::Create());
应用实例 其实App::ConsoleApplication里就有LuaServer, 并且已经注册了一些IO命名. 我们派生一个从命令行读取脚本命令执行的来做测试:
view plaincopy to clipboardprint?
- class ScripTestApp : public App::ConsoleApplication
- {
- public:
- ScripTestApp(void);
-
- /// open the application
- virtual bool Open();
- /// run the application, return when user wants to exit
- virtual void Run();
- };
-
- ScripTestApp::ScripTestApp(void)
- {
- }
-
- bool ScripTestApp::Open()
- {
- if (ConsoleApplication::Open())
- {
- return true;
- }
- return false;
- }
-
- void ScripTestApp::Run()
- {
- Util::String input;
- while (true)
- {
- input = IO::Console::Instance()->GetInput();
- if (!input.IsEmpty())
- {
- this->scriptServer->Eval(input);
- }
- }
- }<PRE></PRE>
class ScripTestApp : public App::ConsoleApplication
{
public:
ScripTestApp(void);
/// open the application
virtual bool Open();
/// run the application, return when user wants to exit
virtual void Run();
};
ScripTestApp::ScripTestApp(void)
{
}
bool ScripTestApp::Open()
{
if (ConsoleApplication::Open())
{
return true;
}
return false;
}
void ScripTestApp::Run()
{
Util::String input;
while (true)
{
input = IO::Console::Instance()->GetInput();
if (!input.IsEmpty())
{
this->scriptServer->Eval(input);
}
}
}
运行结果:
Nebula3的网络子系统提供了基于TCP协议的简单C/S通信模式. 它并没有打算做成大厅,会话管理还有玩家数据同步的面向游戏的高级通信. 这些以后会在更高层的Nebula3子系统中出现. 使用IP地址 一个IpAddress对象通过主机名字或TCP/IP地址加一个端口号定义了一个通信端点. IpAddress对象可以通过多数方式建立: 1: // 从 TCP/IP 地址和端口号: 2: IpAddress ipAddr("192.168.0.2",1234); 3: 4: // 从主机名和端口号: 5: IpAddress ipAddr("www.radonlabs.de",1234); 6: 7: // 从本机(127.0.0.1) 和端口号: 8: IpAddress ipAddr("localhost",1234); 9: 10: // 从"any" 地址 (0.0.0.0) 和端口号: 11: IpAddress ipAddr("any",1234); 12: 13: // 从广播地址 (255.255.255.255) 和端口号: 14: IpAddress ipAddr("broadcast",1234); 15: 16: // 从主机的第一个合法网络适配器的地址和端口号 17: IpAddress ipAddr("self",1234); 18: 19: // 从主机的第一个连接到互联网的网络适配器的地址和端口号: 20: IpAddress ipAddr("insetself",1234); 21: 22: // 从一个定义了主机名的URI和端口号: 23: IpAddress ipAddr(IO::URI("http://www.radonlabs.de:2100")); 一个IpAddress对象可以用于从主机名查找TCP/IP地址: 1: IpAddress ipAddr("www.radonlabs.de",0); 2: String numericalAddr = ipAddr.GetHostAddr(); 建立一个客户端/服务器系统 网络子系统用TcpServer和TcpClient类实现了一个易用的基于TCP协议的C/S系统. 一个TcpServer可以为任意数量的TcpClient服务. 建立一个服务器可以这么做: 1: using namespace Net; 2: 3: Ptr<TcpServer> tcpServer = TcpServer::Create(); 4: tcpServer->SetAddress(IpAddress("any",2352)); 5: if(tcpServer->Open()) 6: { 7: // TcpServer successfully opened 8: } 这样会建立一个在2352端口监听客户端连接请求的服务器. 为了跟TcpServer通信, 需要在客户端建立一个TcpClient对象: 1: using namespace Net; 2: 3: Ptr<TcpClient> tcpClient = TcpClient::Create(); 4: tcpClient->SetBlocking(false); 5: tcpClient->SetAddress(IpAddress("localhost",2352)); 6: TcpClient::Result res = tcpClient->Connect(); 这里假设服务端和客户端运行在同一台机器上(因为客户端连接到了”localhost”). 像上面那样非阻塞的情况, Connect()方法不是返回TcpClient::Success(这意味着连接建立好了)就是TcpClient::Connecting, 如果这样的话, 应用程序需要继续调用Connect()方法. 如果连接错误, 会返回一个TcpClient::Error的返回值. 如果是阻塞的, Connect()方法直到连接建立(结果是TcpClient::Success)或发生错误才会返回. 注意:一个交互式应用程序不应该在网络通信时阻塞, 而应不断地为用户提供反馈. 一旦连接建立, 服务端会为每个客户机建立一个TcpClientConnection对象. TcpClientConnection在服务器上表示客户机, 并且负责从客户机收发数据. 要进行接收和发送数据的话, 需使用IO::Stream对象. 在通信流上连接IO::StreamReader和IO::StreamWriter对象后, 从流中编码和解码数据是一件非常容易的事情. 注意:发送数据并不是即时的, 而是在Send()方法被调用之前会一直保存在发送流当中. 要客户端给服务器发送一些文本数据话, 只要从发送流获取一个指针, 向其中写入数据后调用Send()方法就可以了: 1: using namespace Net; 2: using namespace IO; 3: 4: // obtain pointer to client's send stream and attach a TextWriter 5: const Ptr<Stream>& sendStream = tcpClient->GetSendStream(); 6: Ptr<TextWriter> textWriter = TextWriter::Create(); 7: textWriter->SetStream(sendStream); 8: textWriter->Open()) 9: textWriter->WriteString("Hello Server"); 10: textWriter->Close(); 11: 12: // send off the data to the server 13: if(this->tcpClient->Send()) 14: { 15: // data has been sent 16: } 在服务器端接收客户端数据, 应用程序需要要频繁地(每帧一次)缓存带有客户羰数据的TcpClientConnection. 可能不只一个TcpClientConnection在等待处理, 因此处理循环应该像这样: 1: // get array of client connections which received data since the last time 2: Array<Ptr<TcpClientConnection>> recvConns = tcpServer->Recv(); 3: IndexT i; 4: for(i =0; i < recvConns.Size(); i++) 5: { 6: // get receive stream from current connection, attach a text reader and read content 7: Ptr<TextReader> textReader = TextReader::Create(); 8: textReader->SetStream(recvConns[i]->GetRecvStream()); 9: textReader->Open(); 10: String str = textReader->ReadString(); 11: textReader->Close(); 12: 13: // process received string and send response back to client 14: // create a TextWriter and attach it to the send stream of the client connection 15: Ptr<TextWriter> textWriter = TextWriter::Create(); 16: textWriter->SetStream(recvConns[i]->GetSendStream()); 17: textWriter->Open(); 18: textWriter->WriteString("Hello Client"); 19: textWriter->Close(); 20: 21: // finally send the response back to the client 22: recvConns[i]->Send(); 23: } 在客户端获得服务器的应答, 调用TcpClient::Recv()方法会在数据到达之前一直阻塞(在阻塞模式下), 或者立即返回(在非阻塞模式下), 并在有服务器数据时返回true: 1: // check if data is available from the server 2: if(tcpClient->Recv()) 3: { 4: // yep, data is available, get the recv stream and read the data from it 5: const Ptr<Stream>& recvStream = tcpClient->GetRecvStream(); 6: Ptr<TextReader> textReader = TextReader::Create(); 7: textReader->SetStream(recvStream); 8: textReader->Open(); 9: String responseString = textReader->ReadString(); 10: n_printf("The server said: %s\n", responseString.AsCharPtr()); 11: textReader->Close(); 12: } 客户端也应该通过调用IsConnected()访求检查连接是否有效. 如果因为某些原因使连接断开, 这个方法会返回false. 注意: TcpServer和TcpClient并没有为能够跟不相关的客户端和服务器端而实现一个潜在的通信协议(例如, 一个TcpServer可以跟标准的Web浏览器客户端一起工作, 还有一个TcpClient类可以跟一个标准的HTTP服务器通信). 现实世界的情况是, 一个应用程序应该实现自己的健壮的通信协议, 它至少会编码负载数据的长度. 如果负载比最大包大小还要大, 数据会以多个包发送并在客户端接收. 客户端应该把数据解码成一个完整的消息, 否则需要等待消息的数据接收完毕. 字节次序问题 服务器和客户端可能运行在不同字节次序的的CPU上. 如果二进制数据通过网络发送, 数据必需转换成两个客户端都一致的”网络字节顺序”. Nebula3在IO::BinaryReader和IO::BinaryWriter类中提供字节顺序的自动转换. 只需要简单地调用下面的方法在网络通信流上读写就可以了: 1: binaryReader->SetStreamByteOrder(System::ByteOrder::Network); 2: binaryWriter->SetStreamByteOrder(System::ByteOrder::Network); Socket类 网络子系统提供了一个把传统socket函数包装成C++接口的Socket类. 一般情况下应用程序不直接使用Socket类, 而是使用更高级的像TcpServer这样的类. 但也不是不可能在有的时候直接使用socket函数比Socket类更方便.
上一次熟悉了IO系统后, 写个程序来练练手. 正好这次看到App命名空间, 正好熟悉一下ConsoleApplication的用法. 因为Nebula3内置了ZipFileSystem, 但不支持压缩, 只支持解压缩, 就试着写了一个命令行的unzip.exe, 算是对之前所学的一个总结. 没想解压缩就像拷贝文件一样简单! 因为当zip文件挂载到IO系统后, 可以像本地文件一样使用其中的文件, 呵呵. 1: /********************************************************************
2: created: 2008/07/08
3: created: 8:7:2008 16:15
4: filename: UnZip.cpp
5: author: xoyojank
6:
7: purpose: zip file extract test
8: *********************************************************************/
9:
10: #include "stdneb.h"
11: #include "UnZipApp.h"
12:
13: using namespace Util;
14:
15: //------------------------------------------------------------------------------
16: /**
17: */
18: void __cdecl
19: main(int argc, const char** argv)
20: {
21: CmdLineArgs args(argc, argv);
22: UnZipApp app;
23: app.SetCompanyName("Xoyojank");
24: app.SetAppName("UnZip");
25: app.SetCmdLineArgs(args);
26: if (app.Open())
27: {
28: app.Run();
29: app.Close();
30: }
31: system("pause");
32: app.Exit();
33: }
1: /********************************************************************
2: created: 2008/07/08
3: created: 8:7:2008 16:16
4: filename: UnZipApp.h
5: author: xoyojank
6:
7: purpose: UnZip Application
8: *********************************************************************/
9: #pragma once
10: #include "stdneb.h"
11: #include "app/consoleapplication.h"
12:
13: class UnZipApp : public App::ConsoleApplication
14: {
15: public:
16: UnZipApp(void);
17:
18: /// open the application
19: virtual bool Open();
20: /// run the application, return when user wants to exit
21: virtual void Run();
22:
23: private:
24: /// a recursion method to unzip the files under "dir"
25: void UnZipDir(Util::String& dir);
26: private:
27: Util::String zipFileName;
28: Util::String sourcePath;
29: Util::String targetPath;
30: };
1: /********************************************************************
2: created: 2008/07/08
3: created: 8:7:2008 16:19
4: filename: UnZipApp.cpp
5: author: xoyojank
6:
7: purpose: UnZip Application
8: *********************************************************************/
9: #include "UnZipApp.h"
10:
11:
12: UnZipApp::UnZipApp(void)
13: {
14: }
15:
16: bool UnZipApp::Open()
17: {
18: if (ConsoleApplication::Open())
19: {
20: // help info
21: if (this->args.HasArg("-help"))
22: {
23: n_printf("-file: the .zip file to unzip.\n");
24: n_printf("-path: where are the files unzip to, if this args is omitted, the file will be unzip into current directory.\n");
25: return false;
26: }
27:
28: Util::String zipFile;
29: zipFile = this->args.GetString("-file");
30: // current .exe directory
31: this->sourcePath = Util::String("bin:") + zipFile;
32: bool fileValid = this->ioServer->MountZipArchive(this->sourcePath);
33: if (!fileValid)
34: {
35: // absolute path
36: this->sourcePath = Util::String("file:///") + zipFile;
37: fileValid = this->ioServer->MountZipArchive(this->sourcePath);
38: if (!fileValid)
39: {
40: n_error("Cannot open zip file.\n");
41: return false;
42: }
43: }
44: this->zipFileName = zipFile.ExtractFileName();
45: this->zipFileName.StripFileExtension();
46: this->sourcePath = this->sourcePath.ExtractDirName() + "/";
47:
48: // target directory
49: this->targetPath = this->args.GetString("-path");
50: if (this->targetPath.Length() <= 1 || this->targetPath[1] != ':')
51: {// relative path
52: this->targetPath = Util::String("bin:") + this->targetPath;
53: }
54: else
55: {// absolute path
56: this->targetPath = Util::String("file:///") + this->targetPath;
57: }
58: this->targetPath += "/";
59: if (this->sourcePath == this->targetPath)
60: {
61: n_printf("the source diretory cannot be the same with the destination!");
62: return false;
63: }
64: return true;
65: }
66: return false;
67: }
68:
69: void UnZipApp::Run()
70: {
71: UnZipDir(this->zipFileName);
72: }
73:
74: void UnZipApp::UnZipDir( Util::String& dir )
75: {
76: // create a new directory
77: this->ioServer->CreateDirectory(this->targetPath + dir);
78: // unzip the files in this directory
79: Util::Array<Util::String> listFile = this->ioServer->ListFiles(this->sourcePath + dir, "*");
80: for (IndexT i = 0; i < listFile.Size(); i++)
81: {
82: Util::String curFile = this->targetPath + dir + "/" + listFile[i];
83: this->ioServer->CopyFile(this->sourcePath + dir + "/" + listFile[i], curFile);
84: n_printf("%s\n", curFile.AsCharPtr());
85: }
86: // unzip the sub directories
87: Util::Array<Util::String> listDir = this->ioServer->ListDirectories(this->sourcePath + dir, "*");
88: for (IndexT i = 0; i < listDir.Size(); i++)
89: {
90: Util::String curDir = dir + "/" + listDir[i];
91: n_printf("%s\n", (this->targetPath + curDir).AsCharPtr());
92: UnZipDir(curDir);
93: }
94: }
调试参数:
运行结果:
IO子系统 Nebula3的IO系统相对于Nebula1和2是一个巨大的进步, 新系统的主要设计目标有: - 使用更标准的机制, 如用URI来定位资源, 用MIME类型来区分数据格式
- 一个灵活的流模型, 它不关心数据是来自文件, 内存, HTTP连接还是其它地方
- 从流读写不数据的数据类型也更方便, 例如要读取的XML格式数据来自文件/内存/网络都没问题
- 另外, 新的流和读写类可以在运行时注册到IO系统中
- 相对于系统平台的特定IO函数, 像fopen()这样的C Lib函数会有额外的性能或内存损失. 所以在保证可移植性的前提下不损失性能, 必须使用特定平台的IO函数
IO子系统的一些主要概念: - 一个中枢的IO::Console 对象连接控制台处理器(console handler)来进行文本的输入和输出. 这保证了所有的Nebula3的文本输出都通过一个集中的进出通道. 特定的控制台处理器可以用特定的方式处理文本输出(例如输出到stdout, 游戏控制台, 日志文件或网络连接).
- 重定向符做为路径别名. 大体的功能跟Nebula1和2差不多, 除了从AmigaOS 的重定向符得到的灵感. Nebula3重定向符的一个新特性就是它们可以做为URI的别名. 例如, 重定向符”textures:”可以定义为 "http://www.radonlabs.de/textures", 这样简化的资源路径"textures:mytexture.dds"就会解释成这个绝对路径: "http://www.radonlabs.de/textures/mytexture.dds" (太NB了, 把纹理放到网站上加载? 哈哈, 拿来做内置广告肯定很爽)
- 流(Stream)做为基本的数据进出通道. 它提供了基本的API函数 Open()/Close()/Read()/Write(), 但是可能完全隐藏了传输和存储通道. 典型的例子有IO::FileStream, IO::MemoryStream, 或 Net::HttpStream
- Stream reader 和 writer 是连接到流上并且实现了简单易用的接口来读写数据格式. 例如你可以把IO::XmlReader连接到IO::FileStream来从文件系统读取XML格式的数据, 或者连接到IO::HttpStream来从HTTP连接读取XML格式的数据.
这里有个很好的代码例子可以反映出Nebula3输入输出系统的强大: 1: IO::FileServer::Instance()->CopyFile("http://www.radonlabs.de/index.html", "temp:index.html"); 这一行代码从HTTP服务器拷贝了一个文件到当用户的临时目录里去. 再多加几行代码, 你可以创建一个流对象指向HTTP服务器上的HTML文件, 连接一个XML reader到这个流上, 然后就可以在不存储中间文件的基础上进行解析HTML了. 标准重定向符 Nebula3初始化了以下几个重定向符: - home: 指向应用程序目录, 一般在” C:\Program Files “下. Nebula3把这个目录当成只读的, 为的是不需要管理员权限就能运行.
- user: 这个指向当前登录的用户目录, 一般是指” C:\Documents and Settings\[username] “. Nebula3会自动创建一个本地目录来避免不同程序覆写掉它们的数据. 所以说一般情况下把数据写入用户目录是安全的. 这个地方可以用于保存游戏数据和配置, 或者程序需要调用的持久性数据.
- temp: 这个指向当前用户的临时目录, 一般是可写的, 但是不要假设下一次启动程序时数据还存在.
- bin: 这个指向应用程序可执行文件的目录. 它可以跟home相同, 也可能不同. 这个目录应该也当成是只读的来对待.
其它重定向符可以在程序运行时进行定义. 通常情况下会定义一些抽象资源路径, 如textuers, sound, data等等. 这样的话资源的路径就可以只更改重定向符的定义而是不是去替换所有的路径. 重定向符的另一个好处就是减少了路径字符串的长度, 在一定程序上节省了内存占用. URI(统一资源定位符) 在Nebula3中的资源位置通常都是用URI定义的. URI一般包括下面这几部, 有一些是可选的: - 模式(协议?), 如"http:", "file:", 等... Nebula3 没有硬编码任何模式, 而跟流类绑定在一起注册到IO::StreamServer 单件
- 一个可选的用户信息字段, 这是一个用户名和密码用于HTTP或FTP主机的身份验证
- 一个主机名, 如"www.radonlabs.de"
- 一个在主机名后可选的端口号
- 一个本地路径, 指向主机上的一个资源
- 一个可选的片段, 通常指向资源内部的一个位置
- 一个可选的查询部分, 一般包含一个PHP脚本或其它相似的动态响应机制的参数
IO::URI类用来传递URI并且解析URI字符串到它的各个部分中. 值得注意的是URI对象比字符串占用更多的内存, 所以有时把URI保存在字符串中, 并在需要分割的时候才使用IO::URI类会更好一些. 这里有一些URI的例子: 1: file:///c:/temp/bla.txt 2: file://samba/temp/bla.txt 3: http://www.radonlabs.de/index.html 4: http://user:password@www.myserver.com:8080/index.html#main 通过使用重定位符会大大简化路径名称. 要引用一个程序目录的文件你可以使用”home:bla.txt”, 等价于file:///c:/Program Files/[myapp]/bla.txt. Stream, Reader 和 Writer 流(Stream)提供了用于储存和传输原始数据的接口. 一个流对象提供了传统的Open()/Close()/Read()/Write()/Seek()接口, 其中有些还提供内存映射, 这样数据的读写可以直接通过内存访问来实现. Stream对象用一个IO::URI对象来定义它们的资源位置. 通常情况下, 一个URI格式映射到一个特定的流对象. 例如”http:”URI格式一般映射到Net::HttpStream类, 而”file:”格式则映射到IO:FileStream类. 这个映射由StreamServer构造一个流对象并匹配一个URI. 一个Nebula3应用程序通过StreamServer::Register()方法来注册这个映射关系, 这也是新的流对象和URI格式的注册方法. 让我们来看看有哪些重要的类: - IO::FileStream: 提供了访问主机文件系统的功能
- IO::MemoryStream: 一个具有流接口的动态内存缓冲
- IO::HttpStream: 提供了一个流接口来访问HTTP服务器文件
Stream reader和writer类提供了一些舒适的接口专门处理特定的数据格式. 这里有一些stream reader和writer: - IO::BinaryReader/IOBinaryWriter: 读写二进制数据
- IO::TextReader/IOTextWriter: 读写文本数据
- IO::XmlReader/IOXmlWriter: 读写XML格式的数据
- Messaging::MessageReader/MessagingMessageWriter: 消息序列化
这里有一个用XmlReader从HTTP服务器访问文件的简单例子 1: using namespace IO; 2: 3: Ptr<Stream> stream = StreamServer::Instance()->CreateStream("http://www.radonlabs.de/index.html"); 4: Ptr<XmlReader> xmlReader = XmlReader::Create(); 5: xmlReader->SetStream(stream); 6: if (xmlReader->Open()) 7: { 8: // parse content here using the XmlReader interface 9: } File Server(文件服务器) Nebula3 IO::FileServer类提供了一个单件用于访问主机的文件系统进行一些全局操作, 像定义重定向符, 复制, 删除和检查文件是否存在, 列出目录内容, 等等. 这个代码片断介绍FileServer的一些有用的方法: using namespace IO; using namespace Util; FileServer* fs = FileServer::Instance(); // check if a file or directory exists bool fileExists = fs->FileExists("home:bla.txt"); bool dirExists = fs->DirectoryExists("temp:bla/blub"); // resolve a path with assigns into an absolute filesystem // path, this is sometimes necessary to interface with // 3rd party libraries which don't understand Nebula3 paths directly String absPath = fs->ResolveAssings("user:myapp/savegames"); // create a directory, note that all missing subdirectories will // be created as well fs->CreateDirectory("user:myapp/savegames"); // copy and delete files fs->CopyFile("home:movie.mpg", "temp:movie.mpg"); fs->DeleteFile("temp:movie.mpg"); // list files in a directory matching a pattern Array<String> files = fs->ListFiles("temp:", "*.txt"); // list all subdirectories in temp: Array<String> dirs = fs->ListDirectories("temp:", "*"); 控制台 一般不直接调用IO::Console, 直接n_printf(), n_error(), n_dbgout(), n_warning()@_@
核心子系统 核心库(Core namespace)实现了这些特性: - 一个实现了引用计数的RefCounted基类
- 一个运行时类型信息系统(RTTI)
- 一个模板智能指针, 用于处理RefCounted对象的生命周期
- 一个由类名创建C++对象实例的工厂机制
- 一个中央Server对象用于建立基本的Nebula3运行环境
对象模型 Nebula3在C++对象模型的基础之上实现了下面这些新特性: - 基于引用计数和智能指针的生命周期管理
- 基于类名或四字符编码的对象创建
- 一个运行时类型信息系统
实现一个新的Nebula3类 当实现一个新的类时首先要考虑它是一个传统的C++类还是要从Core::RefCounted继承. 以下几点可以帮你找到答案: - 如果这个类需要使用Nebula3的扩展对象特性, 如引用计数, RTTI等, 则它必须从Core::RefCounted继承.
- 如果这个类是一个典型的小工具类, 如动态数组, 数学向量, 或其它相似的东西, 那么它从Core::RefCounted 继承也没有什么意义.
从Core::RefCounted类继承有一些限制: - RefCounted派生类不应该在栈上创建对象, 因为栈对象的生命周期是由C++来管理的(他们会在离开当前上下文时被销毁, 从而绕过了Nebula3的引用计数生命周期 管理)
- RefCounted的派生类只有一个默认的构造函数.
- RefCounted的派生类必须有一个虚析构函数.
- RefCounted的派生类不能进行拷贝, 因为这样会造成引用计数机制混乱.
要使用Nebula3的对象模型特性, 除了需要从Core::RefCounted继承外, 还需要在头文件新类的声明中进行额外的标注: 一个标准的RefCounted派生类一般这样声明: 1: namespace MyNamespace 2: { 3: class MyClass : public Core::RefCounted 4: { 5: DeclareClass(MyClass); 6: public: 7: /// constructor 8: MyClass(); 9: /// destructor 10: virtual ~MyClass(); 11: ... 12: }; 13: RegisterClass(MyClass); 注意DeclareClass()宏, 构造函数, 析构函数还有类外面的RegisterClass()宏. DeclareClass()宏加入了RTTI和工厂机制所需的最小代价的信息, 它隐藏了Nebula3的对象模型, 希望可以在不影响已有类的基础进上进行内部机制的变更. RegisterClass()宏是可选的, 它把当前类在中央工厂进行注册. 如果你知道这个类永远不会由类名或四字符编码进行创建, 这个宏可以省略. 在这个类的.cpp文件里需要包含Nebula3特有的信息: 1: namespace MyNamespace 2: { 3: ImplementClass(MyNamespace::MyClass, 'MYCL', Core::RefCounted); 4: 5: } ImplementClass()宏注册类的RTTI机制, 第一个参数描述了类的名字(注意命名空间必须包含). 第二个参数是类的四字符编码, 它必须是所有类中唯一的(如果有重复, 你会在启动程序时得到一个错误提示). 第三个参数是父类的名字, 用于RTTI系统去构造类的关系树. 引用计数和智能指针 Nebula3使用传统的引用计数来管理对象的生命周期. 一个模板智能指针类Ptr<>对程序员隐藏了引用计数的实现细节. 一般来说, 应该一直使用智能指针指向RefCounted的派生对象, 除非你能肯定在给出的代码块中这个对象的引用计数不会发生变化. 智能指针相对于一般指针有很多好处: - 访问一个空指针会给你一个断言警告而不是一个内存错误
- 你不需要对引用计数的对象调用AddRef()或Release() (事实上如果你调了, 会了发生严重的错误)
- 智能指针可以在容器类里良好地工作, 一个智能指针的数组会消除所有的一般指针需要的生命周期管理, 你永远不需要考虑去释放指针所指针的对象, 数组包含的像是真正的C++对象一样
- 用智能指针不需要考虑指针的所属, 不需要为谁delete对象而烦恼
智能指针也有一些缺点: - 性能: 拷贝和赋值会引起对象的引用计数的变化, 解除引用会引起指针的断言检查. 这导致的性能消耗一般是可以忽略的, 但是你最好保证它不在内部循环中发生.
- 应该销毁的对象还存在: 因为智能指针管理的对象只有在最后一个引用放弃时才会销毁, 这样会使对象存在超过预订的时间. 这经常会导致一个BUG的产生. 不过引用计数泄露(程序退出时还仍然存在的对象)时Nebula3会提醒你.
创建Nebula3对象 从Core::RefCounted继承的类可以通过3种不同的方式进行创建: 直接通过静态的Create方法: 1: Ptr<MyClass> myObj = MyClass::Create(); 静态的Create()方法是之前提到的DeclareClass()宏加入的, 相对于new操作符来说, 它并没有多做什么. 注意正确使用智能指针来保存新建的对象. 另一种创建方式是通过类名: 1: using namespace Core; 2: Ptr<MyClass> myObj = (MyClass*)Factory::Instance()->Create("MyNamespace::MyClass"); 当你在运行时通过类名来创建十分有用, 特别是对象的反序列化和脚本接口的使用. 注意类型转换是必须的, 因为工厂的Creat()方法返回的是RefCounted指针. 由类名创建的变种是根据四字符编码进行创建: 1: using namespace Core; 2: using namespace Util; 3: Ptr<MyClass> myObj = (MyClass*) Factory::Instance()->Create(FourCC('MYCL')); 这个方法看上去没有那个直观, 但是它比类名创建快得多. 并且四字符编码比类名占用的空间更少, 这更利于对象写入二进制流或从中读取. 运行时类型信息系统 Nebula3的RTTI系统可以让你在运行时访问对象的类型, 检查一个对象是不是某个类的实例, 或者某个派生类的实例. 你也可以直接获得一个对象的类名和四字符编码. 所有这些功能是由DeclareClass() 和 ImplementClass() 宏在背后实现的. 这时有示例程序: 1: using namespace Util; 2: using namespace Core; 3: 4: // check whether an object is instance of a specific class 5: if (myObj->IsInstanceOf(MyClass::RTTI)) 6: { 7: // it's a MyClass object 8: } 9: 10: // check whether an object is instance of a derived class 11: if (myObj->IsA(RefCounted::RTTI)) 12: { 13: // it's a RefCounted instance or some RefCounted-derived instance 14: } 15: 16: // get the class name of my object, this yields "MyNamespace::MyClass" 17: const String& className = myObj->GetClassName(); 18: 19: // get the fourcc class identifier of my object, this yields 'MYCL' 20: const FourCC& fourcc = myObj->GetClassFourCC(); 你也可以向中央工厂查询一个类是否已经注册: 1: using namespace Core; 2: 3: // check if a class has been registered by class name 4: if (Factory::Instance()->ClassExists("MyNamespace::MyClass")) 5: { 6: // yep, the class exists 7: } 8: 9: // check if a class has been registered by class fourcc code 10: if (Factory::Instance()->ClassExists(FourCC('MYCL'))) 11: { 12: // yep, the class exists 13: } Nebula3单件 很多Nebula3的核心对象都是单件, 就是只存在一个实例, 并且所有其它对象都知道它. 你可以通过静态方法Instance()来访问单件, 它返回唯一实例的一个指针. 返回的指针保证是合法的. 如果在调用Instance()方法时对象实例不存在, 一个断点会被抛出: 1: // obtain a pointer to the Core::Server singleton 2: Ptr<Core::Server> coreServer = Core::Server::Instance(); 你也可以检查单件是否存在: 1: // does the Core::Server object exist? 2: if (Core::Server::HasInstance()) 3: { 4: // yep, the core server exists 5: } Nebula3提供了一些辅助的宏来实现单件: 1: // declare a singleton class 2: class MySingletonClass : public Core::RefCounted 3: { 4: DeclareClass(MySingletonClass); 5: DeclareSingleton(MySingletonClass); 6: public: 7: /// constructor 8: MySingletonClass(); 9: /// destructor 10: virtual ~MySingletonClass(); 11: ... 12: }; 13: 14: // implement the singleton class 15: ImplementClass(MyNamespace::MySingletonClass, 'MYSC', Core::RefCounted); 16: ImplementSingleton(MyNamespace::MySingletonClass); 17: 18: //------------------------------------------------------------------------------ 19: /** 20: Implements the Singleton constructor. 21: */ 22: MySingletonClass::MySingletonClass() 23: { 24: ConstructSingleton; 25: } 26: 27: //------------------------------------------------------------------------------ 28: /** 29: Implements the Singleton destructor. 30: */ 31: MySingletonClass:~MySingletonClass() 32: { 33: DestructSingleton; 34: } DeclareSingleton()和ImplementSingleton()宏跟DeclareClass()和ImplementClass()宏差不多.它们在类中添加了一些静态方法(也就是Instance()和HasInstance()). 类的构造函数和析构函数必须包含ConstructSingleton和DestructSingleton宏. ContructSingleton初始化了一个私有的单件指针并保证没有其它的类实例存在(如果不是, 会抛出断言). DestructSingleton让私有的单件指针无效化. 单件的访问默认是只有本地线程. 这意味着在一个线程中创建的单件无法被其他线程访问. 这使得”并行Nebula”大大简化了多线程编程. “并行Nebula”的基本思想是, 一个典型的Nebula3应用程序包含一些”Fat线程”, 每一个Fat线程都是运行在一个单独的CPU核心上. Fat线程可以用于实现异步IO, 渲染, 物理等等. 每一个Fat线程都初始化了它们自己的Nebula3运行环境, 它们执行特性任务所需的最少依赖. 这基本上消除了大部分Nebula3代码的同步问题, 并且把线程相关的代码集中到一个明确定义的代码区域中. “并行Nebula”的另一个好处就是, 程序员在多线程环境中编程时不需要关心太多. 大多数Nebula3代码看起来就像单线程代码一样, 但是它们却运行在各自的Fat线程中. 性能与内存占用的考虑 Nebula3核心层的一个设计目标就是减少底层代码的内存占用, 来更好的适应微型平台, 像手持设备. 这里有一些已经完成的目标: - RefCounted 类在每个实例中只增加了4byte用于引用计数.
- RTTI机制在开头增加了30 到 60 byte, 但是这是对于每个类来说的, 而是不是每个实例.
- 一个智能指针仅仅4 byte, 就像普通指针一样.
- 一些监控结构只会在debug模型下创建, 特别是用来检测引擎计数泄露的RefCountedList.
这里一些用三种不种的创建方法创建一百万个RefCounted 对象所需的时间信息. 这些时间信息是在台Intel Pentium 800 MHz的笔记本上得出的. - Create(): 0.29 seconds
- FourCC: 0.65 seconds
- 类名: 1.45 seconds
|