您在开发过程中不需要做什么的故事

序言:首先,我将讨论该项目,以便对我们如何进行该项目以及如何减轻痛苦产生一些想法。

作为一名开发人员,我在2015-2016年进入了该项目,我不记得确切,但是它在2-3年前就已经生效。 该项目在游戏服务器领域非常受欢迎。 听起来并不奇怪,但是游戏服务器上的项目一直持续到今天,最近我看到了空缺,并在同一团队工作了一段时间。 因此,由于游戏服务器是基于已经创建的游戏构建的,因此脚本语言用于内置在游戏引擎中的开发。

我们正在从Garry的Mod(Gmod)几乎从零开始开发一个项目,需要注意的是,在撰写本文时,Harry已经在虚幻引擎上创建了一个新的S&Box项目。 我们仍然坐在Source上。
通常不适合我们的服务器主题。
图片

“你的故事吓人什么?” -你问。

对于游戏服务器,我们有一个很强的主题,即“潜行者”,即使有角色扮演游戏(RP)的元素,也立即出现了一个问题-“每个人如何在一个服务器上实现它?”

考虑到Source引擎较旧(2013版本也用于32位Gmod),因此您不会做大牌,对Entity,Mesh的数量有小的限制,甚至更多。
谁在引擎上工作会明白。
事实证明,要完成一个干净的多人缠扰者任务,通常是不可能完成的任务,任务包括原始任务本身的RPG元素,最好是一个小地块。

首先,最初的拼写很困难(类别中的许多动作:从头开始写东西,举起对象),希望继续比较容易,但是要求却越来越高。 游戏的机制已经准备就绪,剩下的只是做智力,升级和各种事情。 总的来说,所有人都尽可能地转移了。

图片

这些问题已经在发行第一个版本的工作中开始出现,即(延迟,服务器延迟)。

似乎功能强大的服务器可以从容处理请求并保持整个Gamemode。

游戏模式的简单描述
这是一组描述服务器本身机制的脚本的名称。
例如:我们想要现在流行的“皇家战斗”的主题,这意味着名称也应该与游戏的机制相对应。 “玩家在飞机上产生的东西,可以捡起东西,玩家可以交流,不能戴超过1个头盔,等等。” -所有这些都由服务器上的游戏机制来描述。

由于大量玩家,服务器端出现了延迟,因为一个玩家吃掉了大约80-120 mb的大量RAM(不计算库存,技能等项目),而客户端则大幅减少了第一人称射击

CPU能力不足以进行物理处理,因此必须使用物理属性较少的对象。

此外,还有我们自己编写的脚本,这些脚本根本没有进行优化。

图片

首先,当然,我们阅读了有关Lua中优化的文章。 即使自杀 ,他们也想用C ++编写DLL,但是从服务器将DLL下载到客户端时出现了问题。 使用DLL的C ++,您可以编写一个安静地拦截数据的程序; Gmod开发人员为客户端的异常下载添加了扩展名(安全性,尽管实际上从未发生过)。 尽管这将使Gmod变得更加方便和灵活,但是会更加危险。

接下来,我们查看了事件探查器(幸运的是,聪明的人写了它),这些函数令人恐惧,注意到最初在Gmod引擎库中已经有非常慢的函数。

如果您尝试用Gmod编写,那么您将清楚地知道内置了一个称为math的库。

最慢的功能当然是math.Clamp和math.Round。

在人员代码中反复翻阅,人们注意到这些函数是朝不同的方向抛出的,几乎在所有地方都使用了它,但是错误地使用了它!

让我们开始练习。 例如,我们要舍入位置向量的坐标以移动实体(例如,玩家)。

