ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

ygopro源码分析3:解剖

2022-02-26 11:33:15  阅读:412  来源: 互联网

标签:core ygopro packet server 源码 void len 解剖 客户端


本文简单的整理一下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本地自带服务器,但联机并没有做成类似东方非想天则的模式,本地的服务器基本上没什么用,在本地建立服务器的端口号写死在代码里了,只能使用7911端口,即在同一网络下只能同时存在一个服务器,只能用于测试脚本使用.

目前233服和Mc服使用的服务端是 srvpro,配合[ygopro-server][https://github.com/mycard/ygopro/tree/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)
         return;
      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年还有人使用其开发新游戏The End of Dyeus

,还是个开放世界游戏,

在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 绘制图形
    //接收玩家输入等
}

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这个文件中.可以使用sqlitebrowser来方便的操作这个文件.

card.cdb文件

cards.cdb中有两个表,datas和texts
datas的内容是卡片的信息(用数字表示),texts的内容是跟卡片有关的字符串.

datas的表头如下:

id ot alias setcode type atk def level race attribute category

需注意:这些数值的用途大部分是在卡组构造界面搜索卡片

  • 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库来进行压缩和解压操作.

标签:core,ygopro,packet,server,源码,void,len,解剖,客户端
来源: https://www.cnblogs.com/ray-rain/p/15938789.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有