KBEngine简单RPG-Demo源码解析(2)
七:服务端资产库文件夹结构
http://kbengine.org/cn/docs/concepts/directorys.html
看assets, 注意:demo使用的不是默认的assets资产目录,而是上面章节下载的kbengine_demos_assets,但文件夹结构与意义是一致的。
八:客户端文件夹结构
kbengine_unity3d_demo
             -> Assets                                                         // Unity3d资产库
                      -> Plugins
                              -> kbengine                                   // KBEngine插件层(包含了网络消息处理、客户端实体维护、与服务端对接层)
                      -> Scripts
                              -> kbe_scripts                                // 客户端逻辑脚本层(https://github.com/kbengine/kben ... e_scripts/README.md)
                                       -> Account.cs                       // 对应于服务端的账号实体的客户端部分实现
                                       -> Avatar.cs                          // 对应于服务端的角色实体的客户端部分实现
                                       -> clientapp.cs                      // 按照服务端的概念cellapp、baseapp、etc,这里我们抽象出一个clientapp
                                       -> Combat.cs                       // 对应于服务端的def interfaces/Combat的客户端部分实现
                                       -> GameObject.cs                 // 对应于服务端的def interfaces/GameObject的客户端部分实现
                                       -> Gate.cs                             // 对应于服务端的Gate实体的客户端部分实现
                                       -> Monster.cs                       // 对应于服务端的Monster实体的客户端部分实现
                                       -> NPC.cs                             // 对应于服务端的NPC实体的客户端部分实现
                                       -> Skill.cs                              // 一个简单的不能再简单的技能执行类,服务端cell/skill下面也有,而客户端主要是进行一些检查
                                       -> SkillBox.cs                        // 玩家的技能列表,对应于服务端的def interfaces/Skillbox的客户端部分实现
                                       -> SkillObject.cs                    // 技能对象(施法者、目标、受术者等),服务端cell/skill下面也有
                              -> u3d_scripts                               // 客户端UI等表现层
                                       -> UI.cs                                // 处理UI部分
                                       -> World.cs                          // 处理场景世界部分
                                       -> GameEntity.cs                 // 所有服务端同步过来的实体在表现层都必须继承该类,完成统一的表现(头顶名称、血条等)与控制(实体状态、移动)
------------------------------------------
基本设计结构:
                                                                  -游戏-
                                   |                                                                        |
                  表现层u3d_scripts(UI && 世界)                      KBE层kbe_scripts(插件 && 逻辑)
