牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

[转载]Flash为客户端的多人网络游戏的实现

来源:http://outspace.spaces.live.com/

多人网络游戏的实现

项目开发的基本硬件配置
一台普通的pc就可以了,
安装好windows 2000和vc6就可以了,
然后连上网,局域网和internet都可以,

接下去的东西我都简化,不去用晦涩的术语,

既然是网络,我们就需要网络编程接口,
服务器端我们用的是winsock 1.1,使用tcp连接方式,

[tcp和udp]
tcp可以理解为一条连接两个端子的隧道,提供可靠的数据传输服务,
只要发送信息的一方成功的调用了tcp的发送函数发送一段数据,
我们可以认为接收方在若干时间以后一定会接收到完整正确的数据,
不需要去关心网络传输上的细节,
而udp不保证这一点,
对于网络游戏来说,tcp是普遍的选择。

[阻塞和非阻塞]
在通过socket发送数据时,如果直到数据发送完毕才返回的方式,也就是说如果我们使用send( buffer, 100.....)这样的函数发送100个字节给别人,我们要等待,直到100个自己发送完毕,程序才往下走,这样就是阻塞的,
而非阻塞的方式,当你调用send(buffer,100....)以后,立即返回,此时send函数告诉你发送成功,并不意味着数据已经向目的地发送完毕,甚至有可能数据还没有开始发送,只被保留在系统的缓冲里面,等待被发送,但是你可以认为数据在若干时间后,一定会被目的地完整正确的收到,我们要充分的相信tcp。
阻塞的方式会引起系统的停顿,一般网络游戏里面使用的都是非阻塞的方式,
——————————————————————
注意,仅仅用flash作为客户端,
服务器端,我们使用vc6,
我将陆续的公开服务器端的源代码和大家共享,
并且将讲解一些网络游戏开发的原理,
希望对此感兴趣的朋友能够使用今后的资源或者理论开发出完整的网络游戏。
我们从简单到复杂,
从棋牌类游戏到动作类的游戏,
从2个人的游戏到10个人的游戏,
因为工作忙的关系,我所做的一切仅仅起到抛砖引玉的作用,
希望大家能够热情的讨论,为中国的flash事业垫上一块砖,添上一片瓦。

现在的大型网络游戏(mmo game)都是基于server/client体系结构的,
server端用c(windows下我们使用vc.net+winsock)来编写,
客户端就无所谓,
在这里,我们讨论用flash来作为客户端的实现,

实践证明,flash的xml socket完全可以胜任网络传输部分,
在别的贴子中,我看见有的朋友谈论msn中的flash game
他使用msn内部的网络接口进行传输,
这种做法也是可以的,
我找很久以前对于2d图形编程的说法,"给我一个打点函数,我就能创造整个游戏世界",
而在网络游戏开发过程中,"给我一个发送函数和一个接收函数,我就能创造网络游戏世界."

我们抽象一个接口,就是网络传输的接口,
对于使用flash作为客户端,要进行网络连接,
一个网络游戏的客户端,
可以简单的抽象为下面的流程
1.与远程服务器建立一条长连接
2.用账号密码登陆
3.循环
接收消息
发送消息
4.关闭

我们可以直接使用flash 的xml socket,也可以使用类似msn的那种方式,
这些我们先不管,我们先定义接口,
Connect( "127.0.0.1", 20000 ); 连接远程服务器,建立一条长连接
Send( data, len ); 向服务器发送一条消息
Recv( data, len ); 接收服务器传来的消息


说明一下普通网络游戏中windows下面网络传输的原理,
——————————————————————
 
[有状态服务器和无状态服务器]
在c/s体系中,如果server不保存客户端的状态,称之为无状态,反之为有状态,

在这里要强调一点,
我们所说的服务器不是一台具体的机器,
而是指服务器应用程序,
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序,

我们的网络游戏使用的是有状态服务器,
保存所有玩家的数据和状态,
——————————————————————
一些有必要了解的理论和开发工具

[开发语言]
vc6
我们首先要熟练的掌握一门开发语言,
学习c++是非常有必要的,
而vc是windows下面的软件开发工具,
为什么选择vc,可能与我本身使用vc有关,
而且网上可以找到许多相关的资源和源代码,

[操作系统]
我们使用windows2000作为服务器的运行环境,
所以我们有必要去了解windows是如何工作的,
同时对它的编程原理应该熟练的掌握

[数据结构和算法]
要写出好的程序要先具有设计出好的数据结构和算法的能力,
好的算法未必是繁琐的公式和复杂的代码,
我们要找到又好写有满足需求的算法,
有时候,最笨的方法同时也是很好的方法,
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程,
花费了大量的精力未必能够取得好的效果,

