为什么虚幻 unity 跨平台可以实现跨多个平台

Unity3D游戏开发引擎如何实现跨平台?
Unity3D几乎是市场上最流行的游戏开发引擎了,由Unity Technology(以下简称UT)公司开发,它可用于Windows和Mac OS X系统(Linux系统实验版已发布)。最重要的是,它几乎可以导出到任意平台。
1.关于图形
Unity支持的图形API有OpenGL、OpenGL ES、WebGL、Metal以及DirectX,每个API都对应不同的平台。
OpenGL应用很广泛,一些iOS设备、Mac OS X和Linux,甚至Windows都用到了OpenGL。
OpenGL ES兼容手机设置,支持大多数Android和部分iOS设备。
WebGL是一个新平台,基于浏览器图形运行应用和游戏,无需再安装Flash或Unity WebPlayer这样的插件。
Metal是苹果新出的图形API,兼容大多数近期的iOS设备及Californian公司的电脑。
DirectX是微软自制的图形API解决方案,兼容Windows、Windows Phone以及Xbox。
虽然UT的开发者在图形处理上也废了不少功夫,但大多时间都花在将这些工具集成到引擎中,而非自己写一套图形API。
2.关于物理
说 到物理就比图形单纯多了,Unity至始至终都坚守一个工具:英伟达的PhysX,它支持Unity要导出的所有各个平台。这么说吧,它是市场上最好的物 理引擎之一,并且从其第一代叫做Novodex还不属于英伟达的时候开始,就备受信任且性能表现极好。主要是因为一个引擎就搞定所有平台的物理也很方便, 而游戏在所支持设备中的碰撞和移动表现行为都是一致的,这样才不会因设备导致游戏失衡。
3.关于光照
嗯,Unity的光照也是依赖外部工具实现的,包括烘焙和实时光照。Unity5之前使用Autodesk的Beast作为烘焙光照的工具。Unity5.x用的是Geomeric的Enlighten实现烘焙和实时光照。
4.关于网络
UT 在2014年发布了其自制的网络和多玩家解决方案UNET。之前有一些常用的网络解决方案,最出名的就是Photon。而UNET包含两个部分:网络 API(高层及底层API)和付费的多玩家服务。由于这是内部项目,所以UT必须将代码移植到所有支持的平台,就像上面的图形、物理、光照一样,各个平台 均有区别。5关于脚本
脚本可能是大家最关心的部分,因为使用引擎的目的就是不用自己操心图形或物理底层。Unity支持使用C#和Unity编写脚本,且不需任何转换工具就能将游戏导出到上述提过的任意平台。
看看Xamarin的使用说明:
用C#编写应用,直接从C#调用任意原生平台的API。Xamarin编译器会捆绑.NET运行库并输出原生的ARM可执行文件,打包成iOS或Android应用。
那Mono开发的应用是绑定了整个框架吗?当然不是,框架中不使用的类在链接时就被剔除了。只有用到的部分会被打包进最后的应用。到此Unity跨平台的原理差不多就讲完了,Mono才是大功臣。顺便提一下,Unity本来推出了IL2CPP替换Mono作为脚本后端的,但因为其Bug太多以及导出的C++文件太大一直被开发者诟病,相信短时间内IL2CPP想完全取代Mono也不是容易的事。
希望以上对你将要学习Unity3D开发你有所帮助!蓝鸥有着国内首家完整的移动Unity3D开发课程体系,蓝鸥科技作为 Unity官方授权培训中心,独创“FCBS”教学模式,致力于培养具有实战经验的开发工程师。在课程设置上,蓝鸥科技课程体系基于 3D/2D行业人才需求,适合 于游戏开发、多平台交互、虚拟现实、增强现实、科技创意、仿真、建筑可视化等各个行业的人才发展方向。
我们是 一群热爱IT的年轻人,如果你也爱IT、爱Unity3D开发,欢迎前来蓝鸥中心参观学习,让我们共同为梦想发声。
责任编辑:
声明:本文由入驻搜狐号的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
前端开发IT职业新贵,10万年薪等你!
Unity官方授权课程,高薪职位任你选!
今日搜狐热点Unity3D为何能实现跨平台?_百度知道
Unity3D为何能实现跨平台?
我有更好的答案
所有的平台打包程序里带个mono runtime,ios之外的平台是jit,ios平台是编译时把IL代码转为目标码,也就是Full AOT。
采纳率:70%
为您推荐:
其他类似问题
unity3d的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。28被浏览7,970分享邀请回答0添加评论分享收藏感谢收起1添加评论分享收藏感谢收起Unity3D为何能跨平台?聊聊CIL(MSIL)2 years agoCIL代码:.class private auto ansi beforefieldinit Class1
extends [mscorlib]System.Object
.method public hidebysig static void
Main(string[] args) cil managed
.entrypoint
// 代码大小
void [mscorlib]System.Console::WriteLine(string)
} // end of method Class1::Main
.method public hidebysig specialname rtspecialname
instance void
.ctor() cil managed
// 代码大小
instance void [mscorlib]System.Object::.ctor()
} // end of method Class1::.ctor
} // end of class Class1
好啦。代码虽然简单,但是也能说明足够多的问题。那么和CIL的第一次亲密接触,能给我们留下什么直观的印象呢?以“.”一个点号开头的,例如上面这份代码中的:.class、.method 。我们称之为CIL指令(directive),用于描述.NET程序集总体结构的标记。为啥需要它呢?因为你总得告诉编译器你处理的是啥吧。貌似CIL代码中还看到了private、public这样的身影。姑且称之为CIL特性(attribute)。它的作用也很好理解,通过CIL指令并不能完全说明.NET成员和类,针对CIL指令进行补充说明成员或者类的特性的。市面上常见的还有:extends,implements等等。每一行CIL代码基本都有的,对,那就是CIL操作码咯。小匹夫从网上找了一份汉化的操作码表放在部分,当然英文版的你的vs就有。直观的印象有了,但是离我们的短期目标,说清楚(或者说介绍个大概)CIL是What,甚至是终极目标,搞明白Uniyt3D为何能跨平台还有2万4千9百里的距离。好啦,话不多说,继续乱侃。参照附录中的操作码表,对照可以总结出一份更易读的表格。那就是如下的表啦。在此,小匹夫想请各位认真读表,然后心中默数3个数,最后看看都能发现些什么。基于堆栈如果是小匹夫的话,第一感觉就是基本每一条描述中都包含一个”栈“。不错,CIL是基于堆栈的,也就是说CIL的VM(mono运行时)是一个栈式机。这就意味着数据是推入堆栈,通过堆栈来操作的,而非通过CPU的寄存器来操作,这更加验证了其和具体的CPU架构没有关系。为了说明这一点,小匹夫举个例子好啦。大学时候学单片机(大概是8086,记不清了)的时候记得做加法大概是这样的:add eax,-2
其中的eax是啥?寄存器。所以如果CIL处理数据要通过cpu的寄存器的话,那也就不可能和cpu的架构无关了。当然,CIL之所以是基于堆栈而非CPU的另一个原因是相比较于cpu的寄存器,操作堆栈实在太简单了。回到刚才小匹夫说的大学时候曾经学过的单片
机那门课程上,当时记得各种寄存器,各种标志位,各种。。。,而堆栈只需要简单的压栈和弹出,因此对于虚拟机的实现来说是再合适不过了。所以想要更具体的
了解CIL基于堆栈这一点,各位可以去看一下堆栈方面的内容。这里小匹夫就不拓展了。面向对象那么第二感觉呢?貌似附录的表中有new对象的语句呀。嗯,的确,CIL同样是面向对象的。这意味着什么呢?那就是在CIL中你可以创建对象,调用对象的方法,访问对象的成员。而这里需要注意的就是对方法的调用。回到上表中的右上角。对,就是对参数的操作部分。静态方法和实例方法是不同的哦~静态方法:ldarg.0么有被占用,所以参数从ldarg.0开始。实例方法:ldarg.0是被this占用的,也就是说实际上的参数是从ldarg.1开始的。举个例子:假设你有一个类Murong中有一个静态方法Add(int32 a, int32 b),实现的内容就如同它的名字一样使两个数相加,所以需要2个参数。和一个实例方法TellName(string name),这个方法会告诉你传入的名字。class
public void TellName(string name)
System.Console.WriteLine(name);
public static int Add(int a, int b)
return a +
静态方法的处理:那么其中的静态方法Add的CIL代码如下://小匹夫注释一下。
.method public hidebysig static int32
Add(int32 a,
int32 b) cil managed
// 代码大小
.locals init ([0] int32 CS$1$0000)
//初始化局部变量列表。因为我们只返回了一个int型。所以这里声明了一个int32类型。索引为0
//将索引为 0 的参数加载到计算堆栈上。
//将索引为 1 的参数加载到计算堆栈上。
//从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
//将索引 0 处的局部变量加载到计算堆栈上。
//返回该值
} // end of method Murong::Add
那么我们调用这个静态函数应该就是这样咯。Murong.Add(1, 2);
对应的CIL代码为:
ldc.i4.1 //将整数1压入栈中
ldc.i4.2 //将整数2压入栈中
int32 Murong::Add(int32,
//调用静态方法
可见CIL直接call了Murong的Add方法,而不需要一个Murong的实例。实例方法的处理:Murong类中的实例方法TellName()的CIL代码如下:.method public hidebysig instance void
TellName(string name) cil managed
// 代码大小
//看到和静态方法的区别了吗?
void [mscorlib]System.Console::WriteLine(string)
} // end of method Murong::TellName
看到和静态方法的区别了吗?对,第一个参数对应的是ldarg.1中的参数1,而不是静态方法中的0。因为此时参数0相当于this,this是不用参与参数传递的。那么我们再看看调用实例方法的C#代码和对应的CIL代码是如何的。//C#
Murong murong = new Murong();
murong.TellName("chenjiadong");
CIL:.locals init ([0] class Murong murong)
//因为C#代码中定义了一个Murong类型的变量,所以局部变量列表的索引0为该类型的引用。
instance void Murong::.ctor() //相比上面的静态方法的调用,此处new一个新对象,出现了instance方法。
"chenjiadong" //小匹夫的名字入栈
instance void Murong::TellName(string) //实例方法的调用也有instance
到此,受制于篇幅所限(小匹夫不想写那么多字啊啊啊!)CIL是What的问题大致介绍一下。当然没有再拓展,以后小匹夫可能会再详细写一下这块。How?记得语文老师说过,写作文最重要的一点是要首尾呼应。既然咱们开篇就提出了U3D为何能跨平台的问题,那么接近文章的结尾咱们就再来提问:Q:上面的Why部分,咱们知道了U3D能跨平台是因为存在着一个能通吃的中间语言CIL,这也是所谓跨平台的前提,但是为啥CIL能通吃各大平台呢?当然可以说CIL基于堆栈,跟你CPU怎么架构的没啥关系,但是感觉过于理论化、学术化,那还有没有通俗化、工程化的说法呢?A:原因就是前面小匹夫提到过的,.Net运行时和Mono运行时。也就是说CIL语言其实是运行在虚拟机中的,具体到咱们的U3D也就是mono的运行时了,换言之mono运行的其实CIL语言,CIL也并非真正的在本地运行,而是在mono运行时中运行的,运行在本地的是被编译后生成的原生代码。当然看官博的文章,他们似乎也在开发自己的“mono”,也就是被称为脚本的未来的IL2Cpp,这种类似运行时的功能是将IL再编译成c++,再由c++编译成原生代码,据说效率提升很可观,小匹夫也是蛮期待的。这里为了“实现跨平台式的演示”,小匹夫用mac给各位做个测试好啦:从C#到CIL新建一个cs文件,然后使用mono来运行。这个cs文件内容如下:然后咱们直接在命令行中运行这个cs文件试试~说的很清楚,文件没有包含一个CIL映像。可见mono是不能直接运行cs文件的。假如我们把它编译成CIL呢?那么我们用mono带的mcs来编译小匹夫的Test.cs文件。mcs Test.cs
生成了什么呢?如图:好像没见有叫.IL的文件生成啊?反而好像多了一个.exe文件?可是没听说Mac能运行exe文件呀?可为啥又生成了.exe呢?各位看官可能要说,小匹夫你是不是拿windows截图P的啊?嘿嘿,小匹夫可不敢。辣么真相其实就是这个exe并不是让Mac来运行的,而是留给mono运行时来运行的,换言之这个文件的可执行代码形式是CIL的位元码形态。到此,我们完成了从C#到CIL的过程。接下来就让我们运行下刚刚的成果好啦。mono Test.exe 结果是输出了一个大大的“Hi”。这里,就引出了下一个部分。
从CIL到Native Code这个“HI”可是在小匹夫的MAC终端上出现的呀,那么就证明这个C#写的代码在MAC上运行的还挺“嗨”。为啥呢?为啥C#写的代码能跑在MAC上呢?这就不得不提从CIL如何到本机原生代码的过程了。Mono提供了两种编译方式,就是我们经常能看到的:JIT(Just-in-Time compilation,即时编译)和AOT(Ahead-of-Time,提前编译或静态编译)。这两种方式都是将CIL进一步编译成平台的原生代码。这也是实现跨平台的最后一步。下面就分头介绍一下。JIT即时编译:从名字就能看的出来,即时编译,或者称之为动态编译,是在程序执行时才编译代码,解释一条语句执行一条语句,即将一条中间的托管的语句翻译成一条机器语句,然后执行这条机器语句。但同时也会将编译过的代码进行缓存,而不是每一次都进行编译。所以可以说它是静态编译和解释器的结合体。不过你想想机器既要处理代码的逻辑,同时还要进行编译的工作,所以其运行时的效率肯定是受到影响的。因此,Mono会有一部分代码通过AOT静态编译,以降低在程序运行时JIT动态编译在效率上的问题。不过一向严苛的IOS平台是不允许这种动态的编译方式的,这也是U3D官方无法给出热更新方案的一个原因。而Android平台恰恰相反,Dalvik虚拟机使用的就是JIT方案。AOT静态编译:其实Mono的AOT静态编译和JIT并非对立的。AOT同样使用了JIT来进行编译,只不过是被AOT编译的代码在程序运行之前就已经编译好了。当然还有一部分代码会通过JIT来进行动态编译。下面小匹夫就手动操作一下mono,让它进行一次AOT编译。//在命令行输入
mono --aot Test.exe
结果:从图中可以看到JIT time: 39 ms,也就是说Mono的AOT模式其实会使用到JIT,同时我们看到了生成了一个适应小匹夫的MAC的动态库Test.exe.dylib,而在Linux生成就是.so(共享库)。
AOT编译出来的库,除了包括我们的代码之外,还有被缓存的元数据信息。所以我们甚至可以只编译元数据信息而不变异代码。例如这样://只包含元数据的信息
mono --aot=metadata-only Test.exe
可见代码没有被包括进来。
那么简单总结一下AOT的过程:收集要被编译的方法使用JIT进行编译发射(Emitting)经JIT编译过的代码和其他信息直接生成文件或者调用本地汇编器或连接器进行处理之后生成文件。(例如上图中使用了小匹夫本地的gcc)Full AOT当然上文也说了,IOS平台是禁止使用JIT的,可看样子Mono的AOT模式仍然会保留一部分代码会在程序运行时动态编译。所以为了破解这个问题,Mono提供了一个被称为Full AOT的模式。即预先对程序集中的所有CIL代码进行AOT编译生成一个本地代码映像,然后在运行时直接加载这个映像而不再使用JIT引擎。目前由于技术或实现上的原因在使用Full AOT时有一些限制,不过这里不再多说了。以后也还会更细的分析下AOT。总结:好啦,写到现在也已经到了凌晨3:04分了。感觉写的内容也差不多了。那么对本文的主题U3D为何能跨平台以及CIL做个最终的总结陈词:CIL是CLI标准定义的一种可读性较低的语言。以.NET或mono等实现CLI标准的运行环境为目标的语言要先编译成CIL,之后CIL会被编译,并且以位元码的形式存在(源代码---&中间语言的过程)。这种位元码运行在虚拟机中(.net mono的运行时)。这种位元码可以被进一步编译成不同平台的原生代码(中间语言---&原生代码的过程)。面向对象基于堆栈5收藏分享举报文章被以下专栏收录编程和游戏首先是爱好,其次才是职业。{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&isPending&:false,&contributes&:[{&sourceColumn&:{&lastUpdated&:,&description&:&编程首先是爱好,其次才是职业。专注前沿技术,热爱开源。深信代码改变世界。没有值得吹嘘的项目,只有不断前行的动力。写有个人博客“嘉栋的Coding Blog”: http:\u002F\u002Fwww.chenjd.me&,&permission&:&COLUMN_PUBLIC&,&memberId&:1718122,&contributePermission&:&COLUMN_PUBLIC&,&translatedCommentPermission&:&all&,&canManage&:true,&intro&:&编程和游戏首先是爱好,其次才是职业。&,&urlToken&:&chenjiadong&,&id&:8818,&imagePath&:&v2-c4e393a91e7abcd.png&,&slug&:&chenjiadong&,&applyReason&:&&,&name&:&Runtime&,&title&:&Runtime&,&url&:&https:\u002F\u002Fzhuanlan.zhihu.com\u002Fchenjiadong&,&commentPermission&:&COLUMN_ALL_CAN_COMMENT&,&canPost&:true,&created&:,&state&:&COLUMN_NORMAL&,&followers&:3572,&avatar&:{&id&:&v2-c4e393a91e7abcd&,&template&:&https:\u002F\u002Fpic4.zhimg.com\u002F{id}_{size}.jpg&},&activateAuthorRequested&:false,&following&:false,&imageUrl&:&https:\u002F\u002Fpic4.zhimg.com\u002Fv2-c4e393a91e7abcd_l.jpg&,&articlesCount&:55},&state&:&accepted&,&targetPost&:{&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fcf7a7f19da2411_r.jpg&,&lastUpdated&:,&imagePath&:&cf7a7f19da2411.png&,&permission&:&ARTICLE_PUBLIC&,&topics&:[],&summary&:&前言:其实小匹夫在U3D的开发中一直对U3D的跨平台能力很好奇。到底是什么原理使得U3D可以跨平台呢?后来发现了Mono的作用,并进一步了解到了\nCIL的存在。所以,作为一个对Unity3D跨平台能力感兴趣的U3D程序猿,小匹夫如何能不关注CIL这个话题呢?那么下面各…&,&copyPermission&:&ARTICLE_COPYABLE&,&translatedCommentPermission&:&all&,&likes&:0,&origAuthorId&:1718122,&publishedTime&:&T16:31:53+08:00&,&sourceUrl&:&&,&urlToken&:,&id&:392793,&withContent&:false,&slug&:,&bigTitleImage&:false,&title&:&Unity3D为何能跨平台?聊聊CIL(MSIL)&,&url&:&\u002Fp\u002F&,&commentPermission&:&ARTICLE_ALL_CAN_COMMENT&,&snapshotUrl&:&&,&created&:,&comments&:0,&columnId&:8818,&content&:&&,&parentId&:0,&state&:&ARTICLE_PUBLISHED&,&imageUrl&:&https:\u002F\u002Fpic1.zhimg.com\u002Fcf7a7f19da2411_r.jpg&,&author&:{&bio&:&慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01&,&isFollowing&:false,&hash&:&2ad398c4fbe830&,&uid&:88,&isOrg&:false,&slug&:&jiadongchen&,&isFollowed&:false,&description&:&编程首先是爱好,其次才是职业。http:\u002F\u002Fwww.chenjd.me\u002F\n\n我已加入“维权骑士”(rightknights.com)的版权保护计划。&,&name&:&陈嘉栋&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fjiadongchen&,&avatar&:{&id&:&v2-e580cadf61b81a00e975c5a&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&memberId&:1718122,&excerptTitle&:&&,&voteType&:&ARTICLE_VOTE_CLEAR&},&id&:322146}],&title&:&Unity3D为何能跨平台?聊聊CIL(MSIL)&,&author&:&jiadongchen&,&content&:&\u003Ch2\u003E前言:\u003C\u002Fh2\u003E\u003Cp\u003E其实小匹夫在U3D的开发中一直对U3D的跨平台能力很好奇。到底是什么原理使得U3D可以跨平台呢?后来发现了Mono的作用,并进一步了解到了\nCIL的存在。所以,作为一个对Unity3D跨平台能力感兴趣的U3D程序猿,小匹夫如何能不关注CIL这个话题呢?那么下面各位看官就拾起语文老师教\n导我们的作文口诀(\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.cnblogs.com\u002Fmurongxiaopifu\u002Fp\u002F4211964.html%23why\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EWhy\u003C\u002Fa\u003E,\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.cnblogs.com\u002Fmurongxiaopifu\u002Fp\u002F4211964.html%23what\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EWhat\u003C\u002Fa\u003E,\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.cnblogs.com\u002Fmurongxiaopifu\u002Fp\u002F4211964.html%23how\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EHow\u003C\u002Fa\u003E),和小匹夫一起走进CIL的世界吧~\u003C\u002Fp\u003E\u003Cbr\u003E\u003Ch2\u003EWhy?\u003C\u002Fh2\u003E\u003Cp\u003E回到本文的题目,U3D或者说Mono的跨平台是如何做到的?\u003C\u002Fp\u003E\u003Cp\u003E如果换做小匹夫或者看官你来做,应该怎么实现一套代码对应多种平台呢?\u003C\u002Fp\u003E\u003Cp\u003E其实原理想想也简单,生活中也有很多可以参考的例子,比如下图(谁让小匹夫是做移动端开发的呢,只能物尽其用从自己身边找例子了T.T):\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&224\& data-rawwidth=\&248\& src=\&https:\u002F\u002Fpic2.zhimg.com\u002Fe3a003e5e0be3fc1e2bcf3_b.jpg\& class=\&content_image\& width=\&248\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&224\& data-rawwidth=\&248\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='248'%20height='224'&&\u002Fsvg&\& class=\&content_image lazy\& width=\&248\& data-actualsrc=\&https:\u002F\u002Fpic2.zhimg.com\u002Fe3a003e5e0be3fc1e2bcf3_b.jpg\&\u003E\u003C\u002Ffigure\u003E像这样一根线,管你是安卓还是ios都能充电。所以从这个意义上,这货也实现了跨平台。那么我们能从它身上学到什么呢?对的,那就是从一样的能源(电)到不同的平台(ios,安卓)之间需要一个中间层过度转换一下。\u003Cp\u003E那么来到U3D为何能跨平台,简而言之,其实现原理在于使用了叫CIL(Common Intermediate \nLanguage通用中间语言,也叫做MSIL微软中间语言)的一种代码指令集,CIL可以在任何支持CLI(Common Language \nInfrastructure,通用语言基础结构)的环境中运行,就像.NET是微软对这一标准的实现,Mono则是对CLI的又一实现。由于CIL能运\n行在所有支持CLI的环境中,例如刚刚提到的.NET运行时以及Mono运行时,也就是说和具体的平台或者CPU无关。这样就无需根据平台的不同而部署不\n同的内容了。所以到这里,各位也应该恍然大了。代码的编译只需要分为两部分就好了嘛:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E从代码本身到CIL的编译(其实之后CIL还会被编译成一种位元码,生成一个CLI assembly)\u003C\u002Fli\u003E\u003Cli\u003E运行时从CIL(其实是CLI assembly,不过为了直观理解,不必纠结这种细节)到本地指令的即时编译(这就引出了为何U3D官方没有提供热更新的原因:\u003Cstrong\u003E在iOS平台中Mono无法使用JIT引擎,而是以Full AOT模式运行的,所以此处说的额即时编译不包括IOS\u003C\u002Fstrong\u003E)\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cbr\u003E\u003Ch2\u003EWhat?\u003C\u002Fh2\u003E\u003Cp\u003E上文也说了CIL是指令集,但是不是还是太模糊了呢?所以语文老师教导我们,描述一个东西时肯定要先从外貌写起。遵循老师的教导,我们不妨先通过工具来看看CIL到底长什么样。\u003C\u002Fp\u003E\u003Cp\u003E工具就是ildasm了。下面小匹夫写一个简单的.cs看看生成的CIL代码长什么样。\u003C\u002Fp\u003E\u003Cp\u003EC#代码:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass Class1\n{\n
public static void Main(string[] args)\n
System.Console.WriteLine(\&hi\&);\n
}\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003ECIL代码:\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E.class private auto ansi beforefieldinit Class1\n
extends [mscorlib]System.Object\n{\n
.method public hidebysig static void
Main(string[] args) cil managed\n
.entrypoint\n
\u002F\u002F 代码大小
13 (0xd)\n
void [mscorlib]System.Console::WriteLine(string)\n
} \u002F\u002F end of method Class1::Main\n\n
.method public hidebysig specialname rtspecialname \n
instance void
.ctor() cil managed\n
\u002F\u002F 代码大小
instance void [mscorlib]System.Object::.ctor()\n
} \u002F\u002F end of method Class1::.ctor\n\n} \u002F\u002F end of class Class1\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E好啦。代码虽然简单,但是也能说明足够多的问题。那么和CIL的第一次亲密接触,能给我们留下什么直观的印象呢?\u003Col\u003E\u003Cli\u003E以“.”一个点号开头的,例如上面这份代码中的:.class、.method 。我们称之为\u003Cstrong\u003ECIL指令(directive)\u003C\u002Fstrong\u003E,用于描述.NET程序集总体结构的标记。为啥需要它呢?因为你总得告诉编译器你处理的是啥吧。\u003C\u002Fli\u003E\u003Cli\u003E貌似CIL代码中还看到了private、public这样的身影。姑且称之为\u003Cstrong\u003ECIL特性(attribute)\u003C\u002Fstrong\u003E。它的作用也很好理解,通过CIL指令并不能完全说明.NET成员和类,针对CIL指令进行补充说明成员或者类的特性的。市面上常见的还有:extends,implements等等。\u003C\u002Fli\u003E\u003Cli\u003E每一行CIL代码基本都有的,对,那就是\u003Cstrong\u003ECIL操作码\u003C\u002Fstrong\u003E咯。小匹夫从网上找了一份汉化的操作码表放在\u003Ca href=\&https:\u002F\u002Flink.zhihu.com\u002F?target=http%3A\u002F\u002Fwww.cnblogs.com\u002Fmurongxiaopifu\u002Fp\u002F4211964.html%23fulu\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E附录\u003C\u002Fa\u003E部分,当然英文版的你的vs就有。\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E直观的印象有了,但是离我们的短期目标,说清楚(或者说介绍个大概)CIL是What,甚至是终极目标,搞明白Uniyt3D为何能跨平台还有2万4千9百里的距离。\u003C\u002Fp\u003E\u003Cp\u003E好啦,话不多说,继续乱侃。\u003C\u002Fp\u003E\u003Cp\u003E参照附录中的操作码表,对照可以总结出一份更易读的表格。那就是如下的表啦。\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&830\& data-rawwidth=\&1590\& src=\&https:\u002F\u002Fpic4.zhimg.com\u002Fb0badba378f56c3c4c2b04b2fa69c9cf_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&1590\& data-original=\&https:\u002F\u002Fpic4.zhimg.com\u002Fb0badba378f56c3c4c2b04b2fa69c9cf_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&830\& data-rawwidth=\&1590\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='1590'%20height='830'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&1590\& data-original=\&https:\u002F\u002Fpic4.zhimg.com\u002Fb0badba378f56c3c4c2b04b2fa69c9cf_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic4.zhimg.com\u002Fb0badba378f56c3c4c2b04b2fa69c9cf_b.jpg\&\u003E\u003C\u002Ffigure\u003E在此,小匹夫想请各位认真读表,然后心中默数3个数,最后看看都能发现些什么。\u003Ch4\u003E基于堆栈\u003C\u002Fh4\u003E\u003Cp\u003E如果是小匹夫的话,第一感觉就是基本每一条描述中都包含一个”栈“。不错,CIL是基于堆栈的,也就是说CIL的VM(mono运行时)是一个栈式机。这就意味着数据是推入堆栈,通过堆栈来操作的,而非通过CPU的寄存器来操作,这更加验证了其和具体的CPU架构没有关系。为了说明这一点,小匹夫举个例子好啦。\u003C\u002Fp\u003E\u003Cp\u003E大学时候学单片机(大概是8086,记不清了)的时候记得做加法大概是这样的:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eadd eax,-2\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E其中的eax是啥?寄存器。所以如果CIL处理数据要通过cpu的寄存器的话,那也就不可能和cpu的架构无关了。\u003C\u002Fp\u003E\u003Cp\u003E当然,CIL之所以是基于堆栈而非CPU的另一个原因是相比较于cpu的寄存器,操作堆栈实在太简单了。回到刚才小匹夫说的大学时候曾经学过的单片\n机那门课程上,当时记得各种寄存器,各种标志位,各种。。。,而堆栈只需要简单的压栈和弹出,因此对于虚拟机的实现来说是再合适不过了。所以想要更具体的\n了解CIL基于堆栈这一点,各位可以去看一下堆栈方面的内容。这里小匹夫就不拓展了。\u003C\u002Fp\u003E\u003Ch4\u003E面向对象\u003C\u002Fh4\u003E\u003Cp\u003E那么第二感觉呢?貌似附录的表中有new对象的语句呀。嗯,的确,CIL同样是面向对象的。\u003C\u002Fp\u003E\u003Cp\u003E这意味着什么呢?那就是在CIL中你可以创建对象,调用对象的方法,访问对象的成员。而这里需要注意的就是对方法的调用。\u003C\u002Fp\u003E\u003Cp\u003E回到上表中的右上角。对,就是对参数的操作部分。静态方法和实例方法是不同的哦~\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E静态方法:ldarg.0么有被占用,所以参数从ldarg.0开始。\u003C\u002Fli\u003E\u003Cli\u003E实例方法:ldarg.0是被this占用的,也就是说实际上的参数是从ldarg.1开始的。\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E举个例子:假设你有一个类Murong中有一个静态方法Add(int32 a, int32 b),实现的内容就如同它的名字一样使两个数相加,所以需要2个参数。和一个实例方法TellName(string name),这个方法会告诉你传入的名字。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eclass
Murong\n{\n
public void TellName(string name)\n
System.Console.WriteLine(name);\n
public static int Add(int a, int b)\n
return a +\n
}\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cbr\u003E\u003Ch4\u003E静态方法的处理:\u003C\u002Fh4\u003E\u003Cp\u003E那么其中的静态方法Add的CIL代码如下:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F小匹夫注释一下。\n.method public hidebysig static int32
Add(int32 a,\n
int32 b) cil managed\n{\n
\u002F\u002F 代码大小
.locals init ([0] int32 CS$1$0000)
\u002F\u002F初始化局部变量列表。因为我们只返回了一个int型。所以这里声明了一个int32类型。索引为0\n
\u002F\u002F将索引为 0 的参数加载到计算堆栈上。\n
\u002F\u002F将索引为 1 的参数加载到计算堆栈上。\n
\u002F\u002F计算\n
\u002F\u002F从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。\n
\u002F\u002F将索引 0 处的局部变量加载到计算堆栈上。\n
\u002F\u002F返回该值\n} \u002F\u002F end of method Murong::Add\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E那么我们调用这个静态函数应该就是这样咯。\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003EMurong.Add(1, 2);\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E对应的CIL代码为:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E
ldc.i4.1 \u002F\u002F将整数1压入栈中\n
ldc.i4.2 \u002F\u002F将整数2压入栈中\n
int32 Murong::Add(int32,\n
\u002F\u002F调用静态方法\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E可见CIL直接call了Murong的Add方法,而不需要一个Murong的实例。\u003C\u002Fp\u003E\u003Ch4\u003E实例方法的处理:\u003C\u002Fh4\u003E\u003Cp\u003EMurong类中的实例方法TellName()的CIL代码如下:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E.method public hidebysig instance void
TellName(string name) cil managed\n{\n
\u002F\u002F 代码大小
\u002F\u002F看到和静态方法的区别了吗?\n
void [mscorlib]System.Console::WriteLine(string)\n
ret\n} \u002F\u002F end of method Murong::TellName\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E看到和静态方法的区别了吗?对,第一个参数对应的是ldarg.1中的参数1,而不是静态方法中的0。因为此时参数0相当于this,this是不用参与参数传递的。\u003Cp\u003E那么我们再看看调用实例方法的C#代码和对应的CIL代码是如何的。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002FC#\nMurong murong = new Murong();\nmurong.TellName(\&chenjiadong\&);\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003ECIL:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E.locals init ([0] class Murong murong)
\u002F\u002F因为C#代码中定义了一个Murong类型的变量,所以局部变量列表的索引0为该类型的引用。\n\u002F\u002F....\nIL_0009:
instance void Murong::.ctor() \u002F\u002F相比上面的静态方法的调用,此处new一个新对象,出现了instance方法。\nIL_000e:
stloc.0\nIL_000f:
ldloc.0\nIL_0010:
\&chenjiadong\& \u002F\u002F小匹夫的名字入栈\nIL_0015:
instance void Murong::TellName(string) \u002F\u002F实例方法的调用也有instance\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E到此,受制于篇幅所限(小匹夫不想写那么多字啊啊啊!)CIL是What的问题大致介绍一下。当然没有再拓展,以后小匹夫可能会再详细写一下这块。\u003Cbr\u003E\u003Cbr\u003E\u003Ch2\u003EHow?\u003C\u002Fh2\u003E\u003Cp\u003E记得语文老师说过,写作文最重要的一点是要首尾呼应。既然咱们开篇就提出了U3D为何能跨平台的问题,那么接近文章的结尾咱们就再来\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E提问:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003EQ:上面的Why部分,咱们知道了U3D能跨平台是因为存在着一个能通吃的中间语言CIL,这也是所谓跨平台的前提,但是为啥CIL能通吃各大平台呢?当然可以说CIL基于堆栈,跟你CPU怎么架构的没啥关系,但是感觉过于理论化、学术化,那还有没有通俗化、工程化的说法呢?\u003C\u002Fp\u003E\u003Cp\u003EA:原因就是前面小匹夫提到过的,.Net运行时和Mono运行时。也就是说CIL语言其实是运行在虚拟机中的,具体到咱们的U3D也就是mono的运行时了,换言之mono运行的其实CIL语言,CIL也并非真正的在本地运行,而是在mono运行时中运行的,运行在本地的是被编译后生成的原生代码。当然看官博的文章,他们似乎也在开发自己的“mono”,也就是被称为脚本的未来的IL2Cpp,这种类似运行时的功能是将IL再编译成c++,再由c++编译成原生代码,据说效率提升很可观,小匹夫也是蛮期待的。\u003C\u002Fp\u003E\u003Cp\u003E这里为了“实现跨平台式的演示”,小匹夫用mac给各位做个测试好啦:\u003C\u002Fp\u003E\u003Ch4\u003E从C#到CIL\u003C\u002Fh4\u003E\u003Cp\u003E新建一个cs文件,然后使用mono来运行。这个cs文件内容如下:\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&266\& data-rawwidth=\&518\& src=\&https:\u002F\u002Fpic3.zhimg.com\u002Ff70704feb61139d5fedea5ee5c528c65_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&518\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002Ff70704feb61139d5fedea5ee5c528c65_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&266\& data-rawwidth=\&518\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='518'%20height='266'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&518\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002Ff70704feb61139d5fedea5ee5c528c65_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic3.zhimg.com\u002Ff70704feb61139d5fedea5ee5c528c65_b.jpg\&\u003E\u003C\u002Ffigure\u003E然后咱们直接在命令行中运行这个cs文件试试~\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&322\& data-rawwidth=\&747\& src=\&https:\u002F\u002Fpic4.zhimg.com\u002Ffd9bbbf9a127fc67c6298d_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&747\& data-original=\&https:\u002F\u002Fpic4.zhimg.com\u002Ffd9bbbf9a127fc67c6298d_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&322\& data-rawwidth=\&747\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='747'%20height='322'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&747\& data-original=\&https:\u002F\u002Fpic4.zhimg.com\u002Ffd9bbbf9a127fc67c6298d_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic4.zhimg.com\u002Ffd9bbbf9a127fc67c6298d_b.jpg\&\u003E\u003C\u002Ffigure\u003E说的很清楚,文件没有包含一个CIL映像。可见mono是不能直接运行cs文件的。假如我们把它编译成CIL呢?那么我们用mono带的mcs来编译小匹夫的Test.cs文件。\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Emcs Test.cs\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E生成了什么呢?如图:\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&236\& data-rawwidth=\&675\& src=\&https:\u002F\u002Fpic3.zhimg.com\u002F098172dfdf80f_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&675\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F098172dfdf80f_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&236\& data-rawwidth=\&675\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='675'%20height='236'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&675\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F098172dfdf80f_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic3.zhimg.com\u002F098172dfdf80f_b.jpg\&\u003E\u003C\u002Ffigure\u003E\u003Cp\u003E好像没见有叫.IL的文件生成啊?反而好像多了一个.exe文件?可是没听说Mac能运行exe文件呀?可为啥又生成了.exe呢?各位看官可能要说,小匹夫你是不是拿windows截图P的啊?嘿嘿,小匹夫可不敢。辣么真相其实就是这个exe并不是让Mac来运行的,而是留给mono运行时来运行的,换言之这个文件的可执行代码形式是CIL的位元码形态。到此,我们完成了从C#到CIL的过程。接下来就让我们运行下刚刚的成果好啦。\u003C\u002Fp\u003E\u003Cp\u003Emono Test.exe\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&137\& data-rawwidth=\&620\& src=\&https:\u002F\u002Fpic3.zhimg.com\u002Fa2b4ad2a4d519a2eebb44f_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&620\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002Fa2b4ad2a4d519a2eebb44f_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&137\& data-rawwidth=\&620\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='620'%20height='137'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&620\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002Fa2b4ad2a4d519a2eebb44f_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic3.zhimg.com\u002Fa2b4ad2a4d519a2eebb44f_b.jpg\&\u003E\u003C\u002Ffigure\u003E 结果是输出了一个大大的“Hi”。这里,就引出了下一个部分。\n\u003C\u002Fp\u003E\u003Ch4\u003E从CIL到Native Code\u003C\u002Fh4\u003E\u003Cp\u003E这个“HI”可是在小匹夫的MAC终端上出现的呀,那么就证明这个C#写的代码在MAC上运行的还挺“嗨”。\u003C\u002Fp\u003E\u003Cp\u003E为啥呢?为啥C#写的代码能跑在MAC上呢?这就不得不提从CIL如何到本机原生代码的过程了。Mono提供了两种编译方式,就是我们经常能看到的:JIT(Just-in-Time compilation,即时编译)和AOT(Ahead-of-Time,提前编译或静态编译)。这两种方式都是将CIL进一步编译成平台的原生代码。这也是实现跨平台的最后一步。下面就分头介绍一下。\u003C\u002Fp\u003E\u003Ch4\u003EJIT即时编译:\u003C\u002Fh4\u003E\u003Cp\u003E从名字就能看的出来,即时编译,或者称之为动态编译,是在程序执行时才编译代码,解释一条语句执行一条语句,即将一条中间的托管的语句翻译成一条机器语句,然后执行这条机器语句。但同时也会将编译过的代码进行缓存,而不是每一次都进行编译。所以可以说它是静态编译和解释器的结合体。不过你想想机器既要处理代码的逻辑,同时还要进行编译的工作,所以其运行时的效率肯定是受到影响的。因此,Mono会有一部分代码通过AOT静态编译,以降低在程序运行时JIT动态编译在效率上的问题。\u003C\u002Fp\u003E\u003Cp\u003E不过一向严苛的IOS平台是不允许这种动态的编译方式的,这也是U3D官方无法给出热更新方案的一个原因。而Android平台恰恰相反,Dalvik虚拟机使用的就是JIT方案。\u003C\u002Fp\u003E\u003Ch4\u003EAOT静态编译:\u003C\u002Fh4\u003E\u003Cp\u003E其实Mono的AOT静态编译和JIT并非对立的。AOT同样使用了JIT来进行编译,只不过是被AOT编译的代码在程序运行之前就已经编译好了。当然还有一部分代码会通过JIT来进行动态编译。下面小匹夫就手动操作一下mono,让它进行一次AOT编译。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F在命令行输入\nmono --aot Test.exe\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E结果:\u003C\u002Fp\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&439\& data-rawwidth=\&1260\& src=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&1260\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&439\& data-rawwidth=\&1260\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='1260'%20height='439'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&1260\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_b.jpg\&\u003E\u003C\u002Ffigure\u003E从图中可以看到JIT time: 39 ms,也就是说Mono的AOT模式其实会使用到JIT,同时我们看到了生成了一个适应小匹夫的MAC的动态库Test.exe.dylib,而在Linux生成就是.so(共享库)。\n\u003C\u002Fp\u003E\u003Cp\u003EAOT编译出来的库,除了包括我们的代码之外,还有被缓存的元数据信息。所以我们甚至可以只编译元数据信息而不变异代码。例如这样:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F只包含元数据的信息\nmono --aot=metadata-only Test.exe\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cnoscript\u003E\u003Cimg data-rawheight=\&439\& data-rawwidth=\&1260\& src=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_b.jpg\& class=\&origin_image zh-lightbox-thumb\& width=\&1260\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_r.jpg\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg data-rawheight=\&439\& data-rawwidth=\&1260\& src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='1260'%20height='439'&&\u002Fsvg&\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&1260\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_r.jpg\& data-actualsrc=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a152bd46d2d43a4e1d4664_b.jpg\&\u003E\u003C\u002Ffigure\u003E可见代码没有被包括进来。\n\u003C\u002Fp\u003E\u003Cp\u003E那么简单总结一下AOT的过程:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E收集要被编译的方法\u003C\u002Fli\u003E\u003Cli\u003E使用JIT进行编译\u003C\u002Fli\u003E\u003Cli\u003E发射(Emitting)经JIT编译过的代码和其他信息\u003C\u002Fli\u003E\u003Cli\u003E直接生成文件或者调用本地汇编器或连接器进行处理之后生成文件。(例如上图中使用了小匹夫本地的gcc)\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Ch4\u003EFull AOT\u003C\u002Fh4\u003E\u003Cp\u003E当然上文也说了,IOS平台是禁止使用JIT的,可看样子Mono的AOT模式仍然会保留一部分代码会在程序运行时动态编译。所以为了破解这个问题,Mono提供了一个被称为Full AOT的模式。即预先对程序集中的所有CIL代码进行AOT编译生成一个本地代码映像,然后在运行时直接加载这个映像而不再使用JIT引擎。目前由于技术或实现上的原因在使用Full AOT时有一些限制,不过这里不再多说了。以后也还会更细的分析下AOT。\u003C\u002Fp\u003E\u003Ch4\u003E总结:\u003C\u002Fh4\u003E\u003Cp\u003E好啦,写到现在也已经到了凌晨3:04分了。感觉写的内容也差不多了。那么对本文的主题U3D为何能跨平台以及CIL做个最终的总结陈词:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003ECIL是CLI标准定义的一种可读性较低的语言。\u003C\u002Fli\u003E\u003Cli\u003E以.NET或mono等实现CLI标准的运行环境为目标的语言要先编译成CIL,之后CIL会被编译,并且以位元码的形式存在(源代码---&中间语言的过程)。\u003C\u002Fli\u003E\u003Cli\u003E这种位元码运行在虚拟机中(.net mono的运行时)。\u003C\u002Fli\u003E\u003Cli\u003E这种位元码可以被进一步编译成不同平台的原生代码(中间语言---&原生代码的过程)。\u003C\u002Fli\u003E\u003Cli\u003E面向对象\u003C\u002Fli\u003E\u003Cli\u003E基于堆栈\u003C\u002Fli\u003E\u003C\u002Fol\u003E&,&updated&:new Date(&T08:31:53.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:0,&collapsedCount&:0,&likeCount&:5,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\u002Fpic1.zhimg.com\u002Fcf7a7f19da2411_r.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&reviewers&:[],&topics&:[],&adminClosedComment&:false,&titleImageSize&:{&width&:500,&height&:476},&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&column&:{&slug&:&chenjiadong&,&name&:&Runtime&},&tipjarState&:&inactivated&,&annotationAction&:[],&sourceUrl&:&&,&pageCommentsCount&:0,&hasPublishingDraft&:false,&snapshotUrl&:&&,&publishedTime&:&T16:31:53+08:00&,&url&:&\u002Fp\u002F&,&lastestLikers&:[{&bio&:&LeanCloud,为应用开发加速!&,&isFollowing&:false,&hash&:&5f9bd4c9ffcefe6ca7849d&,&uid&:52,&isOrg&:false,&slug&:&jwfing&,&isFollowed&:false,&description&:&十年老码农&,&name&:&丰俊文&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fjwfing&,&avatar&:{&id&:&da8e974dc&,&template&:&https:\u002F\u002Fpic4.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&Game design&,&isFollowing&:false,&hash&:&b3ea249edc0d5ee9b48d7&,&uid&:80,&isOrg&:false,&slug&:&liang-li-ya&,&isFollowed&:false,&description&:&人间五十年&,&name&:&梁莉亚&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fliang-li-ya&,&avatar&:{&id&:&a371b35db23c7e&,&template&:&https:\u002F\u002Fpic3.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&一个虽然想要变强,但现下满怀内疚的继续瘫着的家伙&,&isFollowing&:false,&hash&:&4df41a13c394ccc&,&uid&:40,&isOrg&:false,&slug&:&OniSaku&,&isFollowed&:false,&description&:&为了成立同人社团而奋斗着的剧毒少女深度中毒患者&,&name&:&OniSaku&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002FOniSaku&,&avatar&:{&id&:&v2-2f6b766aed&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&码医生&,&isFollowing&:false,&hash&:&06de10cfd08dfc9ea73df0&,&uid&:36,&isOrg&:false,&slug&:&ma-yi-sheng-45&,&isFollowed&:false,&description&:&&,&name&:&码医生&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fma-yi-sheng-45&,&avatar&:{&id&:&da8e974dc&,&template&:&https:\u002F\u002Fpic4.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&huster&,&isFollowing&:false,&hash&:&55ec9b52&,&uid&:08,&isOrg&:false,&slug&:&li-chen-xi-8407&,&isFollowed&:false,&description&:&Unity3d学习者 桌游爱好者&,&name&:&李晨曦&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fli-chen-xi-8407&,&avatar&:{&id&:&31da616da&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false}],&summary&:&\u003Cimg data-rawheight=\&224\& data-rawwidth=\&248\& src=\&http:\u002F\u002Fpic4.zhimg.com\u002Fe3a003e5e0be3fc1e2bcf3_200x112.png\& class=\&origin_image inline-img zh-lightbox-thumb\& data-original=\&http:\u002F\u002Fpic4.zhimg.com\u002Fe3a003e5e0be3fc1e2bcf3_r.png\&\u003E前言:其实小匹夫在U3D的开发中一直对U3D的跨平台能力很好奇。到底是什么原理使得U3D可以跨平台呢?后来发现了Mono的作用,并进一步了解到了\nCIL的存在。所以,作为一个对Unity3D跨平台能力感兴趣的U3D程序猿,小匹夫如何能不关注CIL这个话题呢?那么下面各…&,&reviewingCommentsCount&:0,&meta&:{&previous&:{&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\u002Fpic4.zhimg.com\u002F50\u002Fa82dd821dcb0d_xl.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&topics&:[{&url&:&https:\u002F\u002Fwww.zhihu.com\u002Ftopic\u002F&,&id&:&&,&name&:&Unity(游戏引擎)&},{&url&:&https:\u002F\u002Fwww.zhihu.com\u002Ftopic\u002F&,&id&:&&,&name&:&游戏开发&},{&url&:&https:\u002F\u002Fwww.zhihu.com\u002Ftopic\u002F&,&id&:&&,&name&:&C#&}],&adminClosedComment&:false,&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&author&:{&bio&:&慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01&,&isFollowing&:false,&hash&:&2ad398c4fbe830&,&uid&:88,&isOrg&:false,&slug&:&jiadongchen&,&isFollowed&:false,&description&:&编程首先是爱好,其次才是职业。http:\u002F\u002Fwww.chenjd.me\u002F\n\n我已加入“维权骑士”(rightknights.com)的版权保护计划。&,&name&:&陈嘉栋&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fjiadongchen&,&avatar&:{&id&:&v2-e580cadf61b81a00e975c5a&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&column&:{&slug&:&chenjiadong&,&name&:&Runtime&},&content&:&\u003Ch2\u003E0x00 前言\u003C\u002Fh2\u003E\u003Cp\u003E因为临近年关工作繁忙,已经有一段时间没有更新博客了。到了元旦终于有时间来写点东西,既是积累也是分享。如题目所示,本文要来聊一聊在游戏开发中\n经常会涉及到的话题——游戏AI。设计游戏AI的目标之一是要找到一种便于使用并容易拓展的的方案,常见的一些游戏AI方案包括了有限状态机(FSM)、\n分层有限状态机(HFSM)、面向目标的动作规划(GOAP)以及分层任务网络(HTN)和行为树(BT)等等。下面我们就来聊一聊比较有代表性的游戏\nAI方案——状态机。\u003C\u002Fp\u003E\u003Ch2\u003E0x01 有限状态机(FSM)\u003C\u002Fh2\u003E\u003Cp\u003E有限状态自动机 (Finite State Machine,FSM)是表示有限多个状态以及在这些状态(State)之间转移(Transition)和动作(Action)的数学模型。有限状态机的模型体现了两点:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E状态首先是离散的:某一时刻只能处于某种状态之下,且需要满足某种条件才能从一种状态转移到另一种状态。\u003C\u002Fli\u003E\u003Cli\u003E然后状态总数是有限的。\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E从它的定义,我们可以看到有限状态机的几个重要概念:\n\u003Cul\u003E\u003Cli\u003E状态(State):表示对象的某种形态,在当前形态下可能会拥有不同的行为和属性。\u003C\u002Fli\u003E\u003Cli\u003E转移(Transition):表示状态变更,并且必须满足确使转移发生的条件来执行。\u003C\u002Fli\u003E\u003Cli\u003E动作(Action):表示在给定时刻要进行的活动。\u003C\u002Fli\u003E\u003Cli\u003E事件(Event):事件通常会引起状态的变迁,促使状态机从一种状态切换到另一种状态。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E而状态机便是用来控制对象状态的管理器。在满足了某种条件或者说在某个特定的事件被触发之后,对象的状态便会通过转换来变成另外一种状态,而对象在不同的状态之下也有可能会有不同的行为和属性。\u003Cbr\u003E\n当然,有限状态机的应用范围很广,但是显然游戏开发是有限状态机最为成功的应用领域之一。除了游戏AI的实现可以依靠有限状态机之外,游戏逻辑以及动作切换都可以借助有限状态机来实现。因此游戏中的每个角色或者器件或者逻辑都有可能内嵌一个状态机。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Ch2\u003E0x02 HFSM分层有限状态机\u003C\u002Fh2\u003E\u003Cp\u003E如果我们仔细观察一个有限状态机的话,可以发现它在逻辑结构上是没有层次的,如果和行为树来做对比的话可以发现这一点十分明显。在行为树中,节点是\n有层次(Hierarchical)的,子节点由其父节点来控制。例如行为树中有一种节点叫做“序列(Sequence)节点”,它的作用是顺序执行所有\n子节点(如果某个子节点失败返回失败,否则返回成功)。而将行为树的这个优势应用到有限状态机上,分层有限状态机HFSM便诞生了。\u003C\u002Fp\u003E\u003Ch4\u003E分层的好处\u003C\u002Fh4\u003E\u003Cp\u003E那么引入了分层之后的HFSM到底带来了什么好处呢?\u003Cbr\u003E\n最大的好处便是在一定程度上规范了状态机的状态转换,从而有效地减少了状态之间的转换。\u003Cbr\u003E\n举一个简单的小例子:例如RTS游戏中的士兵。如果逻辑没有层次上的划分,那么我们对士兵所定义的若干状态,例如前进、寻敌、攻击、防御、逃跑等等,就需要在这些状态之间定义转移,因为它们是平级的,因此我们需要考虑每一组状态的关系,并维护一大堆没有侧重点的转移。\u003Cbr\u003E\n如果在逻辑上是分层的,我们就可以将士兵的这些状态进行一个分类,把几个低级的状态归并到一个高级的状态中,并且状态的转移只发生在同级的状态中。\u003Cbr\u003E\n例如高级状态包括战斗、撤退,而战斗状态中又包括了寻敌、攻击等几个小状态;撤退状态中又包括了防御、逃跑这几个小状态。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cimg data-rawheight=\&197\& data-rawwidth=\&604\& src=\&http:\u002F\u002Fpic3.zhimg.com\u002Fdbbaa5aab3b3e3a_b.png\& class=\&origin_image zh-lightbox-thumb\& width=\&604\& data-original=\&http:\u002F\u002Fpic3.zhimg.com\u002Fdbbaa5aab3b3e3a_r.png\&\u003E\u003C\u002Ffigure\u003E总而言之,分层状态机HFSM从某种程度上规范了状态机的状态转移,而且状态内的子状态不需要关心外部状态的跳转,这样也做到了无关状态间的隔离。\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Ch2\u003E0x03 有限状态机的实现\u003C\u002Fh2\u003E\u003Cp\u003E那么到底如何实现一个有限状态机呢?主要有两种方式来实现,即集中管理控制以及模块化管理。具体来说,这两种方式的实现如下:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E使用\u003Cb\u003Eswitch\u003C\u002Fb\u003E语句:所有的状态之间的转移逻辑全都写在一个部分,需要根据不同的分支来判断转移条件是否符合。\u003C\u002Fli\u003E\u003Cli\u003E使用\u003Cb\u003E状态模式(State Pattern)\u003C\u002Fb\u003E:一种常见的设计模式。在状态模式中,我们为每个状态创建与之对应的类,这样就将状态转移的逻辑从臃肿的switch语句中分散到了各个类中。\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cp\u003E了解了有限状态机大体上可以分为这两种实现方式,那么接下来我们就具体来看一看这两种方式是如何实现的。\u003C\u002Fp\u003E\u003Ch4\u003Eswitch语句\u003C\u002Fh4\u003E\u003Cp\u003E在实现有限状态机时,使用switch语句是最简单同时也是最直接的一种方式。这种方式的基本思路是为状态机中的每一种\u003Cb\u003E状态\u003C\u002Fb\u003E都设置一个\u003Cb\u003Ecase\u003C\u002Fb\u003E分支,专门用来对该状态进行控制。\u003Cbr\u003E\u003Cfigure\u003E\u003Cimg data-rawheight=\&362\& data-rawwidth=\&530\& src=\&http:\u002F\u002Fpic2.zhimg.com\u002Fc124becb3305_b.png\& class=\&origin_image zh-lightbox-thumb\& width=\&530\& data-original=\&http:\u002F\u002Fpic2.zhimg.com\u002Fc124becb3305_r.png\&\u003E\u003C\u002Ffigure\u003E\u003Cbr\u003E\n上图是一个具体的使用有限状态机实现游戏AI的场景,描述的是一个游戏单位的AI,下面我们就使用switch语句来实现图中的状态机。\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eswitch (state)
\u002F\u002F 处理状态Waiting的分支\n
case State.Waiting: \n
\u002F\u002F 执行等待\n
\u002F\u002F 检查是否有可以攻击\n
if (canAttack()){\n
\u002F\u002F 当前状态转换为Attacking\n
changeState(State.Attacking);\n
\u002F\u002F 若不可攻击,则检查是否有可以移动\n
else if (canMove()) { \n
\u002F\u002F 当前状态转换为Moving\n
changeState(State.Moving)\n
\u002F\u002F 处理状态Moving的分支\n
case State.Moving: \n
\u002F\u002F 执行动作move\n
\u002F\u002F 检查是否可以攻击敌人\n
if (canAttack()) {\n
\u002F\u002F 当前状态转换为Attacking\n
changeState(State.Attacking);\n
\u002F\u002F 若不可攻击,则检查是否可以等待\n
else if (canWait()) {\n
\u002F\u002F 当前状态转换为Waiting\n
changeState(State.Waiting);\n
\u002F\u002F 处理状态Attacking的分支\n
case State.Attacking: \n
\u002F\u002F 执行攻击attack\n
attack();\n
\u002F\u002F 检查是否可以等待\n
if (canWait()) {\n
\u002F\u002F 当前状态转换为Waiting\n
changeState(State.Waiting);\n
}\\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E通过这个小例子,我们可以看到使用switch语句实现的有限状态机的确可以很好的运行。不过我们还可以发现这种方式在实现状态之间的转换时,\u003Cb\u003E1.检查转换条件\u003C\u002Fb\u003E以及\u003Cb\u003E2.进行状态转换\u003C\u002Fb\u003E的代码都是混杂在当前的状态分支中来完成的,这样就会导致代码的可读性降低甚至会增加日后的维护成本。\u003Cbr\u003E\n这是因为在每个具体的状态下,都需要检查多个具体的转换条件,对符合条件的还需要转移到新的具体的状态,这样的代码是难以维护的,因为它们需要在具体的情\n况下处理具体的事物。即便我们将检查转换条件和进行状态转换的代码分别封装成两个专门的函数FuncA(检查转换条件)和FuncB(进行状态转\n换),switch语句中各个具体状态的代码可能会更加清晰。但是随着逻辑复杂度的增加,FuncA和FuncB这两个函数本身的复杂度可能也会增加,甚\n至最后变得臃肿不堪。\u003C\u002Fp\u003E\u003Ch4\u003E状态模式\u003C\u002Fh4\u003E\u003Cp\u003E当控制一个对象状态转换的条件表达式过于复杂时,把状态的判断逻辑转移到一系列类当中,可以把复杂的逻辑判断简单化。因此,使用状态模式来实现状态机虽然不如直接使用switch语句来的直接,但是对于状态更易维护也更易拓展。下面我们就来看一看状态模式中的角色:\u003C\u002Fp\u003E\u003Cp\u003E  1. 上下文环境(Context):它定义了客户程序需要的接口并维护一个具体状态的实例,将与状态相关的操作(1.检查转换条件;2.进行状态转换)交给当前的具体状态对象来处理。\u003Cbr\u003E\n  2. 抽象状态(State):定义一个接口以封装使用上下文环境的的一个特定状态相关的行为。\u003Cbr\u003E\n  3. 具体状态(Concrete State):实现抽象状态定义的接口。\u003Cbr\u003E\n下面,我们就按照这三个角色来实现上一小节图中的状态机吧。\u003Cbr\u003E\u003Cb\u003Econtext类\u003C\u002Fb\u003E:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Epublic class Context\n{\n
private S\n\n
public Context(State state)\n
this.state =\n
public void Do()\n
state.CheckAndTran(this);\n
}\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cb\u003E抽象状态类:\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Epublic abstract class State\n{\n
public abstract void CheckAndTran(Context context);\n}\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cb\u003E具体状态类\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Epublic class WaitingState : State\n{\n
public override void CheckAndTran(Context context)\n
\u002F\u002F执行等待动作\n
\u002F\u002F检查是否可以攻击敌人\n
if (canAttack()){\n
\u002F\u002F 当前状态转换为Attacking\n
context.State = new AttackingState();\n
\u002F\u002F 若不可攻击,则检查是否有可以移动\n
else if (canMove()) { \n
\u002F\u002F 当前状态转换为Moving\n
context.State = new MovingState();\n
}\n}\n...\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E虽然看似状态模式缓解了使用switch语句那种代码臃肿、可读性维护性差的问题,但是状态模式并非没有自己的缺点。可以看出状态模式的使用必然会增加类和对象的个数,如果使用不当将导致程序结构和代码的混乱。\u003C\u002Fp\u003E\u003Ch2\u003E0x04 褒扬和批判\u003C\u002Fh2\u003E\u003Cp\u003E在游戏开发中使用状态机显然不失为一种不错的选择,首先它的概念并不复杂,其次它的实现也十分简单而直接。但它的缺点却也十分明显,例如难以复用,\n因为它往往需要根据具体的情况来做出反应,当然当状态机的模型复杂到一定的程度之后,也会带来实现和维护上的困难。如何选择,可能就是一个仁者见仁智者见\n智的问题了。\u003C\u002Fp\u003E&,&state&:&published&,&sourceUrl&:&&,&pageCommentsCount&:0,&canComment&:false,&snapshotUrl&:&&,&slug&:,&publishedTime&:&T13:49:05+08:00&,&url&:&\u002Fp\u002F&,&title&:&趣说游戏AI开发:对状态机的褒扬和批判&,&summary&:&0x00 前言因为临近年关工作繁忙,已经有一段时间没有更新博客了。到了元旦终于有时间来写点东西,既是积累也是分享。如题目所示,本文要来聊一聊在游戏开发中\n经常会涉及到的话题——游戏AI。设计游戏AI的目标之一是要找到一种便于使用并容易拓展的的方案,…&,&reviewingCommentsCount&:0,&meta&:{&previous&:null,&next&:null},&commentPermission&:&anyone&,&commentsCount&:3,&likesCount&:16},&next&:{&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\u002Fpic4.zhimg.com\u002F50\u002Fa78cfde32d5e60e796f997_xl.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&topics&:[],&adminClosedComment&:false,&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&author&:{&bio&:&慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01&,&isFollowing&:false,&hash&:&2ad398c4fbe830&,&uid&:88,&isOrg&:false,&slug&:&jiadongchen&,&isFollowed&:false,&description&:&编程首先是爱好,其次才是职业。http:\u002F\u002Fwww.chenjd.me\u002F\n\n我已加入“维权骑士”(rightknights.com)的版权保护计划。&,&name&:&陈嘉栋&,&profileUrl&:&https:\u002F\u002Fwww.zhihu.com\u002Fpeople\u002Fjiadongchen&,&avatar&:{&id&:&v2-e580cadf61b81a00e975c5a&,&template&:&https:\u002F\u002Fpic1.zhimg.com\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&column&:{&slug&:&chenjiadong&,&name&:&Runtime&},&content&:&\u003Ch2\u003E0x00 前言\u003C\u002Fh2\u003E\u003Cp\u003E首先要说明的是,本文的标题事实上来自于知乎上的一个同名问题:\u003Ca class=\&internal\& href=\&https:\u002F\u002Fwww.zhihu.com\u002Fquestion\u002F\&\u003E为什么directX里表示三维坐标要建一个4*4的矩阵? - 编程\u003C\u002Fa\u003E
因此,正如Milo Yip大神所说的这个标题事实上是存在问题的:矩阵是用于表示变换而不是坐标的。再了解了矩阵的作用之后,我们就要继续思考为什么变换要使用一个4×4的矩阵而不是3×3的矩阵呢?是不是多此一举呢?下面我们就来聊聊这个话题。\u003C\u002Fp\u003E\u003Ch2\u003E0x01 怎么平移一个三维空间中的点?\u003C\u002Fh2\u003E\u003Cp\u003E我们应该怎么平移一个三维空间中的点呢?答案很简单,我们只需要对这个点的坐标中的每个分量(x,y,z)和对应轴上的平移距离相加即可。\u003C\u002Fp\u003E\u003Cp\u003E例如,点p1(x1,y1,z1)在X轴Y轴以及Z轴上分别平移Δx,Δy,Δz到新的点p2(x2,y2,z2),那么我们只需在坐标对应的分量上加上这些增量就可以确定点p2的坐标了。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cfigure\u003E\u003Cimg src=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a49ada5c150dcb_b.jpg\& data-rawwidth=\&699\& data-rawheight=\&479\& class=\&origin_image zh-lightbox-thumb\& width=\&699\& data-original=\&https:\u002F\u002Fpic3.zhimg.com\u002F5a49ada5c150dcb_r.jpg\&\u003E\u003C\u002Ffigure\u003Ex2 = x1 + Δx\u003C\u002Fp\u003E\u003Cp\u003Ey2 = y1 + Δy\u003C\u002Fp\u003E\u003Cp\u003Ez2 = z1 + Δz
\u003C\u002Fp\u003E\u003Cp\u003E很简单是吗?那么接下来再让我们来看一看另一种变换:旋转。\u003C\u002Fp\u003E\u003Ch2\u003E0x02 再来旋转一个点\u003C\u002Fh2\u003E\u003Cp\u003E旋转相比较平移来说,会略显复杂一些。因为我们需要指明以下几个方面来描述一个旋转:\u003C\u002Fp\u003E\u003Col\u003E\u003Cli\u003E旋转轴\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E旋转方向\u003Cbr\u003E\u003C\u002Fli\u003E\u003Cli\u003E旋转角度\u003Cbr\u003E\u003C\u002Fli\u003E\u003C\u002Fol\u003E\u003Cbr\u003E\u003Cp\u003E在这里,我们假设点p需要绕Z轴顺时针旋转β度。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cfigure\u003E\u003Cimg src=\&https:\u002F\u002Fpic4.zhimg.com\u002F58a9e26cffb7d5cae3a1ec_b.jpg\& data-rawwidth=\&728\& data-rawheight=\&460\& class=\&origin_image zh-lightbox-thumb\& width=\&728\& data-original=\&https:\u002F\u002Fpic4.zhimg.com\u002F58a9e26cffb7d5cae3a1ec_r.jpg\&\u003E\u003C\u002Ffigure\u003E\u003Cp\u003E如这个很难看的图所示,我们的点P1(x1,y1,z1)以Z轴位轴顺时针旋转β度之后来到了点P2(x2,y2,z2)。接下来,让我们假设原点到P1的距离位L,且P1和Y轴之间的夹角位α,那么根据三角函数公式我们就可以计算出P1点的具体坐标了:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E x1 = L·sinα\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E y1 = L·cosα\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E由于是绕Z轴旋转,因此z坐标不变,因此此处不考虑。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E同样根据三角函数公式,我们可以继续计算出P2的具体坐标:\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ex2 = L·sin(α + β)\u003C\u002Fp\u003E\u003Cp\u003Ey2 = L·cos(α + β)\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E再让我们回忆一下中学时代的几何数学的内容,对青春的回忆又把我们带回了课堂上老师声嘶力竭向我们传授的内容——两角和与差:\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ecos(α+β)=cosα·cosβ-sinα·sinβ \u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003Ecos(α-β)=cosα·cosβ+sinα·sinβ\u003C\u002Fp\u003E\u003Cp\u003Esin(α+β)=sinα·cosβ+cosα·sinβ \u003C\u002Fp\u003E\u003Cp\u003Esin(α-β)=sinα·cosβ-cosα·sinβ\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E回忆起老师传授给我们的知识之后,接下来结合P1的坐标表示形式我们就可以把P2的坐标换一个表示形式了。\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ex2 = L·(sinα·cosβ+cosα·sinβ) = cosβ·x1 + sinβ·y1\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003Ey2 = L·(cosα·cosβ-sinα·sinβ) = cosβ·y1 - sinβ·x1\u003C\u002Fp\u003E\u003Cp\u003Ez2 = z1\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E因此,在已知P1点坐标以及旋转角度β的情况下,我们就可以根据以上公式分别求出P2点坐标的各个分量。如果再结合上一小节中平移一个点的公式来看,我们可以发现似乎并不需要矩阵,而仅仅通过两组表达式就能实现坐标的变换。但是......\u003C\u002Fp\u003E\u003Ch2\u003E0x03 带来便捷的矩阵\u003C\u002Fh2\u003E\u003Cp\u003E当然,从理论上讲我们的确可以只通过数学公式就能实现变换,但实际的情况却是在变换十分复杂时,直接使用数学表达式来进行运算也是相当繁复的。因此,在现实中常常使用矩阵(由m × n个标量组成的长方形数组)来表示诸如平移、旋转以及缩放等线性变换。而两个变换矩阵A和B的积P=AB,则变换矩阵P相当于A和B所代表的变换。举一个例子,若A为旋转矩阵,B为平移矩阵,则矩阵P就能够实现旋转和平移变换。不过需要注意的是,矩阵乘法不符合交换律,因此AB和BA并不相等。\u003C\u002Fp\u003E\u003Cp\u003E说了这么多,我们似乎还是没有回答为什么三维空间需要一个4×4矩阵来实现变换呢?下面我们就带着这个问题,分别看看3×3矩阵和4×4矩阵的使用吧。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E3×3矩阵和旋转\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E首先,我们先来看一个矩阵乘以一个三维矢量的算式:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\u002Fwww.zhihu.com\u002Fequation?tex=%0A%5Cbegin%7Balign%7D%5Cnotag+%0A%5Cbegin%7Bbmatrix%7Dx2%5C%5C%0Ay2%5C%5Cz2%5Cend%7Bbmatrix%7D+%26%3D%5Cbegin%7Bbmatrix%7Da%26b%26c%5C%5C%0Ad%26e%26f%5C%5Cg%26h%26i%5Cend%7Bbmatrix%7D%5Cbegin%7Bbmatrix%7Dx1%5C%5C%0Ay1%5C%5Cz1%5Cend%7Bbmatrix%7D%0A%5Cend%7Balign%7D%0A\& alt=\&\n\\begin{align}\\notag \n\\begin{bmatrix}x2\\\\\ny2\\\\z2\\end{bmatrix} &=\\begin{bmatrix}a&b&c\\\\\nd&e&f\\\\g&h&i\\end{bmatrix}\\begin{bmatrix}x1\\\\\ny1\\\\z1\\end{bmatrix}\n\\end{align}\n\& eeimg=\&1\&\u003E\u003Cbr\u003E\u003Cp\u003E可以看到该矩阵为一个3×3的矩阵,矩阵的右侧是点P1的坐标,而矩阵的左侧则是点P2的坐标。根据这个表达式,我们可以求出x2、y2、z2的值:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ex2 = a·x1 + b·y1 + c·z1\u003C\u002Fp\u003E\u003Cp\u003Ey2 = d·x1 + e·y1 + f·z1\u003C\u002Fp\u003E\u003Cp\u003Ez2 = g·x1 + h·y1 + i·z1\u003Cbr\u003E\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E为了将矩阵等式和之前小节的数学表达式联系起来,下面我们就将旋转表达式和该矩阵等式做一个对比。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ex2 = a·x1 + b·y1 + c·z1\u003C\u002Fp\u003E\u003Cp\u003Ex2 = cosβ·x1 + sinβ·y1\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Ey2 = d·x1 + e·y1 + f·z1\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003Ey2 = cosβ·y1 - sinβ·x1\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Ez2 = g·x1 + h·y1 + i·z1\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003Ez2 = z1\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E通过对比x2,我们可以发现a=cosβ,b=sinβ,c=0;\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E对比y2,也可以发现d=-sinβ,e=cosβ,f=0;\u003C\u002Fp\u003E\u003Cp\u003E最后对比z2,可以确定g=0,h=0,i=1;\u003C\u002Fp\u003E\u003Cp\u003E将这个结果带入到之前的矩阵中,我们的等式就可以变成下面这个样子:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\u002Fwww.zhihu.com\u002Fequation?tex=%5Cbegin%7Balign%7D%5Cnotag+%0A%5Cbegin%7Bbmatrix%7Dx2%5C%5C%0Ay2%5C%5Cz2%5Cend%7Bbmatrix%7D+%26%3D%5Cbegin%7Bbmatrix%7Dcos%CE%B2%26sin%CE%B2%260%5C%5C%0A-sin%CE%B2%26cos%CE%B2%260%5C%5C0%260%261%5Cend%7Bbmatrix%7D%5Cbegin%7Bbmatrix%7Dx1%5C%5C%0Ay1%5C%5Cz1%5Cend%7Bbmatrix%7D%0A%5Cend%7Balign%7D\& alt=\&\\begin{align}\\notag \n\\begin{bmatrix}x2\\\\\ny2\\\\z2\\end{bmatrix} &=\\begin{bmatrix}cosβ&sinβ&0\\\\\n-sinβ&cosβ&0\\\\0&0&1\\end{bmatrix}\\begin{bmatrix}x1\\\\\ny1\\\\z1\\end{bmatrix}\n\\end{align}\& eeimg=\&1\&\u003E\u003Cbr\u003E\u003Cp\u003E也就是说,通过这个3×3的变换矩阵,我们就已经实现了三维空间的旋转变换。那么为什么还需要使用4×4矩阵呢?\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E搞不定的平移,4×4矩阵来救场\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E我们已经通过一个3×3矩阵搞定了旋转变换,显然如果这个3×3矩阵真的是完美的解决变换的方案的话,那么它显然也必须要适合于其他的变换,例如平移。但是它到底能否满足平移的需求呢?下面我们还是通过对比矩阵等式和数学表达式的方式,来寻找答案。\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003Ex2 = a·x1 + b·y1 + c·z1\u003C\u002Fp\u003E\u003Cp\u003Ex2 = x1 + Δx\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Ey2 = d·x1 + e·y1 + f·z1\u003C\u002Fp\u003E\u003Cp\u003Ey2 = y1 + Δy\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cp\u003Ez2 = g·x1 + h·y1 + i·z1\u003C\u002Fp\u003E\u003Cp\u003Ez2 = z1 + Δz\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E通过对比,我们发现平移和旋转之间很有趣的一个区别,那就是平移的表达式中带有常量Δx,而无论是旋转的表达式还是矩阵等式中都不存在这样一个常量能够与之对应。那么问题就来,我们没有办法使用3×3的矩阵来表示平移。答案其实很简单,那就是使用4×4矩阵来实现。但是随之而来的一个问题就是如何一个三维坐标如何能和一个4×4的矩阵相乘呢?\u003Cbr\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E齐次坐标\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E为了解决三维矢量和4×4矩阵相乘的问题,我们机智的为三维矢量添加了第四个分量,这样之前的三维矢量(x,y,z)就变成了四维的(x,y,z,w),这样由4个分量组成的矢量便被称为齐次坐标。需要说明的是,齐次坐标(x,y,z,w)等价于三维坐标(x\u002Fw,y\u002Fw,z\u002Fw),因此只要w分量的值是1,那么这个齐次坐标就可以被当作三维坐标来使用,而且所表示的坐标就是以x,y,z这3个值为坐标值的点。\u003C\u002Fp\u003E\u003Cp\u003E因此,为了和4×4矩阵相乘,我们的P1点坐标就变成了(x1,y1,z1,1)。而矩阵等式也变成了下面这个样子:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\u002Fwww.zhihu.com\u002Fequation?tex=%5Cbegin%7Balign%7D%5Cnotag+%0A%5Cbegin%7Bbmatrix%7Dx2%5C%5C%0Ay2%5C%5Cz2%5C%5C1%5Cend%7Bbmatrix%7D+%26%3D%5Cbegin%7Bbmatrix%7Da%26b%26c%26d%5C%5C%0Ae%26f%26g%26h%5C%5Ci%26j%26k%26l%5C%5Cm%26n%26o%26p%5Cend%7Bbmatrix%7D%5Cbegin%7Bbmatrix%7Dx1%5C%5C%0Ay1%5C%5Cz1%5C%5C1%5Cend%7Bbmatrix%7D%0A%5Cend%7Balign%7D\& alt=\&\\begin{align}\\notag \n\\begin{bmatrix}x2\\\\\ny2\\\\z2\\\\1\\end{bmatrix} &=\\begin{bmatrix}a&b&c&d\\\\\ne&f&g&h\

我要回帖

更多关于 unity 跨平台 的文章

 

随机推荐