Rise的自留地

记录生活中的点滴,分享编程技术和游戏开发经验。

0%

跟N2比起来, N3的资源子系统更加开放, 给予了程序员对资源的创建和管理更多的控制. 

Nebula3的资源有下面向个属性:

  • 包装了一些其它Nebula子系统需要的数据
  • 可以用ResourceId共享
  • 可以在任何时候加载(初始化)和卸载
  • 可以同步或异步加载

例如典型的图形资源有网格和纹理, 但资源子系统并不局限于图形资源. 

资源子系统有两个操作层次( 可能以后会把他们放入两个不同的命名空间, 现在他们都是在Resources命名空间下 ):

低层提供了真正的资源对象, 处理资源的共享, 加载和(次要的)保存. 低层的资源类有:

  • ResourceId
  • Resource
  • ResourceLoader
  • ResourceSaver
  • SharedResourceServer. 

高层资源子系统提供了资源管理, 这意味着根据用户的反馈动态的加载和卸载资源. 高层资源子系统的类有:

  • ResourceProxy (又名: ManagedResource)
  • ResourceProxyServer (又名: ResourceManager)
  • ResourceMapper

下面说明资源子系统的各个类是怎么协同工作的:

一个ResourceId是一个唯一的资源标识符. ResourceId用来共享和定位磁盘上的数据(或者资源保存在哪). ResouceId是一些原子字符串(string atoms). Atom是一个常量字符串的唯一32-bit标识符, 这可以大大加快拷贝和比较, 并且可以减少内存占用, 因为标识符字符串只保存一份. 为了定位磁盘上的数据, ResourceId常常分解成一个合法的URL(例如一个ResourceId “texture:materials/granite.dds”, 会在运行时被分解成”file:///C:/Programme/[AppName]/export/textures/materials/granite.dds”. 

一个Resource对象实际上是资源数据的容器. 像纹理和网格这样特定的资源类型都是Resource类的子类, 并且实现了特定的接口. Resource子类通常都是平台相关的(如D3D9Texture), 但是通过有条件的类型定义使其变成平台无关的. 并不像Nebula2那样, 资源对象并不知道怎样去组织, 加载或保存自己. 取而代之的是, 一个合适的ResourceLoader或ResourceSaver必须附属于Resource对象. 因为Nebula程序很少输出数据, ResourceSaver只 是为了完整性而存在的. 换句话说, ResourceLoader是必须的, 因为他们是启用Resource对象的唯一途径. ResourceLoader具有整个资源装载过程的完全控制. 它们可以是平台相关的, 而且也许会依赖于相关联的特定平台的Resource类. 这使得程序员可以对资源的装载过程相比Nebula2有更多的控制. 典型的资源加载类有StreadTextureLoader, MemoryVertexBufferLoader和MemoryIndexBufferLoader(从内存中加载顶点缓存和索引缓存).

Resource类也提供了一个共同的接口用来同步和异步的资源加载. 同步加载可以这样做:

  1. res-> SetResourceId("tex:system/white.dds");
  2. res-> SetLoader(StreamTextureLoader::Create());
  3. res-> SetAsyncEnabled(false)
  4. res-> Load()
  5. if (res-> IsValid()) ... 这时资源加载已经成功了, 否则LoadFailed会返回true.

异步资源加载也很相似:

  1. res->SetResourceId("tex:system/white.dds");
  2. res->SetLoader(StreamTextureLoader::Create());
  3. res->SetAsyncEnabled(true);
  4. res->Load();
  5. 资源这时进入等待状态...
  6. 只要 IsPending() return true, 就要重复地调用Load()... 当然真正的程序会在这时做一些其他的事情
  7. 接下来的某个调用Load()后时刻, 资源的状态要么是Valid(资源已经准备好了), Failed(资源加载失败)或者Cancelled(等待中的资源被取消加载了)

一个应用程序甚至是Nebula3的渲染代码通常都不需要关心这些, 因为资源管理层会处理他们, 并把异步加载的这些细节隐藏到资源代理后面. 

SharedResourceServer单件通过ResourceId来共享资源. 通过SharedResourceServer创建资源确保了每个资源只在内存中加载了一份, 而不管客户端的数目. 如果客户端的数目降低到了0, 资源会被自动卸载(这并不是合适的资源管理, 而应该是ResourceProxyServer应该关心的). 资源共享完全可以直接通过标准的Nebula3的创建机制来绕过. 

ResourceProxy(或ManagedResource)是对于实际资源对象的资源管理包装. 它的思想是包含的资源对象会受资源用途反馈的控制. 例如, 一个纹理代理会在被请求的纹理在后台加载时提供一个占位纹理, 屏幕上所有使用这个资源的物体都很小的话会被提供一张低分辨率的纹理, 一个X帧没有被绘制的纹理会被卸载, 等等. 

ResourceProxyServer(或ResourceManager)单件是资源管理系统的前端. 除了管理附属于它的ResourceMapper的工作外, 它还是ResourceProxy的工厂, 并且把ResourceMapper跟Resource类型联系到了一起. 

ResourceMapper是一个有趣的东西. 一个ResourceMapper跟一种资源类型(如纹理或网格)相关联, 并被应用程序依附到ResourceProxyServer. 它负责从渲染代码的使用反馈来加载/卸载资源. ResourceMapper的子类可以实现不同的资源管理策略, 也可以通过派生特定的ResourceMapper和ResourceLoader来创建一个完全定制的平台和应用相关的资源管理方案. 目标是显而易见的, Nebula3提供了一些好用的ResourceMapper来加载需要的任何东西. 

资源使用反馈是由渲染代码写入ResourceProxy对象的, 而且应该包含这个资源的一些信息:是否会在不久后用到, 是否可见, 并估计物体占用的屏幕空间大小. 特定的反馈依赖于ResourceProxy的子类, ResourceProxy中没有公有的反馈方法. 

基于资源的使用反馈, 一个ResourceMapper应该实现下面的一些操作(这取决于具体的mapper):

  • Load: 根据level-of-detail异步加载资源(如跳过不需要的高分辨率mipmap层次)
  • Unload: 完全卸载资源, 释放珍贵的内存
  • Upgrade: 提高已加载资源的level-of-detail(如加载高分辨率的mipmap层次纹理)
  • Degrade: 降低已加载资源的level-of-detail(如跟上面相反的情况)

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就好了

  1. this->model = ModelEntity::Create();
  2. this->model->SetTransform(matrix44::translation(0.0f, 3.0f, 0.0f));
  3. this->model->SetResourceId(ResourceId("mdl:examples/eagle.n2"));
  4. this->stage->AttachEntity(this->model.upcast<GraphicsEntity>());

模型是黑的? 再往场景里扔个灯就好了:

  1. // attach a light entity
  2.             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()));
  3. this->lightEntity = SpotLightEntity::Create();
  4. this->lightEntity->SetCastShadows(true);
  5. this->lightEntity->SetTransform(lightTransform);
  6. this->lightEntity->SetColor(float4(4.0f, 2.0f, 1.0f, 1.0f));        
  7. this->stage->AttachEntity(this->lightEntity.upcast<GraphicsEntity>());

