ygopro源码分析3:解剖

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

ygopro源码分析3:解剖
本⽂简单的整理⼀下ygopro是如何运⾏的
1.core的运作
core维护了⼀场duel,是ygopro的核⼼.源码⼤概分为4部分:
第⼀部分是card duel effect field group等类的定义⽂件
第⼆部分是lua解释器,负责运⾏lua函数,interpreter源⽂件和头⽂件
第三部分是card duel effect field group等类的lua库函数
第四部分是核⼼处理⽂件processor.cpp等
还记得这个游戏最开始的名字是ygocore.
主循环
和所有游戏⼀样,core也⼀样有⼀个主循环,由于是卡牌游戏,core的主循环是磕磕绊绊的运⾏的.简单来说是这样的:
core与server直接交流,server在掌控core的主循环
core给server传数据使⽤buffer,server给core传数据使⽤results,⼆者都是固定长度的内存
core会⼀直运⾏,直到产⽣buffer,buffer交给server后,core会停⽌运⾏,等待server答复
server收到buffer会发送给对应玩家的client,交给玩家操作,玩家操作后会产⽣results,由server向core转告
如果是sing mode运⾏的话,每次的results都会被设置成⼀个固定值,(DUEL_SIMPLE_AI)
处理
core通过"处理单元unit"来运⾏,每⼀个unit都有若⼲步骤,⼀步⼀步的进⾏,需要玩家操作的时候会停⽌,玩家操作后会继续运⾏.
就像调⽤函数⼀样,unit可以运⾏到⼀半去运⾏新的unit,xinunit调⽤完之后还会接着在原来的unit处接着运⾏.
每个unit都带着⼀个明确的⽬的.
+------------+
| unit3 |
+------------+
| unit2 |
+------------+
| unit1 |
+------------+
processor永远只会优先运⾏最上⾯的unit3,3运⾏完了就会去运⾏2中断的部分,或者在向上⾯加⼀个unit4.
unit1永远都不会结束,它是⼀个loop.unit1是"PROCESSOR_TURN",它的功能是交替的运⾏每个玩家的各个流程.
unit2可以是"PROCESSOR_IDLE_COMMAND",代表了main1流程和main2时可以只有操作的时刻
unit3可以是"PROCESSOR_SELECT_IDLECMD",表⽰正在等待client那边操作.
2.⽹络
⽹络完全由libevent库实现
ygopro的客户端是⾃带服务器和客户端的,类似早期的单机游戏CS 魔兽争霸红警等,只需要在同⼀⽹络下就可以联机.
ygopro本地⾃带服务器,但联机并没有做成类似东⽅⾮想天则的模式,本地的服务器基本上没什么⽤,在本地建⽴服务器的端⼝号在配置⽂件⾥决定了,频繁的去更改这个⽂件也不现实,所以同⼀⽹络下基本上只能同时存在⼀个服务器,只能⽤于测试脚本使⽤.
⽬前233服和Mc服使⽤的服务端是 ,配合[ygopro-server][]使⽤.
搜索游戏房间
只能在同⼀⽹络下进⾏,
房主新建服务器, 会开启⼀个端⼝号为"7920" 的udp⼴播,当接收到任何信息时,会检查这条信息,如果该信息是"NETWORK_CLIENT_ID",该⼴播就会将房间信息发送给对⽅的"7921"端⼝
另⼀边客户端处,点击刷新主机时,会⽴即使⽤"7922"端⼝的⾝份⼀直向"host地址"的 "7920"端⼝发送信息,信息内容是"NETWORK_CLIENT_ID",⼀共会发8次host地址是通过gethostbyname函数获取的地址,⼀般根据⽹卡情况和⽹络配置情况会获取到好⼏个地址,每个地址都是正确地址,都可以⽤于游戏,使⽤ipconfig命令可以看到这⼏个地址的详细情况
8次⾥⾯肯定会有⾄少⼀次命中"host地址"的正确地址,也就是说,会有多条NETWORK_CLIENT_ID信息被发到server的"7920"端⼝中,server会⽴即将房间信息发送到客户端的"7921"端⼝.
客户端收到之后会⽴即将房间信息打印到界⾯上(可能会有多条)
服务器和客户端通信
服务器和客户端之间通信的内容是packet,结构如下:
16bit packet_len 8bit proto exdata_len exdata
+------------------+---------------+-------------------------+
|- data -|
其中第⼀部分为packet_len,长度2个字节,数值是 exdata_len + 1,即后⾯内容的长度总和
第⼆部分是 proto,长度1个字节, 表⽰后⾯ exdata 的类型
第三部分是 exdata,⼀些特定的proto会附带这部分内容,长度不定.上⾯提到的core传出来的buffer在这部分中
后⾯两部分统称为data
这个packet的最终长度是packet_len+2.
服务器和客户端处理packet之前跳过了前2个字节.
客户端给服务器发送数据
void SendPacketToServer(unsigned char proto)
void SendPacketToServer(unsigned char proto, ST& st)
void SendBufferToServer(unsigned char proto, void* buffer, size_t len)
客户端连接服务器并设置读回调函数
client_bev = bufferevent_socket_new(client_base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(client_bev, ClientRead, NULL, ClientEvent, (void*)create_game);
客户端收到服务器数据,触发读回调函数
void DuelClient::ClientRead(bufferevent* bev, void* ctx) {
evbuffer* input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
unsigned short packet_len = 0;
while(true) {
if(len < 2)
evbuffer_copyout(input, &packet_len, 2); //获取前2字节作为packet长度
if(len < (size_t)packet_len + 2)
return;
evbuffer_remove(input, duel_client_read, packet_len + 2); //获取⼀个packet的所有数据
if(packet_len)
HandleSTOCPacketLan(&duel_client_read[2], packet_len); //从第[2]个字节开始处理,跳过了packet_len
len -= packet_len + 2;
}
}
服务器给客户端发送数据
void SendPacketToPlayer(DuelPlayer* dp, unsigned char proto)
void SendPacketToPlayer(DuelPlayer* dp, unsigned char proto, ST& st)
void SendBufferToPlayer(DuelPlayer* dp, unsigned char proto, void* buffer, size_t len)
建⽴监听服务器
当有客户端连接时,会触发ServerAccept回调函数,建⽴⼀个socket连接
listener = evconnlistener_new_bind(net_evbase, ServerAccept, NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, (sockaddr*)&sin, sizeof(sin));
设置读回调函数
当server接收到数据后,会触发ServerEchoRead函数
void NetServer::ServerAccept(evconnlistener* listener, evutil_socket_t fd, sockaddr* address, int socklen, void* ctx)
服务器收到数据会触发该函数
void NetServer::ServerEchoRead(bufferevent *bev, void *ctx) {
evbuffer* input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
unsigned short packet_len = 0;
while(true) {
if(len < 2)
return;
evbuffer_copyout(input, &packet_len, 2); //读取数据的前2个字节作为 packet长度
if(len < (size_t)packet_len + 2)
return;
evbuffer_remove(input, net_server_read, packet_len + 2); //将⼀个packet 的所有数据都存储在net_server_read ⾥
if(packet_len)
HandleCTOSPacket(&users[bev], &net_server_read[2], packet_len); //从第[2]个字节开始处理,跳过了packet_len
len -= packet_len + 2;
}
}
可以多个客户端连接服务器,多余的玩家会成为观战者.
总结:服务器和客户端靠HandleXXXXPacket函数处理packet(跳过了前两个字节)
3.其他
3.1 irrlicht⿁⽕引擎
这是⼀个⾮常⽼的游戏引擎,古⽼到其最新的源码⾥提供的是vs2012的sln⽂件,但也不是说消失在时间⾥了,2021年还有⼈使⽤其开发新游戏,还是个开放世界游戏,
在ygopro⾥,主要应⽤就是主界⾯菜单卡组构筑界⾯还有游戏界⾯.GUI的编程风格挺像QT的.
对于ygopro来说,了解以下两点就可以了:
irrlicht初始化操作
⾸先要获取⼀个IrrlichtDevice,这是irrlicht最根本的对象,有两种⽅法得到
⾃定义params,也是ygopro使⽤的⽅法
IrrlichtDevice* device;
irr::SIrrlichtCreationParameters params = irr::SIrrlichtCreationParameters();
//对params做⼀些⾃定义
device = irr::createDeviceEx(params);
或者使⽤默认params
函数原型:
IrrlichtDevice* createDevice(
video::E_DRIVER_TYPE deviceType = video::EDT_SOFTWARE,
const core::dimension2d<u32>& windowSize = (core::dimension2d<u32>(640,480)),
u32 bits = 16,
bool fullscreen = false,
bool stencilbuffer = false,
bool vsync = false,
IEventReceiver* receiver = 0);
使⽤IrrlichtDevice获取其他对象
获取IVideoDriver,所有图形相关的接⼝
IVideoDriver* driver = device->getVideoDriver();
获取IGUIEnvironment,⽤于管理所有GUI组件
IGUIEnvironment* env = device->getGUIEnvironment();
获取ISceneManager,管理camera 等其他资源
ISceneManager* smgr = device->getSceneManager();
主循环
while(device->run()) {
//绘制GUI 绘制图形
//接收玩家输⼊等
}
irrlicht的GUI还是⽐较落后的.ygopro⽤了七⼋百⾏,来添加GUI元素
添加GUI的步骤
⾸先去要预先定义⼀些宏来表⽰GUI的id(类似QT⾥的信号),如:
#define BUTTON_LAN_MODE 100
#define BUTTON_SINGLE_MODE 101
#define BUTTON_REPLAY_MODE 102
#define BUTTON_TEST_MODE 103
添加btn
btnLanMode = env->addButton(rect<s32>(10, 30, 270, 60), wMainMenu, BUTTON_LAN_MODE, dataManager.GetSysString(1200));
添加checkbox
chkHostPrepReady[i] = env->addCheckBox(false, rect<s32>(250, 75 + i * 25, 270, 95 + i * 25), wHostPrepare, CHECKBOX_HP_READY, L"");
初始化这些元素时都有⼀个参数是id,当GUI元素被操作的时候这些id就代表了不同的信号.
如何使⽤GUI
需要⼀个对象来接受这些信号,从⽽使这些GUI元素发挥作⽤.在irrlicht⾥,这个对象被称为EventReceiver.
同⼀时刻,⼀个device只能有⼀个EventReceiver.
这⾏程序为device设置了⼀个EventReceiver
device->setEventReceiver(&menuHandler);
任意⼀个类,只要重写了OnEvent(const irr::SEvent& event)⽅法,就可以成为EventReceiver
声明
class MenuHandler: public irr::IEventReceiver {
public:
virtual bool OnEvent(const irr::SEvent& event);
};
实现
bool MenuHandler::OnEvent(const irr::SEvent& event) {
switch(event.EventType){ //获取event类型
case irr::EET_GUI_EVENT: //GUI事件
//根据id判断是哪个GUI元素,然后做出相应操作
s32 id = event.GUIEvent.Caller->getID();
case irr::EET_MOUSE_INPUT_EVENT: //⿏标输⼊事件
case irr::EET_KEY_INPUT_EVENT: //键盘输⼊事件
//判断哪个健被按下
switch(event.KeyInput.Key)
}
}
3.2 sqlite数据库
ygopro使⽤sqlite把所有卡⽚的信息存储在cards.cdb这个⽂件中.可以使⽤来⽅便的操作这个⽂件.
card.cdb⽂件
cards.cdb中有两个表,datas和texts
datas的内容是卡⽚的信息(⽤数字表⽰),texts的内容是跟卡⽚有关的字符串.
datas的表头如下:
idotaliassetcodetypeatkdeflevelraceattributecategory
需注意:这些数值的⽤途⼤部分是在卡组构造界⾯搜索卡⽚
id :表⽰卡⽚的官⽅代码
ot :表⽰卡⽚的限制情况 0代表禁⽌ 3代表⽆限制
alias:表⽰别名,有些卡⽚被科乐美复刻了多次,⽐如青眼⽩龙,复刻后的青眼⽩龙的alias就是初版青眼⽩龙的id
setcode :⼀个10进制数,表⽰卡⽚所属的字段,把这个数转换成16进制可得到字段, 每个字段4位16进制数.strings.conf⽂件中存储了所有字段
举个例⼦:数据库中查到的setcode最⼤值的卡⽚是"希望皇拟声乌托邦",所属字段是"刷拉拉(0x8f)" "我我我(0x54)" "隆隆隆(0x59)" "怒怒怒(0x82)",这张卡⽚的setcode是36592129229979791,
转换成16进制是82 0059 0054 008F
type :⼀个10进制数,转换成2进制后,表⽰卡⽚类型.
atk :攻击⼒
def:防御⼒
level: 等级 or 阶级 or link值
race:种族
attribute:属性
category:⼀个10进制数,转换成32bit的⼆进制数,恰好对应卡⽚搜索的中的32个效果.
举个例⼦:随机选择了⼀张卡"暗之⽀配者-佐克",其category值为134217730,写成⼆进制是00001000000000000000000000000010,倒着看,恰好对应第⼆个效果"怪兽破坏"和倒数第五个效果"幸运",
⾄于为什么要倒着看,因为代码中是通过下⾯的⽅式设置过滤选项的
long long filter = 0x1;
for(int i = 0; i < 32; ++i, filter <<= 1)
if(mainGame->chkCategory[i]->isChecked())
filter_effect |= filter;
texts的内容⽐较简单就不展开了.
spmemvfs库
是:A memory vfs implementation for SQLite,⽤于把整个cdb⽂件读到内存中,加快读取速度.
3.3 replay回放
replay的原理是记录下玩家的操作到rep⽂件,然后播放rep时再从⽂件⾥读出操作给到core,让其使⽤那些操作进⾏⼀场全⾃动duel,可以说跟东⽅project的⽅式⼀模⼀样.
这个过程使⽤了lzma库来进⾏压缩和解压操作.。

相关文档
最新文档