Rise的自留地

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

0%

新浪科技讯 北京时间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 没找到官方下载地址,就用这个吧.

未获取函数指针就调用函数(如直接连接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:

  1. //------------------------------------------------------------------------------
  2. //  texture2d.fx
  3. //  texture shader for 2D(UI)
  4. //  (C) xoyojank
  5. //------------------------------------------------------------------------------
  6. float2 halfWidthHeight  : HalfWidthHeight;
  7. texture diffMap     : DiffMap0;
  8. sampler diffMapSampler = sampler_state
  9. {
  10.     Texture = <diffMap>;
  11.     AddressU = Clamp;
  12.     AddressV = Clamp;
  13.     MinFilter = Point;
  14.     MagFilter = Point;
  15.     MipFilter = None;
  16. };
  17. struct VS_INPUT
  18. {
  19.     float3 pos  : POSITION;
  20.     float2 uv       : TEXCOORD;
  21. };
  22. struct VS_OUTPUT
  23. {
  24.     float4 pos  : POSITION;
  25.     float2 uv       : TEXCOORD;
  26. };
  27. //------------------------------------------------------------------------------
  28. /**
  29. */
  30. VS_OUTPUT
  31. VertexShaderFunc(VS_INPUT input)
  32. {
  33.     VS_OUTPUT output;
  34.     output.pos.xy = float2(input.pos.x - halfWidthHeight.x, halfWidthHeight.y - input.pos.y) / halfWidthHeight;
  35.     output.pos.zw = float2(input.pos.z, 1.0f);
  36.     output.uv = input.uv;
  37. return output;
  38. }
  39. //------------------------------------------------------------------------------
  40. /**
  41. */
  42. float4
  43. PixelShaderFunc(float2 uv : TEXCOORD0) : COLOR
  44. {
  45. return tex2D(diffMapSampler, uv);
  46. }
  47. //------------------------------------------------------------------------------
  48. /**
  49. */
  50. technique Default
  51. {
  52.     pass p0
  53.     {
  54.         ColorWriteEnable  = RED|GREEN|BLUE|ALPHA;
  55.         ZEnable           = False;
  56.         ZWriteEnable      = False;
  57.         StencilEnable     = False;
  58.         FogEnable         = False;
  59.         AlphaBlendEnable  = True;
  60.         SrcBlend          = SrcAlpha;
  61.         DestBlend         = InvSrcAlpha;
  62.         AlphaTestEnable   = False;
  63.         ScissorTestEnable = False;
  64.         CullMode          = CW;        
  65.         VertexShader = compile vs_3_0 VertexShaderFunc();
  66.         PixelShader = compile ps_3_0 PixelShaderFunc();
  67.     }
  68. }

值得一提的是CullMode = CW, 为什么? 因为N3用的右手坐标系, 这点又跟D3D不一样了........为什么呢? 难道写MAYA跟MAX的插件的时候比较省事?

还是要跟上一次一样设置顶点格式并载入VertexBuffer:

  1. // vertex
  2.             Array<VertexComponent> vertexComponents;
  3.             vertexComponents.Append(VertexComponent(VertexComponent::Position, 0, VertexComponent::Float3));
  4.             vertexComponents.Append(VertexComponent(VertexComponent::TexCoord, 0, VertexComponent::Float2));
  5. float vertex[4][5] = {
  6.                 {0.0f,  0.0f,   0.0f,   0.0f, 0.0f},
  7.                 {0.0f,  256.0f, 0.0f,   0.0f, 1.0f}, 
  8.                 {256.0f,0.0f,   0.0f,   1.0f, 0.0f}, 
  9.                 {256.0f,256.0f, 0.0f,   1.0f, 1.0f}
  10.             };
  11.             vertexBuffer = VertexBuffer::Create();
  12.             Ptr<MemoryVertexBufferLoader> vbLoader = MemoryVertexBufferLoader::Create();
  13.             vbLoader->Setup(vertexComponents, 4, vertex, 4 * 5 * sizeof(float));
  14.             vertexBuffer->SetLoader(vbLoader.upcast<ResourceLoader>());
  15.             vertexBuffer->Load();
  16.             vertexBuffer->SetLoader(NULL);

纹理的创建其实跟顶点差不多, 因为它都是属于资源的一种, 详见Nebula3资源子系统

  1. // texture
  2.             texture = Texture::Create();
  3.             texture->SetResourceId(ResourceId("bin:razor.jpg"));
  4.             texture->SetLoader(StreamTextureLoader::Create());
  5.             texture->Load();
  6.             texture->SetLoader(NULL);

shader的加载跟上一次一样, 只是参数不同:

  1. // shader
  2. this->shaderInstance = this->shaderServer->CreateShaderInstance(ResourceId("shd:texture2d"));
  3.             Ptr<ShaderVariable> halfWidthHeight = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("HalfWidthHeight"));
  4.             float2 halfWH = float2(this->renderDevice->GetDefaultRenderTarget()->GetWidth(), this->renderDevice->GetDefaultRenderTarget()->GetHeight()) * 0.5f;
  5.             halfWidthHeight->SetFloatArray(&halfWH.x(), 2);
  6.             Ptr<ShaderVariable> diffMap = this->shaderInstance->GetVariableBySemantic(ShaderVariable::Semantic("DiffMap0"));
  7.             diffMap->SetTexture(texture);

绘制嘛, 当然改成矩形了, 图片可贴不到一跟线上:

  1. this->renderDevice->BeginFrame();
  2. this->renderDevice->BeginPass(this->renderDevice->GetDefaultRenderTarget(), this->shaderInstance);
  3.         PrimitiveGroup primGroup;
  4.         primGroup.SetBaseVertex(0);
  5.         primGroup.SetNumVertices(4);
  6.         primGroup.SetPrimitiveTopology(PrimitiveTopology::TriangleStrip);
  7. this->renderDevice->SetVertexBuffer(this->vertexBuffer);
  8. this->renderDevice->SetPrimitiveGroup(primGroup);
  9. this->renderDevice->Draw();
  10. this->renderDevice->EndPass();
  11. this->renderDevice->EndFrame();
  12. this->renderDevice->Present();

上图:

图形子系统是渲染层中图形相关子系统的最高层. 它基本上是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条简单的规则:

  1. 如果一个Cell是完全可见的, 那么它所有的图形实体和子Cell都必须可见.
  2. 如果一个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类也提供了一个共同的接口用来同步和异步的资源加载. 同步加载可以这样做:

  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关卡设计师更加容易.