想控制的话, 再扔个摄像机进去就OK了.......

  1.         GraphicsServer* gfxServer = GraphicsServer::Instance();
  2. // setup the camera util object
  3. this->mayaCameraUtil.Setup(point(0.0f, 0.0f, 0.0f), point(0.0f, 0.0f, 10.0f), vector(0.0f, 1.0f, 0.0f));
  4. // setup a stage
  5. this->stage = gfxServer->CreateStage(StringAtom("DefaultStage"), SimpleStageBuilder::Create());
  6. // attach a camera to the stage
  7. this->cameraEntity = CameraEntity::Create();
  8.         cameraEntity->SetTransform(this->mayaCameraUtil.GetCameraTransform());
  9. this->stage->AttachEntity(cameraEntity.upcast<GraphicsEntity>());
  10. // setup a default view
  11. this->view = gfxServer->CreateView(View::RTTI, StringAtom("DefaultView"), true);
  12. this->view->SetStage(this->stage);        
  13. this->view->SetFrameShader(FrameServer::Instance()->GetFrameShaderByName(ResourceId(DEFAULT_FRAMESHADER_NAME)));
  14. this->view->SetCameraEntity(cameraEntity);

别忘了处理输入事件:

可以参考ViewerApplication::OnProcessInput().

相对于其他的子系统来说, 输入系统是比较简单的. 很多游戏根本就没有对这一块进行封装, 而直接采用了Win32的消息机制.

不过经过封装的输入系统使用起来很方便, 呵呵.

N3中有三种输入设备, 键盘, 鼠标, 手柄. 分别是基于Win32消息, DirectInput, XInput实现的. 这里有一个继承图能够很好的说明输入系统的组织结构:

基本的消息处理机制是这样的一个流程:

InputServer里有默认的一个键盘, 一个鼠标, 一个手柄的"handler", 在每帧开始时InputServer会检测当前的输入消息,  得到一个InputEvent, 由相应的InputHandler来处理.  各个InputHandler都保存着当前帧各种输入状态的缓存(如鼠标左键是否按下), 因此, 在程序运行过程中, 我们只要在绘制结束前检测各个InputHandler的状态就相当于知道当前用户是怎样输入的了.