1:  表现层与KBE层可以配置为不同线程也能配置为同一个线程跑(单线程)
2:  表现层与KBE层使用事件交互, 向KBE层触发的事件使用fireIn(...),KBE层向外部触发的事件使用fireOut(...)。 那么表现层想要监听KBE触发的Out事件,需要注册监听Event.registerOut, KBE需要监听外部触发进来的事件则反之。
3: 使用unity3D插件与服务端配套则服务端中的scripts/client文件夹可以忽略(https://github.com/kbengine/kben ... e_scripts/README.md)
九:游戏配置
服务端demo所有的配置都存放于kbengine_demos_assets\scripts\data之下。
scripts\data\
               d_avatar_inittab.py    // 角色初始化表, 用于新建立的角色设置初始值, 由kbengine\kbe\tools\xlsx2py\rpgdemo\avatar_init.bat导出。
               d_dialogs.py               // NPC对话表, 其中'menu1'对于的是一个对话协议的ID,服务端根据不同的协议ID执行不同的对话功能, 由kbengine\kbe\tools\xlsx2py\rpgdemo\dialogs.bat导出。
               d_entities.py               // 实体类型表,描述某类型怪移动速度,攻击力等,由kbengine\kbe\tools\xlsx2py\rpgdemo\NPC.bat导出。
               d_skills.py                   // 技能表,描述某类型技能判定条件,输出等,由kbengine\kbe\tools\xlsx2py\rpgdemo\skils.bat导出。
               d_spaces.py               // 场景副本表,描述space是大地图还是副本,以及地图名称等,由kbengine\kbe\tools\xlsx2py\rpgdemo\spaces.bat导出。
               d_spaces_spawns.py // NPC、Monster等出生点信息,目前是手填的,也可以采用工具布点导出。
        spawnpoints\
                 xinshoucun_spawnpoints.xml   // 这个出生点信息主要用于warring这个demo,(NPC、Monster等出生点信息,采用Unity3d布点导出, 可以在unity打开warring这个demo,
                                                                 // 在unity3d(菜单上)->Publish->Build Publish AssetBundles(打包所有需要动态加载资源),然后在Assets->StreamingAssets目录下会得到 "场景名称_spawnpoints.xml"的出生点表)。
十:创建账号
客户端部分:
1: kbengine_unity3d_demo\Assets\Scripts\u3d_scripts\UI.cs
        1.1 点击登录按钮导致createAccount()被调用, createAccount中向KBE层触发了一个创建账号事件,参数是账号名与密码。
          注意:KBEngine插件kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs中已经注册了这个“createAccount”事件,对应于KBEngineApp.createAccount函数。
- public void createAccount()
 - {
 - KBEngine.Event.fireIn("createAccount", new object[]{stringAccount, stringPasswd});
 - }
 
复制代码
2. 事件在KBE插件中kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, KBEngineApp.process()中被正式处理
- /*
 - 插件的主循环处理函数
 - */
 - public virtual void process()
 - {
 - // 处理网络
 - _networkInterface.process();
 - // 处理外层抛入的事件
 - Event.processInEvents();
 - // 向服务端发送心跳以及同步角色信息到服务端
 - sendTick();
 - }
 
复制代码
3. 创建账号函数被调用, createAccount_loginapp函数表示请求向服务端loginapp进程要求创建一个账号,而此时可能还没有连接服务器,需要先连接,如果已经连接上了则向loginapp发送一个包“bundle.send”。
            可以看到向Bundle中写入了相关需要的数据,而Bundle会将数据序列化成二进制流,服务端会采用相同的协议将其归原并将调用服务端协议所绑定的方法(后面会讲到服务端具体方法)。
- public void createAccount(string username, string password)
 - {
 - KBEngineApp.app.username = username;
 - KBEngineApp.app.password = password;
 - KBEngineApp.app.createAccount_loginapp(true);
 - }
 - /*
 - 创建账号,通过loginapp
 - */
 - public void createAccount_loginapp(bool noconnect)
 - {
 - if(noconnect)
 - {
 - reset();
 - _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_createAccount_callback, null);
 - }
 - else
 - {
 - Bundle bundle = new Bundle();
 - bundle.newMessage(Message.messages["Loginapp_reqCreateAccount"]);
 - bundle.writeString(username);
 - bundle.writeString(password);
 - bundle.writeBlob(new byte[0]);
 - bundle.send(_networkInterface);
 - }
 - }
 
复制代码
创建返回结果:
UI.cs -> onCreateAccountResult
服务端部分:
1. 通过上面可以得知客户端向服务端发送了一条创建账号的协议, 协议名称为“Loginapp_reqCreateAccount”(注意,所有的协议名称都能在服务端找到对应的方法, Loginapp_代表了协议的作用域仅为Loginapp, 方法名称为reqCreateAccount)
- void Loginapp::reqCreateAccount(Network::Channel* pChannel, MemoryStream& s)
 - {
 - std::string accountName, password, datas;
 - s >> accountName >> password;
 - s.readBlob(datas);
 - if(!_createAccount(pChannel, accountName, password, datas, ACCOUNT_TYPE(g_serverConfig.getLoginApp().account_type)))
 - return;
 - }
 
复制代码
服务端解析出了账号名与密码,在_createAccount函数中会将这条请求最终送到dbmgr,dbmgr检查之后决定是否创建数据库账号,并最终将结果返回到loginapp,然后由loginapp将结果中转至客户端。
十一:登录账号
1: kbengine_unity3d_demo\Assets\Scripts\u3d_scripts\UI.cs, 向KBE层触发了登陆事件
- public void login()
 - {
 - info("connect to server...(连接到服务端...)");
 - KBEngine.Event.fireIn("login", new object[]{stringAccount, stringPasswd});
 - }
 