举个例子,
我当年进入游戏界工作,学习老师的代码,
发现有个函数,要对画面中的npc位置进行排序,
确定哪个先画,那个后画,
他的方法太“笨”,
任何人都会想到的冒泡,
一个一个去比较,没有任何的优化,
我当时想到的算法就有很多,
而且有一大堆优化策略,
可是,当我花了很长时间去实现我的算法时,
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用,
或者说虽然算法本身快了几倍,
可是那是多余的,老师的算法虽然“笨”,
可是他只花了几十行代码就搞定了,
他的时间花在别的更需要的地方,
这就是他可以独自完成一个游戏,
而我可以把一个函数优化100倍也只能打杂的原因

[tcp/ip的理论]
推荐数据用tcp/ip进行网际互连,tcp/ip详解,
这是两套书,共有6卷,
都是国外的大师写的,
可以说是必读的,
——————————————————————
网络传输中的“消息”

[消息]
消息是个很常见的术语,
在windows中,消息机制是个十分重要的概念,
我们在网络游戏中,也使用了消息这样的机制,

一般我们这么做,
一个数据块,头4个字节是消息名,后面接2个字节的数据长度,
再后面就是实际的数据

为什么使用消息??
我们来看看例子,

在游戏世界,
一个玩家想要和别的玩家聊天,
那么,他输入好聊天信息,
客户端生成一条聊天消息,
并把聊天的内容打包到消息中,
然后把聊天消息发送给服务器,
请求服务器把聊天信息发送给另一个玩家,

服务器接收到一条消息,
此刻,服务器并不知道当前的数据是什么东西,
对于服务器来讲,这段数据仅仅来自于网络通讯的底层,
不加以分析的话,没有任何的信息,
因为我们的通讯是基于消息机制的,
我们认为服务器接收到的任何数据都是基于消息的数据方式组织的,
4个字节消息名,2字节长度,这个是不会变的,

通过消息名,服务器发现当前数据是一条聊天数据,
通过长度把需要的数据还原,校验,
然后把这条消息发送给另一个玩家,

大家注意,消息是变长的,
关于消息的解释完全在于服务器和客户端的应用程序,
可以认为与网络传输低层无关,
比如一条私聊消息可能是这样的,

MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
String:anybyte < 256

一条移动消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
TargetPosition:4 byte (x,y)

编程者可以自定义消息的内容以满足不同的需求
——————————————————————
队列

[队列]
队列是一个很重要的数据结构,
比如说消息队列,
服务器或者客户端,
发送的消息不一定是立即发送的,
而是等待一个适当时间,
或者系统规定的时间间隔以后才发送,
这样就需要创建一个消息队列,以保存发送的消息,

消息队列的大小可以按照实际的需求创建,
队列又可能会满,
当队列满了,可以直接丢弃消息,
如果你觉得这样不妥,
也可以预先划分一个足够大的队列,

可以使用一个系统全局的大的消息队列,
也可以为每个对象创建一个消息队列,


这个我们的一个数据队列的实现,
开发工具vc.net,使用了C++的模板,
关于队列的算法和基础知识,我就不多说了,

DataBuffer.h

#ifndef __DATABUFFER_H__
#define __DATABUFFER_H__

#include <windows.h>
#include <assert.h>
#include "g_assert.h"
#include <stdio.h>

#ifndef HAVE_BYTE
typedef unsigned char byte;
#endif // HAVE_BYTE

//数据队列管理类
template <const int _max_line, const int _max_size>
class DataBufferTPL
{
public:

bool Add( byte *data ) // 加入队列数据
{
G_ASSERT_RET( data, false );
m_ControlStatus = false;

if( IsFull() )
{
//assert( false );
return false;
}

memcpy( m_s_ptr, data, _max_size );

NextSptr();
m_NumData++;

m_ControlStatus = true;
return true;
}


bool Get( byte *data ) // 从队列中取出数据
{
G_ASSERT_RET( data, false );
m_ControlStatus = false;

if( IsNull() )
return false;

memcpy( data, m_e_ptr, _max_size );

NextEptr();
m_NumData--;

m_ControlStatus = true;
return true;
}


bool CtrlStatus() // 获取操作成功结果
{
return m_ControlStatus;
}


int GetNumber() // 获得现在的数据大小
{
return m_NumData;
}

public:

DataBufferTPL()
{
m_NumData = 0;
m_start_ptr = m_DataTeam[0];
m_end_ptr = m_DataTeam[_max_line-1];
m_s_ptr = m_start_ptr;
m_e_ptr = m_start_ptr;
}
~DataBufferTPL()
{
m_NumData = 0;
m_s_ptr = m_start_ptr;
m_e_ptr = m_start_ptr;
}

private:

bool IsFull() // 是否队列满
{
G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
if( m_NumData == _max_line )
return true;
else
return false;
}
bool IsNull() // 是否队列空
{
G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
if( m_NumData == 0 )
return true;
else
return false;
}
void NextSptr() // 头位置增加
{
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_s_ptr += _max_size;
if( m_s_ptr > m_end_ptr )
m_s_ptr = m_start_ptr;
}
void NextEptr() // 尾位置增加
{
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_e_ptr += _max_size;
if( m_e_ptr > m_end_ptr )
m_e_ptr = m_start_ptr;
}

private:

byte m_DataTeam[_max_line][_max_size]; //数据缓冲
int m_NumData; //数据个数
bool m_ControlStatus; //操作结果

byte *m_start_ptr; //起始位置
byte *m_end_ptr; //结束位置
byte *m_s_ptr; //排队起始位置
byte *m_e_ptr; //排队结束位置
};


