`

网络游戏制作技术

阅读更多

很不错的教程,可惜只找到了前六章的内容,原创作者也不得而知,希望知情人士告知,感激不尽~

转自:http://todhacker.blog.163.com/blog/static/17099044020110814439790/

当今网络游戏在中国大陆已经在大范围的蔓延,暂且不论这样的一种趋势会带来什么样的游戏产业趋势。这里只就网络游戏的制作和大家进行交流,同时将自己的制作经验写处理,希望为中国的游戏业的发展做出一点点的贡献。。
  
网络游戏的程序开发从某种意义上来看,最重要的应该在于游戏服务器端的设计和制作。对于服务器端的制作。将分为以下几个模块进行:
1.网络通信模块
2.协议模块
3.线程池模块
4.内存管理模块
5.游戏规则处理模块
6.后台游戏仿真世界模块。

现在就网络中的通信模块处理谈一下自己的看法!!

在网络游戏客户端和服务器端进行交互的双向I/O模型中分别有以下几种模型:
1. Select模型
2. 事件驱动模型
3. 消息驱动模型
4. 重叠模型
5. 完成端口重叠模型。
在这样的几种模型中,能够通过硬件性能的提高而提高软件性能,并且能够同时处理成千上百个I/O请求的模型。服务器端应该采用的最佳模型是:完成端口模型。然而在众多的模型之中完成端口的处理是最复杂的,而它的复杂之处就在于多服务器工作线程并行处理客户端的I/O请求和理解完成端口的请求处理过程。
对于服务器端完成端口的处理过程总结以下一些步骤:
1. 建立服务器端SOCKET套接字描述符,这一点比较简单。
例如:
SOCKET  server_socket;
Server_socket = socket(AF_INET,SOCK_STREAM,0);
2.绑定套接字server_socket。
    Const int SERV_TCP_PORT = 5555;
    struct sockaddr_in  server_address.
   
memset(&server_address, 0, sizeof(struct sockaddr_in));
    server_address.sin_family      = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port        = htons(SERV_TCP_PORT);
//绑定
Bind(serve_socket,( struct sockaddr *)&server_address, sizeof(server_address));

2. 对于建立的服务器套接字描述符侦听。
Listen(server_socket ,5);
3. 初始化我们的完成端口,开始的时候是产生一个新的完成端口。

   HANDLE  hCompletionPort;
   HCompletionPort = CreateIoCompletionPort(NULL,NULL,NULL,0);
4. 在我们已经产生出来新的完成端口之后,我们就需要进行系统的侦测来得到系统的硬件信息。从而来定出我们的服务器完成端口工作线程的数量。

SYSTEM_INFO   system_info;
GetSystemInfo(&system_info);

在我们知道我们系统的信息之后,我们就需要做这样的一个决定,那就是我们的服务器系统该有多少个线程进行工作,我一般会选择当前处理器的2倍来生成我们的工作线程数量(原因考虑线程的阻塞,所以就必须有后备的线程来占有处理器进行运行,这样就可以充分的提高处理器的利用率)。
代码:
WORD threadNum = system_info. DwNumberOfProcessors*2+2;
for(int i=0;I{
    HANDLE  hThread;
    DWORD    dwthreadId;
    hThread = _beginthreadex(NULL,ServerWorkThrea,
(LPVOID)hCompletePort,0,&dwthreadId);
        CloseHandle(hThread);        
}
CloseHandle(hThread)在程序代码中的作用是在工作线程在结束后,能够自动销毁对象作用。
6. 产生服务器检测客户端连接并且处理线程。

HANDLE  hAcceptThread;
DWORD   dwThreadId;
hAcceptThread= _beginthreadex(NULL,AcceptWorkThread,NULL,
&dwThreadId);
CloseHandle(hAcceptThread);

7.连接处理线程的处理,在线程处理之前我们必须定义一些属于自己的数据结构体来进行网络I/O交互过程中的数据记录和保存。

首先我要将如下几个函数来向大家进行解析:
1.
HANDLE CreateIoCompletionPort (
      HANDLE FileHandle,              // handle to file
      HANDLE ExistingCompletionPort,  // handle to I/O completion port
      ULONG_PTR CompletionKey,        // completion key
      DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);
参数1:
可以用来和完成端口联系的各种句柄,在这其中可以包括如下一些:
套接字,文件等。

参数2:
已经存在的完成端口的句柄,也就是在第三步我们初始化的完成端口的句柄就可以了。

参数3:
这个参数对于我们来说将非常有用途。这就要具体看设计者的想法了, ULONG_PTR对于完成端口而言是一个单句柄数据,同时也是它的完成键值。同时我们在进行
这样的GetQueuedCompletionStatus(….)(以下解释)函数时我们可以完全得到我们在此联系函数中的完成键,简单的说也就是我们在CreateIoCompletionPort(…..)申请的内存块,在GetQueuedCompletionStatus(……)中可以完封不动的得到这个内存块,并且使用它。这样就给我们带来了一个便利。也就是我们可以定义任意数据结构来存储我们的信息。在使用的时候只要进行强制转化就可以了。

参数4:
引用MSDN上的解释
[in] Maximum number of  threads that the operating system allows to concurrently process I/O completion packets for the I/O completion port. If this parameter is zero, the system allows as many concurrently running threads as there are processors in the system.
这个参数我们在使用中只需要将它初始化为0就可以了。上面的意思我想大家应该也是了解的了!嘿嘿!!

我要向大家介绍的第二个函数也就是
2.
BOOL GetQueuedCompletionStatus(
    HANDLE CompletionPort,       // handle to completion port
    LPDWORD lpNumberOfBytes,     // bytes transferred
    PULONG_PTR lpCompletionKey,  // file completion key
    LPOVERLAPPED *lpOverlapped,  // buffer
    DWORD dwMilliseconds         // optional timeout value
);
参数1:
我们已经在前面产生的完成端口句柄,同时它对于客户端而言,也是和客户端SOCKET连接的那个端口。

参数2:
一次完成请求被交换的字节数。(重叠请求以下解释)

参数3:
完成端口的单句柄数据指针,这个指针将可以得到我们在CreateIoCompletionPort(………)中申请那片内存。
借用MSDN的解释:
[out] Pointer to a variable that receives the completion key value associated with the file handle whose I/O operation has completed.A completion key is a per-file key that is specified in a call to CreateIoCompletionPort.
所以在使用这个函数的时候只需要将此处填一相应数据结构的空指针就可以了。上面的解释只有大家自己摆平了。

参数4:
重叠I/O请求结构,这个结构同样是指向我们在重叠请求时所申请的内存块,同时和lpCompletionKey,一样我们也可以利用这个内存块来存储我们要保存的任意数据。以便于我们来进行适当的服务器程序开发。
[out] Pointer to a variable that receives the address of the OVERLAPPED structure that was specified when the completed I/O operation was started.(MSDN)

3.
int WSARecv(
    SOCKET s,                                              
    LPWSABUF lpBuffers,                                    
    DWORD dwBufferCount,                                   
    LPDWORD lpNumberOfBytesRecvd,                          
    LPDWORD lpFlags,                                       
    LPWSAOVERLAPPED lpOverlapped,                          
    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);
这个函数也就是我们在进行完成端口请求时所使用的请求接受函数,同样这个函数可以用ReadFile(………)来代替,但不建议使用这个函数。

参数1:
已经和Listen套接字建立连接的客户端的套接字。

参数2:
用于接受请求数据的缓冲区。
[in/out] Pointer to an array of WSABUF structures. Each WSABUF structure contains a pointer to a buffer and the length of the buffer.(MSDN)。
参数3:
参数2所指向的WSABUF结构的数量。
[in] Number of WSABUF structures in the lpBuffers array.(MSDN)

参数4:

[out] Pointer to the number of bytes received by this call if the receive operation completes immediately. (MSDN)

参数5:
[in/out] Pointer to flags.(MSDN)
参数6:

这个参数对于我们来说是比较有作用的,当它不为空的时候我们就是提出我们的重叠请求。同时我们申请的这样的一块内存块可以在完成请求后直接得到,因此我们同样可以通过它来为我们保存客户端和服务器的I/O信息。
参数7:
[in] Pointer to the completion routine called when the receive operation has been completed (ignored for nonoverlapped sockets).(MSDN)
4.
int WSASend(
    SOCKET s,                                              
    LPWSABUF lpBuffers,                                    
    DWORD dwBufferCount,                                   
    LPDWORD lpNumberOfBytesSent,                           
    DWORD dwFlags,                                         
    LPWSAOVERLAPPED lpOverlapped,                          
    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);
参数解释可以参考上面或者MSDN。在这里就不再多说了。

下面就关client端用户连接(connect(……..))请求的处理方式进行

举例如下:
const int BUFFER_SIZE = 1024;
typedef  struct  IO_CS_DATA
{
   SOCKET  clisnt_s;         //客户端SOCKET
   WSABUF  wsaBuf;    
   Char   inBuffer[BUFFET_SIZE];
   Char   outBuffer[BUFFER_SIZE];
   Int     recvLen;
   Int     sendLen;
   SYSTEM_TIME start_time;
   SYSTEM_TIME start_time;
}IO_CS_DATA;


UINT WINAPI  ServerAcceptThread(LPVOID param)
{
SOCKET  client_s;
HANDLE  hCompltPort = (HANDLE) param;
struct sockaddr_in  client_addr;
int addr_Len = sizeof(client_addr);
    LPHANDLE_DATA  hand_Data = NULL; 
while(true)
{
   If((client_s=accept(server_socket,NULL,NULL))
==SOCKET_ERROR)
{
printf("Accept() Error: %d",GetLastError());
return 0;
}
hand_Data = (LPHANDLE_DATA)malloc(sizeof(HANDLE_DATA));
hand_Data->socket = client_s;
if(CreateIoCompletionPort((HANDLE)client_s,hCompltPort,
(DWORD)hand_Data,0)==NULL)
{
printf("CreateIoCompletionPort()Error: %d",
GetLastError());
}
else
{
game_Server->RecvDataRequest(client_s);
}
}
return  0;
}

在这个例子中,我们要阐述的是使用我们已经产生的接受连接线程来完成我们响应Client端的connect请求。关于这个线程我们同样可以用我们线程池的方式来进行生成多个线程来进行处理,其他具体的函数解释已经在上面解释过了,希望不懂的自己琢磨。
关于game_Sever object的定义处理将在下面进行介绍。


class   CServerSocket : public CBaseSocket
{
public:
CServerSocket();
virtual ~CServerSocket();
bool   StartUpServer();                  //启动服务器
void   StopServer();                     //关闭服务器
//发送或者接受数据(重叠请求)
bool    RecvDataRequest(SOCKET client_s);    
   bool    SendDataRequest(SOCKET client_s,char *buf,int b_len);

   void    ControlRecvData(SOCKET client_s,char *buf,int b_len);

void    CloseClient(SOCKET client_s); 
private:
friend  UINT WINAPI GameServerThread(LPVOID completionPortID);          //游戏服务器通信工作线程
private:
void   Init();
void   Release();
    bool   InitComplePort();
bool   InitServer();
bool   CheckOsVersion();
bool   StartupWorkThread();
bool   StartupAcceptThread();
private:
enum   { SERVER_PORT = 10006};
UINT   cpu_Num;        //处理器数量
    CEvent g_ServerStop;   //服务器停止事件
CEvent g_ServerWatch;  //服务器监视事件
public:
HANDLE hCompletionPort;     //完成端口句柄
};

在上面的类中,是我们用来处理客户端用户请求的服务器端socket模型。

消息打包处理部分


续上在上面我简单的说了一下服务器完成端口处理部分,接下来我想大家介绍一下关于如何建立服务器和客户端的联系规则,也就是服务器和客户端的游戏协议部分。有不足之处希望大家和我进行交流。

首先解释一下这里协议的概念,协议大家都了解是一种通信规则,例如:TCP/IP,UDP等等,这些是我们在网络通信过程中所处理使用的协议。而我们这里的协议是我们的游戏服务器和客户端的通信规则。简而言之,也就是客户端发送到服务器的数据包和服务器发送的数据包双方解释规则。下面就通过几个部分来具体介绍这种协议的建立和处理。

消息头定义

如果我们能够解释双方的数据包的意义,我们就必须为双方数据包定义一个统一规则的消息头,我是这么定义消息头的。服务器数据包和客户端数据包分别定义不同的消息头。以下就是双方消息头的简单定义。

struct    ServerMsg_Head             //服务器消息头
{
   WORD  s_version;                //版本信息
   BYTE   s_flages;                 //消息标志
   BYTE   s_who;                  //消息驱动者
   BYTE   s_sort;                   //消息类别
   BYTE   s_value;                 //消息值
   WORD  s_len;                   //消息长度
} ;

struct    ClientMsg_Head             //客户端消息头
{
   WORD  c_version;                //版本信息
   WORD  c_flages                  //消息标志
   WORD  c_sort;                   //消息类别
   WORD  c_value;                  //消息值
   WORD  c_scene;                  //场景信息
   WORD  c_len;                    //消息长度
};

以上是我个人简单定义的消息头,具体的各个参数意义,就是需要规划设计的人来定了。这些我就不多说了。

在我们处理完我们的消息头后,我们就可以将我们的具体游戏数据进行打包。关于数据打包,我们必须要处理两件事情:数据打包,数据加密。为此我就建立相应的class来处理这样的一些操作。DataCtrl.h处理如下:

class  Ppackage类可以拆解为两个单独处理类,打包类和解包类。而此处我就用下面一个类来进行处理。只是给大家开个头,要设计的更好还是靠大家共同来进行斟酌呀!!


class   PPackage                                   //游戏数据包处理类
{
public:
PPackage(BYTE msg_type);                      //设置所打包消息类型
virtual ~PPackage();
//消息数据打包部分
void  SetMsgHead(void  *);                    //设置消息头
void  AddByte(BYTE  data);                   //加入一字节
void  AddWord(WORD  data);                 //加入二字节
void  AddDword(DWORD  data);               //加入四字节
void  AddPoint(POINT  data);                  //加入八字节
void  AddBuf(char * data ,int  data_len);          //加入多个字节
//消息内容获取
void  FinishPack();                            //完成打包
char  *GetPackage();                           //获取数据包
int   GetPacketLen();                          //获取数据包长度


//消息数据解包部分
void     SetMsgPackage(char *buf,int _Len);       //将获取消息进行录入
void    *GetMsgHead();                        //获取消息头数据
BYTE   GetByte();                            //获取一字节
WORD  GetWord();                            //获取二字节
DWORD GetDword();                           //获取三字节
POINT * GetPoint();                            //获取四字节
char  *  GetBuf(int buf_len);                    //获取多字节
bool     IfFinishGet();                         //是否完成解包

private:

void     Init();
void     Release();
void     StartBindPakage();       //开始打包
void     StartUndoPackage();     //开始解包
    bool     MessageEncrypt();       //消息加密
    bool     MessageUndo();         //消息解密

private:

private:
    BYTE   msg_type;               / /{1-SERVER_PACKAGE=1,2-CLIENT_PACKAGE=2}
char  *  msg_buffer;
char  *  buffer;                 //后备缓冲区
int      msg_len;
//消息内容长度
Server_Msg_Head  msg_Head;     //消息头
int      buf_Len;
int      current_pos;             //指针的当前位置
protected:
};

以上就是关于服务器和消息打包类的一些建立和解释,这些方面知识其实也没有什么,主要是“仁者见仁,智者见智”了。而对于网络游戏的制作最重要的还是在于Game World的规划和设计,同时这个方面也是最难和最不好处理的。随后将和大家进行探讨。。

线程池处理部分

续上在这里我将要向大家简单介绍一下游戏服务器中必须要处理另外一项主要技术:

线程池技术

开始 我来向大家简单来介绍一下线程池的概念,先简单了解下线程先,线程可以理解为一个function , 是一个为了进行某一项任务或者处理某一项具体事务的函数。例如:

UINT  WINAPI  FunctionCtrl(void *)              //线程处理函数
{
    进行某一项任务或者处理某一项具体事务
    ………….
    return  EXITFUNCTION_CODE;               //退出码
}

而我们的线程池自身可以理解为是很多线程的一个管理者也可以说是一个很多线程的统筹者。因为我们的线程池具有生成线程功能也具有撤消线程的权利。这就是简单的线程池的概念(我的理解,呵呵!!)接下来就来具体介绍线程池了!!

首先 介绍我们为什么要使用线程池技术呢?大家都知道我们的游戏服务器端要处理大量的用户请求,,同时需要发送大量的游戏数据到客户端,从而来驱动客户端程序的执行和维持游戏的进行。那我们的服务器端是如何进行处理的呢?其实在这里我们就充分用到了线程池技术。
那么用这种技术有什么好处和优点呢?以下就来简述这些,有不足之处和不当之处希望有心人指正,呵呵!!

大家都了解在我们服务器整个运行过程中,我们将整个运行时间分成很多个时间片。而对于这些已经分成的各个微小的时间片而言,在各个不同时间片中要处理的用户请求和需要发送到用户端的游戏数据量也将是不一样的。而处理用户的请求和发送数据到客户端的工作都是由一系列的线程来执行的。

鉴于上面,这样我们就可以感性的设想下服务器运行中的两种情况:
第一种在我们服务器运行到某个时间片需要处理大量的用户请求和发送大量数据,有这样繁重的工作任务,我们就需要有很多的工作者线程来处理完成这样的任务,以此来满足我们的工作需要。这样说我们就必须拥有很多工作者线程。

第二种在我们服务器运行到某个时间片需要处理的用户请求和发送数据工作量比较小,由于任务比较少,我们用来处理任务的工作者线程也就不需要很多。也就是说我们只要有少量的工作者线程就可以达到我们的工作要求了。
   
对于上面的两种情况,我们可以说明这样的一个事实,也就是说我们服务器在运行过程中运行状态是动态改变的,呼忙呼闲,时急时慢的。服务器的这样的行为动作和性质可以做一个如下比喻:服务器就是一个企业,在企业业务非常忙的时候,公司的员工数量就必须要增多来满足业务的需要。而在企业不景气的时候,接的业务也就比较少,那么来说就会有很多员工比较闲。那我们该怎么办呢?为了不浪费公司资源和员工自身资源,我们就必须要裁减员工,从而来配合公司的运行。而做这样工作的可能是公司的人力资源部或者其他部分。现在就认为是人力资源部了。呵呵。

对于上面的比喻我们来抓几个关键词和列举关键词和我们主题对象进行对照,以此来帮大家来简单理解服务器运行和线程池。

企业        :  游戏服务器
人力资源部  :  线程池
职员        :  工作者线程

在说了这么多的废话后,就具体的将线程池模型  ThreadPool.h文件提供以供大家参考:


class   GThreadPoolModel
{
friend static   UINT WINAPI  PoolManagerProc(void* pThread);  //线程池管理线程
friend    static   UINT WINAPI  WorkerProc (void* pThread);       //工作者线程
enum SThreadStatus                                        //线程池状态
{
BUSY,
NORMAL,
IDLE
};
enum SReturnvalue  //线程返回值
{
MANAGERPROC_RETURN_value  = 10001,
WORKERPROC_RETURN_value  = 10002,
…………….
};
public:
GThreadPoolModel ();
virtual ~ GThreadPoolModel ();
    virtual bool  StartUp(WORD static_num,WORD max_num)=0;   //启动线程驰
virtual bool  Stop(void )=0;                                //停止线程池
virtual bool  ProcessJob(void *)=0;                          //提出工作处理要求
protected:
virtual bool  AddNewThread(void )=0;                        //增加新线程
virtual bool  DeleteIdleThread(void)=0;                       //删除空闲线程
static  UINT WINAPI PoolManagerProc (void* pThread);        //线程池管理线程
    static  UINT WINAPI WorkerProc (void* pThread);            //工作者线程
GThreadPoolModel::SThreadStatus  GetThreadPoolStatus( void ); //获取线程池当前工作状态
private:
void    Init();
void    Release();
protected:
………………………..
private:
};

以上是线程池模型的一个简单class,而对于具体的工作处理线程池,可以由此模型进行继承。以此来满足具体的需要。到这里就简单的向大家介绍了线程池的处理方式。有不对之处望指正。同时欢迎大家和我交流。

服务器程序设计部分

续上在这里我将要向大家简单介绍一下游戏服务器中必须要处理另外一项主要技术:
内存分配处理技术也可以称为内存池处理技术(这个比较洋气,前面通俗的好,呵呵)

开始向大家介绍一般情况下我们对于内存的一些基本操作。简单而言,内存操作就只有三个步骤:申请、使用、销毁。而对于这些操作我们在C和C++中的处理方式略有不同:

在C中我们一般用malloc(….)函数来进行申请,而对应销毁已经申请的内存使用free(…)函数。
在C++我们一般使用new操作符和delete操作符进行处理申请和销毁。
大家一定要问了,我们一般都是这样处理的呀!!没有什么可以说的哦!!呵呵,我感觉就有还是有一些东东和大家聊的哦。先聊简单几条吧!!
1.Malloc(…..)和free(….), new ….和 delete …必须成对出现不可以混杂哦,混杂的话,后果就不可以想了哦!!(也没有什么,就是内存被泄漏了,呵呵)
2.在我们使用new …和delete ….一定要注意一些细节,否则后果同上哦!!什么细节呢?下面看一个简单的例子:
char  *block_memory  = NULL;
block_memory  = new  char[1024];
delete  block_memory;
block_memory = NULL;
大家沉思一会。。。。。。。。。
大家看有错吗?没有错吧!!
如果说没有错的,就要好好补补课了,是有错的,上面实际申请的内存是没有完全被释放的,为什么呢?因为大家没有注意第一条的完全匹配原则哦,在new 的时候有[ ],我们在delete 怎么就没有看见[ ] 的影子呢? 这就造成了大错有1023个字节没有被释放。正确的是 : delete []block_memory;

关于内存基本操作的我是说这两条,其他要注意还是有的,基本就源于此了。

了解了上面那些接下来就想大家说说服务器内存处理技术了。上面都没有弄清楚了,就算了。呵呵。

大家都知道,我们的服务器要频繁的响应客户端的消息同时要将消息发送到客户端,并且还要处理服务器后台游戏World的运行。这样我们就必须要大量的使用内存,并且要进行大量的内存操作(申请和销毁)。而在这样的操作中,我们还必须要保证我们的绝对正确无误,否则就会造成内存的泄漏,而内存泄漏对于服务器而言是非常可怕的,也可能就是我们服务器设计失败的毒药。而我们如何进行服务器内存的正确和合理的管理呢?那就是我们
必须建立一套适合我们自己的内存管理技术。现在就向大家说一说我在内存管理方面的一些做法。
基本原理先用图形表示一下:

回收内存块
超大块内存
       现在要申请的内存块

上面的意思是:我们在服务器启动过程中就为自己申请一块比较大的内存块,而我们在服务器运行过程中需要使用内存我们就到这样一块比较大已经申请好的内存块中去取。而使用完后要进行回收。原理就是这么简单。而最重要的是我们如何管理这个大的内存块呢?
(非常复杂也比较难,呵呵)
首先 就内存块操作而言就只有申请(类似 new)和回收(类似 delete)。
其次 我们必须要清楚那些内存我们在使用中,那些是可以申请的。

关于上面我简单将这样的一些数据结构和class定义在下面供大家参考使用。

   typedef  struct  MemoryBlock           //内存块结构
{
    void  *buffer;                      //内存块指针
    int    b_Size;                      //内存块尺寸
} MemoryBlock;

class   CMemoryList                  //列表对象类(相当于数组管理类)
{
    public:
        CMemoryList();
        virtual ~ CMemoryList();
        void   InitList(int  data_size,int data_num);//初始化列表数据结构尺寸和数量
        void   AddToList(void *data);           //加入列表中
        void   DeleteItem(int index);            //删除指定索引元素
……………..
private:
    void   Init();
    void   Release();
private:
    void  *memory;
int    total_size;
int    total_num;
    protected:
};

classs   CMemoryPool                                   //内存池处理类
{
    public:
        CMemoryPool();
        virtual ~ CMemoryPool();
        bool   InitMemoryPool(int  size);                 //初始化内存池
void *  ApplicationMemory(int size);               //申请指定size内存
void   CallBackMemory(void *,int size);            //回收指定size内存

    private:
        void   Init();
        void   Release():
        MemoryBlock  *UniteMemory(MemoryBlock  *block_a,MemoryBlock  * block_b);                                                  //合并内存


    private:
       MemoryBlock  memoryPool_Block;                 //内存池块
       CMemoryList  *callBackMemory_List;              //回收内存列表
       CMemoryList  *usingMemory_List;                 //使用中内存列表
       CMemoryList  *spacingMemory_List;               //空白内存列表
    protected:
};

以上就是这个内存管理类的一些基本操作和数据定义,class  CMemoryList  在这里不是重点暂且就不说了,有空再聊。而具体的内存池处理方法简单叙述如下:

   函数InitMemoryPool():  初始化申请一块超大内存。
   函数ApplicationMemory():申请指定尺寸,申请内存成功后,要将成功申请的内存及其尺寸标示到usingMemory_List列表,同时要将spacingMemory_List列表进行重新分配。以便于正确管理。
   函数CallBackMemory():回收指定尺寸内存,成功回收后,要修改spacingMemory_List列表,同时如果有相邻的内存块就要合并成一个大的内存块。usingMemory_List修改使用列表,要在使用列表中的这一项删除。

以上就是一些简单处理说明,更加详细的就需要大家自己琢磨和处理了。我就不细说了。呵呵。不足之处就请大家进行指正,以便让我们大家都提高。先谢谢了。

线程同步和服务器数据保护

最近因为自己主持的项目出现些问题,太忙了,所以好久都没有继续写东西和大家进行探讨制作开发部分了。在这一节中就要向大家介绍另外一个重要的部分,并且也是最头疼的部分:线程同步和数据保护。

关于线程的概念我在前面的章节中已经介绍过了,也就在这里不累赘—“重复再重复”了。有一定线程基础的人都知道,线程只要创建后就如同脱缰的野马,对于这样的一匹野马我们怎么来进行控制和处理呢?简单的说,我们没有办法进行控制。因为我们更本就没有办法知道CPU什么时候来执行他们,执行他们的次序又是什么?

有人要问没有办法控制那我们如何是好呢?这个问题也正是我这里要向大家进行解释和说明的,虽然我们不能够控制他们的运行,但我们可以做一些手脚来达到我们自己的意志。

这里我们的做手脚也就是对线程进行同步,关于同步的概念大家在《操作系统》中应该都看过吧!不了解的话,我简单说说:读和写的关系(我读书的时候,请你不要在书上乱写,否则我就没有办法继续阅读了。)

处理有两种:用户方式和内核方式。
用户方式的线程同步由于有好几种:原子访问,关键代码段等。

在这里主要向大家介绍关键代码段的处理(我个人用的比较多,简单实用)。先介绍一下它的一些函数,随后提供关键代码段的处理类供大家参考(比较小,我就直接贴上来了)

VOID InitializeCriticalSection(    //初始化互斥体
  LPCRITICAL_SECTION lpCriticalSection  // critical section
);

VOID DeleteCriticalSection(        //清除互斥体
  LPCRITICAL_SECTION lpCriticalSection   // critical section
);

VOID EnterCriticalSection(         //进入等待
  LPCRITICAL_SECTION lpCriticalSection  // critical section
);

VOID LeaveCriticalSection(         //释放离开
  LPCRITICAL_SECTION lpCriticalSection   // critical section
);

以上就是关于关键代码段的基本API了。介绍就不必了(MSDN)。而我的处理类只是将这几个函数进行了组织,也就是让大家能够更加理解关键代码端
.h
class   CCriticalSection                //共享变量区类
{
public:
CCriticalSection();
virtual ~CCriticalSection();
void   Enter();                   //进入互斥体
void   Leave();                  //离开互斥体释放资源
private:
   CRITICAL_SECTION  g_CritSect;
};
.cpp
CCriticalSection::CCriticalSection()
{
InitializeCriticalSection(&g_CritSect);
}

CCriticalSection::~CCriticalSection()
{
DeleteCriticalSection(&g_CritSect);
}

void  CCriticalSection::Enter()
{
EnterCriticalSection(&g_CritSect);
}

void  CCriticalSection::Leave()
{
    LeaveCriticalSection(&g_CritSect);
}
由于篇幅有限关键代码段就说到这里,接下来向大家简单介绍一下内核方式下的同步处理。
哎呀!这下可就惨了,这可是要说好多的哦!书上的罗罗嗦嗦我就不说了,我就说一些我平时的运用吧。首先内核对象和一般的我们使用的对象是不一样的,这样的一些对象我们可以简单理解为特殊对象。而我们内核方式的同步就是利用这样的一些特殊对象进行处理我们的同步,其中包括:事件对象,互斥对象,信号量等。对于这些内核对象我只向大家说明两点:
1.内核对象的创建和销毁
2.内核对象的等待处理和等待副作用

第一:内核对象的创建方式基本上而言都没有什么太大的差别,例如:创建事件就用HANDLE CreateEvent(…..),创建互斥对象 HANDLE CreateMutex(…….)。而大家注意的也是这三个内核对象在创建的过程中是有一定的差异的。对于事件对象我们必须明确指明对象是人工对象还是自动对象,而这种对象的等待处理方式是完全不同的。什么不同下面说(呵呵)。互斥对象比较简单没什么说的,信号量我们创建必须注意我们要定义的最大使用数量和初始化量。最大数量>初始化量。再有如果我们为我们的内核对象起名字,我们就可以在整个进程中共用,也可以被其他进程使用,只需要OPEN就可以了。也就不多说了。

第二:内核对象的等待一般情况下我们使用两个API:
DWORD WaitForSingleObject(        //单个内核对象的等待
      HANDLE hHandle,        // handle to object
      DWORD dwMilliseconds   // time-out interval
);

DWORD WaitForMultipleObjects(     //多个内核对象的等待
      DWORD nCount,             // number of handles in array
      CONST HANDLE *lpHandles,  // object-handle array
      BOOL fWaitAll,            // wait option
      DWORD dwMilliseconds      // time-out interval
);
具体怎么用查MSDN了。
具体我们来说等待副作用,主要说事件对象。首先事件对象是分两种的:人工的,自动的。人工的等待是没有什么副作用的(也就是说等待成功后,要和其他的对象一样要进行手动释放)。而自动的就不一样,但激发事件后,返回后自动设置为未激发状态。这样造成的等待结果也不一样,如果有多个线程在进行等待事件的话,如果是人工事件,被激活后所有等待线程成执行状态,而自动事件只能有其中一个线程可以返回继续执行。所以说在使用这些内核对象的时候,要充分分析我们的使用目的,再来设定我们创建时候的初始化。简单的同步我就说到这里了。下面我就将将我们一般情况下处理游戏服务器处理过程中的数据保护问题分析:

首先向大家说说服务器方面的数据保护的重要性,图例如下:

用户列表

                                  用户删除

                                  用户数据修改
                                 
                                  使用数据


加入队列

对于上面的图例大家应该也能够看出在我们的游戏服务器之中,我们要对于我们用户的操作是多么的频繁。如此频繁的操作我们如果不进行处理的话,后果将是悲惨和可怕的,举例:如果我们在一个线程删除用户的一瞬间,有线程在使用,那么我们的错误将是不可难以预料的。我们将用到了错误的数据,可能会导致服务器崩溃。再者我们多个线程在修改用户数据我们用户数据将是没有办法保持正确性的。等等情况都可能发生。怎么样杜绝这样的一些情况的发生呢?我们就必须要进行服务器数据的保护。而我们如何正确的保护好数据,才能够保持服务器的稳定运行呢?下面说一下一些实际处理中的一些经验之谈。

1.我们必须充分的判断和估计我们服务器中有那些数据要进行数据保护,这些就需要设计者和规划者要根据自己的经验进行合理的分析。例如:在线用户信息列表,在线用户数据信息,消息列表等。。。。。

2.正确和十分小心的保护数据和正确的分析要保护的数据。大家知道我们要在很多地方实现我们的保护措施,也就是说我们必须非常小心谨慎的来书写我们的保护,不正确的保护会造成系统死锁,服务器将无法进行下去(我在处理的过程中就曾经遇到过,头都大了)。正确的分析要保护的数据,也就是说,我们必须要估计到我们要保护的部分的处理能够比较快的结束。否则我们必须要想办法解决这个问题:例如:
        
         DATA_STRUCT  g_data;
         CRITICAL_SECTION  g_cs;

         EnterCriticalSection(&g_cs);
         SendMessage(hWnd,WM_ONEMSG,&g_data,0);
         LeaveCriticalSection(&g_cs);
以上处理就有问题了,因为我们不知道SendMessage()什么时候完成,可能是1/1000豪秒,也可能是1000年,那我们其他的线程也就不用活了。所以我们必须改正这种情况。

         DATA_STRUCT  g_data;
         CRITICAL_SECTION  g_cs;

         EnterCriticalSection(&g_cs);
         PostMessage(hWnd,WM_ONEMSG,&g_data,0);
    LeaveCriticalSection(&g_cs);

或者        DATA_STRUCT  temp_data;

         EnterCriticalSection(&g_cs);
         temp_data = g_cs;
         LeaveCriticalSection(&g_cs);
         SendMessage(hWnd,WM_ONEMSG,& temp_data,0);

   3.最好不要复合保护用户数据,这样可能会出现一些潜在的死锁。


简而言之,服务器的用户数据是一定需要进行保护,但我们在保护的过程中就一定需要万分的小心和谨慎。这篇我就说到这里了,具体的还是需要从实践中来进行学习,下节想和大家讲讲服务器的场景处理部分。先做事去了。呵呵!!有好的想法和建议的和我交流探讨,先谢谢了。

场景管理之消息发送

    好久没有写东西出来和大家共同揣摩,真是对不住大家了。现在终于腾了一些时间来继续和大家研究网络游戏制作技术,在这一节中,我就要向大家介绍网络游戏服务器中World场景划分和场景中消息分发问题。

    在前面我基本向大家讲述的都是一些基本的技术问题,从某种意义上讲。属于纯技术范畴的东西,但现在要向大家讲的,应该是属于服务器功能设计范畴的。在这里,我未必讲的很好,有遗漏之处就请大家谅解和指正。

    对于有一定游戏服务器开发基础的朋友而言,应该都明白一个网络游戏服务器和客户端之间的一个基本关系: 玩家客户端是游戏服务器一个局部COPY表现。这个说法听起来可能有一些绕口,简单的解释下也就是说:客户端所具备的区域信息也就是游戏服务器相同区域数据的一份COPY,而表现的意思,也就是说,CLIENT端将这样的数据信息图形化,并且通过屏幕来进行显示,从而来呈现出一个多姿多彩的游戏世界。这样说大家应该能够听懂了吧?再不懂的话,就自己琢磨了。

    说了上面那么多,大家一定要问“SERVER和CLIENT这种关系和我们的消息发送又有什么关系呢?”。其实,我们要讨论的问题点也就在这里。但现在我还不说场景消息到底该如何进行分发。我们还是再来研究一个问题:什么能够使我们的游戏世界变的内容丰富?
大家先不要看我所说的,先自己想想。

    大家应该都思考过了,我就来说一下我的个人想法和理解,可能和大家不一样(我想到的你没有想到的你补上,反之,我补上)。

    在我们的MMOPRG游戏世界中,造成游戏世界变的丰富多彩一般无外乎两个大方面:NPC动作、玩家动作。呵呵,看起来就只有简单的两个方面,但具体分析这些动作起来,可就会让各位包括我都会头大的。先我们来说NPC动作吧,NPC动作通过AI逻辑进行控制,一般情况可以分为一下几个动作:待机、移动、物理攻击、技能攻击、魔法攻击、死亡等,而游戏中玩家动作的产生是由现实中玩家进行操控的,动作类型基本上也不外乎以上几种。既然在服务器游戏世界中存在这样的一些动作,那是如何进行获取的呢?其实就只有两个字:消息(Message)。而消息的产生方又分为两种:Client消息,Server消息。既然有消息产生,我们就将涉及到另外一个问题:如何将产生的消息分发出去呢?

    下面通过我自己的实现经验来简单介绍场景消息的分发原理(详细介绍写的太多,有点懒!!),应该不是最佳的,我只是提一个开头,更好的处理实现方式还是需要大家来共同研究。有好的想法也希望告诉我下,我也从中学习些新的东西。

    先看场景示范图(我画的,比较土,只是表明一个意思。呵呵)


 
    通过上面设计示范图,我来具体介绍:关于场景消息分发,我的设计和分析过程:
    第一步:将场景网络化,也就是说将我们的游戏服务器大场景进行逻辑上的区域划分,每个单独区域所占的面积可以考虑比屏幕区域稍微大点。同时为每一个单独的区域创建Player标志信息(SOCKET或者其他)列表。

    第二步:将单独区域四分化,也就是说对于每一个小区域,再次划分为四个更加小的区域,同时为每一个小的区域建立一个包含三个对象的整数数组。数组的作用是为了保存此小区域的亲缘区域。例如: 小区域1的亲缘区域就是: A、B、H,小区域2的亲缘区域就是:B、C、D等。

    第三步:在上面两步基础上,就是实际处理消息分发了。如果Player/NPC在区域中进行消息动作,我们通过Player/NPC的当前位置就可以首先确定Player/NPC所在大地图中的具体区域。在我们确定好了具体的区域后,我们要继续确定在那个具体的小区域。在这些小区域都确定后,我们就可以将我们的动作消息发送到亲缘区域中的Player(玩家)。

    第四步:对于第三步的改变优化,用CPU处理量来换取消息数量,具体做法也就是,在亲缘区域中继续区域化。也就是说消息不是发送到亲缘区域中的所有Player(玩家),而是有选择的发送到自身一定区域的玩家。这种优化改革从某种意义上讲,可以减少服务器总消息数量,但增大CPU处理量,而对于具体实现,就需要大家去权衡了。

    以上也就是这个分析和处理过程了。同时关于这个场景处理的.h文件,我也就简单的写下,大家参考了。


Class    GmapRegion
{
   public:
GmapRegion();
~ GmapRegion();
void   GetBoardCastMsgList(POINT current_pos,LIST *);  //获取当前位置广播消息的Player列表。
……..
   private:
        void   Init();
        voud   Release();
        bool   InitMapRegion(int  map_wis,int map_hei);         //地图区域化
       
   protected:
}


    大家在看的过程中觉得有设计不妥之处可以直接告诉我,我也就先谢谢了。下一篇我可能会介绍服务器NPC AI处理等。

 

分享到:
评论

相关推荐

    网络游戏制作技术 pdf

    根据提供的文件信息,我们可以从《网络游戏制作技术》这一书籍中提炼出相关的IT知识点。虽然给定的部分内容并未直接提供书籍的具体章节或详细信息,但基于标题、描述以及可能涵盖的主题,我们可以合理推测这本书可能...

    网络游戏制作大师 MMORPGMaker 2.7

    《网络游戏制作大师 MMORPG Maker 2.7》是一款专为游戏开发者设计的软件工具,它允许用户使用C#语言来构建大型多人在线角色扮演游戏(MMORPG)。这款工具提供了完整的源代码,使得开发者能够深入理解其内部工作原理...

    Unity3D网络游戏实战游戏开发与设计技术丛书

    《Unity3D网络游戏实战游戏开发与设计技术丛书》是一本专为学习Unity3D和Lua编程语言的游戏开发者准备的教材。Unity3D是一款强大的跨平台游戏引擎,它被广泛应用于制作2D、3D游戏,虚拟现实(VR)、增强现实(AR)...

    易语言制作网络游戏内存搜索修改器源码

    这款"易语言制作网络游戏内存搜索修改器源码"是针对网络游戏开发的一种工具,主要用于搜索和修改游戏内存中的数据,以达到修改游戏状态的目的。 网络游戏内存搜索修改器,通常称为游戏辅助或外挂,它的工作原理是...

    制作-EM游戏制作大师

    在游戏制作过程中,一个强大的游戏引擎能够支持丰富的图形渲染、物理模拟、人工智能以及网络功能,这些都是构建游戏世界不可或缺的部分。 根据提供的【压缩包子文件的文件名称列表】,只有一个文件名为“em”,这...

    网络游戏的封包技术核心

    网络游戏的封包技术核心是计算机科学与网络编程领域的一个重要话题,它涉及到游戏客户端与服务器之间的数据通信。封包技术是实现游戏外挂、游戏优化和网络调试的关键,因此深受编程爱好者和游戏开发者关注。本文将...

    经典游戏制作教程

    在提供的"经典游戏制作教程.doc"文档中,你可以期待找到上述知识点的详细解释和实践指导,包括但不限于游戏设计原则、使用J2ME开发游戏的技术细节、DataGuard和RAC在游戏服务器架构中的应用,以及如何实现高效的游戏...

    C#应用程序游戏制作C#应用程序游戏制作

    在C#中开发游戏应用程序是一项综合性的任务,...总之,C#应用程序游戏制作是一个综合性的过程,涵盖编程基础、图形处理、网络通信、资源管理等多个方面。熟练掌握这些知识点,结合实践,可以创造出丰富多彩的游戏世界。

    网络游戏-游戏版本制作方法及装置.zip

    在网络游戏领域,游戏版本制作是开发过程中的关键环节,它涉及到游戏内容的更新、优化以及对玩家体验的...通过学习这份资料,无论是游戏开发者还是对游戏制作感兴趣的爱好者,都能深入了解网络游戏版本制作的全过程。

    梁健雄游戏制作方案.doc

    总结来说,游戏制作涉及大量的技术层面,从基础的图形和声音处理,到复杂的网络编程和游戏逻辑,每一步都需要扎实的编程基础和不断的实践。梁健雄的方案展示了一个从零开始学习游戏编程并逐步进阶的过程,对于有志于...

    网络游戏知识产权保护白皮书.pdf

    同时,网络游戏行业也面临技术更新迭代快、市场竞争激烈和法律法规跟进不足等挑战。 知识产权保护的具体内容: 1. 著作权保护:根据中国的《著作权法》,网络游戏作为整体被视为一种视听作品,受到法律保护。著作权...

    网络游戏-流程游戏制作方法及装置.zip

    网络游戏是当今数字娱乐产业的重要组成部分,流程游戏制作方法及装置是开发这类游戏的关键技术之一。流程游戏,顾名思义,是指游戏进程按照特定顺序或流程进行,玩家通过完成一系列任务或挑战,推动游戏故事发展。...

    iphone射击游戏制作

    《使用Java语言制作iPhone射击...总的来说,制作一款iPhone射击游戏虽然涉及到许多技术细节,但借助Java和合适的开发工具,即使是初学者也能逐步掌握。通过不断学习和实践,你可以创造出属于自己的精彩射击游戏世界。

    网络游戏-裸眼3D互动游戏制作设备.zip

    《网络游戏-裸眼3D互动游戏制作设备》 在当今科技日新月异的时代,裸眼3D技术已经不再局限于电影和电视领域,它正在逐步渗透到互动游戏制作中,为玩家带来更为真实、沉浸式的游戏体验。这篇资料主要探讨的是如何...

    网络游戏编程教程

    网络游戏的核心是玩家之间的实时交互,因此网络通信技术是关键。本章将讲解TCP/IP协议、UDP、WebSocket等网络通信协议,以及如何处理延迟和同步问题。 第九章:开发实践 通过实际项目,读者可以应用所学知识,解决...

    网络游戏-可控脱氧核糖核酸网络的制作方法.zip

    在探讨“网络游戏-可控脱氧核糖核酸网络的制作方法”这一主题时,我们首先要理解的是,这个概念可能涉及到生物信息学、计算机科学以及网络游戏设计等多个领域的交叉应用。脱氧核糖核酸(DNA)是生物体内的遗传物质,...

Global site tag (gtag.js) - Google Analytics