游戏开发论坛

 找回密码
 立即注册
搜索
查看: 3430|回复: 11

网络游戏制作心得(四)

[复制链接]

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
发表于 2004-12-20 16:20:00 | 显示全部楼层 |阅读模式
前面讲了一些程序架构上的,不知道大家感觉怎么样,其实只要架构搭好了,无论是用什么样的渲染手段,基本都是一样的,可能3D要做出比较好的效果,需要特别处理一些对象。我本来想讲一讲地图的有些做法,但还是留给大家自己想把,地图是游戏程序中最简单的一个对象。我有个例子带源码,稍后可以看一看,写得很仓促,欢迎大家指教。另外有关游戏的一些经验和问题,欢迎大家访问http://bbs.chaosstars.com/ (网络游戏编程版)交流,群的人数满了,请大家见谅。

下面我谈一谈游戏中用到的网络编程

对于网络编程,我也不谈具体的技术,目前Windows下对于各种网络I/O模型,都有很多资料,像select模型,Windows消息模型,Windows事件模型,重叠I/O模型,完成端口模型。每种模型都有各自的特点,可以根据实际需要来选择,我一般认为,如果你需要进行移植,可以选择select模型,如果只是比较简单的收发数据(客户端),用Windows消息模型比较方便,服务器需要大批量处理网络数据的,可以用完成端口模型

由于是C/S模式,目前网络游戏大多采用TCP数据传输协议,不需要考虑网络数据丢包和接帧的问题,但TCP的数据协议采用了一些延时发送的算法,而且每次发送一个包都要进行确认,所以尽可能每次不要发送很小很小的包,一般一个包的大小在1K左右,而且TCP的包是基于流的,包和包之间是一个接一个,像流水一样,需要进行拆包(稍候介绍)

我先说服务器,TCP的服务器一定有一个监听用的Socket,专门用来处理客户端的连接信息,然后对应每一个客户端有一个对应的Socket来传递数据,怎么组织这些Socket,我想大家都有自己的办法,无论是静态数组也好还是动态表也行,但是有一点就是必须能够很快的检索这些Socket,我一般倾向于用静态数组。
SOCKET g_Sock[MAX_USER];

消息有2种形式,一种是基于二进制的,一种是基于字符串的,每种都有各自的优势,说不上哪种好,但我比较倾向于二进制,能够处理自定义的数据类型,字符串类型的都是可见字符,比较容易扩展,下面看基于二进制的消息的定义
struct tagNetMsg
{
    WORD wSockID;            // 对应的Socket编号,其实也是用户的编号
    WORD wMsgID;             // 命令编号,就是消息头
    WORD wMsgSize;           // 命令大小
    BYTE wMsgBuf[MAX_SIZE];  // 命令缓冲区

    // 这里我们可以定义很多操作符
    tagNetMsg& operator=( tagNetMsg& _right );
    ...
    void SetMsg( BYTE * _buf, _int size );
   
    // 输入流
    int begin_in( WORD wMsgID ); // 清空buf,并设置开始标志,然后将命令头放进缓冲区
    void operator<<( char& _val );
    void operator<<( unsigned char& _val );
    void operator<<( int& _val );
    void operator<<( unsigned int& _val );
    ...
    int end_in( WORD wMsgID ); // 加密函数可以放进这里

    // 输出流
    int begin_out( WORD& wMsgID ); // 解密并将命令头取出
    void operator>>( char& _val );
    void operator>>( unsigned char& _val );
    void operator>>( int& _val );
    void operator>>( unsigned int& _val );
    ...
    int end_out( WORD wMsgID );
};
typedef tagNetMsg NETMSG;
typedef tagNetMsg * LPNETMSG;

对于发送数据,我们直接调用send函数就好了,一般不需要做特别的处理,如果出现发送的时候有Block现象,那么我们可以将发送的内容存储在一个队列里面,稍后再发。
queue<LPNETMSG> g_queSend;
发送数据的时候实际上只是将发送的消息放进队列
LPNETMSG pNewMsg = new NETMSG;

pNewMsg->begin_in( 0x0011 ); // 比如0x0011表示走路
(*pNewMsg)<<1000.0f<<800.0f; // 当前坐标
pNewMsg->end_in( );