//////////////////////////////////////////////////////////////////////////
// 放到这里了!

//ID自动补位列表模板,用于自动列表,无间空顺序列表。
template <const int _max_count>
class IDListTPL
{
public:
// 清除重置
void Reset()
{
for(int i=0;i<_max_count;i++)
m_dwList[i] = G_ERROR;
m_counter = 0;
}

int MaxSize() const { return _max_count; }
int Count() const { return m_counter; }
const DWORD operator[]( int iIndex ) {

G_ASSERTN( iIndex >= 0 && iIndex < m_counter );

return m_dwList[ iIndex ];
}
bool New( DWORD dwID )
{
G_ASSERT_RET( m_counter >= 0 && m_counter < _max_count, false );

//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )
return false;

m_dwList[m_counter] = dwID;
m_counter++;

return true;
}
// 没有Assert的加入ID功能
bool Add( DWORD dwID )
{
if( m_counter <0 || m_counter >= _max_count )
return false;

//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )
return false;

m_dwList[m_counter] = dwID;
m_counter++;
return true;
}
bool Del( int iIndex )
{
G_ASSERT_RET( iIndex >=0 && iIndex < m_counter, false );

for(int k=iIndex;k<m_counter-1;k++)
{
m_dwList[k] = m_dwList[k+1];
}

m_dwList[k] = G_ERROR;
m_counter--;
return true;
}
int Find( DWORD dwID )
{
for(int i=0;i<m_counter;i++)
{
if( m_dwList[i] == dwID )
return i;
}

return -1;
}

IDListTPL():m_counter(0)
{
for(int i=0;i<_max_count;i++)
m_dwList[i] = G_ERROR;
}
virtual ~IDListTPL()
{}

private:

DWORD m_dwList[_max_count];
int m_counter;

};

//////////////////////////////////////////////////////////////////////////


#endif //__DATABUFFER_H__
——————————————————————
socket

我们采用winsock作为网络部分的编程接口,

接下去编程者有必要学习一下socket的基本知识,
不过不懂也没有关系,我提供的代码已经把那些麻烦的细节或者正确的系统设置给弄好了,
编程者只需要按照规则编写游戏系统的处理代码就可以了,

这些代码在vc6下编译通过,
是通用的网络传输底层,
这里是socket部分的代码,

我们需要安装vc6才能够编译以下的代码,
因为接下去我们要接触越来越多的c++,
所以,大家还是去看看c++的书吧,

// socket.h
#ifndef _socket_h
#define _socket_h
#pragma once

//定义最大连接用户数目 ( 最大支持 512 个客户连接 )
#define MAX_CLIENTS 512
//#define FD_SETSIZE MAX_CLIENTS

#pragma comment( lib, "wsock32.lib" )

#include <winsock.h>

class CSocketCtrl
{
void SetDefaultOpt();
public:
CSocketCtrl(): m_sockfd(INVALID_SOCKET){}
BOOL StartUp();
BOOL ShutDown();
BOOL IsIPsChange();

BOOL CanWrite();
BOOL HasData();
int Recv( char* pBuffer, int nSize, int nFlag );
int Send( char* pBuffer, int nSize, int nFlag );
BOOL Create( UINT uPort );
BOOL Create(void);
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
void Close();

BOOL Listen( int nBackLog );
BOOL Accept( CSocketCtrl& sockCtrl );

BOOL RecvMsg( char *sBuf );
int SendMsg( char *sBuf,unsigned short stSize );
SOCKET GetSockfd(){ return m_sockfd; }

BOOL GetHostName( char szHostName[], int nNameLength );

protected:
SOCKET m_sockfd;

static DWORD m_dwConnectOut;
static DWORD m_dwReadOut;
static DWORD m_dwWriteOut;
static DWORD m_dwAcceptOut;
static DWORD m_dwReadByte;
static DWORD m_dwWriteByte;
};


#endif

// socket.cpp