复制代码
2: kbengine_unity3d_demo\Assets\Plugins\kbengine\kbengine_unity3d_plugins\KBEngine.cs, 插件触发登陆函数,并最终向loginapp发送了一个登陆包“Loginapp_login”
- public void login(string username, string password)
 - {
 - KBEngineApp.app.username = username;
 - KBEngineApp.app.password = password;
 - KBEngineApp.app.login_loginapp(true);
 - }
 - /*
 - 登录到服务端(loginapp), 登录成功后还必须登录到网关(baseapp)登录流程才算完毕
 - */
 - public void login_loginapp(bool noconnect)
 - {
 - if(noconnect)
 - {
 - reset();
 - _networkInterface.connectTo(_args.ip, _args.port, onConnectTo_loginapp_callback, null);
 - }
 - else
 - {
 - Dbg.DEBUG_MSG("KBEngine::login_loginapp(): send login! username=" + username);
 - Bundle bundle = new Bundle();
 - bundle.newMessage(Message.messages["Loginapp_login"]);
 - bundle.writeInt8((sbyte)_args.clientType); // clientType
 - bundle.writeBlob(new byte[0]);
 - bundle.writeString(username);
 - bundle.writeString(password);
 - bundle.send(_networkInterface);
 - }
 - }
 
复制代码
服务端部分:
1:服务端loginapp.cpp中“void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)”被触发, 这个函数进行了一系列的检查,
确定合法后向dbmgr发送一个登陆请求包“(*pBundle).newMessage(DbmgrInterface::onAccountLogin);”, dbmgr也会进行一系列的检查并将登陆结果返回到loginapp。
- void Loginapp::login(Network::Channel* pChannel, MemoryStream& s)
 - {
 - ...
 - ...
 - if(loginName.size() > ACCOUNT_NAME_MAX_LENGTH)
 - {
 - INFO_MSG(fmt::format("Loginapp::login: loginName is too long, size={}, limit={}.\n",
 - loginName.size(), ACCOUNT_NAME_MAX_LENGTH));
 - _loginFailed(pChannel, loginName, SERVER_ERR_NAME, datas, true);
 - s.done();
 - return;
 - }
 - if(password.size() > ACCOUNT_PASSWD_MAX_LENGTH)
 - {
 - INFO_MSG(fmt::format("Loginapp::login: password is too long, size={}, limit={}.\n",
 - password.size(), ACCOUNT_PASSWD_MAX_LENGTH));
 - ...
 - ...
 - ...
 - // 向dbmgr查询用户合法性
 - Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
 - (*pBundle).newMessage(DbmgrInterface::onAccountLogin);
 - (*pBundle) << loginName << password;
 - (*pBundle).appendBlob(datas);
 - dbmgrinfos->pChannel->send(pBundle);
 - }
 
复制代码
1.1: loginapp得到dbmgr的登录合法结果后向baseappmgr发送了分配网关(baseapp)请求(registerPendingAccountToBaseapp), 通常是负载较低的一个baseapp进程.
- void Loginapp::onLoginAccountQueryResultFromDbmgr(Network::Channel* pChannel, MemoryStream& s)
 - {
 - ...
 - ...
 - ...
 - // 如果大于0则说明当前账号仍然存活于某个baseapp上
 - if(componentID > 0)
 - {
 - Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
 - (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseappAddr);
 - (*pBundle) << componentID << loginName << accountName << password << entityID << dbid << flags << deadline << infos->ctype;
 - baseappmgrinfos->pChannel->send(pBundle);
 - return;
 - }
 - else
 - {
 - // 注册到baseapp并且获取baseapp的地址
 - Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
 - (*pBundle).newMessage(BaseappmgrInterface::registerPendingAccountToBaseapp);
 - (*pBundle) << loginName;
 - (*pBundle) << accountName;
 - (*pBundle) << password;
 - (*pBundle) << dbid;
 - (*pBundle) << flags;
 - (*pBundle) << deadline;
 - (*pBundle) << infos->ctype;
 - baseappmgrinfos->pChannel->send(pBundle);
 - }
 - }
 
复制代码
1.2:baseappmgr最终返回所分配的baseapp的ip地址等信息,loginapp将其转发给客户端(登录成功协议onLoginSuccessfully,包含baseapp的ip和端口信息)
- void Loginapp::onLoginAccountQueryBaseappAddrFromBaseappmgr(Network::Channel* pChannel, std::string& loginName,
 - std::string& accountName, std::string& addr, uint16 port)
 - {
 - ...
 - ...
 - ...
 - Network::Bundle* pBundle = Network::Bundle::ObjPool().createObject();
 - (*pBundle).newMessage(ClientInterface::onLoginSuccessfully);
 - uint16 fport = ntohs(port);
 - (*pBundle) << accountName;
 - (*pBundle) << addr;
 - (*pBundle) << fport;
 - (*pBundle).appendBlob(infos->datas);
 - pClientChannel->send(pBundle);
 - SAFE_RELEASE(infos);
 - }
 
复制代码
2: 客户端插件得到返回结果后调用KBEngineApp.cs->login_baseapp()函数开始正式登录到baseapp。
3:baseapp收到登录请求
- void Baseapp::loginGateway(Network::Channel* pChannel,
 - std::string& accountName,
 - std::string& password)
 
复制代码
进行了一系列的检查,包括:账号是否已经在线,是否可以在这里登录等等。
当检查合法后,向dbmgr发送了一个查询账号信息的请求“DbmgrInterface::queryAccount”,dbmgr将查询到的账号数据(包括属性等)返回到baseapp, Baseapp::onQueryAccountCBFromDbmgr
当函数结果为合法时,根据配置中定义的账号实体脚本名称“g_serverConfig.getDBMgr().dbAccountEntityScriptType”创建了Account实体, 同时还创建了一个clientMailbox,账号实体中调用clientMailbox->方法()即可与客户端通讯了。
Account实体被创建后, 首先__init__被调用, 接着onEntitiesEnabled被调用, 此时实体正式可用了。
账号登陆成功后, 客户端Account.cs中会调用__init__() -> baseCall("reqAvatarList");来请求获得角色列表,
UI.cs中onReqAvatarList得到结果。
十二:创建角色与选择角色进入游戏
1. 创建角色UI.cs -> void onSelAvatarUI()中
       account.reqCreateAvatar(1, stringAvatarName);
       UI.cs中onCreateAvatarResult得到结果。
2.选择角色进入游戏
UI.cs -> onSelAvatarUI()中
account.selectAvatarGame(selAvatarDBID);
这里使用角色的数据库ID作为标识,服务端上Account实体有角色列表属性,角色列表的数据结构大概为
AvatarList <Dict<AvatarDBID(UINT64), INFOS>>
十三:创建世界(大地图与副本)
1. 创建世界管理器服务端启动之后,baseapp与cellapp准备完毕、准备关闭等事件都会通知到kbengine_defs.xml配置中指定的个性化脚本。kbe默认个性化脚本为kbengine.py,  baseapp进程准备好之后会调用kbengine.py的onBaseAppReady回调函数, demo在这个函数中判定是否为第一个启动的baseapp(假如启动了很多baseapps),
如果是第一个baseapp,脚本创建了一个世界管理实体“spaces”:
- def onBaseAppReady(isBootstrap):
 - """
 - KBEngine method.
 - baseapp已经准备好了
 - @param isBootstrap: 是否为第一个启动的baseapp
 - @type isBootstrap: BOOL
 - """
 - INFO_MSG('onBaseAppReady: isBootstrap=%s' % isBootstrap)
 - # 安装监视器
 - Watcher.setup()
 - if isBootstrap:
 - # 创建spacemanager
 - KBEngine.createBaseLocally( "Spaces", {} )
 
复制代码
2. 世界管理器创建出所有的场景
在spaces.py中, spaces通过initAlloc函数根据配置中scripts/data/d_spaces.py创建出space实体,space实体描述的是一个抽象空间,一个空间可以被逻辑定义为大地图、场景、房间、宇宙等等。
- def initAlloc(self):
 - # 注册一个定时器,在这个定时器中我们每个周期都创建出一些Space,直到创建完所有
 - self._spaceAllocs = {}
 - self.addTimer(3, 1, SCDefine.TIMER_TYPE_CREATE_SPACES)
 - self._tmpDatas = list(d_spaces.datas.keys())
 - for utype in self._tmpDatas:
 - spaceData = d_spaces.datas.get(utype)
 - if spaceData["entityType"] == "SpaceDuplicate":
 - self._spaceAllocs[utype] = SpaceAllocDuplicate(utype)
 - else:
 - self._spaceAllocs[utype] = SpaceAlloc(utype)
 
复制代码
SpaceAlloc: 普通地图,可以理解为大地图,但整个世界中只能有一个这样类型的地图。
SpaceAllocDuplicate:副本地图,可以复制出很多个
上面函数注册了一个定时器, 这里是定时器的回调, 每一秒回调一次。
self._spaceAllocs[spaceUType].init(), 这里真正开始创建这些space实体, 里面调用的createBaseAnywhere函数来创建实体, 如果启动了多个baseapp这个函数根据负载情况将实体选择到合适的进程中创建。
- def createSpaceOnTimer(self, tid, tno):
 - """
 - 创建space
 - """
 - if len(self._tmpDatas) > 0:
 - spaceUType = self._tmpDatas.pop(0)
 - self._spaceAllocs[spaceUType].init()
 - if len(self._tmpDatas) <= 0:
 - del self._tmpDatas
 - self.delTimer(tid)
 
复制代码
Space实体创建出来之后,此时还没有真正创建出空间, 这个实体仅仅是将要与某个真正空间关联的实体, 可以通过它来操控那个空间。
但空间只能在cellapp上存在, 因此我们需要调用API让实体在cell上创建出一个空间,并在cell上创建出一个实体与空间关联, 这个实体就像一个空间的句柄。
- class Space(KBEngine.Base, GameObject):
 - def __init__(self):
 - self.createInNewSpace(None)
 
复制代码
此功能由createInNewSpace完成, __init__可以理解为Space的构造函数。
3. 为这个抽象的空间增加几何数据
有指定几何数据的空间可以被看做是一个特定的场景, 这些几何数据与客户端对应的场景表现相关联, 例如:导航网格(navmesh), 服务端通过这些数据让NPC进行正确的移动,碰撞等。
上面Space创建cell部分之后, cell上的Space._init__也会被调用, 其中addSpaceGeometryMapping API接口完成几何数据加载工作
(注意:为了加载大量数据不让进程卡顿,这个数据加载是多线程的,它会通过一些回调来告诉开发者加载状态,具体参考API手册)。
- class Space(KBEngine.Entity, GameObject):
 - def __init__(self):
 - KBEngine.addSpaceGeometryMapping(self.spaceID, None, resPath)
 
复制代码
KBEngine简单RPG-Demo源码解析(2)的更多相关文章
- 使用.Net Core + Vue + IdentityServer4 + Ocelot 实现一个简单的DEMO +源码
		
运行环境 Vue 使用的是D2admin: https://doc.d2admin.fairyever.com/zh/ Github地址:https://github.com/Fengddd/Perm ...
 - springmvc运行流程简单解释(源码解析,文末附自己画的流程图)
		
首先看一下DispatcherServlet结构: 观察HandlerExecutionChain对象的创建与赋值,这个方法用来表示执行这个方法的整条链. 进入getHandler方法: 此时的变量h ...
 - 动画 ---Animejs 简单使用与源码解析
		
Anime是什么 Anime有什么用 Anime是作何做的 requireAnimationFrame() engine(){ // 处理让多个帧运动起来  play()  step()} ani ...
 - Android 开源项目源码解析(第二期)
		
Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations ...
 - Mybatis源码解析-DynamicSqlSource和RawSqlSource的区别
		
XMLLanguageDriver是ibatis的默认解析sql节点帮助类,其中的方法其会调用生成DynamicSqlSource和RawSqlSource这两个帮助类,本文将对此作下简单的简析 应用 ...
 - Caffe2源码解析
		
写在前面 上一篇文章对Caffe2中的core模块进行了简单拆解Caffe2源码解析之core,本篇给出其它模块的拆解,目的是大致了解每个模块的内容和目标,进一步理解Caffe2的整体框架.内容不多, ...
 - KBEngine简单RPG-Demo源码解析(1)
		
一:环境搭建1. 确保已经下载过KBEngine服务端引擎,如果没有下载请先下载 下载服务端源码(KBEngine): https://github.com ...
 - 简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析
		
简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析 虽然经常用 OAuth 2.0,但是原理却不曾了解,印象里觉得很简单,请求跳来跳去,今天看完相关介绍,就来捋一捋 ...
 - 使用CEF(三)— 从CEF官方Demo源码入手解析CEF架构与CefApp、CefClient对象
		
在上文<使用CEF(2)- 基于VS2019编写一个简单CEF样例>中,我们介绍了如何编写一个CEF的样例,在文章中提供了一些代码清单,在这些代码清单中提到了一些CEF的定义的类,例如Ce ...
 - 实现简单的手写涂鸦板(demo源码)
		
在一些软件系统中,需要用到手写涂鸦的功能,然后可以将涂鸦的结果保存为图片,并可以将"真迹"通过网络发送给对方.这种手写涂鸦功能是如何实现的了?最直接的,我们可以使用Windows提 ...
 
随机推荐
- 用Go造轮子-管理集群中的配置文件
			
写在前面 最近一年来,我都在做公司的RTB广告系统,包括SSP曝光服务,ADX服务和DSP系统.因为是第一次在公司用Go语言实现这么一个大的系统,中间因为各种原因造了很多轮子.现在稍微有点时间,觉着有 ...
 - 第一篇:使用Spark探索经典数据集MovieLens
			
前言 MovieLens数据集包含多个用户对多部电影的评级数据,也包括电影元数据信息和用户属性信息. 这个数据集经常用来做推荐系统,机器学习算法的测试数据集.尤其在推荐系统领域,很多著名论文都是基于这 ...
 - 读Zepto源码之集合操作
			
接下来几个篇章,都会解读 zepto 中的跟 dom 相关的方法,也即源码 $.fn 对象中的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码 ...
 - php如何应对秒杀抢购高并发思路
			
我们常用QPS(Query Per Second,每秒处理请求数)来衡量一个web应用的吞吐率,解决每秒数万次的高并发场景,这个指标非常关键. 举个栗子:假设一个业务请求平均为100ms,同时系统内有 ...
 - 求一个二维整数数组最大子数组之和,时间复杂度为N^2
			
本随笔只由于时间原因,我就只写写思想了 二维数组最大子数组之和,可以 引用 一维最大子数组之和 的思想一维最大子数组之和 的思想,在本博客上有,这里就不做多的介绍了 我们有一个最初的二维数组a[n ...
 - SmartCoder每日站立会议06
			
站立会议内容 讨论了小程序的具体实现方式,主要会加入地图这一元素,使程序看起来更加的方便直观,同时也会使人感到新颖.在对各个点的评论对话功能也在考虑中. 1. 站立会议照片: 2.任务展板 3.燃尽图
 - Android 图片加载框架Picasso基本使用和源码完全解析(巨细无比)
			
写在之前 原本打算是每周更新一篇博文,同时记录一周的生活状态,但是稍微工作忙一点就顾不上写博客了.悲催 还是说下最近的状况,最近两周一直在接公司申请的计费点, 沃商店,银贝壳,微信等等,然后就是不停的 ...
 - npm 一条命令更换淘宝源
			
一条命令更换淘宝源 npm config set registry https://registry.npm.taobao.org
 - 转发:Ubuntu软件卸载安装的命令
			
说明:由于图形化界面方法(如Add/Remove... 和Synaptic Package Manageer)比较简单,所以这里主要总结在终端通过命令行方式进行的软件包安装.卸载和删除的方法. 一.U ...
 - PHP面向对象笔记解析
			
PHP的面向对象是很重要的内容,也是很常用的内容.所以现在就把PHP面向对象进行整理了一下. 顺带,我会在后面把我整理的一整套CSS3,PHP,MYSQL的开发的笔记打包放到百度云,有需要可以直接去百 ...