diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 7b98a3d3..9df202de 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -1,13 +1,14 @@ .. SPDX-License-Identifier: GFDL-1.3-or-later -Dev文档 -============ +开发过程的碎碎念 +================ + +某人写出来的整理思路用的文档= = .. toctree:: :maxdepth: 1 ai.rst - compile.rst database.rst gameevent.rst gamelogic.rst @@ -17,3 +18,4 @@ Dev文档 todo.rst ui.rst hegemony.rst + shendiaochan.rst diff --git a/docs/index.rst b/docs/index.rst index 17b6f119..8f6d6f45 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ usr/index.rst diy/index.rst + inner/index.rst dev/index.rst api/index.rst fkp/usr/index.rst diff --git a/docs/dev/compile.rst b/docs/inner/01-compile.rst similarity index 97% rename from docs/dev/compile.rst rename to docs/inner/01-compile.rst index 8544c284..b50bda55 100644 --- a/docs/dev/compile.rst +++ b/docs/inner/01-compile.rst @@ -3,6 +3,8 @@ 编译 FreeKill ============= +事先声明:编译FK是个比较折磨的过程,而且完全可以跳过直接看下一节,除非你真的很想编译。 + 全平台通用步骤 -------------- diff --git a/docs/inner/02-main.rst b/docs/inner/02-main.rst new file mode 100644 index 00000000..b3c31531 --- /dev/null +++ b/docs/inner/02-main.rst @@ -0,0 +1,87 @@ +从main函数开始 +=============== + +FK说到底,本质上是个C++项目,所以程序的入口自然是在main.cpp的main函数中。 +本文档不至于把代码一行行拿出来分析,而是尽可能基于main函数来解答一些常见问题。 + +由于大家比较关注的点都是程序怎么开始执行Lua,所以尽可能往这方面靠吧。 +至于main函数的其他功能,比如安卓版执行之前先复制文件之类的,请大家自行查看吧。 + +服务器端相关 +------------- + +FK是一个遵循C/S架构的游戏,所以它才支持联机游玩功能。 +从文档可以知道 ``./FreeKill -s`` 就能启动服务端进程,来看相应main代码吧。 + +.. code:: cpp + + // main.cpp: 180行左右 + Server *server = new Server; + if (!server->listen(QHostAddress::Any, serverPort)) { + qFatal("cannot listen on port %d!\n", serverPort); + +上面的代码就是检测到命令行参数 ``-s`` 之后,开始启动服务器的代码。 +这里创造了Server对象,然后在相应端口监听。 +不过如果你去看Server的构造函数的话,却找不到Lua相关的代码。 + +这是因为在服务器端中,可能同时运行多个游戏房间,每个游戏房间是一个单独的线程, +所以是每个游戏房间维护一个Lua虚拟机。总体而言,这是为了让各个房间之间的Lua +不产生冲突。 + +所以服务端只有在创建新房间的时候才会执行Lua,来看Room的构造函数吧: + +.. code:: cpp + + // room.cpp: 31行左右 + L = CreateLuaState(); + DoLuaScript(L, "lua/freekill.lua"); + DoLuaScript(L, "lua/server/room.lua"); + initLua(); + +首先 ``DoLuaScript`` 从神杀抄的,就是个dofile函数的C++封装版本。 +然后调查initLua函数: + +.. code:: cpp + + // swig/server.i 31行左右 + void Room::initLua() + { + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_replace(L, -2); + lua_getglobal(L, "CreateRoom"); + SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0); + int error = lua_pcall(L, 1, 0, -2); + lua_pop(L, 1); + if (error) { + const char *error_msg = lua_tostring(L, -1); + qCritical() << error_msg; + } + } + +这个C++函数翻译成Lua就是 +``pcall(function() CreateRoom(C++层面的Room) end, debug.traceback)`` , +意思就是以这个c++版的Room对象为参数,执行Lua全局函数CreateRoom。 +就不贴代码了,在room.lua的底端,自己看吧。 + +不过即使到了这一步,游戏还是没运行起来。 +游戏运行函数是 ``Room::run()`` ,里面调用的那个函数已经给注释了。 + +至此,服务端的Lua开始执行,游戏逻辑也开始运行了。 +至于这块是如何运行的,相信熟悉Lua的各位能自己去阅读。 + +客户端相关 +----------- + +如果main函数中没有检测到 ``-s`` 命令行参数,那么就绘制UI并显示一个窗口。 + +.. code:: cpp + + // main.cpp: 292行左右 + // 加载完全局变量后,就再去加载 main.qml,此时UI界面正式显示 + engine->load("qml/main.qml"); + +这里就加载了main.qml,然后剩下就交给qml来处理UI了。至此main函数结束。 + +客户端的Lua就简单了,在Client的构造函数里面。客户端的Lua代码只在需要时 +执行,而这个就不在main的讨论范畴了,这个在后面通信模块会提到。 diff --git a/docs/inner/03-protocol.rst b/docs/inner/03-protocol.rst new file mode 100644 index 00000000..e69de29b diff --git a/docs/inner/04-coroutine.rst b/docs/inner/04-coroutine.rst new file mode 100644 index 00000000..63034044 --- /dev/null +++ b/docs/inner/04-coroutine.rst @@ -0,0 +1,76 @@ +协程机制 +=========== + +FK的游戏逻辑是全Lua的,这就有一个很大的问题:Lua是单线程的,那如果我要在 +对局中做一些和游戏流程无关、但又不完全无关的操作(比如预亮技能,cpp可是对 +技能一无所知的),是不是还要创另一个线程去执行? + +由于所有游戏有关的数据全部在Lua,所以去Cpp代码多弄个线程处理预亮、托管、旁观 +等等操作并不可行。事实上这是通过协程机制实现的,请接着看。 + +协程简介 +---------- + +我这里就简单的讲一下得了,关于协程的更多详情自己去查询吧。 + +协程其实就是一种特殊的调用函数的过程,他的特点就是可以随时通过 +`` coroutine.yield `` 中止执行,然后调用者也能通过resume来让中断的函数 +继续执行。 + +通过协程不仅实现了这样的一个预亮处理机制等等,还实现了游戏事件机制, +利用协程可以实现事件的中断机制(结束本回合之类的)。 + +我们先来看预亮技能之类的行为是如何实现的吧。 + +处理客户端的request +-------------------- + +像托管、预亮等等行为,称为“客户端对服务器发起的request”。 +从通信机制一节中我们知道只有服务端才能发起request,这里所谓客户端 +request本质上只是对服务器的一种notify罢了。 + +在Room类(Cpp类)中,维护了一个请求队列,用来保存这些来自客户端的请求。 +而当Lua游戏逻辑有空休息时,就把 **游戏逻辑协程** 暂时挂起(yield),然后再 +切换到 **请求处理协程** 。关于种种请求的处理详情请查看request.lua。 +这里来看看这两个协程是如何创建的,以及如何执行、如何切换。 + +首先是创建协程,这个是在Room(Lua类)启动的时候创建的: + +.. code:: lua + + -- room.lua: 70行左右,代码过长改为伪代码 + local main_co = coroutine.create(function() self:run() end) + local request_co = coroutine.create + (function(rest) self:requestLoop(rest) end) + while not game_finished do + resume(main_co) + -- 如有错误则处理错误 + resume(request_co) + -- 如有错误则处理错误 + end + +从这个无限循环可以看出,在主协程挂起之后,就会去处理请求处理协程。那个协程也 +挂起之后,又开始新一轮循环,再去进入主协程。 + +主协程何时挂起? +----------------- + +执行delay函数,或者正在waitForReply的时候。这两个函数会给服务端留下空闲的 +时间,因此主协程会在此时挂起。 + +全局搜索一下 ``__handleRequest`` 即可知道主协程所有挂起的时机。 + +调查代码可知,request处理协程每次处理一条request就立刻挂起,而请求处理都是 +一些耗时很短的操作。所以这样就可以在不影响游戏逻辑的前提下暂时中断,执行一 +些其他的操作。而他们本质上不是并行执行的,所以也无需操作加锁之类的问题。 + +用协程实现事件机制 +------------------- + +Fk的事件机制也是用协程实现的。每当启动一个新事件后,实际上就创建了一个新的协程 +并执行,还有一些别的什么处理逻辑等等。因为是在协程中实现的,这就有几个好处: + +1. 当发生error时,被杀死的仅仅只是这个协程,而不是所有Lua代码。 +2. 能随时用Yield函数去中断本协程,因此可以实现“终止事件”。 + +具体请自行查阅gameevent.lua吧。 diff --git a/docs/inner/index.rst b/docs/inner/index.rst new file mode 100644 index 00000000..fd51e319 --- /dev/null +++ b/docs/inner/index.rst @@ -0,0 +1,13 @@ +.. SPDX-License-Identifier: GFDL-1.3-or-later + +深入了解FK +============ + +这是一系列深入到FK种种实现细节的文档,其目的是能带着你大致的阅读和理解一些源代码。 + +.. toctree:: + :maxdepth: 1 + + 01-compile.rst + 02-main.rst + 04-coroutine.rst