#include <stdio.h>
#include "msgdef.h"
#include "socket.h"
// 吊线时间
#define ALL_TIMEOUT 120000
DWORD CSocketCtrl::m_dwConnectOut = 60000;
DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwReadByte = 0;
DWORD CSocketCtrl::m_dwWriteByte = 0;

// 接收数据
BOOL CSocketCtrl::RecvMsg( char *sBuf )
{
if( !HasData() )
return FALSE;
MsgHeader header;
int nbRead = this->Recv( (char*)&header, sizeof( header ), MSG_PEEK );
if( nbRead == SOCKET_ERROR )
return FALSE;
if( nbRead < sizeof( header ) )
{
this->Recv( (char*)&header, nbRead, 0 );
printf( "\ninvalid msg, skip %ld bytes.", nbRead );
return FALSE;
}

if( this->Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )
return FALSE;

return TRUE;
}

// 发送数据
int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )
{
static char sSendBuf[ 4000 ];
memcpy( sSendBuf,&stSize,sizeof(short) );
memcpy( sSendBuf + sizeof(short),sBuf,stSize );

if( (sizeof(short) + stSize) != this->Send( sSendBuf,stSize+sizeof(short),0 ) )
return -1;
return stSize;
}


// 启动winsock
BOOL CSocketCtrl::StartUp()
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD( 1, 1 );

int err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return FALSE;
}


return TRUE;

}
// 关闭winsock
BOOL CSocketCtrl::ShutDown()
{
WSACleanup();
return TRUE;
}

// 得到主机名
BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )
{
if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )
return TRUE;
return FALSE;
}

BOOL CSocketCtrl::IsIPsChange()
{
return FALSE;
static int iIPNum = 0;
char sHost[300];

hostent *pHost;
if( gethostname(sHost,299) != 0 )
return FALSE;
pHost = gethostbyname(sHost);
int i;
char *psHost;
i = 0;
do
{
psHost = pHost->h_addr_list[i++];
if( psHost == 0 )
break;

}while(1);
if( iIPNum != i )
{
iIPNum = i;
return TRUE;
}
return FALSE;
}

// socket是否可以写
BOOL CSocketCtrl::CanWrite()
{
int e;

fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 0;

FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;
return FALSE;
}

// socket是否有数据
BOOL CSocketCtrl::HasData()
{
int e;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 0;

FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;
return FALSE;
}

int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )
{
return recv( m_sockfd, pBuffer, nSize, nFlag );
}

int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )
{
return send( m_sockfd, pBuffer, nSize, nFlag );
}

BOOL CSocketCtrl::Create( UINT uPort )
{
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockAddr;
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons( uPort );
if(!::bind(m_sockfd,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
{
SetDefaultOpt();
return TRUE;
}
Close();
return FALSE;

}

void CSocketCtrl::Close()
{
::closesocket( m_sockfd );
m_sockfd = INVALID_SOCKET;
}

BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
{
if(m_sockfd==INVALID_SOCKET) return FALSE;

SOCKADDR_IN sockAddr;

memset(&sockAddr,0,sizeof(sockAddr));
LPSTR lpszAscii=(LPSTR)lpszHostAddress;
sockAddr.sin_family=AF_INET;
sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);
if(sockAddr.sin_addr.s_addr==INADDR_NONE)
{
HOSTENT * lphost;
lphost = ::gethostbyname(lpszAscii);
if(lphost!=NULL)
sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost->h_addr)->s_addr;
else return FALSE;
}
sockAddr.sin_port = htons((u_short)nHostPort);

int r=::connect(m_sockfd,(SOCKADDR*)&sockAddr,sizeof(sockAddr));
if(r!=SOCKET_ERROR) return TRUE;

int e;
e=::WSAGetLastError();
if(e!=WSAEWOULDBLOCK) return FALSE;

fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 100000;

UINT n=0;
while( n< CSocketCtrl::m_dwConnectOut)
{
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL, &tout);

if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;

if( IsIPsChange() )
return FALSE;
n += 100;
}

return FALSE;

}
// 设置监听socket
BOOL CSocketCtrl::Listen( int nBackLog )
{
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( !listen( m_sockfd, nBackLog) ) return TRUE;
return FALSE;
}

// 接收一个新的客户连接
BOOL CSocketCtrl::Accept( CSocketCtrl& ms )
{
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;

int e;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 100000;

UINT n=0;
while(n< CSocketCtrl::m_dwAcceptOut)
{
//if(stop) return FALSE;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
if(e==1) break;
n += 100;
}
if( n>= CSocketCtrl::m_dwAcceptOut ) return FALSE;

ms.m_sockfd=accept(m_sockfd,NULL,NULL);
if(ms.m_sockfd==INVALID_SOCKET) return FALSE;
ms.SetDefaultOpt();

return TRUE;
}