g_queSend.lock( );  // 要注意将队列锁住,大家自己实现这个锁,用临界量就行
g_queSend.push( pNewMsg );
g_queSend.unlock( );

然后在专门的发送消息的线程中
DWORD WINAPI SendThread( LPVOID lpParam )
{
    LPNETMSG pMsg;
    while ( true )
    {
        while ( g_queSend.size( ) > 0 )
        {
            g_queSend.lock( );  // 这里也要锁住
            pNewMsg = g_queSend.front( );
            g_queSend.pop( );
            g_queSend.unlock( );
            SendMsg( pMsg ); // 发送函数放在锁的后面
        }
    }
   
    return 0;
}

void SendMsg( LPNETMSG pMsg )
{
    int nRet = send( g_Sock[pMsg->wSockID], pMsg->wMsgBuf, pMsg->wMsgSize, 0 );

    if ( nRet == SOCKET_ERROR )
    {
        nRet = WSAGetLastError( );
        // 如果发送没有成功,重新压回队列中
        g_queSend.lock( );
        g_queSend.push( pNewMsg );
        g_queSend.unlock( );        
    }
    else
    {
        delete pMsg; // 释放
    }
   
}

发送还是比较简单的,比较麻烦的是接受消息

定义一个要比BUF_SZIE大很多的缓冲区
BYTE g_pRecvBuf[BUF_SZIE * 4];
WORD g_wRecvSize = 0;

void OnRead( WORD wUserID )
{
    BYTE pBufer[BUF_SZIE];
    int nRet;

    // 这里我就简单用recv函数,完成端口的数据读取可以参考资料
    nRet = recv( g_Sock[wUserID], pBufer, BUF_SZIE, 0 );
    ... // 自己处理错误

    // 这个时候读出来的数据不能马上解析,放进那个大的缓冲区里去
    lock( ); // 要加锁
    if ( nRet + g_wRecvSize > BUF_SZIE * 4 ) // 缓冲区不够了,没办法,扔掉了
    {
        g_wRecvSize = 0; // 由于是基于流的,中间有错,所有的数据必须都扔了
    }
    memcpy( g_pRecvBuf[g_wRecvSize], pBufer, nRet );
    g_wRecvSize += nRet;
    unlock( );

    OnBuffer( );
}

为什么要把接收到的数据放进一个大的buffer中呢,难道不能把接收时候的缓冲区摄制的很大一次接受完吗?不能,因为基于流的数据接受,你不能确认每次接受到的数据都是完整的,即使你的缓冲区足够大,TCP也有可能将其分为多个部分。不管了,反正现在g_pRecvBuf里存放的至少有一个完整包,如果没有完整包,那么就不处理,看下面的代码

#define MSG_FLAG_BEGIN  0xDD  // 定义一个包开头的标志
#define MSG_FLAG_END    0xEE  // 定义一个包结尾的标志

queue<LPNETMSG> g_queRecv; // 接受队列

void OnBuffer( )
{
   int nOffset = 0;
   int nMsgSize = 0;
   lock( );
   while ( g_wRecvSize > 0 )
   {
       if ( g_pRecvBuf[nOffset] == MSG_FLAG_BEGIN )
       {
            nOffset += sizeof( BYTE );
            memcpy( &nMsgSize, g_pRecvBuf[nOffset], sizeof( int ) ); // 消息包的长度
            if ( nMsgSize < g_wRecvSize - nOffset )
            {
                LPNETMSG pNewMsg = new NETMSG;
                g_queRecv.lock( ); // 锁住
                g_queRecv.push( pNewMsg );
                g_queRecv.unlock( );
                ... // 自己处理把
            }
            else // 表示后面还有数据,没有接受完
            {
            }
       }
       else
       {
           // 包全部扔了,坏了
           g_wRecvSize = 0;
           break;
       }
   }
   unlock( );
}

上面我就简单的写了一下,大家不知道明白不明白我的意思
这样消息就放进队列了,然后就是处理了,处理要在专门的线程来解决

DWORD WINAPI SendThread( LPVOID lpParam )
{
    LPNETMSG pMsg;
    while ( true )
    {
        while ( g_queRecv.size( ) > 0 )
        {
            g_queRecv.lock( );  // 这里也要锁住
            pNewMsg = g_queRecv.front( );
            g_queRecv.pop( );
            g_queRecv.unlock( );
            OnMsg( pMsg ); //
        }
    }
   
    return 0;
}

