|
http://www.ayssss.cn/ 最近老有人问我是不是出了改dota英雄技能的外挂,什么月骑无限大,剑圣无限斩,巫妖无限弹之类。 我在这里一并回答一下,并且稍微说说原理。因为我对地图方面其实是一窍不通,如果有说的不对的地方,还请指正。 其实这些现象都是使用了作弊地图导致的。本来魔兽争霸是有一个地图验证的,如果你跟主机的图不同,是进不去的(要下载地图)。但是魔兽对地图中的war3map.j文件是进行bcc(block check character)校验的,bcc不同于md5,bcc一般只是用来排错的,并不是加密算法。所以就有人写出了这样的代码,可以在b文件末尾添加上一些不起作用的字串,来让b文件的bcc校验码等于a文件(具体代码我就不贴出来了,很容易搜到)。于是呢,我们就可以做到随意修改地图中的war3map.j ,然后再处理一下,使之跟原来的war3map.j的bcc校验码相同。再把改过并处理后的war3map.j文件替换原来的,这样做出来的作弊地图,暴雪的验证会因为bcc校验相同,而把它认为和原版图是相同的。达到的效果就是,别人用正版图建主机,你可以进入,你用盗版图建主机,别人用正版图也可以进入。但是别以为可以为所欲为的修改war3map.j ,虽然你突破了验证这一关进入了游戏,但是魔兽的联机机制是没有办法突破的。 在这里稍微谈一下魔兽的联机机制,没兴趣的请略过这一段。魔兽联机时,一直有个同步机制,每个联机的玩家都会同时计算所有数据,一旦有不一致,就会导致掉线,这也是为什么用金山游侠之类的游戏修改器单机时可以改钱,联机时一改就掉线。因为你只能修改你自己的机器上的数据,而无法改别人的,单方面修改的结果就是造成你跟其他人不同,你就会掉线。当然,如果所有人同时修改的话,仍然是不会掉线的,所以现在有一些联机修改器,参加游戏的几个玩家一起开这个修改器,可以在玩rpg时改钱什么的,我几个同学就老是用这种修改器来通关一些很难打的rpg图。顺便说一下,这样玩下来保存的replay是无法正常播放的,因为replay只记录动作,你使用修改器的改动不会被记录,播放replay时会因为你并未像你游戏时那样修改数据,造成replay不合逻辑而出错。再顺便说一下吧,为什么所谓的人品外挂并不能实现。曾有人发帖抱怨,怎么蓝胖子次次放招都多重施法,怎么某人每次都暴击,他们是不是用了人品挂。其实这是不可能的,有人以为魔兽中的随机数据都是由主机计算的,这样主机就可以找到办法来修改随机数,造成每次都对他有利的结果。但是实际中并非如此,随机数也是所有人一起计算的,也就是说魔兽里的随机是个伪随机。在一局游戏一开始时,主机会发给每个玩家一个随机数种子(这个种子很有可能就是主机从建立主机到游戏开始所经历的毫秒数),之后的一整局中,所有的随机数都根据这个随机数种子,依照事先定好的算法计算出来,这样也就保证了所有人计算出同样的“随机”结果。另外,这个随机种子也会记录进replay,这也从一个侧面说明了魔兽里的随机是伪随机,如果是真的随机,replay就无法重现了。说的有点多了,下面回到正题。 因为魔兽联机机制的存在,你要是随意改了war3map.j,例如改成给自己增加10000的钱,但是别人是按照的没有修改的war3map.j,在别人机器中你是没有那么多钱的。这时你买一个8000的物品,在你自己机器上是可以的,因为你有10000的钱,但是在其他人机器上,你钱却根本不够!这样的不合理动作就会造成你跟其他人断开连接。 也就是说,你只能修改那些不会造成冲突的地方。例如有些作弊图可以显示出地图全开的效果,因为这些显示的东西只是在你本地机器上显示出来的,并不会对其他玩家照成冲突。类似这样的修改都是可行的,不会掉线。 那么,为什么会出现这种有变态技能效果的dota作弊图呢?我刚开始也很困惑,这么夸张的改动怎么竟然没有掉线?我跟朋友要了个作弊图玩的replay,在我的机器上,用正版dota地图播放,竟然完全再现了那些变态效果!因为我对地图方面并不了解,所以开始上网找资料,并通过qq向某些搞地图的高人请教,又下载了那个变态版dota作弊图和某平台私自山寨的所谓“原版”dota图,提取出来war3map.j来进行对比。经过n久的努力,总算搞明白他是怎么改出来这种效果的了。 原来是因为dota使用到了game cache,而作弊图是单方面修改了game cache中的数据,然后通过函数同步给了所有的玩家。通俗点说,game cache相当于一个池子,所有玩家共享这块区域,任意一个玩家都可以修改这个池中的数据,也可以发出通知,让所有人都来同步这个池子,这样就变相修改了其他人的数据。举个例子,例如dota里黑曜石的放逐技能,它可以减少一个人的智力,一分钟后再归还给他,dota里关于这个技能的函数,把目标和要归还的智力值记录在game cache中,1分钟之后会再从game cache取出目标和智力值,给目标加上相应的智力值,就完成了归还这个人的智力的过程。但是在作弊图中,这里增加了代码,先进行一个判断,如果黑曜石是本机玩家,会把game cache中记录的目标改成本方随机的一个队友,然后把game cache中记录的智力值改为500,然后通知所有玩家同步game cache中的这两个值,这样就完成了对所有人game chche中这两个值的修改。1分钟一到,dota就会向这个目标“归还”智力,这样,本方的一个玩家就凭空增加了500智力。(那个被减少智力的倒霉玩家就无法被归还了,可怜) 大致的原理就是这样了,具体细节我就不详细叙述了。不过dota用到game cache的地方其实并不多,所以能改的地方也就那几个。这也是为什么作弊图要专门改这几个地方,而不是改成例如加钱或者加攻击力或者直接胜利之类的,不是不想改,而是无法实现。另外,暴雪官方的地图是不会这样使用game cache的,所以不用担心对战地图被改(另外对战图还有暴雪标志的保护)。其他的rpg地图,如果本身没有用到game cache的,也就改不出来什么花样,最多显示个全图之类。 暴雪将会在1.23修补这个地图验证漏洞,目前1.23的补丁已经在测试中了,相信升级之后,这种改图作弊将不复存在。只是不知国内玩家到时是不是还要继续死守bug频出的1.20呢? 强行插入广告一则:浩方平台会再对地图进行自己的验证,md5验证,作弊图是无法通过的。 至于做山寨dota图的某平台嘛,就我目前来看,它是没有任何地图验证的,唉。 应广大群众强烈要求,这里给出山寨版dota 6.57c的作弊图链接地址,请大家自行围观(话说我参照这个做出了58b和59c的作弊图,活活活): http://sc2dota.com/news/310.html
新浪科技讯 北京时间1月10日消息,据国外媒体报道,由于大量用户争相排队下载Windows 7 Beta导致服务器不堪重负,微软周五下午宣布延迟发布Windows 7 Beta。 微软CEO史蒂夫·鲍尔默(Steve Ballmer)周三在消费电子展(CES)上发表主题演讲时宣布,将于本周五面向公众发布Windows 7 Beta。但由于排队下载的用户过多,导致微软服务器被挤爆。 微软在Windows 7官方博客中称:“由于用户对Windows 7 Beta热情较高,导致服务器超负荷运转。在发布Beta之前,我们将对Microsoft.com网站增加额外的硬件支持。” 微软还称:“我们将确保为用户提供最佳的下载体验,一旦发布Beta,我们会立即通知用户。”周五早上,在微软上传Beta文件之前,就已经有迹象表明微软服务器不堪重负。按计划,微软只提供250万份Windows 7 Beta下载。 刚下的,每秒1MB多啊(刚刚链接上的速度是10MB多,吓了我一跳,等稳定下来就1MB多) 补充两个windows 7地址: Windows 7 Beta 32bit(2.44GB): http://download.microsoft.com/download/6/3/3/633118BD-6C3D-45A4-B985-F0FDFFE1B021/EN/7000.0.081212-1400_client_en-us_Ultimate-GB1CULFRE_EN_DVD.iso Windows 7 Beta 64bit(3.15GB): http://download.microsoft.com/download/6/3/3/633118BD-6C3D-45A4-B985-F0FDFFE1B021/EN/7000.0.081212-1400_client_en-us_Ultimate-GB1CULXFRE_EN_DVD.iso 语言包:http://dl.pconline.com.cn/download/53202-1.html 没找到官方下载地址,就用这个吧. 还有几个Server服务器的版本:(cd-key) 标准版/企业版/数据中心版(2850.0MB): http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_server_en-us-GB1SXFRE_EN_DVD.iso Web版(2724.3MB): http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_serverweb_en-us-GB1WXFRE_EN_DVD.iso 安腾版(2512.2MB): http://download.microsoft.com/download/0/E/7/0E76E1CE-BFB2-475B-B14C-3445E6C27B4E/7000.0.081212-1400_serverenterpriseia64_en-us-GB1SIAIFRE_EN_DVD.iso 此次发布的测试版可免费试用30天,输入以下相应序列号后可以一直使用到今年8月1日。 标准版:2T88R-MBH2C-M7V97-9HVDW-VXTGF 企业版:TFGPQ-J9267-T3R9G-99P7B-HXG47 数据中心版:GQJJW-4RPC9-VGW22-6VTKV-7MCC6 Web版:GT8BY-FRKHB-7PB8W-GQ7YF-3DXJ6 安腾版:CQ936-9K2T8-6GPRX-3JR9T-JF4CJ 如果不能下的话,就是之前有个高手猜测为什么微软要提前泄露的原因,服务器不能承受全世界的人下载的原因,我刚才截取的图。 ![downlaod form microsoft downlaod form microsoft](http://www.cppblog.com/images/cppblog_com/rise-worlds/WindowsLiveWriter/Windows7beta1_B41D/downlaod%20form%20microsoft_thumb.jpg) ![omg omg](http://www.cppblog.com/images/cppblog_com/rise-worlds/WindowsLiveWriter/Windows7beta1_B41D/omg_thumb.jpg)
未获取函数指针就调用函数(如直接连接mswsock..lib并直接调用AcceptEx)的消耗是很大的,因为AcceptEx 实际上是存在于Winsock2结构体系之外的。每次应用程序常试在服务提供层上(mswsock之上)调用AcceptEx时,都要先通过WSAIoctl获取该函数指针。如果要避免这个很影响性能的操作,应用程序最好是直接从服务提供层通过WSAIoctl先获取这些APIs的指针。 奇迹世界 network 类里面就进行指针获取 void MsWinsockUtil::LoadExtensionFunction( SOCKET ActiveSocket ) { //AcceptEx 窃荐 啊廉坷扁 (dll俊辑..) GUID acceptex_guid = WSAID_ACCEPTEX; LoadExtensionFunction( ActiveSocket, acceptex_guid, (void**) &m_lpfnAccepteEx); //TransmitFile 窃荐 啊廉坷扁 (dll俊辑..) GUID transmitfile_guid = WSAID_TRANSMITFILE; LoadExtensionFunction( ActiveSocket, transmitfile_guid, (void**) &m_lpfnTransmitFile); //GetAcceptExSockaddrs 窃荐 啊廉坷扁 GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS; LoadExtensionFunction( ActiveSocket, guidGetAcceptExSockaddrs, (void**) &m_lpfnGetAcceptExSockAddrs); //DisconnectEx 窃荐 啊廉坷扁 GUID guidDisconnectEx = WSAID_DISCONNECTEX; LoadExtensionFunction( ActiveSocket, guidDisconnectEx, (void**) &m_lpfnDisconnectEx ); } bool MsWinsockUtil::LoadExtensionFunction( SOCKET ActiveSocket, GUID FunctionID, void **ppFunc ) { DWORD dwBytes = 0; if (0 != WSAIoctl( ActiveSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, &FunctionID, sizeof(GUID), ppFunc, sizeof(void *), &dwBytes, 0, 0)) { return false; } return true; } LPFN_ACCEPTEX MsWinsockUtil::m_lpfnAccepteEx = NULL; LPFN_TRANSMITFILE MsWinsockUtil::m_lpfnTransmitFile = NULL; LPFN_GETACCEPTEXSOCKADDRS MsWinsockUtil::m_lpfnGetAcceptExSockAddrs = NULL; LPFN_DISCONNECTEX MsWinsockUtil::m_lpfnDisconnectEx = NULL; 收包和发包循环: 服务器需要进行的连接如下: 1、 与其他服务器连接 2、监听绑定端口 这个2个内容都封装进SESSION内里面,通过NETWORKOBJECT对象判断该进行哪部分的包处理 if( !pIOCPServer->Init( &desc, 1 ) ) 根据参数&desc ,对完成端口进行设置 内容有:创建 io_thread(工作者线程), accept_thread(绑定端口),connect_thread(连接其他服务器), send_thread(收包线程),并根据连接的最大数目分配好session pool。 if( !pIOCPServer->StartListen( CLIENT_IOHANDLER_KEY, "127.0.0.1", 6000 ) ) { printf( "监听出错" ); return 0; } pIOCPServer->Connect( CLIENT_IOHANDLER_KEY, pNetObj, "127.0.0.1", 7000 ); 收包: pIOCPServer->Update() ---------》 IOHANDLER_MAP_ITER it->second->Update() ----------》 VOID IoHandler::Update() { ProcessActiveSessionList(); if( !m_pAcceptedSessionList->empty() ) { ProcessAcceptedSessionList(); } if( !m_pConnectSuccessList->empty() ) { ProcessConnectSuccessList(); } if( !m_pConnectFailList->empty() ) { ProcessConnectFailList(); } KickDeadSessions(); } 收包循环 if( !pSession->ProcessRecvdPacket( m_dwMaxPacketSize ) ) { pSession->Remove(); } 发包循环 unsigned __stdcall send_thread( LPVOID param ) { IOCPServer *pIOCPServer = (IOCPServer*)param; IOHANDLER_MAP_ITER it; while( !pIOCPServer->m_bShutdown ) { Sleep( 10 ); for( it = pIOCPServer->m_mapIoHandlers.begin(); it != pIOCPServer->m_mapIoHandlers.end(); ++it ) { it->second->ProcessSend(); } } return 0; }
d、接受SOCKET连接并进行完成端口绑定 VOID IoHandler::ProcessAcceptedSessionList() { SESSION_LIST_ITER it; Session *pSession; // 立加俊 己傍茄 技记甸阑 罐酒敌 烙矫 府胶飘肺 颗辫 m_pAcceptedSessionList->Lock(); m_pTempList->splice( m_pTempList->end(), *m_pAcceptedSessionList );//将m_pAcceptedSessionList 合并到TEMPLIST m_pAcceptedSessionList->Unlock(); // 立加俊 己傍茄 技记俊 措茄 贸府 for( it = m_pTempList->begin(); it != m_pTempList->end(); ++it ) { pSession = *it; // 弥绊悼立荐甫 檬苞窍绰 版快 角菩 if( m_numActiveSessions >= m_dwMaxAcceptSession ) { printf( "connection full! no available accept socket!\n" ); m_pTempList->erase( it-- ); ReuseSession( pSession ); continue; } // IOCP绑定 CreateIoCompletionPort( (HANDLE)pSession->GetSocket(), m_hIOCP, (ULONG_PTR)pSession, 0 ); // Recv俊 角菩窍绰 版快 贸府 if( !pSession->PreRecv() ) { m_pTempList->erase( it-- ); ReuseSession( pSession ); continue; } //-------------------------------- // 己傍利栏肺 立加等 技记 贸府 //-------------------------------- // 匙飘亏 坷宏璃飘 积己 夸没 NetworkObject *pNetworkObject = m_fnCreateAcceptedObject(); assert( pNetworkObject ); // 匙飘亏 坷宏璃飘 官牢爹 pSession->BindNetworkObject( pNetworkObject ); // 立加矫 檬扁拳 棺 NetworkObject肺 立加 烹瘤 pSession->OnAccept(); // 悼立荐 刘啊 ++m_numActiveSessions; } if( !m_pTempList->empty() ) { // 立加俊 己傍茄 技记甸阑 ActiveSessionList俊 眠啊 m_pActiveSessionList->Lock(); m_pActiveSessionList->splice( m_pActiveSessionList->begin(), *m_pTempList ); m_pActiveSessionList->Unlock(); } } PreRecv() 的动作判断SOCKET是否继续有效 BOOL Session::PreRecv() { WSABUF wsabuf; m_pRecvBuffer->GetRecvParam( (BYTE**)&wsabuf.buf, (int&)wsabuf.len ); ZeroMemory( &m_recvIoData, sizeof(OVERLAPPEDEX) ); m_recvIoData.dwOperationType = RECV_POSTED; int ret = WSARecv( GetSocket(), &wsabuf, 1, &m_recvIoData.dwIoSize, &m_recvIoData.dwFlags, &m_recvIoData, NULL ); if( ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ) { return FALSE; } return TRUE; }
b、代码实现连接 连接每个服务器都用继承自ServerSession 的类实现 有如下类 AgentServerSession BattleServerSession FieldServerSession GameDBProxySession GuildServerSession MasterServerSession 基类ServerSession 有 update 实现心跳连接 VOID ServerSession::Update() { if( IsForConnect() ) { // heartbeat 焊郴扁 DWORD dwCurTick = GetTickCount(); if( dwCurTick - m_dwLastHeartbeatTick > 10000 ) { m_dwLastHeartbeatTick = dwCurTick; MSG_HEARTBEAT msg; msg.m_byCategory = 0; msg.m_byProtocol = SERVERCOMMON_HEARTBEAT; Send( (BYTE*)&msg, sizeof(MSG_HEARTBEAT) ); } } } 每个Session要连接服务器的时候 VOID GameDBProxySession::OnConnect( BOOL bSuccess, DWORD dwSessionIndex ) { ServerSession::OnConnect( bSuccess, dwSessionIndex ); if( bSuccess ) { ServerSession::SendServerType(); g_pGameServer->ConnectTo( AGENT_SERVER ); } else { //SUNLOG( eFULL_LOG, "Can't connect to game DB proxy." ); } } VOID GameServer::ConnectTo( eSERVER_TYPE eServerType ) { switch( eServerType ) { case MASTER_SERVER: ConnectToServer( m_pMasterServerSession, (char*)m_pMasterServerSession->GetConnectIP().c_str(), m_pMasterServerSession->GetConnectPort() ); break; case GAME_DBPROXY: ConnectToServer( m_pGameDBProxySession, (char*)m_pGameDBProxySession->GetConnectIP().c_str(), m_pGameDBProxySession->GetConnectPort() ); break; case AGENT_SERVER: ConnectToServer( m_pAgentServerSession, (char*)m_pAgentServerSession->GetConnectIP().c_str(), m_pAgentServerSession->GetConnectPort() ); break; case GUILD_SERVER: ConnectToServer( m_pGuildServerSession, (char*)m_pGuildServerSession->GetConnectIP().c_str(), m_pGuildServerSession->GetConnectPort() ); break; default: ASSERT( !"弊繁 辑滚 鸥涝篮 绝绢夸" ); } } DWORD GameServer::ConnectToServer( NetworkObject * pNetworkObject, char * pszIP, WORD wPort ) { return m_pIOCPServer->Connect( SERVER_IOHANDLER, pNetworkObject, pszIP, wPort ); } DWORD IOCPServer::Connect( DWORD dwIoHandlerKey, NetworkObject *pNetworkObject, char *pszIP, WORD wPort ) { if( pNetworkObject == NULL ) return 0; IOHANDLER_MAP_ITER it = m_mapIoHandlers.find( dwIoHandlerKey ); assert( it != m_mapIoHandlers.end() ); return it->second->Connect( pNetworkObject, pszIP, wPort ); } c、代码实现监听 VOID GameServer::StartListen() { SERVER_ENV * pServerEnv = m_pFileParser->GetServerEnv(); if( !m_pIOCPServer->IsListening( SERVER_IOHANDLER ) ) { DISPMSG( "[GameServer::StartListen] Starting listen(%s:%d)...\n", pServerEnv->ServerIoHandler.szIP, pServerEnv->ServerIoHandler.wPort ); if( !m_pIOCPServer->StartListen( SERVER_IOHANDLER, pServerEnv->ServerIoHandler.szIP, pServerEnv->ServerIoHandler.wPort ) ) { DISP_FAIL; return ; } DISP_OK; } } BOOL IOCPServer::StartListen( DWORD dwIoHandlerKey, char *pIP, WORD wPort ) { IOHANDLER_MAP_ITER it = m_mapIoHandlers.find( dwIoHandlerKey ); assert( it != m_mapIoHandlers.end() ); return it->second->StartListen( pIP, wPort ); }
1、服务器内容 a、不同机器上的分为 DBProxy //数据库 Guild //公会数据 Master //主服务器 Agent //副本服务器 4种服务器,代码提供了很清晰的每个服务器的HANDLER FUNC TABLE(HASH)。 class PacketHandler : public Singleton<PacketHandler> { typedef VOID (*fnHandler)( CScence * pScence, GamePackHeader * pMsg, WORD wSize ); //typedef VOID (*fnHandler_CG)( Player * pPlayer, GamePackHeader * pMsg, WORD wSize ); public: PacketHandler(); ~PacketHandler(); BOOL RegisterHandler_DG(); //BOOL RegisterHandler_CG(); BOOL RegisterHandler_GM(); BOOL RegisterHandler_AG(); BOOL RegisterHandler_Actor(); VOID ParsePacket_DG( CScence * pScence, GamePackHeader * pMsg, WORD wSize ); //VOID ParsePacket_CG( Player * pPlayer, GamePackHeader * pMsg, WORD wSize ); VOID ParsePacket_GM( CScence * pScence, GamePackHeader * pMsg, WORD wSize ); VOID ParsePacket_AG( CScence * pScence, GamePackHeader * pMsg, WORD wSize ); VOID ParsePacket_Actor( CScence * pScence, GamePackHeader * pMsg, WORD wSize ); private: BOOL AddHandler_DG( BYTE category, BYTE protocol, fnHandler fnHandler ); //BOOL AddHandler_CG( BYTE category, BYTE protocol, fnHandler_CG fnHandler ); BOOL AddHandler_GM( BYTE category, BYTE protocol, fnHandler fnHandler ); BOOL AddHandler_AG( BYTE category, BYTE protocol, fnHandler fnHandler ); BOOL m_FunctionMap_Acotr( BYTE category, BYTE protocol, fnHandler fnHandler ); struct FUNC_DG : public BASE_FUNC { fnHandler m_fnHandler; }; struct FUNC_GM : public BASE_FUNC { fnHandler m_fnHandler; }; struct FUNC_AG : public BASE_FUNC { fnHandler m_fnHandler; }; struct FUNC_ACTOR : public BASE_FUNC { fnHandler m_fnHandler; }; FunctionMap m_FunctionMap_DG; FunctionMap m_FunctionMap_CG; FunctionMap m_FunctionMap_GM; FunctionMap m_FunctionMap_AG; FunctionMap m_FunctionMap_Actor; }; CPP。 #include "PacketHandler.h" PacketHandler::PacketHandler() { } PacketHandler::~PacketHandler() { } BOOL PacketHandler::RegisterHandler_DG() { //#define HANDLER_DG( c, p ) if( !AddHandler_DG( c, p, Handler_DG_CHARINFO::On##p ) ) return FALSE return TRUE; } BOOL PacketHandler::RegisterHandler_Actor() { #define HANDLER_GZ( c, p ) if( !AddHandler_Actor( c, p, Handler_GZ_GUILD::On##p ) ) return FALSE return TRUE; } BOOL PacketHandler::RegisterHandler_GM() { //if( !AddHandler_GM( GM_CONNECTION, GM_CONNECTION_SERVER_INFO_CMD, Handler_GM::OnGM_CONNECTION_SERVER_INFO_CMD ) ) // return FALSE; //if( !AddHandler_GM( GM_OPERATION, GM_RELOAD_DATA_CMD, Handler_GM::OnGM_RELOAD_DATA_CMD ) ) // return FALSE; //if( !AddHandler_GM( SERVERCOMMON, SERVERCOMMON_SERVERSHUTDOWN_REQ, Handler_GM::OnSERVERCOMMON_SERVERSHUTDOWN_REQ ) ) // return FALSE; return TRUE; } BOOL PacketHandler::RegisterHandler_AG() { // CG_CHARINFO //if( !AddHandler_AG( CG_CHARINFO, CG_CHARINFO_SELECT_INFO_SYN, Handler_CG_CHARINFO::OnCG_CHARINFO_SELECT_INFO_SYN)) // return FALSE; return TRUE; } VOID PacketHandler::ParsePacket_Actor( CScence * pScence, GamePackHeader * pMsg, WORD wSize ) { if( 0xff == pMsg->m_byCategory ) { } FUNC_GZ * pFuncInfo = (FUNC_GZ *)m_FunctionMap_GZ.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) ); if( NULL == pFuncInfo ) { //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_GZ] PacketType Error GZ!!"); return ; } pFuncInfo->m_fnHandler( pScence, pMsg, wSize ); } VOID PacketHandler::ParsePacket_DG( CScence * pScence, GamePackHeader * pMsg, WORD wSize ) { if( 0xff == pMsg->m_byCategory ) { } FUNC_DG * pFuncInfo = (FUNC_DG *)m_FunctionMap_DG.Find( MAKEWORD( pMsg->wType,pMsg->m_byProtocol ) ); if( NULL == pFuncInfo ) { //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_DG] PacketType Error DG!!"); return ; } pFuncInfo->m_fnHandler( pScence, pMsg, wSize ); } VOID PacketHandler::ParsePacket_GM( CScence * pScence, GamePackHeader * pMsg, WORD wSize ) { if( 0xff == pMsg->m_byCategory ) { } FUNC_GM * pFuncInfo = (FUNC_GM *)m_FunctionMap_GM.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) ); if( NULL == pFuncInfo ) { //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_GM] PacketType Error!! GM"); return ; } pFuncInfo->m_fnHandler( pScence, pMsg, wSize ); } VOID PacketHandler::ParsePacket_AG( CScence * pScence, GamePackHeader * pMsg, WORD wSize ) { if( 0xff == pMsg->m_byCategory ) { } FUNC_AG * pFuncInfo = (FUNC_AG *)m_FunctionMap_AG.Find( MAKEWORD( pMsg->m_byCategory,pMsg->m_byProtocol ) ); if( NULL == pFuncInfo ) { //SUNLOG( eCRITICAL_LOG, "[PacketHandler::ParsePacket_AG] PacketType Error!! AG Category[%d] Protocol[%d] ", pMsg->m_byCategory,pMsg->m_byProtocol); return ; } pFuncInfo->m_fnHandler( pScence, pMsg, wSize ); } BOOL PacketHandler::AddHandler_Acotr( BYTE category, BYTE protocol, fnHandler fnHandler) { FUNC_ACTOR * pFuncInfo = new FUNC_ACTOR; pFuncInfo->m_dwFunctionKey = MAKEWORD( category, protocol ); pFuncInfo->m_fnHandler = fnHandler; return m_FunctionMap_Actor.Add( pFuncInfo ); } BOOL PacketHandler::AddHandler_DG( BYTE category, BYTE protocol, fnHandler fnHandler) { FUNC_DG * pFuncInfo = new FUNC_DG; pFuncInfo->m_dwFunctionKey = MAKEWORD( category, protocol ); pFuncInfo->m_fnHandler = fnHandler; return m_FunctionMap_DG.Add( pFuncInfo ); } BOOL PacketHandler::AddHandler_GM( BYTE category, BYTE protocol, fnHandler fnHandler) { FUNC_GM * pFuncInfo = new FUNC_GM; pFuncInfo->m_dwFunctionKey = MAKEWORD( category, protocol ); pFuncInfo->m_fnHandler = fnHandler; return m_FunctionMap_GM.Add( pFuncInfo ); } BOOL PacketHandler::AddHandler_AG( BYTE category, BYTE protocol, fnHandler fnHandler) { FUNC_AG * pFuncInfo = new FUNC_AG; pFuncInfo->m_dwFunctionKey = MAKEWORD( category, protocol ); pFuncInfo->m_fnHandler = fnHandler; return m_FunctionMap_AG.Add( pFuncInfo ); } 值得注意的是此类是singleton,这样只能实例化一次,带来的好处就是没有多个实例造成的代码泛滥 b、代码实现
上次已经绘制过基本图元了, 这次只不过要贴张图而已..... 本来我想用Graphics的Model渲染流程来做, 不过这一层太高级了, 都是什么场景管理资源映射之类的 做低级的事情, 就要用低级的API嘛 图形渲染的底层是CoreGraphics, 这个层我不打算再单独写(翻译)一篇了, 因为都是Direct3D概念的一些抽象. 也就是说D3D用熟了基本上一看就明白(用GL的我就不清楚啦, 嘿嘿, N3的作者都放弃用GL去实现@_@). 还记得D3D Tutorial中的Textured例子不? 需要的东西有带纹理坐标的点, 纹理. N3中也一样, 不过, 这里没法用固定管线了. N3的设计的时候就放弃了固定管线(多么明智呀, 别喷我-_-, 我只会shader.......), 所以在这之前我们要先写一个shader来进行绘制. 因为我们只是进行简单的演示, 就尽量简单了, 写一个2D的纹理绘制, 你可以用来做UI: - //------------------------------------------------------------------------------
- // texture2d.fx
- // texture shader for 2D(UI)
- // (C) xoyojank
- //------------------------------------------------------------------------------
-
- float2 halfWidthHeight : HalfWidthHeight;
- texture diffMap : DiffMap0;
- sampler diffMapSampler = sampler_state
- {
- Texture = <diffMap>;
- AddressU = Clamp;
- AddressV = Clamp;
- MinFilter = Point;
- MagFilter = Point;
- MipFilter = None;
- };
-
- struct VS_INPUT
- {
- float3 pos : POSITION;
- float2 uv : TEXCOORD;
- };
-
- struct VS_OUTPUT
- {
- float4 pos : POSITION;
- float2 uv : TEXCOORD;
- };
-
- //------------------------------------------------------------------------------
- /**
- */
- VS_OUTPUT
- VertexShaderFunc(VS_INPUT input)
- {
- VS_OUTPUT output;
-
- output.pos.xy = float2(input.pos.x - halfWidthHeight.x, halfWidthHeight.y - input.pos.y) / halfWidthHeight;
- output.pos.zw = float2(input.pos.z, 1.0f);
- output.uv = input.uv;
-
- return output;
- }
-
- //------------------------------------------------------------------------------
- /**
- */
- float4
- PixelShaderFunc(float2 uv : TEXCOORD0) : COLOR
- {
- return tex2D(diffMapSampler, uv);
- }
-
- //------------------------------------------------------------------------------
- /**
- */
- technique Default
- {
- pass p0
- {
- ColorWriteEnable = RED|GREEN|BLUE|ALPHA;
- ZEnable = False;
- ZWriteEnable = False;
- StencilEnable = False;
- FogEnable = False;
- AlphaBlendEnable = True;
- SrcBlend = SrcAlpha;
- DestBlend = InvSrcAlpha;
- AlphaTestEnable = False;
- ScissorTestEnable = False;
- CullMode = CW;
- VertexShader = compile vs_3_0 VertexShaderFunc();
- PixelShader = compile ps_3_0 PixelShaderFunc();
- }
- }
值得一提的是CullMode = CW, 为什么? 因为N3用的右手坐标系, 这点又跟D3D不一样了........为什么呢? 难道写MAYA跟MAX的插件的时候比较省事? 还是要跟上一次一样设置顶点格式并载入VertexBuffer: - // vertex
- Array<VertexComponent> vertexComponents;
- vertexComponents.Append(VertexComponent(VertexComponent::Position, 0, VertexComponent::Float3));
- vertexComponents.Append(VertexComponent(VertexComponent::TexCoord, 0, VertexComponent::Float2));
- float vertex[4][5] = {
- {0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
- {0.0f, 256.0f, 0.0f, 0.0f, 1.0f},
- {256.0f,0.0f, 0.0f, 1.0f, 0.0f},
- {256.0f,256.0f, 0.0f, 1.0f, 1.0f}
- };
- vertexBuffer = VertexBuffer::Create();
- Ptr<MemoryVertexBufferLoader> vbLoader = MemoryVertexBufferLoader::Create();
- vbLoader->Setup(vertexComponents, 4, vertex, 4 * 5 * sizeof(float));
- vertexBuffer->SetLoader(vbLoader.upcast<ResourceLoader>());
- vertexBuffer->Load();
- vertexBuffer->SetLoader(NULL);
纹理的创建其实跟顶点差不多, 因为它都是属于资源的一种, 详见Nebula3资源子系统 - // texture
- texture = Texture::Create();
- texture->SetResourceId(ResourceId("bin:razor.jpg"));
- texture->SetLoader(StreamTextureLoader::Create());
- texture->Load();
- texture->SetLoader(NULL);
shader的加载跟上一次一样, 只是参数不同: - // shader
- this->shaderInstance = this->shaderServer->CreateShaderInstance(ResourceId("shd:texture2d"));
- Ptr<ShaderVariable> halfWidthHeight = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("HalfWidthHeight"));
- float2 halfWH = float2(this->renderDevice->GetDefaultRenderTarget()->GetWidth(), this->renderDevice->GetDefaultRenderTarget()->GetHeight()) * 0.5f;
- halfWidthHeight->SetFloatArray(&halfWH.x(), 2);
- Ptr<ShaderVariable> diffMap = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("DiffMap0"));
- diffMap->SetTexture(texture);
绘制嘛, 当然改成矩形了, 图片可贴不到一跟线上: - this->renderDevice->BeginFrame();
- this->renderDevice->BeginPass(this->renderDevice->GetDefaultRenderTarget(), this->shaderInstance);
- PrimitiveGroup primGroup;
- primGroup.SetBaseVertex(0);
- primGroup.SetNumVertices(4);
- primGroup.SetPrimitiveTopology(PrimitiveTopology::TriangleStrip);
-
- this->renderDevice->SetVertexBuffer(this->vertexBuffer);
- this->renderDevice->SetPrimitiveGroup(primGroup);
- this->renderDevice->Draw();
- this->renderDevice->EndPass();
- this->renderDevice->EndFrame();
- this->renderDevice->Present();
上图: ![](http://p.blog.csdn.net/images/p_blog_csdn_net/xoyojank/EntryImages/20081130/N3_Texture_Quad.JPG)
图形子系统是渲染层中图形相关子系统的最高层. 它基本上是Mangalore图形子系统的下一个版本, 但是现在整合进了Nebula, 并且与低层的渲染代码结合得更加紧密. 最基本的思想是实现一个完全自治的图形”世界”, 它包含模型, 灯光, 还有摄像机实体, 而且只需要与外部世界进行最少的通信. 图形世界的最主要操作是加入和删除实体, 还有更新它们的位置. 因为Mangalore的图形子系统跟Nebula2的完全分界线从Nebula3中移除了, 很多设想都可以用更少的代码和交互来实现. 图形子系统也会为了异步渲染而多线程化, 它和所有的底层渲染子系统都会生存在它们自己的fat-thread中. 这本应是Nebula3层次结构中更高级的东西, 但是我选择了这个位置, 因为这是游戏跟渲染相关通信最少的一部分代码. 正是因为图形代码有了更多的”自治权”, 游戏相关的代码可以跟图形以完全不同的帧率来运行, 不过这需要实践来证明一下. 但是我一定会尝试, 因为完全没有必要让游戏逻辑代码运行在10帧以上(格斗游戏迷们可能会反对吧). 图形子系统中最重要的公有类有: - ModelEntity
- CameraEntity
- LightEntity
- Stage
- View
一个ModelEnity表示了一个可见的图形对象, 它包括位置, 包围体和内嵌的Model资源. 一个Model资源是一个完全的3D模型, 包括几何体, 材质, 动画, 层级变换等…(后面会提到). 一个CameraEntity描述了图形世界中的一个视景体, 为渲染提供View和Project矩阵. 一个LightEntity描述了一个动态光源. Nebula3的光源属性还没有最终确定, 但是我的目标是一个相对灵活地近似(最后一个光源不会超过几个shader参数). Stage和View是Nebula3图形子系统新增的内容. 在Mangalore中, 图形实体是生存在一个单独的图形Level类里, 任何时候只能有一个Level和一个摄像机. 这对于只需要渲染一个世界到帧缓存(frame buffer)的情况来说还是不错的. 但许多游戏程序需要更复杂的渲染, 如在GUI中渲染一个使用单独灯光的3D对象, 而它又跟其它的图形世界是隔离的. 还有反射或像监视器之类的东西都需要一个额外的视口, 诸如此类. 在Mangalore中, 这个问题通过OffscreenRenderer类得到解决, 虽说比较容易使用, 但是具有一些使用限制并且需要更多事后的思考. Nebula3提供了一个基于State和View的更加简洁的解决方案. 一个Stage就是一个图形实体的容器, 表示一个图形世界. 同一时间可能存在多个Stage, 但是它们之间是互相隔绝的. 每个实体在一个时刻只连接到了一个Stage(虽说克隆一个已有实体是一件很简单的事情). 除了简单地把实体组织到一起外, Stage的主要工作是根据它们之间的关系来加速可见性查询. 应用程序可以派生Stage的子类来实现完全不同的可见性查询方案. 一个View对象通过一个CameraEnity渲染stage到一个RenderTarget. 任何stage都可以连接任意数量的View对象. View对象可能会互相依赖(也可能是连接到不同stage的View), 所以更新一个View会首先强制更新另一个View的RenderTarget(这在一个View渲染需要使用另一个View的RenderTarget做为纹理时很方便). View对象完全实现了自己的渲染循环. 应用程序可以在View的子类中方便地实现它自己的渲染策略(如每个light一个pass VS 每个pass多个light, 渲染到cubemap, 等等). 总而言之, 一个Stage完全控制了可见性查询流程, 而一个View则完全控制了渲染流程. 图形子系统的一个最主要的工作就是根据可见性查询的结果来决定哪些实体需要被渲染. 一个可见性查询在实体间建立了一个双向的链接, 它有两种形式: 摄像机链接和灯光链接. 摄像机链接把一个摄像机和在它视景体内的模型连接到了一起. 因为链接是双向的, 所以摄像机知道所有的在它视景体范围内的模型, 而模型也知道所有可以看到它的摄像机. 灯光链接在灯光与模型之间建立了相似的关系, 一个灯光具有所有受它影响的模型的链接, 一个模型也知道所有影响它的灯光. 加速可见性查询最重要的类就是Cell类. 一个Cell是一个图形实体和子Cell的可见性容器, 它必须遵循2条简单的规则: - 如果一个Cell是完全可见的, 那么它所有的图形实体和子Cell都必须可见.
- 如果一个Cell是完全不可见的, 那么它所有的图形实体和子Cell都必须不可见.
Cell是附属于Stage的, 它们形成了一棵有根Cell的树形层次结构. 标准的Cell支持简单的空间划分方案, 如四叉树和八叉树, 但如果像其它的可见性方案, 如portal, 就需要派生Cell的子类来实现了. 子类唯一的功能限制就是上面标出的那两条规则. 当一个图形体连接到一个Stage时, 它会被插入”接受” (通常仅仅是容纳)它的最低级的Cell中. 当更新图形实体的变换信息或改变包围体时, 它会根据需要改变在Cell层次中的位置. Stage居住在StageBuilder类当中, 应用程序应当派生StageBuilder来创建一个Stage的初始状态(通过加入Cell和实体). Nebula3会提供一些标准的StageBuilder集合, 这应该能够满足大多数应用程序的需要了. 这只是图形子系统的一个粗略的概述. 因为当前只有一个最基本的实现, 很多细节接下来可能会有所更改.
Nebula3的代码运行在两种根本不同的方案中. 第一种方案我称之为”Fat Thread”. 一个Fat Thread在一个线程中运行一个完整的子系统(如渲染, 音频, AI, 物理, 资源管理), 并且基本上锁定在一个特定的核心上. 第二种类型的线程我叫它”Job”. 一个job是一些数据和用于处理这些数据的包装成C++对象的代码. 工作调度程序掌管了Job对象, 并且把工作分配给低负载的核心来保持它们一直处于忙碌状态. 显然, 挑战就是设计一个经过全面考虑的系统, 以保持所有的核心一直均匀地忙碌着. 这不但意味着连续的活动需要在游戏每帧的空闲时期内轮流交替, 而且要求job对象不得不事先(如每帧前)创建好, 这样才能在各种Fat Thread空闲时填充当前帧的空白. 这是我希望进行更多试验和调整的地方. 第二个挑战就是让程序员的工作尽量的简单. 一个游戏应用程序员(逻辑程序员)在任何时候都不应该关心他运行在一个多线程的环境中, 不应该担心会产生死锁或改写了其它线程的数据, 也不应该瞎搞一些临界区, 事件和信号量. 同样, 整个引擎的架构也不应该是”脆弱的”. 大部分传统的多线程代码在一定程度上都会发生紊乱, 或者忘记了临界区而打乱数据. 当线程间需要进行数据共享和通信时, 多线程就变得很棘手. 像两个临界区这样的解决方案也会导致脆弱代码问题. 从大的角度来说, Nebula3通过一个”并行Nebula”的概念解决了这个两个问题. 其思想就是运行了一个完整子系统的”Fat Thread”都有自己的最小Nebula运行库, 这个最小运行库刚好包含了这个子系统需要的部分. 因此, 如果这个运行在它自己线程中的子系统需要进行文件访问, 它会有一个跟其它Fat Thread完全分离的文件服务器(file server). 这个解决方案的优点是, 大部分Nebula中的代码都不需要知道它运行在一个多线程的环境中, 因为在fat thread之间没有数据进行共享. 运行着的每个最小Nebula内核是跟其它Nebula内核完全隔离的. 缺点就是, 重复的数据会浪费一些内存, 但是我们只是占用几KB, 而不是MB. 这些数据冗余消除了细密的锁定, 并且解决把程序员从思考每一行代码的多线程安全性中解放了出来. 当然, 从某种意义上说Fat Thread间的通信是肯定会发生的, 要不然这整个思想就没有意义了. 方法就是建立一个且只有一个的标准通信系统, 并且保证这个通信系统是可靠而快速的. 这就是消息系统的由来. 要跟一个Fat Thread通信的话只有发送一个消息给它. 消息是一个简单的C++对象, 它包含了一些带有get/set方法的数据. 通过这个标准的通信手段, 实际上只有消息子系统才需要是线程安全的(同样, 访问跟消息相关的资源时, 如内存缓冲区, 必须受到约束, 因们它们代表了共享数据). (xoyojank: 我说咋那么多Message…) 这样虽然解决了Fat Thread方案中大多数的多线程问题, 但没有解决Job对象的任何事情. Nebula3很有可能需要约束一个Job对象能做什么和不能做什么. 最直接的行为就是限制job做内存缓冲区的计算. 那样的话, job中就不能存在复杂的运行库(不能文件I/O, 不能访问渲染等等). 如果这样还不够的话, 必须定义一个”job运行时环境”, 就像Fat Thread中的那样. 因为一个job不会发起它自己的线程, 而且还会被调度到一个已经存在的线程池中. 就这个方面来说, 这不存在什么问题. 到现在为止(xoyojank: 2007/01/21, 最新版本已经实现了多数子系统的多线程化), 只有IO子系统作为概念证明在Fat Thread中得到实现, 并且它运行得很今人满意. 在做传统的同步IO工作时, 一个Nebula3程序可以直接调用本地线程的IO子系统. 所以像列出文件夹的内容或删除一个文件, 只会调用一个简单的C++方法. 对于异步IO工作, 定义了一些常见的IO操作消息(如ReadStream, WriteStream, CopyFile, DeleteFile, 等等). 进行异步IO只需要几行代码: 创建一个消息对象, 填充数据, 并发送这个消息到一个IOInterface单件. 如果必要的话, 这可能会需要等待和轮询异步操作. 这样的好处就是, 整个IO子系统没有一行多线程意义上的代码, 因为各个在不同的Fat Thread中的IO子系统是完全隔离的(当然, 同步肯定会发生在一些IO操作上, 但那都留给操作系统了).
跟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类也提供了一个共同的接口用来同步和异步的资源加载. 同步加载可以这样做: - res-> SetResourceId("tex:system/white.dds");
- res-> SetLoader(StreamTextureLoader::Create());
- res-> SetAsyncEnabled(false)
- res-> Load()
- if (res-> IsValid()) ... 这时资源加载已经成功了, 否则LoadFailed会返回true.
异步资源加载也很相似: - res->SetResourceId("tex:system/white.dds");
- res->SetLoader(StreamTextureLoader::Create());
- res->SetAsyncEnabled(true);
- res->Load();
- 资源这时进入等待状态...
- 只要 IsPending() return true, 就要重复地调用Load()... 当然真正的程序会在这时做一些其他的事情
- 接下来的某个调用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(如跟上面相反的情况)
|