BOOL CSocketCtrl::Create(void)
{
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockAddr;

memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons(0);
//if(!::bind(m_sock,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
{
SetDefaultOpt();
return TRUE;
}
Close();
return FALSE;
}

// 设置正确的socket状态,
// 主要是主要是设置非阻塞异步传输模式
void CSocketCtrl::SetDefaultOpt()
{
struct linger ling;
ling.l_onoff=1;
ling.l_linger=0;
setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);
int bKeepAlive = 1;
setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(int));
BOOL bNoDelay = TRUE;
setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&bNoDelay, sizeof(BOOL));
unsigned long nonblock=1;
::ioctlsocket(m_sockfd,FIONBIO,&nonblock);
}
——————————————————————
今天晚上写了一些测试代码,
想看看flash究竟能够承受多大的网络数据传输,

我在flash登陆到服务器以后,
每隔3毫秒就发送100次100个字符的串 "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" 给flash,
然后在flash里面接收数据的函数里面统计数据,


var g_nTotalRecvByte = 0;
var g_time = new Date();
var g_nStartTime = g_time.getTime();
var g_nCounter = 0;

mySocket.onData=function(xmlDoc)
{
g_nTotalRecvByte += xmlDoc.length;
// 每接收超过1k字节的数据,输出一次信息,
if( g_nTotalRecvByte-g_nCounter > 1024 )
{
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartTime;
trace( "花费时间:"+nPassedTime+"毫秒" );
g_nCounter = g_nTotalRecvByte;
trace( "接收总数:"+g_nTotalRecvByte+"字节" );
trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字节/秒" );

}
结果十分令我意外,
这是截取的一段调试信息,
//
花费时间:6953毫秒
接收总数:343212字节
接收速率:49361.7143678988字节/秒
花费时间:7109毫秒
接收总数:344323字节
接收速率:48434.800956534字节/秒
花费时间:7109毫秒
接收总数:345434字节
接收速率:48591.0817273878字节/秒
。。。
。。。
。。。
。。。
花费时间:8125毫秒
接收总数:400984字节
接收速率:49351.8769230769字节/秒
花费时间:8125毫秒
接收总数:402095字节
接收速率:49488.6153846154字节/秒
花费时间:8125毫秒
接收总数:403206字节
接收速率:49625.3538461538字节/秒

我检查了几遍源程序,没有发现逻辑错误,
如果程序没有问题的话,
那么我们得出的结论是,flash的xml socket每秒可以接收至少40K的数据,
这还没有计算xmlSocket.onData事件的触发,调试代码、信息输出占用的时间。

比我想象中快了一个数量级,
够用了,
flash网络游戏我们可以继续往下走了
——————————————————————
有朋友问到lag的问题,
问得很好,不过也不要过于担心,
lag的产生有的是因为网络延迟,
有的是因为服务器负载过大,
对于游戏的设计者和开发者来说,
首先要从设计的角度来避免或者减少lag产生的机会,
如果lag产生了,
也不要紧,找到巧妙的办法骗过玩家的眼睛,
这也有很多成熟的方法了,
比如航行预测法,路径插值等等,
都可以产生很好的效果,
还有最后的绝招,就是提高服务器的配置和网络带宽,

从我开发网络游戏这段时间的经验来看,
我们的服务器是vc开发的,
普通pc跑几百个玩家,几百个怪物是没有问题的,


又作了一个flash发送的测试,

网络游戏的特点是,
出去的信息比较少,
进来的信息比较多,

这个很容易理解,
人操作游戏的速度是很有限的,
控制指令的产生也是随机的,
离散的,

但是多人游戏的话,
因为人多,信息的流量也就区域均匀分布了,

在昨天接收数据的基础上,
我略加修改,
这次,
我在_root.enterFrame写了如下代码,
_root.onEnterFrame = function()
{
var i;
for( i = 0; i < 10; i++ )
mySocket.send( ConvertToMsg( "01234567890123456789012345678901234567890123456789" ) );
return;
}

服务器端要做的是,
把所有从flash客户端收到的信息原封不动的返回来,

这样,我又可以通过昨天onData里面的统计算法来从侧面估算出flash发送数据的能力,
这里是输出的数据
//
花费时间:30531毫秒
接收总数:200236字节
接收速率:6558.44878975468字节/秒
花费时间:30937毫秒
接收总数:201290字节
接收速率:6506.44858906811字节/秒
花费时间:31140毫秒
接收总数:202344字节
接收速率:6497.88053949904字节/秒
花费时间:31547毫秒
接收总数:203398字节
接收速率:6447.45934637208字节/秒

可以看出来,发送+接收同时做,
发送速率至少可以达到5k byte/s

有一点要注意,要非常注意,
不能让flash的网络传输满载,
所谓满载就是flash在阻塞运算的时候,
不断的有数据从网络进来,
而flash又无法在预计的时间内处理我这些信息,
或者flash发送数据过于频繁,
导致服务器端缓冲溢出导致错误,

对于5k的传输速率,
已经足够了,
因为我也想不出来有什么产生这么大的数据量,
而且如果产生了这么大的数据量,
也就意味着服务器每时每刻都要处理所有的玩家发出的海量数据,
还要把这些海量数据转发给其他的玩家,
已经引起数据爆炸了,
所以,5k的上传从设计阶段就要避免的,
我想用flash做的网络游戏,
除了动作类游戏可能需要恒定1k以内的上传速率,
其他的200个字节/秒以内就可以了,
——————————————————————
使用于Flash的消息结构定义

我们以前讨论过,
通过消息来传递信息,
消息的结构是
struct msg
{
short nLength; // 2 byte
DWORD dwId; // 4 byte

....
data
}

但是在为flash开发的消息中,
不能采用这种结构,

首先Flash xmlSocket只传输字符串,
从xmlSocket的send,onData函数可以看出来,
发出去的,收进来的都应该是字符串,

而在服务器端是使用vc,java等高级语言编写的,
消息中使用的是二进制数据块,
显然,简单的使用字符串会带来问题,

所以,我们需要制定一套协议,
就是无论在客户端还是服务器端,
都用统一的字符串消息,
通过解析字符串的方式来传递信息,

我想这就是flash采用xml document来传输结构化信息的理由之一,
xml document描述了一个完整的数据结构,
而且全部使用的是字符串,
原来是这样,怪不得叫做xml socket,
本来socket和xml完全是不同的概念,
flash偏偏出了个xml socket,
一开始令我费解,
现在,渐渐理解其中奥妙。

——————————————————————
Flash Msg结构定义源代码和相关函数

在服务器端,我们为flash定义了一种msg结构,
使用语言,vc6
#define MSGMAXSIZE 512
// 消息头
struct MsgHeader
{
short stLength;
MsgHeader():stLength( 0 ){}

};
// 消息
struct Msg
{
MsgHeader header;
short GetLength(){ return header.stLength; }
};
// flash 消息
struct MsgToFlashublic Msg
{
// 一个足够大的缓冲,但是不会被整个发送,
char szString[MSGMAXSIZE];
// 计算设置好内容后,内部会计算将要发送部分的长度,
// 要发送的长度=消息头大小+字符串长度+1
void SetString( const char* pszChatString )
{
if( strlen( pszChatString ) < MSGMAXSIZE-1 )
{
strcpy( szString, pszChatString );
header.stLength = sizeof( header )+
(short)strlen( pszChatString )+1;
}
}

};

在发往flash的消息中,整个处理过后MsgToFlash结构将被发送,
实践证明,在flash 客户端的xmlSocket onData事件中,
接收到了正确的消息,消息的内容是MasToFlash的szString字段,
是一个字符串,

比如在服务器端,
MsgToFlash msg;
msg.SetString( "move player0 to 100 100" );
SendMsg( msg,............. );
那么,在我们的flash客户端的onData( xmlDoc )中,
我们trace( xmlDoc )
结果是
move player0 to 100 100


然后是flash发送消息到服务器,
我们强调flash只发送字符串,
这个字符串无论是否内部拥有有效数据,
服务器都应该首先把消息收下来,
那就要保证发送给服务器的消息遵循统一的结构,
在flash客户端中,
我们定义一个函数,
这个函数把一个字符串转化为服务器可以识别的消息,

补充:现在我们约定字符串长度都不大于97个字节长度,


var num_table = new array( "0","1","2","3","4","5","6","7","8","9" );
function ConvertToMsg( str )
{
var l = str.length+3;
var t = "";
if( l > 10 )
t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+str;
else
t = num_table[0]+num_table[l]+str;
return t;
}

比如
var msg = ConvertToMsg( "client login" );
我们trace( msg );
看到的是
15client login

为什么是这个结果呢?
15是消息的长度,
头两个字节是整个消息的长度的asc码,意思是整个消息有15个字节长,
然后是信息client login,
最后是一个0(c语言中的字符串结束符)

当服务器收到15client login,
他首先把15给分析出来,
把"15"字符串转化为15的数字,
然后,根据15这个长度把后面的client login读出来,
这样,网络传输的底层就完成了,
client login的处理就交给逻辑层,
——————————————————————
前阵子我开发了Match3D,
一个可以把三维动画输出成为swf的工具,
而且实现了swf渲染的实时三维角色动画,
这可以说是我真正推出的第一个flash第三方软件,
其实这以前,
我曾经开发过几个其他的flash第三方软件,
都中途停止了,
因为不实用或者市场上有更好的同类软件,

随着互联网的发展,
flash的不断升级,
我的flash第三方软件目光渐渐的从美术开发工具转移到网络互连,
web应用上面来,
如今已经到了2004版本,
flash的种种新特性让我眼前发光,

我最近在帝国的各个板块看了很多贴子,
分析里面潜在的用户需求,
总结了以下的几个我认为比较有意义的选题,
可能很片面,

flash源代码保护,主要是为了抵御asv之类的软件进行反编译和萃取
flash与远端数据库的配合,应该出现一个能够方便快捷的对远程数据库进行操作的方法或者控件,
flash网际互连,我认为flash网络游戏是一块金子,

这里我想谈谈flash网络游戏,
我要谈的不仅仅是技术,而是一个概念,
用flash网络游戏,
我本身并不想把flash游戏做成rpg或者其他剧烈交互性的游戏,
而是想让flash实现那些节奏缓慢,玩法简单的游戏,
把网络的概念带进来,

你想玩游戏的时候,登上flash网络游戏的网站,
选择你想玩的网络游戏,
因为现在几乎所有上网的电脑都可以播放swf,
所以,我们几乎不用下载任何插件,
输入你的账号和密码,
就可以开始玩了,

我觉得battle.net那种方式很适合flash,
开房间或者进入别人开的房间,
然后2个人或者4个人就可以交战了,

这种游戏可以是棋类,这是最基本的,
用户很广泛,
我脑海中的那种是类似与宠物饲养的,
就像当年的电子宠物,
每个玩家都可以到服务器认养宠物,
然后在线养成宠物,
还可以邀请别的玩家进行宠物比武,
看谁的宠物厉害,

就这样简简单单的模式,
配合清新可爱的画面,
趣味的玩法,
加入网络的要素,
也许可以取得以想不到的效果,

今天就说到这里吧,
想法那么多,要实现的话还有很多路要走,

希望大家多多支持,积极参与,
让我们的想法不仅仅停留于纸上。
——————————————————————
格斗类游戏和休闲类游戏不同,
最大的差别在于对响应时间的要求不在同一数量级上,

休闲类游戏允许很大的延迟,
而动作类游戏需要尽可能小的延迟,

服务器的作用,
要起到数据转发和数据校验的作用,
在格斗游戏设计上,
如果采用和mmorpg相同的服务器结构,
会产生较大的延迟,
你可以想象,当我打出一招升龙拳,
然后把这个信息传递给服务器,
假设有100毫秒的延迟,
服务器收到以后,
转发给其他的人,
又经过100毫秒的延迟,
也就是说,
你打出一招,对方要得知需要200毫秒的时间,
实际情况也许更糟,
这对于格斗类游戏来说是很大的,
所以现在市面上几乎没有真正意义上的多人格斗类网络游戏,

如果要实现格斗的感觉,
尽可能减少延迟,
有很多现有经过验证的方法,
比如把服务器建立在两个对战玩家的机器上,
这样就省掉了一个延迟,
或者每个玩家机器上都建一个服务器,

局域网摸使得联机游戏很多采用这种的,
比如星际争霸,它的联网模式服务器就是建在玩家主机上的,

减少网络延迟是必要的,
但是,一个平滑爽快的网络游戏,
他的客户端需要有很多预测的策略,
用来估计未来很短的时间内,
某个玩家会做什么,
这样,就可以在不等待服务器返回正确信息的情况下,
直接估计出正确的结果或者接近正确的结果,
当服务器返回的结果和预测结果的误差在很小范围内,
则预测成功,否则,可以强行校正客户端的状态,
这样做,可以尽量减少网络延迟带来的损失,
绝大部分成功的联网游戏,
在这个策略上,都花了很大的功夫,
——————————————————————
你提到的那个游戏模式是经典模式battle.net,
也就是战网,
暗黑,星际,魔兽都有战网,

你下面提到的是一个缓冲模式,
当用户发出一招升龙拳,
并不立即向服务器发送指令,
而是等待5ms(不过在网络游戏中5ms设定也许太乐观),
如果在这期间服务器下发了新指令,
与在缓冲中的升龙拳指令进行运算,
用来得到是否可以发出升龙拳的评估,
这样做是可以的,
不过实际开发过程中,
这种缓冲统计的模式,
逻辑非常的复杂,
网络游戏毕竟是在网络上玩的,
而且是多人玩的,


你如果经常上战网打星际的话,
会知道它的菜单里面有一个设定操作延迟的菜单,
高延迟的话,
你的操作会缓冲到一定的程度然后统一向服务器发出,
低延迟的话,
会尽可能的立刻把你的操作向服务器发出,

我的感觉是,
网络格斗类游戏与单机格斗类游戏是不同的,
想在网络上完美实现单机格斗游戏的内涵难度极大,
对于开发者来说,绝对是一个挑战,
我想,可以积极的考虑如何在游戏设计的层面去规避一些问题。
——————————————————————
我的最近一条谚语是西点军校的军官对新兵的训斥,
。。。。如果一个愚蠢的方法能够奏效的话,那他就不是愚蠢的方法。。

这句话简直太妙了,
我立刻删除了手中那些“完美的”代码,
用最简单的方法快速达到我的目的,
虽然新写的代码张得很丑陋,

可以说说题外话,
最近策划一直希望能够实现一个实时渲染的怪物能够时不时的眨巴眨巴眼睛

主流的想法是更换眼部的贴图,
也有使用骨骼控制眼部肌肉的方法,
同时还有其他“技术含量”更高的解决方案,
不过经过评估,实现这两者难度不大,但是需要耗费比较多的资源和制作时间,更严重点的是程序需要升级代码来支持这个新的特性,
后来,在大家集思广益之下,
有人提出了高明的办法,

把怪物的眼睛镂空,
然后在后面放一个画有眼睛的矩形,
这个矩形的正面是一个睁大的眼睛,
而反面是一个闭上的眼睛
这样,眨眼就是这个矩形正反翻转的过程,
不错,是个好方法,让人眼前一亮,
大家一致通过,
在现有技术下,这个“愚蠢”的方法解决了这个“复杂”的问题,

在有一个更有趣的例子,
看似调侃,其实折射出人的智慧,
日本人做游戏是非常讲究细节的,
他们最早做三维格斗游戏的时候遇到一个问题,
当我打出一记重拳,命中的时候,
我的拳头在画面上嵌入到敌方模型的肉里面去了,
日本人觉得不能接受,拳头怎么能打到肉里面去呢,
应该刚刚好碰到敌方的身体才行,
可是大家想想,想要刚刚好碰到,
谈何容易,
美术要求严格不说,程序可能要精确的计算,
还不一定能够满足要求,
于是高招出现了,嵌到肉里就嵌到肉里吧,
我给你来一个闪光,把有问题的画面给你全遮住,
这招可真绝,
美工,程序,策划不但偷了懒,把玩家给骗了不说,
玩家还觉得这个游戏真酷,打中了还有闪光的特效,
;),实在是高,