一般只需要关心这么几个函数就够了:

  1. ////////////////////// Mouse////////////////////////////
  2. /// return true if button is currently pressed
  3. bool ButtonPressed(Input::MouseButton::Code btn) const;
  4. /// return true if button was down at least once in current frame
  5. bool ButtonDown(Input::MouseButton::Code btn) const;
  6. /// return true if button was up at least once in current frame
  7. bool ButtonUp(Input::MouseButton::Code btn) const;
  8. /// return true if a button has been double clicked
  9. bool ButtonDoubleClicked(Input::MouseButton::Code btn) const;
  10. /// return true if mouse wheel rotated forward
  11. bool WheelForward() const;
  12. /// return true if mouse wheel rotated backward
  13. bool WheelBackward() const;
  14. /// get current absolute mouse position (in pixels)
  15. const Math::float2& GetPixelPosition() const;
  16. /// get current screen space mouse position (0.0 .. 1.0)
  17. const Math::float2& GetScreenPosition() const;
  18. /// get mouse movement
  19. const Math::float2& GetMovement() const;
  1. //////////////////////Keyboard//////////////////////
  2. /// return true if a key is currently pressed
  3. bool KeyPressed(Input::Key::Code keyCode) const;
  4. /// return true if key was down at least once in current frame
  5. bool KeyDown(Input::Key::Code keyCode) const;
  6. /// return true if key was up at least once in current frame
  7. bool KeyUp(Input::Key::Code keyCode) const;
  8. /// get character input in current frame
  9. const Util::String& GetCharInput() const;

GamePad先略过, 原理相同

测试例子, 在上一次的代码中添加一段:

  1. void OnRenderFrame()
  2.     {
  3. if (this->inputServer->GetDefaultMouse()->ButtonDown(MouseButton::LeftButton))
  4.         {
  5.             MessageBoxA(this->displayDevice->GetHwnd(), "Left Button Down", NULL, 0);
  6.         }
  7. //...//
  8.     }

效果:

概述

  • 一些为了兼容Nebula2的代码所做的修改, 主要是一些宏的名字受到影响(DeclareClass -> __DeclareClass, ImplementSingleton -> __ImplementSingleton etc...)
  • 着手删除#ifndef/#define/#endif 这些防止重复include的宏, 因为几乎所有的编译器(VStudio, GCC, Codewarrior) 都支持#pragma once 
  • 把同的样Win32 和Xbox360 代码移动到一个共同的Win360 命名空间来消除代码冗余
  • 加入了一个新的Toolkit层, 它包含了一些导出工具和辅助类
  • 加入和整理了一些 Doxygen(文档) 页面

编译系统

  • 重新组织了 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?

  1. class Print : public Scripting::Command   
  2. {   
  3.     DeclareClass(Print);   
  4. public:   
  5. virtual void OnRegister();   
  6. virtual bool OnExecute();   
  7. virtual Util::String GetHelp() const;   
  8. private:   
  9. void Callback(const Util::String& str);   
  10. };<PRE></PRE> 
class Print : public Scripting::Command

{

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

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:[email protected]: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()@_@

Nebula3工具库, 包含一些工具类, 容器类, 还有一个强大的String类.
下面分别来看一下有哪些东东:
Array< TYPE >
动态数组, 类似std::vector, 自带了排序方法和二分查找
Atom< TYPE >
对于持续存在对象的共享引用. 简单得来说, 就是一个生命周期很长的对象的智能指针, Atom<String>是最常用的, 作为常量字符串的封装.
Blob
大块内存空间的封装, 可以比较, 复制, 计算Hash值
CmdLineArgs
通用的命令行参数解析器, 格式: cmd arg0[=]value0 arg1[=]value1 arg2[=]value2
Crc
计算一段内存的CRC值
Dictionary< KEYTYPE, VALUETYPE >
词典类, 用于存储映射. 类似于std::map. 取元素的时间复杂度为O(log n). 内部是一个排序的Array实现的. 注意它只是在需要排序时才排, 所以加入元素很快, 而第一次的搜索会慢一些.
FixedArray< TYPE >
定长数组, 一维
FixedTable< TYPE >
表格, 定长二维数组
FourCC
四字符编码, 相当于一个uint, 可以做为ID, 具有可读性. 前面的工厂方法就用到了. (第一次见单引号里写多个字符@_@, 如uint = ‘ABCD’;)
Guid
全局统一标识符(GUID), 每台机器在不同时间生成的都不一样, 可以说是唯一性的.
HashTable< KEYTYPE, VALUETYPE >
跟Dictionary很像, 不过内部是用哈希表实现的, 搜索时间更快(O(1)), 内存占用要大一些. 相当于stdext::hash_map
做KEY的类必需实现这个方法: IndexT HashCode() const
KeyValuePair< KEYTYPE, VALUETYPE >
相当于std::pair
List< TYPE >
双向链表, 相当于std::list
Proxy< TYPE >
相当于带引用计数的智能指针, 普通类也可以用它进行包装, 而不用继承Core::RefCounted
Queue< TYPE >
队列, 相当于std::queue
SimpleTree< VALUETYPE >
简单的树型结构, 结构存储在Array中
Stack< TYPE >
堆栈, 相当于std::stack
String
字符串类, 相当于std::string, 但是功能强大得多. 提供了与其它Nebula数据类型的转换方法, 还有文件名操作函数.
Variant
通用数据类型, 相当于COM中的VARIANT
关于各个类的详细用法,可以参考testfoundation_win32工程.