双开游戏tgp双开怎么同步步操作

聊聊多人游戏同步那点事 - 知乎专栏
{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"contributes":[{"sourceColumn":{"lastUpdated":,"description":"编程首先是爱好,其次才是职业。专注前沿技术,热爱开源。深信代码改变世界。没有值得吹嘘的项目,只有不断前行的动力。写有个人博客“嘉栋的Coding Blog”: http://www.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":"/chenjiadong","commentPermission":"COLUMN_ALL_CAN_COMMENT","canPost":true,"created":,"state":"COLUMN_NORMAL","followers":3124,"avatar":{"id":"v2-c4e393a91e7abcd","template":"/{id}_{size}.png"},"activateAuthorRequested":false,"following":false,"imageUrl":"/v2-c4e393a91e7abcd_l.png","articlesCount":49},"state":"accepted","targetPost":{"titleImage":"/v2-7d28dbb42bd5cbbbbae086_r.jpg","lastUpdated":,"imagePath":"v2-7d28dbb42bd5cbbbbae086.jpg","permission":"ARTICLE_PUBLIC","topics":[8],"summary":"0x00 前言16年年底的时候我从当时的公司离职,来到了目前任职的一家更专注于游戏开发的公司。接手的是一个platform游戏项目,基本情况是之前的团队完成了第一个版本,即单人模式的基础玩法,但是之后对该项目的定位又变成了一个本地局域网的联机手游(2-4个…","copyPermission":"ARTICLE_COPYABLE","translatedCommentPermission":"all","likes":0,"origAuthorId":0,"publishedTime":"T09:49:24+08:00","sourceUrl":"","urlToken":,"id":2361463,"withContent":false,"slug":,"bigTitleImage":false,"title":"聊聊多人游戏同步那点事","url":"/p/","commentPermission":"ARTICLE_ALL_CAN_COMMENT","snapshotUrl":"","created":,"comments":0,"columnId":8818,"content":"","parentId":0,"state":"ARTICLE_PUBLISHED","imageUrl":"/v2-7d28dbb42bd5cbbbbae086_r.jpg","author":{"bio":"慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01","isFollowing":false,"hash":"2ad398c4fbe830","uid":88,"isOrg":false,"slug":"jiadongchen","isFollowed":false,"description":"编程首先是爱好,其次才是职业。http://www.chenjd.me/\n\n我已加入“维权骑士”()的版权保护计划。","name":"陈嘉栋","profileUrl":"/people/jiadongchen","avatar":{"id":"v2-e3e4f2a706dc7ec9d2866d2aba059c0d","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},"memberId":1718122,"excerptTitle":"","voteType":"ARTICLE_VOTE_CLEAR"},"id":560168}],"title":"聊聊多人游戏同步那点事","author":"jiadongchen","content":"0x00 前言16年年底的时候我从当时的公司离职,来到了目前任职的一家更专注于游戏开发的公司。接手的是一个platform游戏项目,基本情况是之前的团队完成了第一个版本,即单人模式的基础玩法,但是之后对该项目的定位又变成了一个本地局域网的联机手游(2-4个玩家)。因此,重写项目底层外加确定网络同步方案就成了第一件需要去认真考虑的事情了。那么本文就来聊聊网络同步这件事吧。0x01 游戏同步中的主次开发网络多人游戏是一件十分有趣的事情,但是和单机游戏相比无疑增加了更多的挑战。例如,我们之前开发的单机版本并不需要多么担心作弊的问题。这是因为购买我们游戏的玩家(假如我们的单机游戏不免费发布的话)即便作弊,影响的也仅仅是他自己的游戏体验,不会影响到别人。但是开发多人游戏就不是这样了,为了保证让每个人都有好的游戏体验,防止作弊总是需要去考虑的。但是开发多人游戏就不是这样了,为了保证让每个人都有好的游戏体验,防止作弊总是需要去考虑的。除此之外,在开发多人游戏时我们还需要考虑如何“欺骗”玩家的眼睛,让他们认为他们在同一个世界中。当2个或4个玩家一起在手机上玩游戏时,看上去他们确实像是在共享同一个虚拟世界,在同一个世界中游玩。但事实却是,玩家自己的手机只是对“同一个”虚拟世界的近似模拟。换言之,他们的游戏世界每一个都是独一无二的,只不过从外观上看起来像。因此,为了达到这种看上去近似的效果,我们需要确认哪些状态是需要同步的,只要同步了这些状态,这个游戏世界就看上去一样了。而哪些状态是无需同步的,即这些对象的状态是否同步对整个游戏是否看上去一样并没有特别大的影响。在我们的游戏中,玩家的各种属性、在世界中的坐标、游戏世界中的敌人各种属性、道具获取以及各种触发器的触发等等都有可能会对游戏的表现产生影响,因此需要考虑同步;但是像例如海底的水泡粒子效果、道具获取后的碎裂效果,甚至是背景音乐则不会对游戏的表现产生特别的影响,因此并没有必要去同步这些内容。0x02 同步输入or同步状态既然明确了不存在两个完全一样的游戏世界,每个游戏世界无非都是近似的模拟。那么接下来我们就要来选择一个适合的网络同步方案以满足这种需求了。最基本的游戏网络同步模型大概可以分为以下4种(画图水平一般,见谅):client-server:专用服务器 client-server:玩家之一作为服务器 peer-to-peer peer-to-peer:帧同步client-server上面的两种client-server模型的相同点都在于有一台机器负责整个游戏世界的模拟,而这台负责整个游戏世界模拟的机器是谁则是这两者最大的区别。在我们的项目中,我们借助其中一个玩家的手机作为服务器,我们叫它Master主机,而一般的玩家设备则被称为Client。当然,更常见的一种情景是游戏开发商或发行商管理的计算机作为服务器,这也往往需要更多的计算机和运维人员。通常,基于这种同步模型的游戏中客户端不能做出真正的决定。一个情景就是当客户端的玩家按下一个按键,客户端并不会真正的执行影响游戏状态的操作,相反操作会被发往服务器,并在服务器执行它,之后服务器将执行完这个操作之后的结果(通常是游戏世界的状态变化)返回给客户端。由于大家都知道的网络延迟,因此服务器和客户端并非时刻保持一致的,为了使游戏玩家的状态变化自然(主要是指玩家的位置、角度等状态),我们使用的是一种基于插值的同步算法(当然,这种方式也常常被称为影子跟随算法):服务器间隔固定的时间向客户端同步状态数据客户端收到数据之后进行同步,一般的属性数据例如血量等等直接根据服务器的值来同步。而诸如位置等信息在客户端则保存为ServerPosition或者称为影子,而客户端的位置则不断向ServerPosition靠拢。位置同步的过程为了更加平滑,要使用插值,步进距为玩家的移动速度。因此,虽然ServerPosition是跳变的,但是在客户端的表现上却是连续平滑的。如下图所示,左侧的画面为Server的状态,右侧的画面为客户端的状态,玩家和场景内的怪物位置通过Server告诉客户端,客户端于是开始追赶Server发来的状态。 当然,将所有的逻辑放到服务器并经过服务器的模拟之后再将结果返回给客户端的过程会带来一些滞后感,当玩家对操作的敏感度要求较高时,这显然不是一个很好的解决方案。因此,客户端的输入预测和服务端的延迟补偿开始得到应用。通过在客户端侧的输入预测,可以让玩家的输入得到立刻的反馈。而延时补偿则保证了结果的正确性。这个过程可以基本概括为以下几个阶段:当玩家按下按钮时,客户端立刻执行相应的操作例如开始播放某个动作或是开始移动。与此同时,客户端还会向服务器发送一条包含了时间戳的消息。服务器经过一段延迟后收到了客户端发来的按钮被按下的消息,于是服务器会回滚到按钮被按下的时刻,在这个时刻执行按钮对应操作,之后再重新模拟到当前时刻。之后服务器将当前的状态同步给客户端。客户端收到服务器同步过来的数据,此时由于网络延迟的缘故,客户端收到服务器的消息时也已经过去一段时间。所以客户端同样需要回滚到服务器发出消息的时刻,并根据服务器发送的状态来修正自己的状态。虽然这样做能够更好的保证玩家的手感,但是我们发现无论是客户端还是服务器,一旦收到消息包之后都需要回滚。而这种回滚机制相对来说较为复杂,并且也不容易在已有的游戏中加入这种机制。综上,我们可以看到在这两种同步模型中,服务器获取客户端的操作指令并在服务器内模拟整个游戏世界,之后服务器是将服务器所维护的游戏世界内的状态同步给各个客户端,因此这里主要是做状态同步。Peer to PeerPeer to Peer点对点同步模型是一种很经典的网络游戏网络同步模型。带有帧同步模型的Peer to Peer在很多RTS游戏中得到了大量应用,不过在讨论帧同步模型之前,我们先来聊聊一般的Peer to Peer。相对于C/S模型拥有一个计算机负责整个游戏世界的模拟,Peer to Peer模式并没有单一的计算机来负责模拟游戏世界。相反它将对游戏世界的模拟分配给了所有玩家,因而每个玩家的客户端都在模拟着自己的游戏世界。这样做的一大好处在于玩家的输入总是立刻响应的,我按下一个按钮,按钮造成的结果便发生了,同时我需要做的是将我的操作发送给和我相连的客户端,让他们也去根据我发送的操作模拟游戏世界。但是这样做的一大弊端在于不能保证客户端看到的游戏画面是一样的。例如上图上方的怪物射出的子弹可以通过画线来阻挡,但是由于client1和client2都是在模拟自己的游戏世界,因此延迟或是不同移动设备本身的性能问题就有可能会造成client1的画线操作同步到client2上时产生不同的结果。所以我们发现只是简单的让每个客户端模拟自己游戏世界(就像单机那样),同时简单的将操作同步给别的客户端,至少在同步这个问题上是不靠谱的。因此,游戏行业大多会采用帧同步模型来保证同步的可靠性。很多早期的RTS游戏都采用了帧同步来作为网络同步的方案。至于为什么很多人在介绍帧同步的时候,都喜欢把早期的RTS游戏搬出来作为一个例子呢?我想各位看一眼RTS游戏的游戏截图就能猜到个大概了。 RTS游戏中常常伴随着数十上百甚至上千个逻辑实体单位,如果采用状态同步的话数据量相对要大很多。但是如果只同步玩家的操作呢?如果每个客户端在相同的情况下开始游戏,并且运行完全相同的步骤,那么客户端就可以不通过接收状态同步信息就能保证游戏的同步了。这也是这种模型的一大优势,我们除了发送玩家的操作之外几乎不需要再发送任何数据。这种同步输入的方式可以说非常适合RTS游戏,因为它们有那么多的单位,同步所有单位的状态是不容易的。因此,采用这种模型就可以把游戏的过程分为一个一个的回合。游戏的每一步都需要通过网络来收集所有玩家的操作输入,然后再往下执行。当然,一提到“回合”这个词,大家想到往往是所谓的回合制游戏,但事实上只要回合的频率足够快,仍然是可以做出即时游戏的感觉。当然,由于没有同步游戏的状态,而是同步玩家在游戏内的输入操作,因此实现完全同步还是有一些事情需要注意的。因为一旦一个小小的不同步发生,就会产生蝴蝶效应,从而引起很明显的不同步。一个典型的例子便是我以前在开发一个战斗回放系统时,发现由于一个士兵在寻路的时候稍微走到有点不一样的地方,就导致了一场战斗的结果大不相同。虽然我们目前的项目并没有采用帧同步的方案,但是还是想和大家分享一点教训。例如不要使用浮点型数据,这是由于舍入会造成误差,所以建议各位使用整形数据。同样,另一个又被重视又被忽略的是随机数的问题。大家都知道帧同步要保证随机数也完全一致。因此,大家都会去同步随机数生成器的种子和它们的使用方式。但是一个潜在的可能性是某一方的非游戏逻辑对象使用了随机数生成器,从而造成不同步。例如某一方的移动设备性能更好,也因此屏幕上有一些额外粒子特效,这些粒子特效是有可能会使用随机数发生器的,如果这些游戏逻辑之外的对象使用了随机数发生器就会造成不同步的发生。哦,对了,最后需要说明的一点是帧同步还可以和C/S模型组合使用,我们可以通过服务器来转发客户端的操作数据,而不必让各个客户端直接通讯。公司内有项目组采用的就是这种方案。0x03 后记当然,以上只是一些基本的同步模型。在这里只是结合我们的项目经验和大家做一个简单的分享,我想基于这些基本的模型还会衍生出一些别的方案。也欢迎大家来一起交流。欢迎大家关注我的公众号慕容的游戏编程:chenjd01最后打个广告,欢迎支持我的书~","updated":"T01:49:24.000Z","canComment":false,"commentPermission":"anyone","commentCount":9,"collapsedCount":0,"likeCount":130,"state":"published","isLiked":false,"slug":"","lastestTipjarors":[],"isTitleImageFullScreen":false,"rating":"none","titleImage":"/v2-7d28dbb42bd5cbbbbae086_r.jpg","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"游戏开发"},{"url":"/topic/","id":"","name":"Unity(游戏引擎)"},{"url":"/topic/","id":"","name":"网络游戏"}],"adminClosedComment":false,"titleImageSize":{"width":640,"height":400},"href":"/api/posts/","excerptTitle":"","column":{"slug":"chenjiadong","name":"Runtime"},"tipjarState":"activated","tipjarTagLine":"真诚赞赏,手留余香","sourceUrl":"","pageCommentsCount":9,"tipjarorCount":0,"annotationAction":[],"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T09:49:24+08:00","url":"/p/","lastestLikers":[{"bio":null,"isFollowing":false,"hash":"6454dbc1287ccde4497aeab63695ce24","uid":88,"isOrg":false,"slug":"evil-ted","isFollowed":false,"description":"Don't Be Hopeless.","name":"Eternal Flame","profileUrl":"/people/evil-ted","avatar":{"id":"36fd77ce0","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"从历史学的角度来分析人的命运会让人更积极","isFollowing":false,"hash":"deb2f9eb20f681d4a5294","uid":902700,"isOrg":false,"slug":"wu-xing-zhuang-bzui-wei-zhi-ming-61","isFollowed":false,"description":"1.我可以为科学贡献最最微薄的力量,虽然不是必要的,但是是有益的。\n2.科学可以解答我的疑惑,在一些情况下,也可以指导我的生活。\n3.科学与政治有关,但是这种关联不是必然的联系,只是历史因素使然。\n4.追求真理与人文主义可以并存。\n\n如果说历史是人类命运的集合,如同寰宇中宏伟的银河。个人的命运就像漂浮在银河中的微尘,被全部星河的光芒所照耀。","name":"一剑入云生长啸","profileUrl":"/people/wu-xing-zhuang-bzui-wei-zhi-ming-61","avatar":{"id":"v2-7f4ba689c97d9e64d9c2","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"","isFollowing":false,"hash":"a2bd2813dcadee3","uid":88,"isOrg":false,"slug":"zhang-heng-73","isFollowed":false,"description":"","name":"张衡","profileUrl":"/people/zhang-heng-73","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"IT程序猿;游戏攻城狮","isFollowing":false,"hash":"fd3d5dbc198c09","uid":80,"isOrg":false,"slug":"ic_leefeng","isFollowed":false,"description":"","name":"李峰","profileUrl":"/people/ic_leefeng","avatar":{"id":"v2-a3fbfd9e990ff95d6c0b9980","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"Game Developer @ Tencent","isFollowing":false,"hash":"829fed312cb327e3628fe2","uid":92,"isOrg":false,"slug":"ye-qianqian","isFollowed":false,"description":"游戏开发者","name":"ye qianqian","profileUrl":"/people/ye-qianqian","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"0x00 前言16年年底的时候我从当时的公司离职,来到了目前任职的一家更专注于游戏开发的公司。接手的是一个platform游戏项目,基本情况是之前的团队完成了第一个版本,即单人模式的基础玩法,但是之后对该项目的定位又变成了一个本地局域网的联机手游(2-4个…","reviewingCommentsCount":0,"meta":{"previous":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"/50/v2-091a98a3c560f8beff9cc7_xl.jpg","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"游戏开发"},{"url":"/topic/","id":"","name":"游戏设计"},{"url":"/topic/","id":"","name":"Unity(游戏引擎)"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01","isFollowing":false,"hash":"2ad398c4fbe830","uid":88,"isOrg":false,"slug":"jiadongchen","isFollowed":false,"description":"编程首先是爱好,其次才是职业。http://www.chenjd.me/\n\n我已加入“维权骑士”()的版权保护计划。","name":"陈嘉栋","profileUrl":"/people/jiadongchen","avatar":{"id":"v2-e3e4f2a706dc7ec9d2866d2aba059c0d","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},"column":{"slug":"chenjiadong","name":"Runtime"},"content":"0x00 前言:自从年底换了公司之后已经有一段时间没有写博客了。可能是这段时间工作的更愉快了,所谓“业精于勤,而荒于嬉”吧(此处有表情)。这周终于意识到了这点,看来还是要恢复之前写博客的状态。今天就来写写游戏引擎和脚本编程的话题吧。0x01 游戏脚本,用还是不用?这是一个问题?游戏开发提供对脚本的支持可谓源远流长。2002年的无冬之夜,玩家可以方便的开发Mod。2005年的黑与白2,按照剧本演绎大电影的感觉。2011年的老滚5,玩家可以方便的开发自己的Mod,生命力直到6年后的现在依然旺盛。当然,让更多人知道游戏脚本的是网络游戏的兴起。开发网络游戏的插件或是外挂,游戏脚本总是要打交道的。好像大家都在使用脚本开发游戏。0x02 什么是游戏脚本使用c/c++开发项目,随着项目的扩大,源码的编译时间也会越来越长。有时修改一些常量就需要重新编译整个项目。是否感觉到了痛苦?见招拆招,为了解决这个问题,可以将很多常量放到一个初始化文件中。并且编写一段代码去读取并解析这个文件。这样,常量就以初始化文件/配置文件的形式独立出来了。修改常量的值再也不用重新编译整个项目了!这就是一个最初级的游戏脚本!而且初始化文件中的文本就是最基本的游戏脚本语言。但是,仅仅是提供一些常量的读取和解析似乎还不能够满足游戏开发的需求,为了提高游戏开发的效率,节约编译c/c++项目时泡咖啡的时间,更高级的游戏脚本语言显然有必要引入。So,更高级的脚本语言增加了脚本和c/c++的交互性,我们不但可以初始化变量,而且可以创建游戏逻辑甚至时游戏对象。游戏脚本的运行通过虚拟机来进行,我们使用脚本语言的语法编写的函数可以被虚拟机读取并运行。换言之,脚本可以通过虚拟机与c/c++进行通信,使得数据可以在两者之间来回传递。脚本可以是解释执行的,也可以是编译执行的。解释执行的脚本通过解释器,逐行的读取、解析和执行。但是,为了节约在运行时的开销,一些解释执行的脚本会自动编译脚本。Lua便是其中的代表。当然,脚本还可以是编译执行的。编译执行的脚本是通过这种脚本语言的编译器编译为某种形式的中间语言(IL)的脚本。虚拟机是可以直接执行这种中间语言的。这种中间语言是平台无关的,换言之,这种中间语言是供虚拟机使用的,而不关心具体的平台。例如现在很流行的Unity引擎所使用的C#语言。在使用Unity开发游戏的过程中,脚本代码的编译只需要分为两部分就可以了:第一部分是从C#代码本身到CIL的编译(其实之后 CIL还会被编译成一种位元码,生成一个 CLI assembly);第二部分 是运行时从 CIL(其实是 CLI assembly,不过为了直观理解,此处不必纠结这种细节)到本地 指令的即时编译(这就引出了为何 Unity 3D官方没有提供热更新的原因:在 iOS平台中 Mono 无法使用 JIT 引擎,而是以 Full AOT 模式运行的,所以此处说的即时编译不包括 iOS 平台)。0x03为什么要使用脚本?便捷的初始化文件:最简单的应用便是我们可以使用脚本作为初始化文件,这是一个读入/修改游戏数据快速而方便的方法。节约游戏开发的时间,提高效率:随着游戏规模的日益庞大,C/C++代码的编译时间会越来越久。一些需要经常调试的功能如果每次修改都编译一遍整个项目,无疑会造成效率的地下。例如ai程序员,常常需要调试查看效果。因此如果都在C/C++中实现,会造成效率的低下。设想一下像辐射4这样的大型沙盒游戏,如果每次调试修改游戏逻辑都要重新编译C/C++代码,其开发效率无疑会降低很多。降低了编程的门槛,提高了创造性:不可否认的一点,一些脚本语言比C/C++更高级,这主要体现在语法更加直观、上手更加容易。因此,一些非程序员也可以使用脚本语言来实现自己的需求。当然,另一方面来说更容易招到初级程序员。不过,我认为这同时也导致了另外一个问题,即很多程序员因此成为了API搬运工,只知其然而不知其所以然。带来了拓展性,Mod:使用脚本语言,游戏开发商可以把游戏引擎的功能交给玩家。反过来,通过玩家创造的Mod又为游戏提供了新的生命力。这方面老滚5是一个经典的例子。11年发售的老滚5由于其拓展性,生命力十分顽强。0x04 游戏开发中脚本的常见使用场景对话流使用脚本来管理游戏中的对话是一个最常见的应用。这类脚本常常随某个特定的事件而触发。例如场景设计师设计主角在靠近某个NPC时触发和该NPC的对话。对话内容可以直接由场景设计师来制作。舞台设计脚本在游戏中常见的一个应用还包括设计舞台。通过脚本来控制游戏场景中的对象和各种事件可以营造出很有沉浸式的游戏场景。这里游戏脚本就十分类似电影的剧本。玩法逻辑由于逻辑开发需要经常的调试,如果每次代码修改都要进行一次冗长的重新编译,这会造成工作效率和开发人员身体健康的下降。因此使用脚本可以避免这一点。0x05 后记好了,关于游戏编程和游戏脚本的话题暂时先告一段落。在下一篇文章中我们就来聊聊使用Unity游戏引擎开发游戏的过程中和游戏脚本有关的话题吧。欢迎大家关注我的公众号“慕容的游戏编程”:chenjd01最后打个广告,欢迎支持我的书~","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T10:17:58+08:00","url":"/p/","title":"游戏开发和游戏脚本的那些事","summary":"0x00 前言:自从年底换了公司之后已经有一段时间没有写博客了。可能是这段时间工作的更愉快了,所谓“业精于勤,而荒于嬉”吧(此处有表情)。这周终于意识到了这点,看来还是要恢复之前写博客的状态。今天就来写写游戏引擎和脚本编程的话题吧。0x01 游戏脚…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":12,"likesCount":39},"next":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"/50/v2-7c7698e3abff89f9c4baf_xl.jpg","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"C#"},{"url":"/topic/","id":"","name":"编程语言"},{"url":"/topic/","id":"","name":"计算机"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01","isFollowing":false,"hash":"2ad398c4fbe830","uid":88,"isOrg":false,"slug":"jiadongchen","isFollowed":false,"description":"编程首先是爱好,其次才是职业。http://www.chenjd.me/\n\n我已加入“维权骑士”()的版权保护计划。","name":"陈嘉栋","profileUrl":"/people/jiadongchen","avatar":{"id":"v2-e3e4f2a706dc7ec9d2866d2aba059c0d","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},"column":{"slug":"chenjiadong","name":"Runtime"},"content":"前言:关于MSIL的文章中我为MSIL正名的篇幅比较多,反而忽略了写那篇文章初衷--即通过写MSIL代码来熟悉它,了解它。那么既然有上一篇文章做基础(炮灰),想必各位对MSIL的存在也就释然了,兴许也燃起了一点探索它,掌握它的欲望。那么我就继续扯一扯MSIL,接下来的几篇文章也都以上一篇文章中的那个MSIL实现的Hello Wolrd程序为基础,继续通过写MSIL代码实现一些功能的方式来和各位探讨交流,同时也加深自己对MSIL的掌握和印象。人生就是做加法最近我和女朋友登记结婚了,回首往事发现人生就是一个在不断做加法的过程。因此我决定用MSIL代码实现一个把2个数相加的功能,让自己的情愫谱写在代码的字里行间里。好啦,言归正传,其实选择实现一个加法功能是因为我上一篇文章中举过一个关于加法的例子,同时上一篇文章也大体上介绍了一下MSIL中如何声明一个函数。所以趁热打铁,一鼓作气,直接用MSIL实现一个做加法的函数既是对上一篇文章的一个呼应,也能达到本篇文章的目的---聊聊如何用MSIL实现函数。那么这个函数的功能呢?简单的分一下各个功能点,我想到的大概就是这些了:显示“请输入第一个加数”,并获取输入的值,记为num1。显示“请输入第二个加数”,并获取输入的值,并记为num2。将输入的两个数,num1和num2相加求和,结果记为result。要将结果显示给用户,所以我们做的更彻底一点:显示整个算式:num1+num2 = result。局部变量说到函数,就不得不提局部变量了,因为你总得操作一些东西吧?而局部变量正是那些在函数中被你操作的家伙。假设我们要实现的函数叫做AddLife好啦。AddLife需要实现将我们输入的2个数字相加,并且将“和”作为结果返回。所以局部变量的个数是3个。我们的局部变量的声明如下:.locals init (int32 num1,\n
int32 num2,\n
int32 result)\n在这里:我们使用.locals指令标识我们要声明的是局部变量。init标识我们所声明的3个int32型变量都被初始化为int32型的默认值,当然如果我们声明的不是int32型的变量,则被初始化为该变量相应类型的初始值。int32,代表变量的类型当然,在CIL代码中我们也可以不写变量的名字num1,num2以及result。因为你可以通过它们的索引获得它们,这里写它们的名字仅仅是为了易读。OK,我们声明了函数中的局部变量。可之后呢?这毕竟不是C#或者别的高级语言啊,否则也不会有很多人讨厌去读它更别说写它了。所以,这里我要对MSIL的执行做个小说明,以便各位不和我产生较大的分歧。基于堆栈记心头首先的一点,就是MSIL是基于堆栈的。也就是说它的执行离不开堆栈。所以要搞清楚MSIL的执行,就是要搞清楚MSIL到底是如何使用堆栈的。因为是基于堆栈的,所以MSIL指令所取的值来自堆栈。也就是说,值的传递要经过堆栈这个“桥梁”。所以当你想要为某个MSIL指令传值(值类型),所传的值就必须要先入栈,之后该指令再将这个值出栈进而使用。OK,那让我更进一步。假如我的MSIL指令是call(或者callvirt,二者区别以后会聊)呢?这需要调用一个函数,那call是如何使用堆栈的呢?2种情况。首先,你调用的是某个类的实例函数,那么就要把调用的某个类的实例的引用压栈。然后呢?当然如果需要的话,这个函数所需要的参数也要压栈。在调用函数的过程中,这个实例的引用以及它的参数都会出栈,供MSIL指令使用。其二,你需要用一个引用类型作为参数传入某个方法中。同样,这个引用类型的参数在被使用之前,也要把它的引用压栈。那么,一直说压栈和出栈。可处理压栈和出栈的MSIL指令是啥呢?2个最基本也是本文要用到的:(更具体的可以看匹夫之前的文章)ldloc,用来做压栈操作。将变量的值压入堆栈中。stloc,用来做出栈操作。将堆栈的值传入变量中。之后,各位可能还会注意到上面我加粗的几个字,如果对所谓的值类型和引用类型有一个简单的概念的话,是不是觉得恍然大悟呢?值类型的值直接存在堆栈上。(当然引用类型,比如某个类的实例中的值类型字段不再此列,它也会和该类的实例一起出现在堆上,不过这并非我们今天要探讨的主要内容)引用类型的实例并非存在堆栈上,堆栈上存的是它的引用。 第一个功能:显示提示输入加数,并获取输入的值好啦。来到我们要实现的第一个功能了,那就是要显示“请输入第一个加数”这样一个字符串,同时要读取用户输入的数值,然后赋给我们的变量num1。饭要一口一口吃,码要一行一行写。所以就从显示字符串开始吧。其实我上一篇文章介绍如何输出一个Hello World的时候,就已经实现了字符串的输出。那么让我们依样画葫芦,输出“请输入第一个加数”这样的语句吧。//在屏幕上显示“请输入第一个加数”\nldstr \"请输入第一个加数\"\ncall void [mscorlib]System.Console::WriteLine(string)\n首先将“请输入第一个加数”压栈,然后使用call来调用mscorlib程序集中System.Console类的WriteLine方法,此时屏幕上会显示“请输入第一个加数”。第二步,我们要获取用户输入的值。//获取用户的输入值\ncall string [mscorlib]System.Console::ReadLine()\n调用mscorlib程序集中System.Console类的ReadLine方法,并返回一个字符串。因为返回的是一个字符串,所以我们在将正确的值赋给变量num1之前,还需要对这个字符串进行转化,转化成int32的过程如下://将输入的字符串转化成int\ncall int32 [mscorlib]System.Int32::Parse(string)\n注意,以上的三步,虽然看上去只有一个ldstr将字符串压栈,但是其实每一步,每一行都伴随着压栈或出栈的过程。简单的叙述下这个过程是这样的:将“请输入第一个加数”压栈。call调用Write方法,同时会将“请输入第一个加数”出栈,作为WriteLine的参数。call调用ReadLine方法,该方法从用户处获得一个输入,并且将该值作为一个string型压栈。由于这个从用户处得到的string型已经在堆栈中了,所以在最后一个call调用[mscorlib]System.Int32::Parse之前,无需对那个string型压栈。反而是直接从堆栈中弹出该string值,作为[mscorlib]System.Int32::Parse的参数。之后在将已经转化为int型的值压栈。简单描述了下过程。在这里,我只想说:记住,只要涉及到数据,就要用到堆栈。好啦,完成上面的过程,也就是完成了从用户处获取值的过程,此时的值已经躺在堆栈中了。之后,我们还要将这个值赋给变量num1://值出栈,赋给局部变量num1\nstloc num1\n 之后获取第二个加数的过程就是上面几步的重复。各位可以自己实现下。第二个功能:相爱相杀,不对,应该是相爱相加...好啦,用户输入的数值我们已经搞到手了,那么是不是就该实现这个方法最核心的功能,对2个数相加求和呢?答案是yes。不过这里我们会涉及到这篇文章已经介绍过的一个指令----ldloc。顾名思义,ldloc?不就是loadlocal嘛~所以其作用也就十分明了了:使用ldloc我们可以将局部变量num1,num2中的值压入堆栈,这样才能供之后使用。所以我们将值压栈的语句就是://将值从变量中压入堆栈\nldloc num1\nldloc num2\n当然在本文一开头就说过,局部变量什么的作为我们人类也可以不给它们起名字,只需要使用它们的索引就可以了。所以你也可以这样写来实现数值压栈的过程://如果不写变量名\nldloc.0\nldloc.1\n反正我是挺讨厌这种不是给人看的写法的。变量已经躺进堆栈了,那么下一步呢?求和呗,MSIL可是带求和指令的哦~没错:add。//求和\nadd\nadd指令会将刚刚压入堆栈的2个值弹出,然后计算和,最后将结果在压入堆栈中。可是我们还有一个局部变量result没用呢,所以我们还要将结果赋值给result变量。//将结果赋值给result\nstloc result\n 最后一个功能,关键的其实是装箱好啦好啦,求和这个事情其实已经做完了,但是我们总的输出一点东西好让用户看到我们的确已经求过和了。那么如何实现文章一开始时,我定下的最后一条功能呢?也就是按照”num1 + num2 = result“这个格式显示结果呢?首先,我们把显示的字符串的格式规定好://显示的格式\nldstr \"{0} + {1} = {2}\"\n其次我们要把格式中的{0},{1},{2}替换成具体的数值,或者说”object“。因为我们要调用WriteLine方法,所以这里就会涉及到一个值类型到object的装箱的过程。首先我们还是将变量中的值压栈,之后再对栈中的值进行装箱。//将num1,num2,result装箱,供之后的writeLine使用。\nldloc num1\nbox int32\nldloc num2\nbox int32\nldloc result\nbox int32\n这里终于聊到了装箱这个话题,所以我继续扯一扯装箱,也就是box指令在MSIL中的执行过程吧:首先将压栈的值弹出。同时在堆上构造一个新的object,并且这个object包含该值类型的值的拷贝。最后将这个新的object的引用压栈。所以,各位是不是觉得C#中的一些概念通过MSIL来看更加直观呢?其实这也是我对MSIL感兴趣的一个原因。最后一步,就是输出结果咯,因为3个值类型的值已经装箱了,所以我们就这样写://将算式显示出来\ncall void [mscorlib]System.Console::WriteLine(string, object, object, object)\n[mscorlib]System.Console::WriteLine(string, object, object, object)中的第一个参数string代表第一个被压栈的格式字符串\"{0} + {1} = {2}\",之后依次是经过装箱的num1的值,num2的值,result的值。此时,以上一篇文章中实现Hello World输出的chen.il文件为基础,再加入我们的AddLife函数之后,大体上就长这个样子了(26行之前是上篇文章时写的代码):此时眼尖的小伙伴一定会发现,好像是少了点什么呀?对,没有.entrypoint。因为.entrypoint还在上一篇文章中的Fanyou()这个方法里呢。然后呢?还少了.maxstack,因为匹夫光顾着看逻辑了(04/02/15-23:35刚写完。。。)所以,没有提前考虑到底需要多大的堆栈槽。那么到底最多到底需要使用多少堆栈槽呢?答案是4,使用最多堆栈槽的地方就是我们最后显示算式的时候,WriteLine要用4个参数。现在我们把.entrypoint和.maxstack 4加入到现在的AddLife方法,编译并运行它。和上文一样的顺序://编译chen.il\nilasm chen.il\n运行生成的chen.exe//运行生成的chen.exe\nmono chen.exe \n结果如图:输入1和2,最后显示结果为:1 + 2 = 3。OK,大功告成。","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T13:35:07+08:00","url":"/p/","title":"用MSIL写程序:写个函数做加法","summary":"前言:关于MSIL的文章中我为MSIL正名的篇幅比较多,反而忽略了写那篇文章初衷--即通过写MSIL代码来熟悉它,了解它。那么既然有上一篇文章做基础(炮灰),想必各位对MSIL的存在也就释然了,兴许也燃起了一点探索它,掌握它的欲望。那么我就继续扯一扯…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":3,"likesCount":7}},"annotationDetail":null,"commentsCount":9,"likesCount":130,"FULLINFO":true}},"User":{"jiadongchen":{"isFollowed":false,"name":"陈嘉栋","headline":"编程首先是爱好,其次才是职业。http://www.chenjd.me/\n\n我已加入“维权骑士”()的版权保护计划。","avatarUrl":"/v2-e3e4f2a706dc7ec9d2866d2aba059c0d_s.jpg","isFollowing":false,"type":"people","slug":"jiadongchen","bio":"慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01","hash":"2ad398c4fbe830","uid":88,"isOrg":false,"description":"编程首先是爱好,其次才是职业。http://www.chenjd.me/\n\n我已加入“维权骑士”()的版权保护计划。","profileUrl":"/people/jiadongchen","avatar":{"id":"v2-e3e4f2a706dc7ec9d2866d2aba059c0d","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{"experimentFeatures":{"ge3":"ge3_9","ge2":"ge2_1","appStoreRateDialog":"close","nwebStickySidebar":"sticky","qrcodeLogin":"qrcode","favAct":"default","default":"None","mobileQaPageProxyHeifetz":"m_qa_page_nweb","newMore":"new","iOSNewestVersion":"4.2.0","newMobileColumnAppheader":"new_header","sendZaMonitor":"true","homeUi2":"default","answerRelatedReadings":"qa_recommend_by_algo_related_with_article","wechatShareModal":"wechat_share_modal_show","liveReviewBuyBar":"live_review_buy_bar_2","qaStickySidebar":"sticky_sidebar","androidProfilePanel":"panel_b","liveStore":"ls_a2_b2_c1_f2","zcmLighting":"zcm"}},"columns":{"next":{},"chenjiadong":{"following":false,"canManage":false,"href":"/api/columns/chenjiadong","name":"Runtime","creator":{"slug":"jiadongchen"},"url":"/chenjiadong","slug":"chenjiadong","avatar":{"id":"v2-c4e393a91e7abcd","template":"/{id}_{size}.png"}}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"edition":{},"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{},"message":{"newCount":0},"pushNotification":{"newCount":0}}

我要回帖

更多关于 dnf双开同步怎么用 的文章

 

随机推荐