|
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().
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關卡設計師更加容易.
相對于其他的子系統來說, 輸入系統是比較簡單的. 很多游戲根本就沒有對這一塊進行封裝, 而直接采用了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
|