一、复盘原理
我们都知道所谓回放游戏过程,只不过把游戏消息存储下来,在执行一遍。
我的复盘原理是这样实现(麻将):
1)游戏结束,服务器会发送这局执行的所有消息。
2)客户端接受到消息,通过使用lua绑定C++方法读出数据(这也是第二点要讲解的)
3)lua读写文件,把读出的数据以二进制形式存储到本地
4)从本地读出二进制文件,解析内容,得到消息和数据,从而实现回放。
二、读取数据
对于一般的网游公司,都有自己的一套网络通讯代码。
1)一般都是服务器定义一个结构体
2)发送给客户端
3)客户端接受数据,解析数据执行,并返回结果给服务器。
4)循环1-3。。。
因为从服务器接受过来的数据是占据连续的一片内存空间,这有什么用呢?
用处就是:我们可以通过按字节移位的方式来读取数据了(按一字节对齐)。
举个列子:
struct AAA
{
DWORD a1;
WORD a2;
};
服务器发来的结果体是这样的,那我们应该如何读取a1,a2中的数据呢?
DWORD MsgClass::readDWORD()
{
WORD value = 0;
if (m_Pos + 4 <= m_Len)
{
WORD t = 0;
memcpy(&t,(const BYTE*)m_Buffer + m_Pos,4);
value = t;
}
m_Pos += 4;
return value;
}
WORD MsgClass::readWORD()
{
WORD value = 0;
if (m_Pos + 2 <= m_Len)
{
WORD t = 0;
memcpy(&t,(const BYTE*)m_Buffer + m_Pos,2);
value = t;
}
m_Pos += 2;
return value;
}
按照上面写的代码,绑定解析类MsgClass来解析结构体,这就是按字节移位的方式来读取数据了。
我们就能在lua中使用绑定方法来解析数据:
local a1 = readDWORD()
local a2 = readWORD()
三、存储文件
从第二点可以知道,不管传过来的数据如何,都是可以解析出来的。
那下面有2种形式来存储了:table 和 二进制存储
1、table 存储:顾名思义把传过来的数据以table形式存储
网上有存储脚本,就不列出了,最后存储的形式:(大致相同)
require "SaveTableToFile"
local ex = {}
ex.year = 2017
ex.month = 1
ex.day = 4
ex.name = "msdb1989"
table.save(ex, "file.dat")
----------------------------------------
return {
-- Table: {1}
{
["day"]=4,
["year"]=2017,
["month"]=1,
["name"]="msdb1989",
},
}
存储形式是个表, 读出来也是一个表。
存储方法的优点:一目了然,存储的数据很明确。
存储方法的缺点:增加了存储文件的大小,io操作效率低,对于大型数据不适合。
2、二进制存储:这个其实是我自己定义的(不好意思),就是服务器传来的数据,不做任何处理,直接存储字节流(换句话说就是来什么存什么)。
例如:
struct CMD_S_Task
{
DWORD dwUserID;
WORD wKindID;
WORD wServerID;
....
....
....
char szNickName[MAX_LEN];
};
对于这样很大的结果体,或者数据很多时,用table来存储性能就很低了。
做复盘的时候,有个复盘头和复盘数据。复盘头是记录玩家信息,复盘数据则是记录玩家操作,而我一开始用table来实现,发现复盘数据很多的情况下,根本来不及存储,所以我才改为二级制存储。
1)保存文件
那怎么做到把发来的数据一下全部存储呢。
/// 读取一个void*数据
char* MsgClass::readvoids(int length) {
if (length < 0) {
return "";
}
char* value = new char[length + 1];
memset(value, 0, sizeof(char)*(length + 1));
if (m_Pos + 1 <= m_Len) {
memcpy(value, (const char*)m_Buffer + m_Pos, length);
}
m_Pos += length;
return value;
}
要读取这个数据包内容,self.m_AryReplayBuffer = msgclass:readvoids(512) 这样就可以了,512表示字节为这个数据包大小,目前最大支持20K,当然还能更大。
这样存下来就是二进制文件了。
2)读取文件
//写入void
void MsgClass::WritevoidToBuffers(const char* ch, int len) {
memcpy((char*)&m_socketBuffers[m_len], ch, len);
m_len += len;
}
file = io.open("123.dat", "rb")
if file == nil then
return
end
ResetBuffers()
WritevoidToBuffers(file:read(512), 512)
SendForMessage("主消息", "子消息")
这样就能读取保存在123.dat里的512字节,然后把消息派发下去,就能模拟服务器发来的消息,这样就能实现复盘了。
四、后续工作
当然还有很多工作没有处理
比如:回退,前进,最开始,结束等等。
这些其实,只不过是发不同的消息而已。
例如:上面例子512位头数据,那么512之后就是游戏开始消息了,开始消息发完就是第二个游戏消息了,既然知道所有数据,那前后,开始结束也就知道了。