都一点了,我去睡了,
——————————————————————
关于flash com server、remoting
最近研究了一下flash com server
是个好东西,
他提供了集成的flash网络服务,

我认为,flash com server最具吸引我的是他的媒体能力,

案例
使用flash作为客户端的围棋游戏,
flash实现这个游戏的界面和玩法是没有问题的,
但是计算围棋的死活需要大量运算,
这恰恰不是flash的强项,
而且我们是网络游戏,
我们会把计算交给flash comm server,
但是flash comm server的负担也很重,
他要承担视频,音频和数据流的传输,
而且他是一个面向web应用的服务器,
并不适合做大量的计算,
所以我们需要一个专门进行运算的服务器,


remoting是一个网关,
用户可以通过flash com server+remoting间接的取得应用程序服务器的服务,

flash客户端可以仅仅和com server交流,
不必关心其他应用程序服务器的存在,

不过因为要实现应用程序服务器,
就要用到remoting,
而mm提供的remoting有for java的和for c#的,
我本人对java和c#不了解,
我只会使用c++,
所以我设计的体系就放弃了remoting,


现在这个交互性很强的flash娱乐平台的体系建立起来了,
flash comm server提供媒体的服务,
比如视频,音频和数据流,
使用xml socket直连由vc开发的应用服务器,
这个应用服务器提供计算服务,

这样,就把现在网络游戏服务器的大吞吐量的运算和flash com server的媒体服务融合到一起,

网络游戏盈利模式明确是众所周知的,
而且运营模式已经比较成熟了,

我认为以flash为客户端的休闲娱乐平台一定是很有商业前景的,
而在此贴中,一个由flash构成似于联众的网络游戏平台就这样诞生了,
他的特点是不需要安装专门的插件(几乎所有的上网电脑都有flashplayer),
图形表现力强,
客户端可以实时下载,
这种模式可以统一到现在的网络游戏收费模式中,
因为至此flash的网络游戏和普通的网络游戏没有什么区别了,
要玩游戏就要买点卡,
我们还可以提供视频服务(这种服务可能要付更多的钱),
一边玩,一边视频聊天,

这种平台用来运作那些节奏稍慢的休闲益智类网络游戏再适合不过了,
比如纸牌,棋类,
flash客户端开发成本低,
运行期间稳定,占用资源少,

我现在已经拥有成熟的技术和解决方案,
最近考虑投资作这个项目,
如果有对此感兴趣的或者想进行商业合作的朋友,
发email到gamemind@sina.com联系我
 
                                       闪之主宰

posted on 2007-02-05 15:33 杨粼波 阅读(989) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理