local x = 12.5 local y = 14.9122133 local z = 12.111 LocalPlayer():SetPos( Vector( Math.Round(x), Math.Round(y), Math.Round(z) ) 

3个复杂的舍入函数,但是没什么大不了的,除非当然是循环且不经常使用,但是Clamp更加困难。

以下代码经常用于项目中,没有人愿意更改任何内容。

 self:setLocalVar("hunger", math.Clamp(current + 1, 0, 100)) 

例如,自我指向玩家的对象,并且拥有一个我们发明的局部变量,当重置到服务器时,该局部变量将重置为math.Clamp本质上是作为循环进行平滑分配,他们希望在Clamp上实现平滑接口。

当它对访问服务器的每个玩家起作用时,就会出现问题。 极少数情况,但如果5-15(立即取决于服务器配置)在某个时间点进入服务器,并且此小而简单的功能开始对每个人都起作用,则服务器将具有良好的CPU延迟。 如果math.Clamp处于循环中,则情况更糟。

优化实际上非常简单,您可以将重载功能本地化。 看起来很原始,但在3游戏模式和许多附加组件中,我看到了此缓慢的代码。

如果您需要获取价值并在将来使用它,并且它不会改变,则无需再次获取它。 毕竟,无论如何,进入服务器的玩家都会收到等于100的饥饿感,因此此代码的速度要快许多倍。

 local value = math.Clamp(current + 1, 0, 100) self:setLocalVar("hunger", value) 

一切都很好,他们开始进一步研究它的工作原理。 结果,我们开始疯狂地优化一切。

我们注意到循环的标准很慢,因此我们决定想出自己的自行车,速度会更快(我们并没有忘记二十一点),然后游戏开始了。

图片

扰流板
我们甚至设法在Lua Gmod上进行了最快的循环,但是条件是必须有100个以上的元素。

从花费在循环上的时间及其在代码中的使用来判断,我们徒劳地尝试执行此操作,因为它仅在抛出并清除异常后才在异常图的生成中找到应用程序。
这样的代码。 例如,您需要找到所有在别名开头都带有名称的实体,我们在此类名称中存在异常。

这是Lua Gmod上的普通脚本:

 local anomtable = ents.FindByClass("anom_*") for k, v in pairs(anomtable) do v:Remove() end 

这是给吸烟者的:

显而易见的是,这样的g *代码显然比标准的“成对使用”要慢,但事实证明并非如此。

 local b, key = ents.FindByClass("anom_*"), nil repeat key = next(b, key) b[key]:Remove() until key != nil 


为了全面分析这些循环选项,您需要将它们转换为常规的Lua脚本。
例如,omomtable将具有5个元素。
删除由通常的添加替换。 最主要的是要看到用于实现for循环的两个选项之间指令数量的差异。

香草周期:

 local anomtable = { 1, 2, 3, 4, 5 } for k, v in pairs(anomtable) do v = v + 1 end 

我们的很棒:

 local b, key = { 1, 2, 3, 4, 5 }, nil repeat key = next(b, key) b[key] = b[key] + 1 until key ~= nil 

让我们看一下解释器代码( 就像汇编器一样,不建议将它看做是高级程序员 )。

以防万一,从屏幕上删除琼斯。 我警告过

香草循环拆装机
 ; Name: for1.lua ; Defined at line: 0 ; #Upvalues: 0 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 7 1 [-]: NEWTABLE R0 5 0 ; R0 := {} 2 [-]: LOADK R1 K0 ; R1 := 1 3 [-]: LOADK R2 K1 ; R2 := 2 4 [-]: LOADK R3 K2 ; R3 := 3 5 [-]: LOADK R4 K3 ; R4 := 4 6 [-]: LOADK R5 K4 ; R5 := 5 7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5 8 [-]: GETGLOBAL R1 K5 ; R1 := pairs 9 [-]: MOVE R2 R0 ; R2 := R0 10 [-]: CALL R1 2 4 ; R1,R2,R3 := R1(R2) 11 [-]: JMP 13 ; PC := 13 12 [-]: ADD R5 R5 K0 ; R5 := R5 + 1 13 [-]: TFORLOOP R1 2 ; R4,R5 := R1(R2,R3); if R4 ~= nil then begin PC = 12; R3 := R4 end 14 [-]: JMP 12 ; PC := 12 15 [-]: RETURN R0 1 ; return 


自行车拆装机
 ; Name: for2.lua ; Defined at line: 0 ; #Upvalues: 0 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 6 1 [-]: NEWTABLE R0 5 0 ; R0 := {} 2 [-]: LOADK R1 K0 ; R1 := 1 3 [-]: LOADK R2 K1 ; R2 := 2 4 [-]: LOADK R3 K2 ; R3 := 3 5 [-]: LOADK R4 K3 ; R4 := 4 6 [-]: LOADK R5 K4 ; R5 := 5 7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5 8 [-]: LOADNIL R1 R1 ; R1 := nil 9 [-]: GETGLOBAL R2 K5 ; R2 := next 10 [-]: MOVE R3 R0 ; R3 := R0 11 [-]: MOVE R4 R1 ; R4 := R1 12 [-]: CALL R2 3 2 ; R2 := R2(R3,R4) 13 [-]: MOVE R1 R2 ; R1 := R2 14 [-]: GETTABLE R2 R0 R1 ; R2 := R0[R1] 15 [-]: ADD R2 R2 K0 ; R2 := R2 + 1 16 [-]: SETTABLE R0 R1 R2 ; R0[R1] := R2 17 [-]: EQ 1 R1 K6 ; if R1 == nil then PC := 9 18 [-]: JMP 9 ; PC := 9 19 [-]: RETURN R0 1 ; return 


没有经验的人会简单地说,由于指令较少(15 vs. 19),所以定期循环会更快。

但是我们一定不能忘记解释器中的每个指令都有处理器周期。
从第一个循环中的反汇编程序代码来看,有一个for循环的预先编写指令可用于处理数组,该数组已加载到内存中,成为全局数组,我们跳过了元素并添加了一个常量。

在第二个变体中,方法有所不同,它更多地基于内存,它接收表,更改元素,设置表,检查nil并再次调用。
我们的第二个周期很快,因为一条指令中的条件和动作太多(R4,R5:= R1(R2,R3);如果R4〜= nil则开始PC = 12; R3:= R4结束)它吃了很多东西吃了CPU时钟周期来执行代码,过去又与内存息息相关。

包含大量元素的forloop指令在所有元素通过的速度上都屈服于我们的周期。 这是由于以下事实:直接寻址到该地址比任何成对的好东西都快。 (我们没有否认)
通常,秘密地使用代码中的任何否定都会使它变慢;这已经通过测试和时间进行了测试。 由于处理器ALU具有独立的计算单元“反相器”,因此负逻辑的工作速度会变慢,对于一元操作数(不是!),要工作,您需要访问反相器,这将花费额外的时间。
结论:一切标准都不会总是更好,您的自行车可能会有用,但是在实际项目中,如果发布速度对您很重要,则不应该提出建议。 结果,我们已经完成了从2014年到今天的全面开发,这是又一次“等待”。 尽管它看起来像是一台普通的游戏服务器,但在1天之内就已安装完毕,并且在2天之内已针对游戏进行了完整配置,但您仍需要能够引入一些新功能。

这个长期项目仍然看到了它本身的第二个版本,其中代码中有很多优化,但是我将在以下文章中讨论其他优化。 支持批评或评论,如果我弄错了,请更正。

Source: https://habr.com/ru/post/zh-CN431786/


All Articles