游戏lua很多arg0luai是什么意思思

33179 条评论分享收藏感谢收起08:19:13 UTC
这个接口lua无法用?第三个参数直接无视了?
09:24:37 UTC
麻烦把问题说得清楚些。为什么说是无用的?是遇到什么错误了,还是什么效果不对。另外,你贴的这部分代码是什么意思?
03:57:47 UTC
可能是我说的不够清楚,我在看源码的时候发现setUniformFloatv这个lua导出的接口void setUniformFloatv(const std::string& uniformName, ssize_t size, const float* pointer);void setUniformFloatv(GLint uniformLocation, ssize_t size, const float* pointer);看定义是3个参数,第一个是opengle相关的(就不说了,就是对应定义的uniform变量),第二个是数组的长度, 第三个是数组的指针。然后我贴的这段代码是lua导出的lua_cocos2dx_GLProgramState_setUniformFloatv 完整如下int lua_cocos2dx_GLProgramState_setUniformFloatv(lua_State* tolua_S){
int argc = 0;
cocos2d::GLProgramState* cobj =
if COCOS2D_DEBUG &= 1
tolua_Error tolua_
if COCOS2D_DEBUG &= 1
if (!tolua_isusertype(tolua_S,1,"cc.GLProgramState",0,&tolua_err)) goto tolua_
cobj = (cocos2d::GLProgramState*)tolua_tousertype(tolua_S,1,0);
if COCOS2D_DEBUG &= 1
if (!cobj)
tolua_error(tolua_S,"invalid 'cobj' in function 'lua_cocos2dx_GLProgramState_setUniformFloatv'", nullptr);
endif //下面是重点
argc = lua_gettop(tolua_S)-1;
if (argc == 3) { //参数数量 = 3
ok &= luaval_to_int32(tolua_S, 2,(int *)&arg0, "cc.GLProgramState:setUniformFloatv"); //拿到第一个参数并返回成功状态
if (!ok) { }
ssize_t arg1;
ok &= luaval_to_ssize(tolua_S, 3, &arg1, "cc.GLProgramState:setUniformFloatv");//拿到第2个参数并返回成功状态
if (!ok) { }
const float* arg2;
** #pragma warning NO CONVERSION TO NATIVE FOR float*//!!!!!!!!!!这里怎么不拿第三个参数?**
**ok = //然后默认是失败的!!!这是要我们自己实现的节奏么?**
if (!ok) { }
cobj-&setUniformFloatv(arg0, arg1, arg2);
lua_settop(tolua_S, 1);
}while(0);
if (argc == 3) {
std::string arg0;
ok &= luaval_to_std_string(tolua_S, 2,&arg0, "cc.GLProgramState:setUniformFloatv");
if (!ok) { }
ssize_t arg1;
ok &= luaval_to_ssize(tolua_S, 3, &arg1, "cc.GLProgramState:setUniformFloatv");
if (!ok) { }
const float* arg2;
#pragma warning NO CONVERSION TO NATIVE FOR float*
if (!ok) { }
cobj-&setUniformFloatv(arg0, arg1, arg2);
lua_settop(tolua_S, 1);
}while(0);
luaL_error(tolua_S, "%s has wrong number of arguments: %d, was expecting %d \n",
"cc.GLProgramState:setUniformFloatv",argc, 3);
if COCOS2D_DEBUG &= 1
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'lua_cocos2dx_GLProgramState_setUniformFloatv'.",&tolua_err);
03:59:27 UTC
引擎是3.10的
07:21:05 UTC
js_cocos2dx_GLProgramState_setUniformFloatv意思是 自动脚本绑定 GLProgramState 方法中 不管怎么传参都会失败,
jsb_cocos2dx_auto.cpp 文件 js_cocos2dx_GLProgramState_setUniformFloatv :
if (argc == 3) {
std::string arg0;
ok &= jsval_to_std_string(cx, args.get(0), &arg0);
if (!ok) { ok = }
ssize_t arg1 = 0;
ok &= jsval_to_ssize(cx, args.get(1), &arg1);
if (!ok) { ok = }
const float* arg2 = 0;
#pragma warning NO CONVERSION TO NATIVE FOR float*
问题在这里
if (!ok) { ok = }
cobj-&setUniformFloatv(arg0, arg1, arg2);
args.rval().setUndefined();
} while(0);因为目前不支持将脚本的 float32Array 对象 转成 c++ 的 float*
那我要问 这个方法不能用 我能怎么办?手动改C++代码还是有其他方案等同于使用改方法的?&figure&&img src=&https://pic3.zhimg.com/v2-1b4715bdf49ab47ed12d366635deb696_b.jpg& data-rawwidth=&345& data-rawheight=&605& class=&content_image& width=&345&&&/figure&&p&小说君三年前写了篇面试经验,前几天偶然打开,发现很多地方写的也是不忍直视。&/p&&p&比如,当时小说君的简历上写了一个计划中的、基于golang的「协程特性」、写RPC库的项目。golang老司机可能直接就能看出这个想法的问题——所以三年多过去了,小说君还没写下这个库的第一行代码。&/p&&br&&p&抛开golang这门语言不谈,所谓「协程特性」,其实就是Coroutine。&/p&&br&&p&Coroutine可以说是一个相当久远的概念,wiki这样说:&/p&&blockquote&&p&&em&According to Donald Knuth, the term &strong&coroutine&/strong& was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963.&/em&&/p&&/blockquote&&br&&p&那这个几十年前就有的概念,又是为什么在三四年前开始火起来了呢?&br&&/p&&br&&p&具体原因小说君也不知道,只能在这里列举一下当时(13年中)的一些事实:&/p&&br&&ul&&li&&p&golang刚release 1.1。虽然社区都在喷golang的gc——2013年了居然不分代,但是goroutine模型确实让程序员,接触到了与传统编程(各种借助或不借助闭包的callback机制)不一样的异步模型。各种趋势在证明,除了线程池,我们还有更多非抢占式的并发编程可能性。&/p&&br&&/li&&li&&p&Unity3D借助C#的Generator机制实现的「Coroutine」,适用于不少游戏特定的业务情景。最常见的例子就是定时器,我们不再需要在主循环里检查或是注册callback,现在只要定义成IEnumerator,在中间插入Wait代码,跟写同步逻辑基本一样。&/p&&br&&/li&&li&&p&一些主流语言还没有原生的异步语义支持。除了C#很早就支持了async/await,但是像JS和python这种互联网中流砥柱都是过了一两年才逐渐开始支持。没有async/await,想用同步方式写异步逻辑就只能借助coroutine。&br&&/p&&/li&&/ul&&br&&p&前段时间,小说君刚接触了Lua的coroutine机制,于是有了写这篇入门向的想法。&/p&&p&不过,Coroutine话题的热度已经远不如以前,所以小说君这篇文章就从不同的角度探讨这个问题。&/p&&br&&br&&p&首先,Coroutine是什么?&br&&/p&&br&&p&wiki定义:&/p&&blockquote&&p&&em&Coroutines are computer program components that &strong&generalize subroutines&/strong& for &strong&non-preemptive multitasking&/strong&, by allowing &strong&multiple entry points &/strong&for &strong&suspending&/strong& and &strong&resuming&/strong&execution at certain locations.&/em&&/p&&/blockquote&&br&&p&定义非常简单,小说君标记了几个重点:&/p&&ul&&li&&p&泛化的subroutine。&/p&&/li&&li&&p&多entry point。&/p&&/li&&li&&p&非抢占式多线程。&/p&&/li&&li&&p&suspend(为了习惯,下面都称yield)和resume语义。&/p&&/li&&/ul&&br&&p&下面我们逐条讨论。&/p&&br&&br&&p&&strong&泛化的subroutine&/strong&&/p&&br&&blockquote&&p&Subroutines are special cases of ... coroutines.&/p&&p&
— Donald Knuth.&/p&&/blockquote&&p&比较容易理解:&/p&&p&subroutine是只能受让caller的控制流,return的时候作为callee把控制流交出去。&/p&&p&而coroutine可以在任意时刻把控制流交给其他coroutine。&/p&&br&&br&&p&&strong&多entry point&/strong&&/p&&br&&p&首先,我们要建立一个「context」的概念。&/p&&p&与「environment」不同。每个执行流都有私有的「context」,而「environment」则有可能是由多个closure共享的。&/p&&br&&p&跑个题,操作系统中的「context switch」,切换的就是「context」。&/p&&p&内核做context switch的时候需要还原哪些状态?&/p&&p&——所有能够还原现场的信息,如果不考虑切进程的话,那就至少要有PC,栈帧,寄存器等状态。&/p&&br&&p&我们不论call一个subroutine还是resume一个coroutine,这两种「routine」在获得控制流的时候都有一个「context」。&/p&&p&对于subroutine来说,第一次调用和第N次调用的context是不会产生什么联系的。&/p&&p&对于coroutine来说,因为需要有不同的entry point,所以每次调用的context也都取决于上次调用。&/p&&br&&br&&p&&strong&非抢占式多线程&/strong&&/p&&br&&p&这个也比较容易理解了,协程跟线程不一样,控制流是显式让出的,而不是由调度器来抢占式调度的。&/p&&br&&br&&p&&strong&yield/resume&/strong&&/p&&br&&p&简单地说,就是我们既可以resume,把控制权交给被resume的协程,也可以在一个协程中yield,把控制权交出去。&/p&&br&&br&&p&除此之外,wiki上还对coroutine做了分类:&/p&&ul&&li&&p&非对称式协程,asymmetric coroutine。&/p&&/li&&li&&p&对称式协程,symmetric coroutine。&/p&&/li&&li&&p&半协程,semi-coroutine。&/p&&/li&&/ul&&br&&p&其中前两者的区别仅仅是语义上的。&/p&&p&对称式协程不论yield还是resume都可以把控制权交由任意其它coroutine;&/p&&p&非对称式协程的yield只能把控制权交给当前coroutine的caller(说resumer更合适——resume了当前coroutine)。&/p&&br&&p&当然,显然两者的实现是等价的,而对称式协程理解起来也不如对称式协程直观,因此本文就不再讨论对称式协程。&/p&&br&&p&至于半协程,其实就是受限制的协程实现。各种语言中常见的generator,甚至是有些async/await的实现,都属于半协程。&/p&&p&只要不能做到在某个协程的call hierarchy中随意跳转控制流,理论上都是半协程。&/p&&br&&br&&p&形而上的闲扯就到这里,下面我们结合一个具体的例子先感性认识下Coroutine。&/p&&br&&p&还是老生常谈的rpc,这次换成lua描述:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&k&&function&/span& &span class=&nf&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&n&&arg0&/span&&span class=&p&&,&/span&&span class=&n&&cb&/span&&span class=&p&&)&/span&
&span class=&c1&&-- ...&/span&
&span class=&k&&end&/span&
&/code&&/pre&&/div&&p&有这样一个lua函数定义,函数的实现不再多说,形参中arg0就是RPC调用的参数,cb表示回调。简单说下基本流程:&/p&&ol&&li&&p&调用底层RPC模块的打包函数序列化掉参数和服务、方法ID,发给网络层。&/p&&/li&&li&&p&把这次调用的handle存下来,cb也存下来。&/p&&/li&&li&&p&当远端返回了对应本次RPC的response,就由逻辑线程取出handle,并回调cb。&/p&&/li&&/ol&&br&&p&用起来是这样的效果:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&,&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err0&/span&&span class=&p&&,&/span& &span class=&n&&r0&/span&&span class=&p&&)&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&callback1&&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&p&正是因为语言层面支持closure,我们写异步逻辑很方便。&/p&&p&语言本身可以帮你capture到上下文,一些不需要double check的状态就不用应用层自己去拿了。&/p&&br&&p&按正常文章的套路,接下来会讲callback hell,类似于这样:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name1&&/span&&span class=&p&&,&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err1&/span&&span class=&p&&,&/span& &span class=&n&&r1&/span&&span class=&p&&)&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&callback1&&/span&&span class=&p&&)&/span&
&span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name2&&/span&&span class=&p&&,&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err2&/span&&span class=&p&&,&/span& &span class=&n&&r2&/span&&span class=&p&&)&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&callback2&&/span&&span class=&p&&)&/span&
&span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name3&&/span&&span class=&p&&,&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err3&/span&&span class=&p&&,&/span& &span class=&n&&r3&/span&&span class=&p&&)&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&callback3&&/span&&span class=&p&&)&/span&
&span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name4&&/span&&span class=&p&&,&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err4&/span&&span class=&p&&,&/span& &span class=&n&&r4&/span&&span class=&p&&)&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&callback4&&/span&&span class=&p&&)&/span&
&span class=&c1&&-- todo&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&p&如果每句print之后都有大段流程相关的逻辑,试想下接盘的维护者的绝望。&/p&&br&&p&然后继续套路,我们借助Lua的coroutine机制,瞬间就可以改写成这样:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&n&&_&/span&&span class=&p&&.&/span&&span class=&n&&Fork&/span&&span class=&p&&(&/span&&span class=&k&&function&/span& &span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&begin&&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&err1&/span&&span class=&p&&,&/span& &span class=&n&&r1&/span& &span class=&o&&=&/span& &span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&):&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&resume1 &&/span&&span class=&p&&,&/span& &span class=&n&&err1&/span&&span class=&p&&,&/span& &span class=&n&&r1&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&err2&/span&&span class=&p&&,&/span& &span class=&n&&r2&/span& &span class=&o&&=&/span& &span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&):&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&resume2 &&/span&&span class=&p&&,&/span& &span class=&n&&err2&/span&&span class=&p&&,&/span& &span class=&n&&r2&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&err3&/span&&span class=&p&&,&/span& &span class=&n&&r3&/span& &span class=&o&&=&/span& &span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&):&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&resume3 &&/span&&span class=&p&&,&/span& &span class=&n&&err3&/span&&span class=&p&&,&/span& &span class=&n&&r3&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&err4&/span&&span class=&p&&,&/span& &span class=&n&&r4&/span& &span class=&o&&=&/span& &span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&):&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&resume4 &&/span&&span class=&p&&,&/span& &span class=&n&&err4&/span&&span class=&p&&,&/span& &span class=&n&&r4&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&p&清爽了许多,可以说有一种银弹的感觉了。&/p&&br&&p&当然,这个示例稍微对原生的API做了下包装,不过核心还是用Fork的参数构造一个coroutine:&br&&/p&&p&主逻辑执行 =& resume&/p&&p&异步操作 =& yield =& 主逻辑执行 =& callback =& resume&/p&&p&异步操作 =& yield =& 主逻辑执行 =& callback =& resume&/p&&p&...&/p&&p&一直到coroutine执行完毕为止。&/p&&br&&br&&p&以上就是协程的感性入门,接下来聊聊如何劝退。&br&&/p&&br&&p&首先简化下问题。&/p&&p&我们看一个最简单的coroutine形式,然后围绕着这个case讨论如何用各种方式实现出来。&br&&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&n&&_&/span&&span class=&p&&.&/span&&span class=&n&&Fork&/span&&span class=&p&&(&/span&&span class=&k&&function&/span& &span class=&p&&()&/span&
&span class=&kd&&local&/span& &span class=&n&&t&/span& &span class=&o&&=&/span& &span class=&p&&{}&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&begin&&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&err0&/span&&span class=&p&&,&/span& &span class=&n&&r0&/span& &span class=&o&&=&/span& &span class=&n&&M&/span&&span class=&p&&:&/span&&span class=&n&&RequestTest&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&name&&/span&&span class=&p&&):&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&nb&&print&/span&&span class=&p&&(&/span&&span class=&s2&&&&/span&&span class=&s&&resume &&/span&&span class=&p&&,&/span& &span class=&n&&err0&/span&&span class=&p&&,&/span& &span class=&n&&r0&/span&&span class=&p&&,&/span& &span class=&n&&t&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&p&Fork应该向应用层保证这样几点语义:&/p&&ul&&li&&p&Fork仅需要调用一次。&/p&&/li&&li&&p&调用时会一直同步执行到第一个yield为止,然后Fork返回,上层不会阻塞在Fork调用。&/p&&/li&&li&&p&之后由底层做调度,保证resume回来的时候仍然由主线程继续执行yield后面的语句,直到遇到下个yield为止。&/p&&/li&&li&&p&循环往复,直到作为Fork参数的coroutine执行完毕。&/p&&/li&&/ul&&br&&p&这个例子对真实环境的协程管理器做了相当程度的简化。&/p&&p&比如示例中只出现了一个协程,我们不需要再考虑多协程的调度机制、协程间通信机制。&/p&&p&只用考虑这样一个简单的rpc异步调用例子。&/p&&br&&p&再次感慨,这种异步模型确实是相当优雅——写起来是同步函数,实际上在第四行和第五行之间是异步的。&br&&/p&&br&&p&简单分析下:&/p&&ul&&li&&p&整个函数,被中间的一句Yield分割成了两个block。&/p&&/li&&li&&p&协程第一次resume的时候,同步执行block1。&/p&&/li&&li&&p&第二次resume的时候,同步执行block2。&/p&&/li&&/ul&&br&&p&为了有更感性的认识,小说君画了个简单的流程图:&/p&&br&&p&协程本身(就是Fork的参数)有自己的上下文,可以在任意时间点把控制流让出(示例中,只会让出给Caller),然后可以在任意时间点接受控制流(示例中,是由Caller把控制流交回给协程)。&/p&&br&&p&下面看下具体的函数实现,Yield的逻辑很简单,就是包装了下coroutine的API:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&k&&function&/span& &span class=&nf&&RPCTask&/span&&span class=&p&&:&/span&&span class=&n&&Yield&/span&&span class=&p&&()&/span&
&span class=&k&&return&/span& &span class=&nb&&coroutine.yield&/span&&span class=&p&&(&/span&&span class=&n&&self&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&
&/code&&/pre&&/div&&p&yield self表达的意思是,yield的同时把self作为参数抛出去。&/p&&br&&p&Fork利用的是现成的callback机制,再做些处理:&/p&&div class=&highlight&&&pre&&code class=&language-lua&&&span&&/span&&span class=&kd&&local&/span& &span class=&k&&function&/span& &span class=&nf&&Inner&/span&&span class=&p&&(&/span&&span class=&n&&co&/span&&span class=&p&&,&/span& &span class=&n&&ret&/span&&span class=&p&&,&/span& &span class=&n&&task&/span&&span class=&p&&)&/span&
&span class=&k&&if&/span& &span class=&ow&&not&/span& &span class=&n&&ret&/span& &span class=&ow&&or&/span& &span class=&ow&&not&/span& &span class=&n&&task&/span& &span class=&k&&then&/span&
&span class=&k&&return&/span&
&span class=&k&&end&/span&
&span class=&n&&task&/span&&span class=&p&&.&/span&&span class=&n&&Callback&/span& &span class=&o&&=&/span& &span class=&k&&function&/span& &span class=&p&&(&/span&&span class=&n&&err&/span&&span class=&p&&,&/span& &span class=&o&&...&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&ret&/span&&span class=&p&&,&/span& &span class=&n&&task0&/span& &span class=&o&&=&/span& &span class=&nb&&coroutine.resume&/span&&span class=&p&&(&/span&&span class=&n&&co&/span&&span class=&p&&,&/span& &span class=&n&&err&/span&&span class=&p&&,&/span& &span class=&o&&...&/span&&span class=&p&&)&/span&
&span class=&n&&Inner&/span&&span class=&p&&(&/span&&span class=&n&&co&/span&&span class=&p&&,&/span& &span class=&n&&ret&/span&&span class=&p&&,&/span& &span class=&n&&task0&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&
&span class=&k&&end&/span&
&span class=&k&&function&/span& &span class=&nc&&M&/span&&span class=&p&&.&/span&&span class=&nf&&Fork&/span&&span class=&p&&(&/span&&span class=&n&&f&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&co&/span& &span class=&o&&=&/span& &span class=&nb&&coroutine.create&/span&&span class=&p&&(&/span&&span class=&n&&f&/span&&span class=&p&&)&/span&
&span class=&kd&&local&/span& &span class=&n&&ret&/span&&span class=&p&&,&/span& &span class=&n&&task&/span& &span class=&o&&=&/span& &span class=&nb&&coroutine.resume&/span&&span class=&p&&(&/span&&span class=&n&&co&/span&&span class=&p&&)&/span&
&span class=&n&&Inner&/span&&span class=&p&&(&/span&&span class=&n&&co&/span&&span class=&p&&,&/span& &span class=&n&&ret&/span&&span class=&p&&,&/span& &span class=&n&&task&/span&&span class=&p&&)&/span&
&span class=&k&&end&/span&
&/code&&/pre&&/div&&p&串起来简单解释一下:&/p&&ol&&li&&p&Fork先用f构造一个Lua的coroutine,然后resume。&/p&&/li&&li&&p&Yield把每次异步操作的handle yield出去,再把控制流转回Fork。&/p&&/li&&li&&p&这时,为了不阻塞Fork的caller,Fork需要直接返回掉,同时在异步操作的handle上挂一个callback,以供callback被调用时,继续resume,把控制流转回coroutine。&/p&&/li&&li&&p&递归,直到coroutine无法再yield出值,表示coroutine结束。&/p&&/li&&/ol&&br&&br&&p&Lua的coroutine语言机制非常强大,实现这个简单的示例没什么难度。&/p&&p&但是很多时候,我们手头的语言可能并没有这么强大的表达能力。&/p&&br&&p&那在讨论如何在其他语言中实现协程之前,先考虑一个问题:&/p&&br&&p&协程的核心特点是什么?&/p&&br&&p&其实就两个:&/p&&ul&&li&&p&可以跳转控制流。&/p&&/li&&li&&p&可以还原执行状态。&/p&&/li&&/ul&&br&&p&跳转控制流的做法很多,在常规语言中,只要routine能return,以及外部能多次进入同一个routine,再借助一些内部状态判断每次进入时的执行点,就能模拟出控制流跳转。&/p&&p&但是还原执行状态的要求就高了,一定需要语言层面的支持。大致分为以下三类:&/p&&ul&&li&&p&C/C++这种系统级语言,可以用setjmp/longjmp,ucontext。前者能还原PC、栈相关的一些指针,后者还能在前者基础上还原栈帧。&/p&&/li&&li&&p&支持generator/async/await语义的静态语言,会在编译期把打了标记的流程中的栈变量lift到堆上(比如C#会维护在某个匿名类实例中)。那所谓还原执行状态,就是取到实例,拿出状态。&/p&&/li&&li&&p&最后就是原生支持coroutine或continuation的语言,那自然会在runtime层面支持上下文还原机制了。&/p&&/li&&/ul&&br&&p&那接下来,我们就看看如果有这三类语言构件,我们分别应该怎么样实现前面示例中的rpc同步编程形式。&/p&&br&&br&&p&先看一些系统编程语言中能用的手段。&/p&&p&像汇编这种能直接操作栈帧和寄存器的就不说了,要实现肯定是能实现的,不过并没有什么意义。&/p&&br&&p&由于小说君也挺久没用C了,所以这小节就以伪码为主,各位意会即可,代码别说能正确表现结果了,小说君甚至不能保证可以编译通过。&/p&&br&&p&首先看setjmp/longjmp。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int value);
&/code&&/pre&&/div&&p&C标准库API,属于一种non-local exit。大家应该很熟悉了。&/p&&br&&p&jmp_buf是一个平台特定的结构,在setjmp的时候会把上下文保存下来。longjmp的时候还原上下文,直接在setjmp的下条指令返回。&/p&&p&setjmp的首次调用返回值是0,longjmp跳转时,setjmp的返回值是传给longjmp的value。&/p&&br&&p&代码写起来差不多就是这种感觉:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void fork(func *f){
if (setjmp(main_env) == 0){
void routine(){
int t = 0;
printf(&begin&);
struct rpc_task *task = request_test(&name&);
int handle = yield(task);
// 根据handle拿到结果
printf(&resume&);
struct rpc_task *
request_test(const char *name){
struct rpc_task *task = __invoke(/*...*/);
void yield(struct rpc_task *task){
task-&cb = request_test_
if (setjmp(task_env) == 0){
longjmp(main_env, 1);
void request_test_cb(struct rpc_task *task, int errId, int ret){
task-&errId = errId;
task-&ret =
longjmp(task-&env, task-&handle);
&/code&&/pre&&/div&&p&不过,setjmp/longjmp由于只有控制流跳转的能力——毕竟大部分使用情景还是用来实现try-catch-finally。&/p&&p&虽然可以还原PC和栈指针,但是无法还原栈帧,因此会出现很多问题。&/p&&br&&p&比如longjmp的时候,setjmp的作用域已经退出,当时的栈帧已经销毁。这时就会出现未定义行为。&/p&&br&&p&假设有这样一个调用链:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func0() -& func1() -& ... -& funcN()
&/code&&/pre&&/div&&p&只有在funci中setjmp,在func(i+k)中longjmp的情况下,程序的行为才是可预期的。&/p&&br&&p&也是因为这个原因,我们上面的代码示例中,fork的实现有一个非常强的限制,那就是外层一定要有一个不会退出的caller,帮fork以及fork的所有call hierarchy维持栈帧。&/p&&p&维持栈帧两个方法:&/p&&ul&&li&&p&caller用alloca来预留栈空间,避免已经退出的subroutine的栈帧被后面的subroutine破坏。&br&&/p&&/li&&li&&p&对运行体系结构做一定假设后,做栈拷贝。&/p&&/li&&/ul&&br&&p&但是两种方法都属于奇技淫巧,要么是限制大,要么是可移植性差,那我们不如借助一些平台特定的、更强大的库,比如ucontext,来实现需求。&/p&&br&&p&ucontext和setjmp/longjmp非常像,有两个API是这样的:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void makecontext(ucontext_t *ucp, void *func(), int argc, ...);
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
&/code&&/pre&&/div&&p&ucontext_t是比jmp_buf更完整的上下文状态,维护了私有栈。&/p&&p&makecontext可以初始化一个入口为func的ucontext。&/p&&p&swapcontext的语义更直接了,可以原子地把当前的执行上下文保存在oucp中,并且切换到ucp的上下文。&/p&&br&&p&所以,上面的例子可以改造成这样:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&ucontext_ucontext_void fork(func *f){
char stack[1024];
//指定栈空间
task.uc_stack.ss_sp =
task.uc_stack.ss_size = sizeof(stack);
task.uc_stack.ss_flags = 0;
//设置后继上下文
task.uc_link = &
//构造task的context
makecontext(&task,(void (*)(void))f,0);
//切换到task上下文,保存当前上下文到main
swapcontext(&main, &task);
//如果设置了后继上下文,f函数执行完后会返回此处
puts(&fork&);
void routine(){
int t = 0;
printf(&begin&);
struct rpc_task *task = request_test(&name&);
yield(task);
// 根据task拿到结果
printf(&resume&);
struct rpc_task *
request_test(const char *name){
struct rpc_task *task = __invoke(/*...*/);
void yield(struct rpc_task *task){
task-&cb = request_test_
swapcontext(task-&ctx, &main);
void request_test_cb(struct rpc_task *task, int errId, int ret){
task-&errId = errId;
task-&ret =
swapcontext(&main, task-&ctx);
&/code&&/pre&&/div&&p&整体上跟用setjmp/longjmp很像,但是不需要自己处理栈帧的问题。&/p&&br&&p&再次声明,这只是纯示例用的伪代码,不保证正确性。&/p&&p&github上用ucontext实现的生产级coroutine库有很多,核心原理都是借助swapcontext切换上下文,外部再做一些栈分配相关的优化,不会像示例代码这样直接fork的时候栈上分配一个固定大小的协程栈,有兴趣的同学可以搜索下研究研究。&/p&&br&&br&&p&接下来看generator。&/p&&br&&p&Generator是一种相当常见的语言构件。&/p&&p&python里就是generator,C#里就是IEnumerator。&/p&&p&由于小说君对C#比较熟悉,这一节就以C#为例了。&/p&&br&&p&C#如此定义Generator:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&IEnumerator Routine(int n)
for(var i = 0; i & i++)
void Test()
foreach (var r in Routine(10))
Console.Writeline(r);
&/code&&/pre&&/div&&p&我们可以总结下Generator最典型的特征:&/p&&ul&&li&&p&有yield语义,可以把控制流抛给caller。&/p&&/li&&li&&p&yield可以把值传出去,但是caller交回控制流时没办法带回来值。&/p&&/li&&/ul&&br&&p&不过,如果有一个全局的dispatcher,我们还是能模拟出来标准协程的语义的。比如unity中的协程。&/p&&br&&p&常用姿势大概是这样:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&IEnumerator Routine()
DoSomething();
yield return new WaitForSeconds(1f);
DoSomething();
void Foo()
StartCoroutine(Routine());
&/code&&/pre&&/div&&p&StartCoroutine的逻辑大概就是这样:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void StartCoroutine(IEnumerator routine)
if (!routine.MoveNext())
var obj = routine.C
if (obj is WaitForSeconds)
&/code&&/pre&&/div&&p&在注释处,会利用现有的定时器机制,注册定时器,并在回调里再继续调用routine。&/p&&p&这样,Routine的逻辑就相当连贯了,先做一些事情,再异步等1秒,再继续做一些事情。&/p&&br&&p&那我们再回过头看需求的效果。由于C#中不支持匿名定义generator,所以只能这样写:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&_.Fork(Routine());
&/code&&/pre&&/div&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&IEnumerator Routine()
int t = 0;
Console.Writeline(&begin&);
var task = M.RequestTest(&name&);
Console.Writeline($&resume {task.Err} {task.R0} {t}&);
&/code&&/pre&&/div&&p&由于yield表达式并没有返回值,所以看着有点别扭。&/p&&br&&p&然后是Fork的流程:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void Fork(IEnumerator routine)
if (!routine.MoveNext())
var obj = routine.Current as RPCT
if (obj != null)
obj.Callback = r =&
Fork(routine);
&/code&&/pre&&/div&&p&也是伪代码,实现得比较粗糙。&/p&&p&基本思路跟上面演示的简单版StartCoroutine是一样的,借助底层RPC的callback机制,异步驱动routine。&/p&&br&&p&接下来,我们看看generator是如何有还原现场的能力的。&/p&&br&&p&如前所述,C# compiler会把这个generator中的栈上变量给lift,存在一个匿名类对象中。&/p&&p&看下compiler处理过后的代码,大概转成了这个样子:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&class RoutineClass
private RPCT
private int state = 0;
public object MoveNext()
if (state == 0)
Console.WriteLine(&begin&);
task = RequestTest(&name&);
else if (state == 1)
Console.WriteLine($&resume {task.Err} {task.R0} {t}&);
throw new Exception();
&/code&&/pre&&/div&&p&编译器用了一些犯规的手段,为每个generator定义了匿名类,把栈上变量全部定义为该类的成员。&/p&&p&然后利用switch-case实现多次进入,entry-point不同的效果,相当巧妙。&/p&&br&&p&所以,我们才能在应用层利用这个特性,以及语言本身的GC机制,&/p&&p&一是实现了上下文的还原。变量lift。&/p&&p&二是模拟出了resume语义时的值传递。caller拿到task,同时把task和routine缓存下来,等到IO有返回,把值放到task中,然后回调routine。&/p&&br&&br&&p&最后我们看看表达能力更强的continuation。&/p&&br&&p&continuation是一个函数,表示程序某一瞬间的状态。如果apply了这个函数,会在apply的同时,进入到continuation表示的程序状态。&/p&&br&&p&以免有同学对接下来的内容一头雾水,小说君先举个简单的例子:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(display
(call-with-current-continuation
(lambda (kont)
(kont 5))))
&/code&&/pre&&/div&&p&简单地说,就是调用call-with-current-continuation(简称call/cc)时,会保存当前上下文。&/p&&p&然后把这个上下文作为一个参数(kont),传递给call/cc调用的routine。&/p&&p&在routine中,只要apply了这个kont,会做三件事情:&/p&&ul&&li&&p&把控制流抛回这个kont所匹配的调用call/cc的地方。&/p&&/li&&li&&p&把apply的参数yield给匹配的call/cc表达式本身。&/p&&/li&&li&&p&还原整个执行上下文。&/p&&/li&&/ul&&br&&p&很显然,这跟协程的语义非常像,因此我们可以很容易地实现最开始的示例需求。&/p&&br&&p&而且,我们能支持的feature还比之前用generator模拟的协程更多——借助continuation,既可以把值yield给caller,也可以在caller中resume值给routine。&/p&&br&&p&还是从简单的开始,先看看一个异步rpc做的事情:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(define request-test
(lambda (name) 'tmp-task))
&/code&&/pre&&/div&&p&定义了request-test,仅仅是返回个task。&/p&&br&&p&然后callback部分也做了简化,默认是设置callback的时候直接就当作是IO层有返回,直接设置了结果。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(define set-callback
(lambda (task cb) (cb &result&)))
&/code&&/pre&&/div&&p&然后我们预期可以这样写协程:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(define my-routine
(coroutine (yield)
(let ((tmp 1))
(display &begin&)
(let ((retval (yield (request-test &name&))))
(display &resume&)
(display tmp)
(display retval)
(let ((retval2 (yield (request-test &name&))))
(display &resume2&)
(display retval2))))))
&/code&&/pre&&/div&&p&其实就是最开始的lua例子的翻译版。&/p&&br&&p&那看到这个期望的形式,其实我们对coroutine实际是个什么也就猜的八九不离十了。&/p&&p&如果把这个my-routine当作一个函数,那么调用开始的时候就需要借助call/cc做个bookmark(称作yield-to)。&/p&&p&然后执行my-routine,直到中间调用到yield,首先要再借助call/cc做一次bookmark(称作resume-to)。&/p&&p&yield出去之后,外部需要拿到resume-to对应的continuation,这样才能在需要resume的时候还原上下文,把控制流再交回coroutine。&/p&&p&与此同时,外部在resume的时候还要再借助call/cc做一次bookmark,因为之后coroutine再做yield,需要yield-to的现场已经不是最开始的yield-to。&/p&&br&&p&这样一梳理,coroutine宏的定义就有了:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(define-syntax coroutine
(syntax-rules ()
(coroutine (yieldfunc) body)
(yield-to #f)
(lambda ()
(call-with-current-continuation
(lambda (tmp-yield-to)
(set! yield-to tmp-yield-to)
(yieldfunc
(lambda (x)
(call-with-current-continuation
(lambda (resume)
(yield-to (
lambda(resume-val)
(call-with-current-continuation
(lambda (tmp-resume-yield-to)
(set! yield-to tmp-resume-yield-to)
(resume (cons #t resume-val))))))
(yield-to (cons #f 'finished))))))))))
&/code&&/pre&&/div&&p&如前所述,yield-to现在是一个自由变量,coroutine在执行过程中会不断修改。&/p&&p&yieldfunc就是我们用宏模拟的yield关键字,实际上就是个函数。&/p&&p&小说君在这里实现的比较偷懒,简化了流程——外部resume coroutine,等coroutine yield的时候,可以拿到一个函数,用来做后续的resume。&/p&&br&&p&我们在实现scheme版的fork之前,先看看这个coroutine怎么调用。&br&&/p&&p&第一次的resume是这样:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(let ((ret (my-routine)))
(resume (car ret))
(task (cdr ret)))
(set-callback
(lambda (val)
(resume val)))))
&/code&&/pre&&/div&&p&callback里面直接resume,前面说过了,为了简化问题,会在set-callback的时候直接调用callback。&/p&&p&所以这里会直接通过调用resume再把控制流交给coroutine,同时传一个值进去。&/p&&br&&p&那有了之前Lua版的Fork实现,用Scheme改写也很容易,核心是一个递归:&/p&&div class=&highlight&&&pre&&code class=&language-racket&&&span&&/span&&span class=&p&&(&/span&&span class=&k&&define&/span& &span class=&n&&fork&/span&
&span class=&p&&(&/span&&span class=&k&&lambda&/span& &span class=&p&&(&/span&&span class=&n&&routine&/span&&span class=&p&&)&/span&
&span class=&p&&(&/span&&span class=&k&&letrec&/span& &span class=&p&&(&/span&
&span class=&p&&(&/span&&span class=&k&&inner&/span& &span class=&p&&(&/span&
&span class=&k&&lambda&/span& &span class=&p&&(&/span&&span class=&n&&resume&/span& &span class=&n&&task&/span&&span class=&p&&)&/span&
&span class=&p&&(&/span&&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&nb&&procedure?&/span& &span class=&n&&resume&/span&&span class=&p&&)&/span&
&span class=&p&&(&/span&&span class=&n&&set-callback&/span&
&span class=&n&&task&/span&
&span class=&p&&(&/span&&span class=&k&&lambda&/span& &span class=&p&&(&/span&&span class=&n&&val&/span&&span class=&p&&)&/span&
&span class=&p&&(&/span&&span class=&k&&let&/span& &span class=&p&&((&/span&&span class=&n&&resume-ret&/span& &span class=&p&&(&/span&&span class=&n&&resume&/span& &span class=&n&&val&/span&&span class=&p&&)))&/span&
&span class=&p&&(&/span&&span class=&k&&inner&/span& &span class=&p&&(&/span&&span class=&nb&&car&/span& &span class=&n&&resume-ret&/span&&span class=&p&&)&/span& &span class=&p&&(&/span&&span class=&nb&&cdr&/span& &span class=&n&&resume-ret&/span&&span class=&p&&)))))&/span&
&span class=&o&&'&/span&&span class=&ss&&finished&/span&&span class=&p&&))))&/span&
&span class=&p&&(&/span&&span class=&k&&let&/span& &span class=&p&&((&/span&&span class=&n&&ret&/span& &span class=&p&&(&/span&&span class=&n&&routine&/span&&span class=&p&&)))&/span&
&span class=&p&&(&/span&&span class=&k&&let&/span& &span class=&p&&(&/span&
&span class=&p&&(&/span&&span class=&n&&resume&/span& &span class=&p&&(&/span&&span class=&nb&&car&/span& &span class=&n&&ret&/span&&span class=&p&&))&/span&
&span class=&p&&(&/span&&span class=&n&&task&/span& &span class=&p&&(&/span&&span class=&nb&&cdr&/span& &span class=&n&&ret&/span&&span class=&p&&)))&/span&
&span class=&p&&(&/span&&span class=&k&&inner&/span& &span class=&n&&resume&/span& &span class=&n&&task&/span&&span class=&p&&))))))&/span&
&/code&&/pre&&/div&&br&&p&最后写起来跟Lua版本的是一致的,这里就不再就地展开my-routine了:&/p&&div class=&highlight&&&pre&&code class=&language-racket&&&span&&/span&&span class=&p&&(&/span&&span class=&n&&fork&/span& &span class=&n&&my-routine&/span&&span class=&p&&)&/span&
&/code&&/pre&&/div&&br&&br&&p&coroutine的话题就到这里,当然,coroutine并不是异步编程的全部,还有其他成熟的异步模型,以及并发模型。&/p&&p&比如actor,比如promise,比如CSP,比如Rx等等。各种模型都有各自的适应业务情景,比如文章中举的最简单的同步写RPC调用的例子,可能coroutine模型就是最适合的。&/p&&br&&br&&p&专栏好久没发文章,主要是太懒了没有每次把订阅号的文章搬运过来,有兴趣的同学可以关注下订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。&/p&&figure&&img data-rawheight=&344& data-rawwidth=&344& src=&https://pic4.zhimg.com/d1ac14f4ae84ddf3ab02aff_b.jpg& class=&content_image& width=&344&&&/figure&&p&&a href=&https://link.zhihu.com/?target=http%3A//mp.weixin.qq.com/mp/qrcode%3Fscene%3Dsize%3D102%26__biz%3DMzIwNDU2MTI4NQ%3D%3D& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&看不到二维码点这里&/a&&/p&
小说君三年前写了篇面试经验,前几天偶然打开,发现很多地方写的也是不忍直视。比如,当时小说君的简历上写了一个计划中的、基于golang的「协程特性」、写RPC库的项目。golang老司机可能直接就能看出这个想法的问题——所以三年多过去了,小说君还没写下这…
&p&(本文根据我博客上的文章&a href=&https://link.zhihu.com/?target=http%3A//www.klayge.org/%3Fp%3D3560& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&跨平台shader编译的过去、现在和未来&/a&改写。知乎编辑器不支持多个级别的标题,以至于我只能用加粗不加粗来区分了。。。)&br&&/p&&p&很多跨平台游戏引擎都有统一shader的需求。比如KlayGE从建立伊始,就强调一份代码跨多个平台,shader代码也不例外。如果需要对不同平台都分别写一遍shader,那样的工作量和可维护性都很糟糕。&/p&&p&既然有这样的需求,就必然会在技术上遇到一个问题,如何把一份代码编译成不同API上的shader。从目前的API上,我们至少需要应对HLSL/GLSL/ESSL,以后还有Vulkan加入战团。这里就打算探讨一下跨平台shader编译的情况,希望对大家有启发意义。&/p&&h2&&b&过去&/b&&/h2&&p&刚有shader高级语言的时候,Cg是几乎唯一的shader语言。后来才在D3D9时代衍生出了HLSL,再往后有了GLSL和ESSL。所以自然而然一开始都会从Cg入手。在KlayGE发展的过程中,这还分为两个阶段。&/p&&h3&运行中使用Cg&/h3&&p&因为早期的Cg和HLSL几乎一样,我的做法是用HLSL写shader,在D3D上用HLSL编译器编译,OpenGL上用Cg编译器编译。遇到Cg不支持的个别语法,就用#ifdef隔开。一开始这个做法工作得还不错,得益于Cg的跨平台,Windows和Linux都能用全套Cg
runtime来跑。不管是编译还是设置状态还是渲染,都通过Cg来实现。这样的系统工作流如下。&/p&&p&&figure&&img src=&https://pic4.zhimg.com/v2-f8dc3dd64d9a2d63a62b99_b.jpg& data-rawwidth=&694& data-rawheight=&639& class=&origin_image zh-lightbox-thumb& width=&694& data-original=&https://pic4.zhimg.com/v2-f8dc3dd64d9a2d63a62b99_r.jpg&&&/figure& 图中模糊边缘的组件表示比较不可靠的组件。比如有性能问题,或缺乏社区支持,或已经停止开发。后同。&/p&&p&这个做法的缺点也很明显。&/p&&ul&&li&&b&问题1&/b&,不支持AMD和Intel的显卡。当时Cg对OpenGL的支持是通过把Cg编译成GL的asm来实现的。而这个过程直接用到了NV的扩展,在其他厂商的卡上跑不了。&/li&&li&&b&问题2&/b&,CPU端的性能损失。即便在NV的卡上,用Cg runtime也会带来一定得性能损失。虽然随着Cg的升级,这个损失在一点点减少。但比起直接用API的,总是慢一截。&/li&&li&&b&问题3&/b&,发展速度。到了D3D10时代,HLSL和GLSL都支持GS,但Cg对其的支持慢了一步,而且仍然严重依赖于NV扩展。再往后的D3D11时代,干脆Cg就进入了龟速发展阶段。&/li&&/ul&&h3&离线使用Cg&/h3&&p&既然用全套Cg
runtime有诸多问题,那么能不能把Cg编译器离线使用呢?在某个版本的Cg里,加了glslv这样的profile,可以把Cg代码编译成GLSL。那么就可以在载入Cg代码后,立刻编译成GLSL保存下来。之后运行过程中完全不需要跟Cg打交道了。这么做解决了之前的问题1和2。因为用的都是GLSL和OpenGL,其他厂商的卡也能跑,因为Cg
runtime造成的性能损失也不存在了。改进后的系统工作流如下。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-df7aad4c025cdb4dca02_b.jpg& data-rawwidth=&712& data-rawheight=&520& class=&origin_image zh-lightbox-thumb& width=&712& data-original=&https://pic3.zhimg.com/v2-df7aad4c025cdb4dca02_r.jpg&&&/figure&&p&但是,问题3仍然存在。即便是编译成GLSL,还是需要受到Cg编译器能力的制约。没法跟上API的发展速度,结果只能用很保守的shader,再用#ifdef等方法仔细隔开。这也是之前很长一段时间,KlayGE里的OpenGL插件在功能上比D3D插件有所欠缺的原因。&/p&&p&另外又多了一个新问题:&/p&&ul&&li&&b&问题4&/b&,兼容性。Cg编译出来的GLSL,虽然说是“按照标准”,但在OpenGL世界,你还得多问一句“按照哪家的标准”。当然,既然Cg是NV的,就肯定是遵照NV的标准。那样的GLSL,还得自己做一遍解析,改掉一些地方才能用于AMD和Intel的卡。甚至有些内置的attribute,生成的时候多了个下划线前缀,也得自己修。&/li&&/ul&&p&正因为有这些问题,大概在2011年的时候,我才会有放弃Cg,自己搞一套编译或转换系统的想法,从HLSL连到GLSL/ESSL。&/p&&h2&&b&现在&/b&&/h2&&p&碰巧在打算放弃Cg的时候,看到了mesa的一个神奇commit,&a href=&https://link.zhihu.com/?target=https%3A//cgit.freedesktop.org/mesa/mesa/commit/%3Fid%3D92617aeaccbb& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&d3d1x for linux&/a&。这个commit是再给mesa增加D3D10/11的原生支持。虽然这个项目最后挂了,但给人们留下了一个叫d3d1xshader的库。这是第一个非官方的完整DX
shader字节码(DXBC)解析的库。用它可以很容易做出来一个反汇编工具。以这个为基础,我们可以实现我设想的转换系统。&/p&&h3&DXBC2GLSL&/h3&&p&经过KlayGE团队成员林胜华(我不知道在不在知乎)的努力,一个称为DXBC2GLSL的库出现了。它可以解析DXBC,生成对应的GLSL和ESSL。但需要特别注意的是,因为官方的D3D
shader编译器是个dll,只能在Windows上跑。在Mac和Linux上需要wine的帮助。这一部分是由&a href=&https://www.zhihu.com/people/96ebf60da4f1295abab79cce388e85c0& data-hash=&96ebf60da4f1295abab79cce388e85c0& class=&member_mention& data-hovercard=&p$b$96ebf60da4f1295abab79cce388e85c0& data-editable=&true& data-title=&@钱康来&&@钱康来&/a& 完成的,用winegcc编译一个dll的wrapper
exe,之后再通过wine调用那个exe,达到调用dll里的函数的能力。 这个把它当做一个新问题吧。&/p&&ul&&li&&b&问题5&/b&,在非Windows平台需要通过wine来执行HLSL编译。&/li&&/ul&&p&有了这么个系统,跨平台shader编译工作流就变成了这样。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-8b56fa94f0f4ed6a798e8e_b.jpg& data-rawwidth=&850& data-rawheight=&702& class=&origin_image zh-lightbox-thumb& width=&850& data-original=&https://pic1.zhimg.com/v2-8b56fa94f0f4ed6a798e8e_r.jpg&&&/figure&&p&在DXBC2GLSL的基础上,我又增加了Hull shader和Domain shader的支持,所以OpenGL/OpenGLES插件在功能上已经基本赶上了D3D11的。虽说前面的4个问题都解决了。但还是引入了一个新的性能问题。&/p&&ul&&li&&b&问题6&/b&,生成质量/GPU端的性能损失。可以认为这里的GLSL是通过DXBC反编译得到的,质量并不是特别高。所以GPU端的运行效率也会下降。虽然说常规解决方案是用&a class=& wrap external& href=&https://link.zhihu.com/?target=https%3A//github.com/aras-p/glsl-optimizer& target=&_blank& rel=&nofollow noreferrer&&glsl-optimizer&/a&对生成的GLSL做进一步优化,但这个任务因为时间关系目前还没去做。&/li&&/ul&&p&同时期还有个功能相同的库,叫HLSLcc。也是从d3d1xshader发展而来。&/p&&h2&&b&未来&/b&&/h2&&p&说到底,造成这些问题的原因在于没有一个通用并开放的中间格式。否则用HLSL的前端,接到不同的后端生成不同的目标代码就解决了。UE4的做法是自己写了一个HLSL解析。这么做可以避免问题5,但因为HLSL经常会变,作为个人项目的KlayGE如果这么做会花很多时间在兼容性上。&/p&&p&这时候,Vulkan来了。不但有新API,还带来了一个新的shader中间格式,SPIR-V。而我认为这正是通往统一的跨平台shader编译路上最更要的一级台阶。&/p&&h3&SPIR-V&/h3&&p&SPIR-V和DXBC类似,都是一个shader的二进制中间语言。但比起DXBC,SPIR-V的标准是开放的,有相应的生态系统,解析起来容易得多。根据之前的工作流,我们可以设想一下用到了SPIR-V的话,未来的新工作流会是什么样子。&/p&&p&&figure&&img src=&https://pic1.zhimg.com/v2-ffbe264be65a42b3fe054b_b.jpg& data-rawwidth=&780& data-rawheight=&684& class=&origin_image zh-lightbox-thumb& width=&780& data-original=&https://pic1.zhimg.com/v2-ffbe264be65a42b3fe054b_r.jpg&&&/figure& 看起来在结构上,比以前几个版本都要简练高效得多,所有不可靠的组件都已经去掉。问题5直接消失了。又因为很多新的OpenGL驱动实际上可以直接输入SPIR-V,老驱动上可以用官方的GLSL/ESSL生成器,所以问题6也消失了。&/p&&p&这个图景就好象一个巨大的拼图,但目前为止,我们还缺了几块没介绍。需要把它们凑齐。&/p&&h4&HLSL-&SPIR-V编译器&/h4&&p&这件事情,已经有Khronos的人在做了&a class=& wrap external& href=&https://link.zhihu.com/?target=https%3A//github.com/KhronosGroup/glslang/issues/362& target=&_blank& rel=&nofollow noreferrer&&Complete HLSL -& SPIR-V translator · Issue #362 · KhronosGroup/glslang&/a&,但终归不是最好的方法,进度和发展速度都缺乏管理。而因为现在微软开源的HLSL编译器&a href=&https://link.zhihu.com/?target=https%3A//github.com/Microsoft/DirectXShaderCompiler& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Microsoft/DirectXShaderCompiler&/a&已经发布,我们可以在那基础上做一个SPIR-V的后端,解决HLSL编译的问题。&/p&&h4&SPIR-V-&GLSL/ESSL&/h4&&p&这有&a class=& wrap external& href=&https://link.zhihu.com/?target=https%3A//github.com/KhronosGroup/SPIRV-Cross& target=&_blank& rel=&nofollow noreferrer&&KhronosGroup/SPIRV-Cross&/a&,是一个已经解决的问题。&/p&&h3&&b&还有什么可能&/b&&/h3&&p&除了前面提到的工作流,我们其实还有一些可能的选项。&/p&&h4&DXBC-&SPIR-V&/h4&&p&除了前面提到的从HLSL直接编译成SPIR-V,其实还可以从DXBC生成SPIR-V。这个难度比直接搞编译器小得多,但很可能做完之后发现HLSL-&SPIR-V已经完善了。&/p&&h4&SPIR-V-&DXBC/DXIL&/h4&&p&这个就更彻底了,连D3D上都用HLSL-&SPIR-V,然后从SPIR-V转成D3D的DXBC或未来的DXIL。但这么做好处实在有限,并且损失了DXBC/DXIL的高效。换句话说,并没有什么原先不能解决的通过这样解决了,也没有什么原先能解决的通过这样解决得更好。&/p&&h4&用GLSL/ESSL&/h4&&p&这样就能都编译到SPIR-V,然后转成DXBC/DXIL。但问题和前面的一样,好处有限。&/p&&h4&Vulkan取代OpenGL/OpenGLES&/h4&&p&在未来某个时候,Vulkan一定会取代OpenGL和OpenGLES。到那时候,跨平台shader编译变得更简单了。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-65b5fb52cf_b.jpg& data-rawwidth=&396& data-rawheight=&504& class=&content_image& width=&396&&&/figure&&h2&&b&总结&/b&&/h2&&p&本文整理了一下一个典型的跨平台shader编译需要需要走过的路程。到现在为止,我们其实离最终目标非常近了。在以后的KlayGE中,我希望能一步步补全这个拼图,让现有的shader编译系统更完善更高效。&/p&
(本文根据我博客上的文章改写。知乎编辑器不支持多个级别的标题,以至于我只能用加粗不加粗来区分了。。。) 很多跨平台游戏引擎都有统一shader的需求。比如KlayGE从建立伊始,就强调一份代码跨多个平台,shader代码也…
&figure&&img src=&https://pic1.zhimg.com/v2-abf21ac2e3c_b.jpg& data-rawwidth=&660& data-rawheight=&430& class=&origin_image zh-lightbox-thumb& width=&660& data-original=&https://pic1.zhimg.com/v2-abf21ac2e3c_r.jpg&&&/figure&&h1&引言&br&&/h1&&p&如果让你来制作一款3D游戏引擎,你会怎么设计其结构?&/p&&p&尽管游戏的类型有很多种,市面上也有众多的3D游戏引擎,但绝大部分游戏引擎都得解决一个基本问题:抽象模拟一个3D游戏世界。根据基本的图形学知识,我们知道,为了展示这个世界,我们需要一个个带着“变换”的“游戏对象”,接着让它们父子嵌套以表现更复杂的结构。本质上,其他的物理模拟,游戏逻辑等功能组件,最终目的也只是为了操作这些“游戏对象”。 &br&这件事,在Unity那里就直接成了“GameObject”和“Component”;在Cocos2dx那里是一个个的“CCNode”,操纵部分直接内嵌在了CCNode里面;在Medusa里是一个个“INode”和“IComponent”。 &br&那么在UE4的眼中,它是怎么看待游戏的3D世界的?&/p&&br&&h2&创世记&/h2&&h1&&blockquote&&p&UE创世,万物皆UObject,接着有Actor。&/p&&/blockquote&&/h1&&h3&UObject:&/h3&&p&起初,UE创世,有感于天地间C++原始之气一片混沌虚无,便撷取凝实一团C++之气,降下无边魔力,洒下秩序之光,便为这个世界生成了坚实的土壤UObject,并用UClass一一为此命名。 &/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-750c05a282e15a481d549e_b.jpg& data-rawwidth=&821& data-rawheight=&283& class=&origin_image zh-lightbox-thumb& width=&821& data-original=&https://pic3.zhimg.com/v2-750c05a282e15a481d549e_r.jpg&&&/figure&&br&藉着UObject提供的元数据、反射生成、GC垃圾回收、序列化、编辑器可见,Class Default Object等,UE可以构建一个Object运行的世界。(后续会有一个大长篇深挖UObject)&/p&&br&&h3&Actor:&/h3&&p&世界有了土壤之后,但还少了一些生动色彩,如果女娲造人一般,UE取一些UObject的泥巴,派生出了Actor。在UE眼中,整个世界从此了有了一个个生动的“演员”,众多的“演员”们,一起齐心协力为观众上演一场精彩的游戏。 &br&&/p&&figure&&img src=&https://pic4.zhimg.com/v2-9348f6bdadbe382a505aff9be7d5d99e_b.jpg& data-rawwidth=&361& data-rawheight=&517& class=&content_image& width=&361&&&/figure&&p&脱胎自Object的Actor也多了一些本事:Replication(网络复制),Spawn(生生死死),Tick(有了心跳)。 &br&Actor无疑是UE中最重要的角色之一,组织庞大,最常见的有StaticMeshActor, CameraActor和 PlayerStartActor等。Actor之间还可以互相“嵌套”,拥有相对的“父子”关系。&/p&&p&&b&思考:为何Actor不像GameObject一样自带Transform? &/b&&br&我们知道,如果一个对象需要在3D世界中表示,那么它必然要携带一个Transform matrix来表示其位置。关键在于,在UE看来,Actor并不只是3D中的“表示”,一些不在世界里展示的“不可见对象”也可以是Actor,如AInfo(派生类AWorldSetting,AGameMode,AGameSession,APlayerState,AGameState等),AHUD,APlayerCameraManager等,代表了这个世界的某种信息、状态、规则。你可以把这些看作都是一个个默默工作的灵体Actor。所以,Actor的概念在UE里其实不是某种具象化的3D世界里的对象,而是世界里的种种元素,用更泛化抽象的概念来看,小到一个个地上的石头,大到整个世界的运行规则,都是Actor. &br&当然,你也可以说即使带着Transform,把坐标设置为原点,然后不可见不就行了?这样其实当然也是可以,不过可能因为UE跟贴近C++一些的缘故,所以设计哲学上就更偏向于C++的哲学“不为你不需要的东西付代价”。一个Transform再加上附带的逆矩阵之类的表示,内存占用上其实也是挺可观的。要知道UE可是会抠门到连bool变量都要写成uint bPending:1;位域来节省一个字节的内存的。 &br&换一个角度讲,如果把带Transform也当成一个Actor的额外能力可以自由装卸的话,那其实也可以自圆其说。经过了UE的权衡和考虑,把Transform封装进了SceneComponent,当作RootComponent。但在权衡到使用的便利性的时候,大部分Actor其实是有Transform的,我们会经常获取设置它的坐标,如果总是得先获取一下SceneComponent,然后再调用相应接口的话,那也太繁琐了。所以UE也为了我们直接提供了一些便利性的Actor方法,如(Get/Set)ActorLocation等,其实内部都是转发到RootComponent。&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&cm&&/*~&/span&
&span class=&cm&& * Returns location of the RootComponent &/span&
&span class=&cm&& * this is a template for no other reason than to delay compilation until USceneComponent is defined&/span&
&span class=&cm&& */&/span&
&span class=&k&&template&/span&&span class=&o&&&&/span&&span class=&k&&class&/span& &span class=&nc&&T&/span&&span class=&o&&&&/span&
&span class=&k&&static&/span& &span class=&n&&FORCEINLINE&/span& &span class=&n&&FVector&/span& &span class=&n&&GetActorLocation&/span&&span class=&p&&(&/span&&span class=&k&&const&/span& &span class=&n&&T&/span&&span class=&o&&*&/span& &span class=&n&&RootComponent&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&k&&return&/span& &span class=&p&&(&/span&&span class=&n&&RootComponent&/span& &span class=&o&&!=&/span& &span class=&k&&nullptr&/span&&span class=&p&&)&/span& &span class=&o&&?&/span& &span class=&n&&RootComponent&/span&&span class=&o&&-&&/span&&span class=&n&&GetComponentLocation&/span&&span class=&p&&()&/span& &span class=&o&&:&/span& &span class=&n&&FVector&/span&&span class=&p&&(&/span&&span class=&mf&&0.f&/span&&span class=&p&&,&/span&&span class=&mf&&0.f&/span&&span class=&p&&,&/span&&span class=&mf&&0.f&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&kt&&bool&/span& &span class=&n&&AActor&/span&&span class=&o&&::&/span&&span class=&n&&SetActorLocation&/span&&span class=&p&&(&/span&&span class=&k&&const&/span& &span class=&n&&FVector&/span&&span class=&o&&&&/span& &span class=&n&&NewLocation&/span&&span class=&p&&,&/span& &span class=&kt&&bool&/span& &span class=&n&&bSweep&/span&&span class=&p&&,&/span& &span class=&n&&FHitResult&/span&&span class=&o&&*&/span& &span class=&n&&OutSweepHitResult&/span&&span class=&p&&,&/span& &span class=&n&&ETeleportType&/span& &span class=&n&&Teleport&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&RootComponent&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&k&&const&/span& &span class=&n&&FVector&/span& &span class=&n&&Delta&/span& &span class=&o&&=&/span& &span class=&n&&NewLocation&/span& &span class=&o&&-&/span& &span class=&n&&GetActorLocation&/span&&span class=&p&&();&/span&
&span class=&k&&return&/span& &span class=&n&&RootComponent&/span&&span class=&o&&-&&/span&&span class=&n&&MoveComponent&/span&&span class=&p&&(&/span&&span class=&n&&Delta&/span&&span class=&p&&,&/span& &span class=&n&&GetActorQuat&/span&&span class=&p&&(),&/span& &span class=&n&&bSweep&/span&&span class=&p&&,&/span& &span class=&n&&OutSweepHitResult&/span&&span class=&p&&,&/span& &span class=&n&&MOVECOMP_NoFlags&/span&&span class=&p&&,&/span& &span class=&n&&Teleport&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&k&&else&/span& &span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&OutSweepHitResult&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&o&&*&/span&&span class=&n&&OutSweepHitResult&/span& &span class=&o&&=&/span& &span class=&n&&FHitResult&/span&&span class=&p&&();&/span&
&span class=&p&&}&/span&
&span class=&k&&return&/span& &span class=&nb&&false&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&同理,Actor能接收处理Input事件的能力,其实也是转发到内部的UInputComponent* InputC同样也提供了便利方法。&/p&&h2&Component&/h2&&p&世界纷繁复杂,光有一种Actor可不够,自然就需要有各种不同技能的Actor各司其职。在早期的远古时代,每个Actor拥有的技能都是与生俱有,只能父传子一代代的传下去。随着游戏世界的越来越绚丽,需要的技能变得越来越多和频繁改变,这样一组合,唯出身论的Actor数量们就开始爆炸了,而且一个个也越来越胖,最后连UE这样的神也管理不了了。终于,到了第4个纪元,UE窥得一丝隔壁平行宇宙Unity的天机。下定决心,让Actor们轻装上阵,只提供一些通用的基本生存能力,而把众多的“技能”抽象成了一个个“Component”并提供组装的接口,让Actor随用随组装,把自己武装成一个个专业能手。&/p&&p&看见UActorComponent的U前缀,是不是想起了什么?没错,UActorComponent也是基础于UObject的一个子类,这意味着其实Component也是有UObject的那些通用功能的。(关于Actor和Component之间Tick的传递后续再细讨论)&/p&&p&&b&下面我们来细细看一下Actor和Component的关系: &/b&&br&TSet&UActorComponent*& OwnedComponents 保存着这个Actor所拥有的所有Component,一般其中会有一个SceneComponent作为RootComponent。 &br&TArray&UActorComponent*& InstanceComponents 保存着实例化的Components。实例化是个什么意思呢,就是你在蓝图里Details定义的Component,当这个Actor被实例化的时候,这些附属的Component也会被实例化。这其实很好理解,就像士兵手上拿着把武器,当我们拥有一队士兵的时候,自然就一一对应拥有了不同实例化的武器。但OwnedComponents里总是最全的。ReplicatedComponents,InstanceComponents可以看作一个预先的分类。&/p&&p&一个Actor若想可以被放进Level里,就必须实例化USceneComponent* RootComponent。但如果你光看代码的话,OwnedComponents其实也是可以包容多个不同SceneComponent的,然后你可以动态获取不同的SceneComponent来当作RootComponent,只不过这种用法确实不太自然,而且也得非常小心维护不同状态,不推荐如此用。在我们的直觉印象里,一个封装过后的Actor应该是一个整体,它能被放进Level中,拥有变换,这一整个整体的概念更加符合自然意识,所以我想,这也是UE为何要在Actor里一一对应一个RootComponent的原因。&/p&&p&再来说说Component下面的家族(为了阐明概念,只列出了最常见的):&/p&&p&&figure&&img src=&https://pic1.zhimg.com/v2-dc7b7f3cedfb7_b.jpg& data-rawwidth=&686& data-rawheight=&784& class=&origin_image zh-lightbox-thumb& width=&686& data-original=&https://pic1.zhimg.com/v2-dc7b7f3cedfb7_r.jpg&&&/figure&ActorComponent下面最重要的一个Component就非SceneComponent莫属了。SceneComponent提供了两大能力:一是Transform,二是SceneComponent的互相嵌套。 &/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-bc32dd04c7221ac9dcc56d0_b.jpg& data-rawwidth=&672& data-rawheight=&524& class=&origin_image zh-lightbox-thumb& width=&672& data-original=&https://pic3.zhimg.com/v2-bc32dd04c7221ac9dcc56d0_r.jpg&&&/figure&&br&&b&思考:为何ActorComponent不能互相嵌套?而在SceneComponent一级才提供嵌套? &/b&&br&&/p&&p&首先,ActorComponent下面当然不是只有SceneComponent,一些UMovementComponent,AIComponent,或者是我们自己写的Component,都是会直接继承ActorComponent的。但很奇怪的是,ActorComponent却是不能嵌套的,在UE的观念里,好像只有带Transform的SceneComponent才有资格被嵌套,好像Component的互相嵌套必须和3D里的transform父子对应起来。 &br&老实说,如果让我来设计Entity-Component模式,我很可能会为了通用性而在ActorComponent这一级直接提供嵌套,这样所有的Component就与生俱来拥有了组合其他Component的能力,灵活性大大提高。但游戏引擎的设计必然也经过了各种权衡,虽然说架构上显得并不那么的统一干净,但其实也大大减少了被误用的机会。实体组件模式推崇的“组合优于继承”的概念确实很强大,但其实同时也带来了一些问题,如Component之间如何互相依赖,如何互相通信,嵌套过深导致的接口便利损失和性能损耗,真正一个让你随便嵌套的组件模式可能会在使用上更容易出问题。 &br&从功能上来说,UE更倾向于编写功能单一的Component(如UMovementComponent),而不是一个整合了其他Component的大管家Component(当然如果你偏要这么干,那UE也阻止不了你)。 &br&而从游戏逻辑的实现来说,UE也是不推荐把游戏逻辑写在Component里面,所以你其实也没什么机会去写一个很复杂的Component.&/p&&p&&b&思考:Actor的SceneComponent哲学 &/b&&br&很多其他游戏引擎,还有一种设计思路是“万物皆Node”。Node都带变换。比如说你要设计一辆汽车,一种方式是车身作为一个Node,4个轮子各为车身的子Node,然后移动父Node来前进。而在UE里,一种很可能的方式就变成,汽车是一个Actor,车身作为RootComponent,4个轮子都作为RootComponent的子SceneComponent。请读者们细细体会这二者的区别。两种方式都可以实现出优秀的游戏引擎,只是有些理念和侧重点不同。 &br&从设计哲学上来说,其实你把万物看成是Node,或者是Component,并没有什么本质上的不同。看作Node的时候,Node你就要设计的比较轻量廉价,这样才能比较没有负担的创建多个,同理Component也是如此。Actor可以带多个SceneComponent来渲染多个Mesh实体,同样每个Node带一份Mesh再组合也可以实现出同样效果。 &br&个人观点来说,关键的不同是在于你是怎么划分要操作的实体的粒度的。当看成是Node时,因为Node身上的一些通用功能(事件处理等),其实我们是期望着我们可以非常灵活的操作到任何一个细小的对象,我们希望整个世界的所有物体都有一些基本的功能(比如说被拾取),这有点完美主义者的思路。而注重现实的人就会觉得,整个游戏世界里,有相当大一部分对象其实是不那么动态的。比如车子,我关心的只是整体,而不是细小到每一个车轱辘。这种理念就会导成另外一种设计思路:把要操作的实体按照功能划分,而其他的就尽量只是最简单的表示。所以在UE里,其实是把5个薄薄的SceneComponent表示再用Actor功能的盒子装了起来,而在这个盒子内部你可以编写操作这5个对象的逻辑。换做是Node模式,想编写操作逻辑的话,一般就来说就会内化到父Node的内部,不免会有逻辑与表现掺杂之嫌,而如果Node要把逻辑再用组合分离开的话,其实也就转化成了某种ScriptComponent。&/p&&p&&b&思考:Actor之间的父子关系是怎么确定的?&/b&&br&你应该已经注意到了Actor里面的TArray&AActor*& Children字段,所以你可能会期望看到Actor:AddChild之类的方法,很遗憾。在UE里,Actor之间的父子关系却是通过Component确定的。同一般的Parent:AddChild操作原语不同,UE里是通过Child:AttachToActor或Child:AttachToComponent来创建父子连接的。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&void AActor::AttachToActor(AActor* ParentActor, const FAttachmentTransformRules& AttachmentRules, FName SocketName)
if (RootComponent && ParentActor)
USceneComponent* ParentDefaultAttachComponent = ParentActor-&GetDefaultAttachComponent();
if (ParentDefaultAttachComponent)
RootComponent-&AttachToComponent(ParentDefaultAttachComponent, AttachmentRules, SocketName);
void AActor::AttachToComponent(USceneComponent* Parent, const FAttachmentTransformRules& AttachmentRules, FName SocketName)
if (RootComponent && Parent)
RootComponent-&AttachToComponent(Parent, AttachmentRules, SocketName);
&/code&&/pre&&/div&&p&3D世界里的“父子”关系,我们一般可能会认为就是3D世界里的变换的坐标空间“父子”关系,但如果再度扩展一下,如上所述,一个Actor可是可以带有多个SceneComponent的,这意味着一个Actor是可以带有多个Transform“锚点”的。创建父子时,你到底是要把当前Actor当作对方哪个SceneComponent的子?再进一步,如果你想更细控制到Attach到某个Mesh的某个Socket(关于Socket Slot,目前可以简单理解为一个虚拟插槽,提供变换锚点),你就更需要去寻找到特定的变换锚点,然后Attach的过程分别在Location,Roator,Scale上应用Rule来计算最后的位置。&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&/** Rules for attaching components - needs to be kept synced to EDetachmentRule */
enum class EAttachmentRule : uint8
/** Keeps current relative transform as the relative transform to the new parent. */
KeepRelative,
/** Automatically calculates the relative transform such that the attached component maintains the same world transform. */
KeepWorld,
/** Snaps transform to the attach point */
SnapToTarget,
&/code&&/pre&&/div&&p&所以Actor父子之间的“关系”其实隐含了许多数据,而这些数据都是在Component上提供的。Actor其实更像是一个容器,只提供了基本的创建销毁,网络复制,事件触发等一些逻辑性的功能,而把父子的关系维护都交给了具体的Component,所以更准确的说,其实是不同Actor的SceneComponent之间有父子关系,而Actor本身其实并不太关心。&br&&/p&&p&接下来的左侧派生链依次提供了物理,材质,网格最终合成了一个我们最普通常见的StaticMeshComponent。而右侧的ChildActorComponent则是提供了Component之下再叠加Actor的能力。&br&&/p&&p&&b&聊一聊ChildActorComponent &/b&&br&同作为最常用到的Component之一,ChildActorComponent担负着Actor之间互相组合的胶水。这货在蓝图里静态存在的时候其实并不真正的创建Actor,而是在之后Component实例化的时候才真正创建。&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&kt&&void&/span& &span class=&n&&UChildActorComponent&/span&&span class=&o&&::&/span&&span class=&n&&OnRegister&/span&&span class=&p&&()&/span&
&span class=&p&&{&/span&
&span class=&n&&Super&/span&&span class=&o&&::&/span&&span class=&n&&OnRegister&/span&&span class=&p&&();&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&ChildActor&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&ChildActor&/span&&span class=&o&&-&&/span&&span class=&n&&GetClass&/span&&span class=&p&&()&/span& &span class=&o&&!=&/span& &span class=&n&&ChildActorClass&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&n&&DestroyChildActor&/span&&span class=&p&&();&/span&
&span class=&n&&CreateChildActor&/span&&span class=&p&&();&/span&
&span class=&p&&}&/span&
&span class=&k&&else&/span&
&span class=&p&&{&/span&
&span class=&n&&ChildActorName&/span& &span class=&o&&=&/span& &span class=&n&&ChildActor&/span&&span class=&o&&-&&/span&&span class=&n&&GetFName&/span&&span class=&p&&();&/span&
&span class=&n&&USceneComponent&/span&&span class=&o&&*&/span& &span class=&n&&ChildRoot&/span& &span class=&o&&=&/span& &span class=&n&&ChildActor&/span&&span class=&o&&-&&/span&&span class=&n&&GetRootComponent&/span&&span class=&p&&();&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&ChildRoot&/span& &span class=&o&&&&&/span& &span class=&n&&ChildRoot&/span&&span class=&o&&-&&/span&&span class=&n&&GetAttachParent&/span&&span class=&p&&()&/span& &span class=&o&&!=&/span& &span class=&k&&this&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&c1&&// attach new actor to this component&/span&
&span class=&c1&&// we can't attach in CreateChildActor since it has intermediate Mobility set up&/span&
&span class=&c1&&// causing spam with inconsistent mobility set up&/span&
&span class=&c1&&// so moving Attach to happen in Register&/span&
&span class=&n&&ChildRoot&/span&&span class=&o&&-&&/span&&span class=&n&&AttachToComponent&/span&&span class=&p&&(&/span&&span class=&k&&this&/span&&span class=&p&&,&/span& &span class=&n&&FAttachmentTransformRules&/span&&span class=&o&&::&/span&&span class=&n&&SnapToTargetNotIncludingScale&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&c1&&// Ensure the components replication is correctly initialized&/span&
&span class=&n&&SetIsReplicated&/span&&span class=&p&&(&/span&&span class=&n&&ChildActor&/span&&span class=&o&&-&&/span&&span class=&n&&GetIsReplicated&/span&&span class=&p&&());&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&span class=&k&&else&/span& &span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&ChildActorClass&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&n&&CreateChildActor&/span&&span class=&p&&();&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&n&&UChildActorComponent&/span&&span class=&o&&::&/span&&span class=&n&&OnComponentCreated&/span&&span class=&p&&()&/span&
&span class=&p&&{&/span&
&span class=&n&&Super&/span&&span class=&o&&::&/span&&span class=&n&&OnComponentCreated&/span&&span class=&p&&();&/span&
&span class=&n&&CreateChildActor&/span&&span class=&p&&();&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&这就导致了一个问题,当你把一个ActorClass拖进Level后,这个Actor实际是已经实例化了,你可以直接调整这个Actor的属性。但是你把它拖到另一个Actor Class里,它只会给你空空白白的ChildActorComponent的DetailsPanel,你想调整Actor的属性,就只能等生成了之后,用蓝图或代码去修改。这一点来说,其实还是挺不方便的,我个人觉得应该是还有优化的空间。&br&&/p&&h2&修订&/h2&&br&&h3&4.14 Child Actor Templates&/h3&&p&UE终于听到了人民群众的呼声,在4.14里增加了Child Actor Templates来支持在子ChildActor的DetailsPannel里查看和修改属性。 &/p&&figure&&img src=&https://pic2.zhimg.com/v2-3e72b51efd4bbcd7bb55915f_b.jpg& data-rawwidth=&1100& data-rawheight=&778& class=&origin_image zh-lightbox-thumb& width=&1100& data-original=&https://pic2.zhimg.com/v2-3e72b51efd4bbcd7bb55915f_r.jpg&&&/figure&&br&&h2&后记&/h2&&p&花了这么多篇幅,才刚刚讲到Actor和Component这两个最基本的整体设计,而关于Actor,Component生命周期,Tick,事件传递等机制性的问题,还都没有展开。UE作为从1代至今4代,久经磨练的一款成熟引擎,GamePlay框架部分其实也就不到十个类,而这些类之间怎么组织,为啥这么设计,有什么权衡和考虑,我相信这里面其实是非常有讲究的。如果是UE的总架构师来讲解的话,肯定能有非常多的心得体会故事。而我们作为学习者,也应该尽量去体会琢磨它的用心,一方面}

我要回帖

更多关于 luai啥意思 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信