void OnMsg( LPNETMSG pMsg )
{
    switch ( pMsg->wMsgID )
    {
    case 0x0011: // 走路
        {
            ...
        }
        break;
    }
}

当然上面的OnMsg是基于switch的处理方式,我觉得也是很不错的,效率也比较高,当然,你可以采用面向对象的模式,将消息定义成一个基类
class CMsg
{
    ...
    virtual void OnMsg( ... );
};

然后派生一个走路的类
class CMsgMove : public CMsg
{
    virtual void OnMsg( ... )
    {
        // 实现走路
        ...
    }
};

然后在线程中这样处理
DWORD WINAPI SendThread( LPVOID lpParam )
{
    CMsg * pMsg;
    while ( true )
    {
        while ( g_queRecv.size( ) > 0 )
        {
            g_queRecv.lock( );  // 这里也要锁住
            pNewMsg = g_queRecv.front( );
            g_queRecv.pop( );
            g_queRecv.unlock( );
            OnMsg( pMsg ); //
        }
    }
   
    return 0;
}

void OnMsg( CMsg * pMsg )
{
    switch ( pMsg->wMsgID )
    {
    case 0x0011: // 走路
        {
            (CMsgMove * )pMsg->OnMsg( ... ); // 走路了
        }
        break;
    }
}

呵呵,就这样了,希望大家给我意见,今天又差不多了,下次给大家讲讲客户端的网络处理,

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
 楼主| 发表于 2004-12-21 11:16:00 | 显示全部楼层

Re:网络游戏制作心得(四)

这篇文章写的太匆忙,里面有一些不是很清楚,大家如果要参考,最好自己亲自动手,这样可能会发现一些问题

9

主题

290

帖子

290

积分

中级会员

Rank: 3Rank: 3

积分
290
发表于 2004-12-21 13:50:00 | 显示全部楼层

Re:网络游戏制作心得(四)

怎么没人顶吗? 偶看偶顶

3

主题

107

帖子

112

积分

注册会员

Rank: 2

积分
112
发表于 2004-12-21 14:16:00 | 显示全部楼层

Re:网络游戏制作心得(四)

要顶要顶!

89

主题

822

帖子

847

积分

高级会员

Rank: 4

积分
847
 楼主| 发表于 2004-12-21 15:14:00 | 显示全部楼层

Re:网络游戏制作心得(四)

呵呵,我写这些的目的是希望能跟大家交流,发现我的问题,上面的东西都是我长期积累的习惯性的东西,习惯并不一定都是好的,每个人都有自己的经验,我的经验也是向别人学习得来的

欢迎大家对我写的发表看法,如果有人说看不懂,那么我重写让大家看懂,如果有人说写得太肤浅,那么我希望能看到更深刻的方法

谢谢

24

主题

229

帖子

229

积分

中级会员

Rank: 3Rank: 3

积分
229
发表于 2004-12-21 16:18:00 | 显示全部楼层

Re:网络游戏制作心得(四)

好贴,顶。
希望楼主能提供打包下载,自己慢慢看。呵呵:)

1

主题

12

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2004-12-22 11:22:00 | 显示全部楼层

Re:网络游戏制作心得(四)

好贴就得顶!!!

0

主题

15

帖子

15

积分

新手上路

Rank: 1

积分
15
发表于 2004-12-23 11:14:00 | 显示全部楼层

Re:网络游戏制作心得(四)

楼主好贴。希望多点像你这样愿意分享技术的人。

0

主题

1

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2004-12-23 16:47:00 | 显示全部楼层

Re:网络游戏制作心得(四)

我也是一个早就和大家一样喜欢电脑,喜欢游戏的人,在这里我想多认识点朋友,可以和大家一起学习制作游戏的经验。我的QQ:21460695 希望大家能多帮小弟

谢谢!

50

主题

244

帖子

319

积分

中级会员

Rank: 3Rank: 3

积分
319
QQ
发表于 2004-12-25 16:41:00 | 显示全部楼层

Re:网络游戏制作心得(四)

楼主可以帮忙吗?如果要同时发送用户名和密码,应该怎样发送啊?

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-12-23 20:54

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表