中国虚拟军事网(VME)- 专注于武装突袭系列虚拟军事游戏

 找回密码
 加入VME

QQ登录

只需一步,快速开始

搜索
楼主: FFUR2007SLX2_5

[教程] 《武装突袭3》脚本编写高级教程【255楼,武装突袭3——疯狂的戴夫和他的重量】

    [复制链接]
 楼主| 发表于 2014-2-11 10:43:11 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-14 20:14 编辑

211楼《武装突袭3》——distanceSqr,linearConversion和drawIcon3D



在说distanceSqr前,我们先来看看distance。我们在脚本中一直都在使用distance来计算两个物体间的三维距离但又没有人质疑过ARMA给出的三维距离是否真的准确呢?
我们是否还记得高中数学立体几何计算三维空间中两点间的公式?

D(p,q) = sqrt ((p0 – q0)^2 + (p1 – q1)^2 + (p2 – q2)^2)


套用公式我们来计算[0,0,0]和[2,2,2]之间的距离应该是3.4641m,可在ARMA3中直接用distance计算得出的距离却是3.41953m,这就意味着,distance计算出的距离始终存在着误差。我们翻看FX官方的函数库没有看到准确计算三维距离的函数,所以在这里我写了一个BIS_fnc_RelDistance以便需要的朋友添加进自己的函数库。

  1. /*
  2. BIS_fnc_RelDistance
  3. Author: ffur2007slx2_5 (VME PLA VBS3 Dev)
  4. Description: Evaluate real three dimensional distance between two objects or positions
  5. Arguments: [Obj(Position),Obj(Position)] call BIS_fnc_RelDistance
  6. Return: Number
  7. */
  8. private ["_pos0","_pos1"];
  9. _pos0 = [_this,0,objnull,[objnull,[]]] call bis_fnc_param;
  10. _pos1 = [_this,1,objnull,[objnull,[]]] call bis_fnc_param;
  11. if(typename _pos0 == "OBJECT") then {_pos0 = _pos0 modelToWorld [0,0,0];};
  12. if(typename _pos1 == "OBJECT") then {_pos1 = _pos1 modelToWorld [0,0,0];};
  13. sqrt((((_pos0 select 0) - (_pos1 select 0))^2) + (((_pos0 select 1) - (_pos1 select 1))^2) + (((_pos0 select 2) - (_pos1 select 2))^2))
复制代码


虽然速度没有C++来的快,不过应该是最直观的方法吧。



作为扩展我们知道在ARMA3中我们有position, getPos, getPosATL, getPosASLW, visiblePosition, visiblePositionASL,getPosASL和modelToWorld总共8个求得实体三维坐标的代码,他们各自到底扮演什么样的功能,有什么样的区别又是一个问题,这些内容我将在下一篇中讲。

回到DistanceSqr,它其实就是一个C++版的distance开方,除了代码优化使用它外其他没有什么要说的。

接下来看看linearConversion,翻看FX函数库我们可以看到BI DEV Joris-Jan van 't Land(现在的ARMA3项目主负责人)曾经写过的BIS_fnc_linearConversion函数,但现在我们不用了,因为linearConversion直接代替了这个函数。
BIWiki的唯一缺点就是Dev们没时间写,有些新代码解释草率导致晦涩难懂。新代码替换旧代码是无法抗拒的事,但多数人却知之甚少,所以有必要在这里科普一下。

线性转换, 简单来说就是如果你想计算出20个橘子在所有390个橘子中所占的百分比,就会这么写:

  1. _percentage = 20 * 100 / 390;
复制代码


可以到用到线性转换中去,就是:

  1. _percentage = linearConversion [0,390,20,0,100,false];
复制代码


从wiki上我们可以清楚的看到其解释:

  1. linearConversion [min, max, value, newMin, newMax, clamp]
复制代码


min和max是原始的最小和最大值域,程序会自动得出它们的结果,随后value则是你所要的值,可以在原始值域中,也可以超出。得出的结果会放入另一个值域中去进行比较,最终得出在第一个值域中所存在的数,在该值域中对应存在的数应该是多少。最后的clamp则表示是否支持超出值域的数。

BI forum上你找不到实例,所以我们来看看其实际应用。我们做个假设,战场上除了指挥处的友军,我需要计算出其他地方死伤友军占所有友军数的一个比例,随后那这个数据和敌军做对比,除了HQ的敌军,我们还需撂倒多少敌军才能和我们的阵亡率相同。

  1. linearConversion [HQ_friendly, All_friendly, KIA, HQ_enemy, All_enemy, false];
复制代码


得出的数字就是我们现在需要撂倒的敌军数。或许你可以写一坨算法达到相同的目的,但作为高级篇,显然linearConversion显得更加优化!

最后再来看看DrawIcon3D。相信很多人都看过弹道视频,感觉在天空中划出那一条条弹道觉得很神奇。其实这不难,不过限于篇幅我不在这里介绍画线的方法,而是介绍它的另一个同门师兄:DrawIcon3D。大家应该还记得https://community.bistudio.com/wiki/BIS_fnc_3Dcredits BIS_fnc_3DCredits吧,这也是Karel Moricky在PMC第一章中写的函数,现在则由DrawIcon3D来替代了。

由于DrawIcon3D是需要每帧显示的,所以千万不要用原始的循环,而是要用ARMA3最新的BIS_fnc_addStackedEventHandler!这是由新招按的Nelson Duarte亲自操刀的一款函数,同时配合BIS_fnc_removeStackedEventHandler一起使用。每帧运行的脚本其兼容性一直都很不理想,直到BIS_fnc_addStackedEventHandler的出现才彻底解决了这个问题。我们知道OnEachFrame只能单个运行,所以一旦发生多个脚本同时出现OnEachFrame的情况就会导致bug。关于这个函数我将另开新篇,至于这里的drawIcon3D我们先用最简单的方法看看它如何运作的。

  1. onEachFrame {
  2.     private "_private";
  3.     _playerPos = getPosATL player;
  4.     drawIcon3D [
  5.         "\a3\ui_f\data\IGUI\Cfg\Radar\radar_ca.paa",
  6.         [0,0,1,0.5],
  7.         [_playerPos select 0,_playerPos select 1,2.3],
  8.         5,
  9.         5,
  10.         direction player,
  11.         "COMPASS",
  12.         0,
  13.         0.03,
  14.         "PuristaMedium"
  15.     ];
  16. };
复制代码




总之就是超级酷,我们可以通过DrawIcon3D画出任何我们想要的东西!

下一篇我们先开始详述ArmA3的一个新函数:BIS_fnc_addStackedEventHandler!

213楼继续教程,《武装突袭3》——addStackedEventHandler

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
发表于 2014-2-11 11:19:17 | 显示全部楼层
懂程序编写真好!
 楼主| 发表于 2014-2-11 21:04:22 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-17 19:59 编辑

213楼《武装突袭3》——addStackedEventHandler



BIS_fnc_addStackedEventHandler是由BI Dev: Nelson Duarte给我们带来的base fn。

如上一篇我所说过的,对于任何低级可笑的赤裸裸OnEachFrame或While loop是不应该出现的,还记得killZone_Kid与kju的交锋,killZone_kid一贯坚持代码的简洁高效性而著称,可惜当时的OnEachFrame还没有很好解决通用性的问题;Kju则一贯坚持community的通用性而设计代码的,因而主张While循环,不过优化确实存在问题。虽然addStackedEventHandler只是一个临时过渡的方案,但毕竟能解决问题就是好猫。

我们先来看看这个函数的作用,它一共支持6种每帧执行的eh,分别是oneachframe, onpreloadstarted, onpreloadfinished, onmapsingleclick, onplayerconnected, onplayerdisconnected
我会给出6个示例以便大家更详细的掌握它。

该函数可以任意删除或添加需要每帧执行的代码,每一个语块都有独立的编号以便随时添加或移除,使通用性成为了可能。

我们先来看看onplayerconnected和onplayerdisconnected这一对。它们这组本身就是server块的代码,也就是说只有服务器方才会记录所有连入游戏或离开游戏的玩家信息,我没有在dedicated上做过测试,所以还不敢说dedicated的情况。现在我们要写一些代码来执行以下功能,当有玩家接入游戏后便会收到消息”hallo!”,当他们离开后会受到”Bye”,消息只有正在接入或离开的玩家才能收到,其他正在游戏的玩家则不受干扰。

  1. fn_connectText = {
  2.   _id publicvariableclient format ["Hallo, %1",_name];
  3. };
  4. fn_DisconnectText = {
  5.   _id publicvariableclient format ["Bye, %1",_name];
  6. };
  7. private ["_hallo","_bye"];
  8. _hallo = ["id_hallo", "onplayerconnected", "fn_ConnectText"] call BIS_fnc_addStackedEventHandler;
  9. _bye = ["id_bye", "onplayerDisconnected", "fn_DisconnectText"] call BIS_fnc_addStackedEventHandler;
复制代码


id作为特定的身份识别,我们有必要给自己所需的句块添加可供识别的编号,onplayerconnected和onplayerDisconnected的_id和_name作为特殊私用变量我们将其直接转递到publicvariableclient中去使用。

如果中途我们要修改问候语句,或是直接删除eh,亦或是在原有的基础上再新增一些内容,那么可以这么做,这时我们之前的id编号就派上了用场,他们用来告诉程序识别你所对应eh的操作:

  1. private "_replaced";
  2. _replaced = ["id_hallo", "onplayerconnected", "Replace_connectText"] call BIS_fnc_addStackedEventHandler;
  3. private "_addNew";
  4. _AddNew = ["id_new", "onplayerconnected", "Add_ConnectText"] call BIS_fnc_addStackedEventHandler;
  5. private "_removed";
  6. _removed = ["id_bye", "onplayerDisconnected"] call BIS_fnc_removeStackedEventHandler;  
复制代码


下面我们再来看看onpreloadstarted和onpreloadfinished。这两个是OA时期BIS_fnc_PreloadManager的基础代码,它们可以比喻为是OnEachFrame的替补队员,在任务加载时会出现receiving状态,所以在init.sqf我们运行它们时就会开始每帧侦测整个任务的receiving,不过需要注意的是onpreloadstarted和onpreloadfinished这两个eh在侦测到startLoadingScreen或endLoadingScreen时只会fire一次,而非像OnEachFrame那样每帧探测的同时每帧运行代码。由于onpreloadstarted和onpreloadfinished会产生和OnEachFrame相同的通用性问题,即其他MOD使用的onpreloadstarted和onpreloadfinished会覆盖其他MOD的代码,所以我们同样需要Nelson Duarte的帮忙来解决通用性问题。

现在我们来看一下示例:

  1. Start_Text = {diag_log "preload started";};
  2. End_Text = {diag_log "preload finished";};
  3. private ["_start","_end"];
  4. _start = ["ID_start","onPreloadStarted",Start_Text] call BIS_fnc_addStackedEventHandler;
  5. _end = ["ID_end","onPreloadFinished",End_Text] call BIS_fnc_addStackedEventHandler;
  6. startLoadingScreen ["Loading"];
  7. progressLoadingScreen 0;
  8. endLoadingScreen;
复制代码


作为LoadingScreen的扩展和定义我将在下一篇中进行详解,同时这套代码也有其所特有的优势,当然还有一些注意事项也是需要避免的。

现在我们来看看onmapsingleclick,这个代码同样存在通用时相互覆盖的问题,下面我们来看一下示例:

  1. fn_code = {
  2.   hint format ["Array: %1, Pos: %2, Shift: %3, alt: %4",_units,_pos,_shift,_alt];
  3.   true;
  4. };
  5. private "_my_id";
  6. _my_id = ["BIS_id","onMapSingleClick",fn_code] call BIS_fnc_addStackedEventHandler;
复制代码


这里的key识别我们当然也可以用默认的BIS_id,两种均可。这么做就统一了口径,这也是为什么说不要去使用任何新人做的MOD或未经优化的代码。许多人只了解皮毛却开始叫唤发布MOD了,其实他们却不知道如何将自己的MOD与其他人的MOD口径进行统一,这么做的结果就是极差的兼容性和优化性。至于OnEachFrame,我这里就和大家分享一下时间快速切换的炫美效果吧。

  1. private ["_sec","_hour","_id","_removed"];
  2. _sec = [_this,0,0,[""]] call BIS_fnc_param;
  3. _hour = [_this,1,0,[""]] call BIS_fnc_param;
  4. fn_SkipTime = {
  5. skipTime (_hour/((floor diag_fpsmin)*_sec));
  6. };
  7. _id = ["BIS_id", "onEachFrame", "fn_SkipTime"] call BIS_fnc_addStackedEventHandler;
  8. sleep _sec;
  9. _removed = [_id, "onEachFrame"] call BIS_fnc_removeStackedEventHandler;
复制代码




作为结语,再和大家分享一些小技巧,在编辑器模式下,如果玩家需要切换位置,直接在地图上按住alt点击鼠标到你想要的地方,下面我将详细阐述8个定位代码以及将EULA说完。

214楼继续教程,《武装突袭3》——定位码家族及EULA规则

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
 楼主| 发表于 2014-2-14 15:22:41 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-20 13:08 编辑

214楼,《武装突袭3》——宙斯的visiblePosition




是时候兑现之前一直提到的8个定位码了。当你还在纠结于单位定位时,这篇帖子该好好看看了,有些人定位只靠GetPosASL这一个?我想说简直是弱爆了,到目前为止ARMA3 1.10共有8个定位码:position, getPos, getPosATL, getPosASLW, visiblePosition, visiblePositionASL,getPosASL和modelToWorld。配合这8个定位码,我会将setPos, SetPosASL, setPosATL, SetPosASL2, setVehiclePosition, ATLtoASL和ASLtoATL这7个设位码作分析。

我们先将这8个定位码进行归类:受地形影响的为一类,不受地形影响的为另一类。

不受地形影响的并且定位结果相同的是:
position, getPos, visiblePosition和modelToWorld。定位结果如下所示:



既然它们的定位结果都一样,那究竟有什么区别,是不是随便选一个?那是绝对不可以的,不要被biwiki上的所谓定位大比拼的结果所混淆,为什么?因为这些比较根本就没有任何实际意义而且不准确,所以我们需要看一下真正的区别:
Position的定位是本地的,也就是说这个代码省去了C++在MP模式下的编写,所以不支持MP模式。

Getpos是全面定位的,支持C++ MP段下的编写,所以其作用是全局的。

modelToWorld表现形式和position无异,同样是本地作用。

visiblePosition,一个非常怪异,难以理解的代码,可见位置?是不是说获取实体的可视坐标?答案是错的!visiblePosition其实是一个大量使用于ZEUS的代码!难以理解?





当然直接解释visiblePosition会显得苍白无力而且没有带入感,首先我们有没有看到ZEUS上许多的小标记?(ZEUS是5个人搞出来的东西,力气最大的肯定是Moricky,因为他本身就是负责ARMA3 GUI这一块的,当然作为1.12与ZEUS相关的C++新代码,则由Filip Sadovský负责了,一些新功能当然也就得以实现,比如在地图上对单位的直接选择,新的GUI rsctree等等)在学习了BIS_fnc_AddStackedEventhandler和DrawIcon之后应该不会是一个很大的问题,那我们先来试着做一下,我们先要采集一个能用的图片文件,随后就能开始最简单的示例。

作为题外话,关于cfg的采取是一项基本功,对于许多人不明就里的采取众多不必要的Cfg,很明显,不仅浪费计算资源,而且更本就不理解其中的逻辑而直接抄袭代码,显然意味着没有掌握。Moricky为cfg的采取写了一套函数却没人用,下一篇中我会好好介绍下它们。

  1. fn_Draw3D = {
  2.     {
  3.     drawIcon3D [
  4.         (configfile >> "CfgVehicleIcons" >> "iconMan") call bis_fnc_getcfgdata,
  5.         [0,1,1,0.5],
  6.         [
  7.                 (visiblePosition _x) select 0,
  8.                 (visiblePosition _x) select 1,
  9.                 ((visiblePosition _x) select 2) + 1
  10.                 ],
  11.         1,
  12.         1,
  13.         direction _x,
  14.         name _x,
  15.         0,
  16.         0.03,
  17.         "PuristaMedium"
  18.     ];
  19.         } forEach AllUnits;
  20. };
  21. private ["_addNew"];
  22. _addNew = ["BIS_id", "onEachFrame", "fn_Draw3D"] call BIS_fnc_addStackedEventHandler;
复制代码


(编者注:在此感谢BI Dev zGuba对本段代码的建议:http://forums.bistudio.com/showt ... wfull=1#post2624546 )

当然我们还能用addMissionEventhandler来替代,也可以不断做拓展写出我们自己的Zeus系统,这我会在后面讲,这里暂不作讨论。visiblePosition在这里的作用就是每帧动态捕捉,是没有延迟的,如果你用其他任何的定位码都无法替代visiblePosition,因为它们都是会发生捕捉延迟。





这只是测试效果,Zeus非常小的一部分供演示。

回到正题,其实这四个定位码的测评还未完全结束,我们需要对它们的优化性进行测试:
Position耗时:


Getpos耗时:


ModelToWorld耗时:


Visibleposition耗时:


所以最后的结果就是,position因为其过慢的速度和过少的功能而彻底淘汰,其而代之由getpo担当定位重任,visiblePosition和modelToWorld有其对应的特殊功能但却并不拥有MP兼容性所以谨慎选择你的定位码,而至于另外4个,显然它们本身的坐标定位就不一样,所以不必做更多比较,wiki上已经很详细了,相信你也不会用错,只不过这4个的相同结果容易混淆,所以有必要在此放出此教程。下一篇我将讲讲《武装突袭3》——LoadingScreen家族。

217楼继续教程,《武装突袭3》——MP编写全教程

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
发表于 2014-2-14 19:10:23 | 显示全部楼层
问个问题哈,学习BIS脚本从思维上有什么需要转变的地方吗?

——这个问题针对的是:
一个只认真学过DOS下C语言编程
VB、Pascal不熟,唯一学过的脚本语言是LOGO[笑]
只理解调用中断写VGA内存之类的孩纸。
 楼主| 发表于 2014-2-14 19:34:05 | 显示全部楼层
hiddenzone 发表于 2014-2-14 19:10
问个问题哈,学习BIS脚本从思维上有什么需要转变的地方吗?

——这个问题针对的是:

不需要转变啊,BI的sqf其实是一种耗能却高效的语言。

所谓耗能就是不环保,计算时容易产生大量的“二氧化碳”,不过这也是情理之中的事,毕竟BI的程序员们一代接一代花了近14年时间去完善她才有了今天1500个词汇量。

高效就是简单易懂,产量大,只要学一点大家都能写自己的脚本,没有C++来的严谨,所以入门简单。

其实sqf的乐趣就是随着不断的深入你会发现越来越多的bug,在bug的世界中寻找方法去解决这些bug或许就是训练大脑机能的一种运动吧:)
 楼主| 发表于 2014-2-17 19:41:37 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-20 12:59 编辑

217楼,《武装突袭3》——MP教程入门及废弃代码



很多人并非从2001年起就开始跟随波米一路走到现在,2005年起武装突袭:女王开场白发布后带来了一些关于初始化代码的优化命令,但随着MP作弊手段的不断进步和反作弊手段的不断增强,至ARMA3发布后便彻底废弃了这些代码,还有些功能虽然保持原有的支持但也逐渐被功能更强大的代码或函数代替而渐渐退出历史的舞台。
BIS_fnc_addStackedEventhandler虽然支持loadingScreen家族但显然没什么大用处,新的mission presentation支持新的loading screen rsc,有机会我会在后面几篇讲,当然,出于对老同志的告别礼,还是介绍一下loadingScreen家族的工作机制:

这里我们用一套wiki提供的示例:

  1. class RscText
  2. {
  3.         type = 0;
  4.         idc = -1;
  5.         x = 0;
  6.         y = 0;
  7.         h = 0.037;
  8.         w = 0.3;
  9.         style = 0x100;
  10.         font = Zeppelin32;
  11.         SizeEx = 0.03921;
  12.         colorText[] = {1,1,1,1};
  13.         colorBackground[] = {0, 0, 0, 0};
  14.         linespacing = 1;
  15. };
  16. class RscPicture
  17. {
  18.         access=0;
  19.         type=0;
  20.         idc=-1;
  21.         style=48;
  22.         colorBackground[]={0,0,0,0};
  23.         colorText[]={1,1,1,1};
  24.         font="TahomaB";
  25.         sizeEx=0;
  26.         lineSpacing=0;
  27.         text="";
  28. };
  29. class RscLoadingText : RscText
  30. {
  31.         style = 2;
  32.         x = 0.323532;
  33.         y = 0.666672;
  34.         w = 0.352944;
  35.         h = 0.039216;
  36.         sizeEx = 0.03921;
  37.         colorText[] = {0.543,0.5742,0.4102,1.0};
  38. };
  39. class RscProgress
  40. {
  41.         x = 0.344;
  42.         y = 0.619;
  43.         w = 0.313726;
  44.         h = 0.0261438;
  45.         texture = "#(argb,8,8,3)color(0,0,0,0)";
  46.         colorFrame[] = {0,0,0,0};
  47.         colorBar[] = {1,1,1,1};
  48. };
  49. class RscProgressNotFreeze
  50. {
  51.         idc = -1;
  52.         type = 45;
  53.         style = 0;
  54.         x = 0.022059;
  55.         y = 0.911772;
  56.         w = 0.029412;
  57.         h = 0.039216;
  58.         texture = "#(argb,8,8,3)color(0,0,0,0)";
  59. };

  60. class Harrier_loadingScreen
  61. {
  62.         idd = -1;
  63.         duration = 10e10;
  64.         fadein = 0;
  65.         fadeout = 0;
  66.         name = "loading screen";
  67.         class controlsBackground
  68.         {
  69.                 class blackBG : RscText
  70.                 {
  71.                         x = safezoneX;
  72.                         y = safezoneY;
  73.                         w = safezoneW;
  74.                         h = safezoneH;
  75.                         text = "";
  76.                         colorText[] = {0,0,0,0};
  77.                         colorBackground[] = {0,0,0,1};
  78.                 };
  79.                 class nicePic : RscPicture
  80.                 {
  81.                         style = 48 + 0x800;
  82.                         x = safezoneX + safezoneW/2 - 0.25;
  83.                         y = safezoneY + safezoneH/2 - 0.2;
  84.                         w = 0.5;
  85.                         h = 0.4;
  86.                         text = "#(argb,8,8,3)color(0,0,0,0)";
  87.                 };
  88.         };
  89.         class controls
  90.         {
  91.                 class Title1 : RscLoadingText
  92.                 {
  93.                         text = "LOADING";
  94.                 };
  95.                 class CA_Progress : RscProgress
  96.                 {
  97.                         idc = 104;
  98.                         type = 8;
  99.                         style = 0;
  100.                         texture = "#(argb,8,8,3)color(0,0,0,0)";
  101.                 };
  102.                 class CA_Progress2 : RscProgressNotFreeze
  103.                 {
  104.                         idc = 103;
  105.                 };
  106.                 class Name2: RscText
  107.                 {
  108.                         idc = 101;
  109.                         x = 0.05;
  110.                         y = 0.029412;
  111.                         w = 0.9;
  112.                         h = 0.04902;
  113.                         text = "";
  114.                         sizeEx = 0.05;
  115.                         colorText[] = {0.543,0.5742,0.4102,1.0};
  116.                 };
  117.         };
  118. };
复制代码




由于其始终存在模块的可定义性所以并未被完全废除,任务发烧友们如果喜欢的话可以试试(注意不要将其定义在rscTitle下,GUI的教程我也会逐个放出)。还有点提醒下,当加载画面出现时不要使用延时语句,否则游戏就会被冻结。第二是在这个过程中我们可以运行脚本,此时游戏处于disableSimulation阶段,所以脚本会全速运行。(当然如果函数写得好的根本就不必要,因为我们的脚本本身就是最佳优化)

现在我们来看看被ban的代码:processInitCommands,setVehicleInit,clearVehicleInit和finishMissionInit。现在我们的问题是黑客们是如何通过这些代码来黑的呢?

这得从MP脚本编写开始,MP的脚本编写是一块大头,高级的代码编写者必须要掌握MP编写的基本技巧。MP的编写及表现形式与SP截然不同,很多人并不懂得基本的编写技巧,上手ARMA3后就直接开始编写MP任务开打,当然BI提供了大量的MP兼容性模组和定义方便我们编写,不过稍微高级点立马各种出错。

开始MP时首先必须搞清楚哪些代码是AG-global,哪些是EG-global,而哪些是EL-Local或AL-Local

在解释它们四个之前我们先要把“本地性”给搞清楚。在MP编写时我们常常会碰到这样的问题,明明我开了一辆坦克但为什么人家看不到我的坦克?明明我生成了一组人马为什么他们无法识别其他玩家,而其他玩家也看不到这群人?明明我把人质救了可为什么其他玩家还显示人质未拯救?果断放弃MP的脚本编写?发现ARMA的世界疯了?还是自己疯了?

好吧,我只想说,既然疯一把,那为什么不彻底?

多人模式有非常多的代码,我们甚至有必要对之前所学的所有代码都重新梳理一遍。ARMA3共有三种形式的“玩家”。

1、        arma3server.exe这是专用服务器,它有自己独立的判断方式isDedicated
2、        arma3.exe当你建房时你是服务器,判断方式isServer
3、        普通玩家为client,判断方式!isserver

他们三个全部是独立判断,别混淆了,isdedicated不是isserver!

在MP中地图上的每一个单位,物体(有生命,无生命的所有东西)都有一个主人,我们通过owner obj判断他们的主人,主人就是每个玩家。(只有当这个东西属于所有玩家时,所有玩家才能看到它并使用它,控制它,比如说在我的PC上脚本刷出来一堆兵准备作弊,我只想说太天真了,这堆兵只属于你却不属于任何其他玩家,虽然你看得到这堆兵但在其他玩家电脑上更本就不存在,这只是幻觉…)

每台游戏的机器都有一个编码,就是id号,通过owner我们就能返回id号从而判断物体属于哪台机器,凡是属于这台机器的全部返回local,玩家正在控制的单位全部属于控制机器,所以永远都是local。
(比如我脚本刷出来的车子神马的只有我看得见,在其他人那儿是不存在的,刨根问底,那如果我开飞机会怎么样?很简单,其他人会看到你腾空而起像超人一样…国内的服务器很少碰到这种情况,第一,目前hacker们还没有泛滥到这种程度,第二,battleEye也不是吃素的,不是脚本高手不想冒着被ban的危险,第三,如果你不幸遇到了,要么踢掉,要么赶紧加好友膜拜学习…)

那接下来你要问为什么菜鸟也能编MP任务,玩得不亦乐乎呢?好问题,第一菜鸟不会MP脚本,他们所作的一切全部是sqm形式的任务,就是全部使用编辑器做出来的,那在编辑器上的所有东西全部属于所有人!也就是说凡是是地图编辑器上的单位是不会出现问题的,出问题的地方就是后期的脚本!

回到“本地性”,虽然player对控制机local,但也是可以变的,比如teamSwitch,遥控,脚本更换玩家等等,这时本地性将发生变化,还有比如说地图上的一辆车属于所有人,我上了它,它就属于我的了,就对我local了,owner就是我的id, owner就属于我,除非等到下一个玩家上了它,owner id才会变成他的, local并不是与owner一个概念,你上了这辆车坐后排,这辆车对你local但owner不是你而是驾驶员,如果你开车开到一半你女友把你电源拔了,那这辆车的owner马上变成了服务器拥有。MP是一个“婊子”,“本地性”的各种变化导致许多人疯掉,(在ARMA3新引入的EH “local”能够每帧侦测实体本地性的变化,一旦owner id发生变化就会fire一次,此乃后话)

现在我们来到最闹心的地方,先来介绍下4个属性中的第一个:



AL-Local:全称叫做Argument Local,神马意思?https://community.bistudio.com/wiki/File:arguments_local.gif
先来讲个故事,那儿有辆车,我想把它setfuel为0,漏油。在SP中很简单,但到了MP各种疯狂,我找到了那辆车随后setfuel了但为什么它TMD根本没有漏油,油箱满满的,其他玩家依旧开着它大摇大摆的开走了,接着我疯狂的写循环脚本甚至OnEachFrame每帧把它setfuel 0但为什么根本不管用!一场游戏下来我写的脚本各种不符合逻辑。

现在记住咯,AL-Local属性的命令首先必须得要找到这辆车的主人!当你找到它的主人后就可以判断这辆车对主人local,你甚至不能找它的副驾驶座是谁,因为副驾驶不是这辆车的主人。找到主人后你必须得在主人的机器上运行setfuel 0,这样这辆车才会彻底没油。

有时候我想把车子setDamage 1直接爆了不得了?奇怪,为什么这个代码不需要找到主人就可以直接弄爆一辆车呢?这里向大家介绍:

AG-Global https://community.bistudio.com/wiki/File:arguments_global.gif
这种属性的代码是不需要找到主人的,你在任何PC上都可以搞定。接下来又碰到问题了,为什么setdamage那么顺利,就一个代码这辆车就爆了而且不需要BIS_fnc_MP公布?

这时我们引入EG-Global概念:
EG-Global https://community.bistudio.com/wiki/File:effects_global.gif
这种属性的代码相当于内置的publicVariable,或相当于内置的SetDamage(Global)MP核心代码,它们不需要公布就能够产生于所有PC的效果,那问题是为什么有些代码要publicvariable呢?

这时我们引入EL-Local概念:https://community.bistudio.com/wiki/File:effects_local.gif
这种属性的代码必须pubilcVariable,这就是为什么我们看到各种MP addon需要使用pubilcvariable,否则它们的效应只能在拥有者上产生效应,对其他人而言根本没有用,这也就是为什么在MP中刷兵要publicVariable。

我们这只是管中窥豹,只见一斑。MP任务的编写对不愿意思考的人来说简直就是地狱,但对于爱动脑子的人来说比SP脚本编写来得更刺激。所以当某些菜鸟们在发帖求助我怎么网速那么慢??我怎么会看到有人在DayZ上作弊??我怎么越玩越卡??等等,我也只能“呵呵“了。

MP的脚本高手既可以造福于社区编写出高质量的MP任务一举夺魁MANW的MP任务设计大奖;同样也可以进入你的房间把你精心组织的一场coop黑得玩不下去,我们只玩到了ARMA的冰山一角,程序员有其双面性我们不能否认,当然我不希望有人看了教程后走火入魔黑别人服务器去了...

Locailty研究将是一个新领域,国内的天空太小了,必须接触那些script guru们,不是说什么我们简单的修改一下脚本,拼凑一下脚本就行了,一些好的函数必须拆开研究!

AI对队长local,车辆对驾驶员local,sqm的小队长对服务器local,刷出来的兵对owner PC local,刷出来的空车对代码执行端的PC local,代码的四个MP属性,locality的改变,玩家的死亡,重生,断线,返回,各种脚本命令moveIndriver,join,selectplayer等等对locailty的干预等等。

欢迎来到武装突袭3 MP的世界。

休息一下,关于废弃代码我还没讲完,下一篇我将继续。

218楼继续教程,《武装突袭3》——MP教程及废弃代码追加

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
 楼主| 发表于 2014-2-20 12:57:45 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-22 18:29 编辑

218楼,《武装突袭3》——MP教程及废弃代码追加



ARMA3“本地性”的介绍我们才刚刚揭开序幕,在biwiki中所有代码最多只拥有两种属性,有些则甚至没有。怎么办,如果BI Devs没有时间更新它们,那就得靠我们自己了,命令究竟在MP中的表现如何只能靠我们自己琢磨,然而sqf就像是一个不断成长的孩子,你总会发现她各种各样的缺点,这也就是为什么BI需要feedback tracker来追踪各种命令在每次更新后可能出现的新问题并加以改正。

现在我们知道了如果命令是AL-Local的话,我们有必要首先通过local命令判断对象是否local,随后第二部判断代码是否EL-Local,如果是的话,根据你的需要是否要将效果作用于所有玩家,还是你所指定的几个玩家。这同样也解释了我们的对MP的最基本疑问,为什么BI要将命令分成四个属性,目的是为了更好的控制,如果MP与SP的编写逻辑完全相同,那么PC效果的分别作用将不可能实现。(记住,不是所有的任务设计者都希望所有玩家都保持同步,举个例子,我想让A看到B着火了但却不想让C看到,这就区分出AG+EG和AL+EL属性;更进一步,或许你会问那直接取消AG+EG属性不就得了,直接全部改成AG+EL不是更简单?聪明的BI Devs不会让你这么做,至少他们要保证最基本的自然常识正确,比如说我想把C炸死,A看到爆炸的全过程却不让B看到,这显然不符合逻辑,C有没有炸死只有一个结果,非死即活,所以一些基本逻辑比如setdamage那就不能EL而必须是EG属性)

检查实体的“本地性”不仅能够通过local判断,同样也可以通过字符串判断。通过在MP中引擎返回的实体后缀来判断实体是否local。(这种测试没有办法在SP模式下做到,只有到MP中至少两台机子进行测试)在SP中由于所有实体都是local的所以不可能返回remote后缀,反之亦然。



上图意为连入服务器的玩家拥有后缀remote,就是非local实体。这里我们有专门的工具来进行MP代码的测试,但是代价不是一般性玩家所能承受的,因为你至少要有两台能够跑得动ARMA3的机子和两套正版的ARMA3 Dev版。除非你是技术土豪,否则就是作死。我们没有这种硬件设备也没关系,我们只要了解了MP的工作原理,一台机子照样能够写MP任务。如果你有两台机子那更好。玩MP的玩家都知道是不能使用debug console的(那是绝对的不行,否则岂不是个个孙悟空大闹天宫了!)
不过如果你愿意这么做也没人拦着你,作为Dev,debug console是绝对需要的,即使是在MP模式下,所以请在description.ext中加上enableDebugConsole = 2; (0代表编辑器启用,1代表编辑器+SP启用,2代表编辑器+SP+MP启用)
现在回到经典的问题:我想把一辆车setfuel 0; 首先判断车是否local,如果玩家还没上车,那车是local的所以服务器可以直接setfuel 0; 如果玩家上车了,local判断为false显然setfuel没有用,怎么办?那我必须得把local的判断写到那个玩家的PC上去,怎么写过去?
我们得了解一下MP的基本常识,服务器开房,玩家加入的同时下载你的任务,这样不就解决了吗?玩家下载了你的任务,你任务中的脚本也下载了,local的判断也随之到了他的PC上,当所有PC同时运行这个脚本时只有一个人的PC上会把这辆车的locality返回为true,那只要他true了,其他人false都无所谓,由于setfuel具有EG属性所以虽然脚本只在他那里继续跑了下去但车子没油的作用传递到了所有玩家PC上,数据同步更新,所有在线PC就都自动把车设成无油状态了,类似于我之前教程提到的publicVariable。

差不多搞清楚之后我们可以开始介绍一下新玩具了,addEH Local(之后的篇章eventHandler我全部使用EH代替,MPeventHandler全部用MPEH代替,AddStackedEventHandler OnEachFrame全部用FPH代替,MissionEventHandler全部用MEH代替,PublicVariableEventhandler全部使用PVEH代替)
addEH Local我之前已经介绍过了 https://community.bistudio.com/wiki/Arma_3:_Event_Handlers#Local ,其属性是AG+EL,就是说全局参数+局部作用,我可以在任何PC上给实体添加EH,fire之后的作用为局部。现在我先不说EH Local的用法,毕竟EH Local的侦测涉及更高级的功能,我们先来第一个实验,在MP模式下,如何才能通过脚本来设定一辆车的油:

  1. fnc_setFuel = {
  2.     private ["_veh","_fuel"];
  3.     _veh = _this select 0;
  4.     _fuel = _this select 1;
  5.     if (local _veh) then [{
  6.         _veh setFuel _fuel;
  7.     },{
  8.         PVsetFuel = _this;
  9.         if (isDedicated) then [{
  10.             (owner _veh) publicVariableClient "PVsetFuel";
  11.         },{
  12.             publicVariableServer "PVsetFuel";
  13.         }];
  14.     }];
  15. };
  16. "PVsetFuel" addPublicVariableEventHandler {
  17.     (_this select 1) call fnc_setFuel;
  18. };
复制代码


SP模式下只需要简单的setfuel即可,但是我们看到在MP模式中的编写却是相当的复杂!(刚开始的时候我假设MP的编写是相当复杂的,毕竟万事开头难嘛,不过只要过了这道坎,这就会成了最基础的基本功,我们才有可能编写出更加复杂的脚本,几个章节下来这些东西就会变成最为简单的东西)

我们先来分析一下,MP中我们会接触到相当多的MP特有判断和代码,先来看看这段代码中我们可能会碰到的困难,第一是else在这里省略了,在高级篇中出于代码优化的考虑请使用if () then [{},{}];
第二是isDedicated和isServer的区别,永远记住专用服务器不是普通的server,它是arma3server.exe的独立进程,现在这段代码是客户端的init.sqf代码,脚本非运行阶段所有代码全部处于待激活状态,就是说我们现在看到的这段init.sqf并没有被激活。激活后当判断到_veh非local时接下来的步骤就是马上检查谁拥有这辆车,专用服务器判断出来的车需要通过owner得到服务器的id随机将代码发送到所有客户端publicVariableClient,其他客户端检查出来的车则将代码直接发送到专用服务器上。最后的步骤是PVEH,内部参量的第二个值直接将数组重新传递函数的开头判断本地性。其作用就是先找到owner,随后直接setfuel全局作用搞定。相当于自己做了一个超官方的setfuelGlobal。

貌似复杂但这是必须的,只有搞定了最基本的东西我们才能玩新玩具。

现在我们再稍微深入一点,MP编写的高手都必须考虑绝大多数人不曾考虑的问题,带宽的利用率。

在SP编写中我们绝对不可能引进“带宽利用率”的概念但是Dev级别的就必须考虑进去。这是一个什么概念?就是如果说你写出来的MP代码没有优化,不仅跑得慢,多而颓废,而且还耗带宽的利用率,那么结果就是一场MP任务越打越卡,帧数不稳定,就算网络再好也没用,问题不在硬件,而在你的代码!

所以,如果再看到菜鸟发帖求助各种卡神马的,抱歉,先看看自己的任务有没有问题再抱怨ARMA3。

为什么会发生这种情况呢?第一,所有EG属性的命令运行一次增加一次带宽利用,所有PV也会增加一次带宽利用,那么在这段代码中我们最多要两次带宽利用。高级MP编写的原则是利用率越少,游戏越流畅,反之亦然!

好了,休息一下,在《武装突袭3》——MP教程及废弃代码追加2中我们继续。


220楼继续教程,MP教程及废弃代码追加2

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
发表于 2014-2-20 23:20:17 | 显示全部楼层
本帖最后由 qevhytpl 于 2014-2-20 23:27 编辑
FFUR2007SLX2_5 发表于 2014-2-17 19:41
217楼,《武装突袭3》——MP教程入门及废弃代码


精品教程,一直等待版主的MP的教程,信息量很大,我战地2⃣️的编辑工作是纯sqf组合,整个思维框架也是我独立完成,主意是EA的,但技术细节是我原创,脚本也是一句句写的,我不搬别人的,感觉研究别人编写思路的时间就够自己写新的了,如你所说,全是线性思维,看你的教程我想我的归宿应该是Sqm+function,期待后续!有时间完善战地2。
 楼主| 发表于 2014-2-22 18:28:19 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-23 17:30 编辑

220楼,《武装突袭3》——MP教程及废弃代码追加2



“带宽优化”的概念同代码优化的概念有异曲同工之妙,在优化上面我相信现在已经不会是问题了因为到了这里相信我们已经养成了代码优化的习惯,现在我们得开始渐渐向那些废弃的代码靠拢了,第一个问题肯定是为什么这些代码会被废弃。其中包含大量的信息量,biwiki解释道出于MP的安全考虑,那什么是MP的安全呢?

现在我们进入MP脚本编写的第三阶段——代码安全性。

现在我们已经学会了MP编写的基本法则,也明白了MP任务的一些基本编写思路,比如说我们想要在哪些PC上执行代码,哪些PC上不执行某些代码。这些都可以通过PVServer,PVClient,isDedicated,isServer等进行限制。然而现在所有大型的MP任务所面临的一个问题就是作弊!PVServer,PVClient在带给我们MP编写便利的同时也埋下了黑客进入的种子,这是一把双刃剑!

我在这里所说的“黑客”们不是简单的什么作弊插件,作弊MOD等等,什么Kronzky的cheat mod,kju的dev console,TroopMon2,MCC,VTS神马的,我看许多人都喜欢玩这些东西而且不亦乐乎,却不搞搞明白为什么KRON没有把MOD写成MP capable ? kju为什么限制dev console的MP功能?TroopMon2,MCC为什么永远都不可能在MP中得到加载?(至少我能保证Moriky的ZEUS不会是这种类型,我们应该明白MP中的平衡性意味着什么,没人愿意被别人耍,当然耍别人的后果也是严重的)

第二我想说的是如果说有人认为自己可以用这些MOD在MP上去作弊是非常愚蠢的,如果你碰到的admin是精通MP编写的高手那你就完了,等着被ban然后再花300大洋重新买一个号吧,除非你钱多送给BI。在此我想再声明一下,看了教程后不要将你的小聪明花在错误的地方,魔高一尺道高一丈,在这里我不会透露任何关于作弊的信息但MP脚本的安全性必须要让你们了解作弊是如何进行的,这样才有更加深入的认识,这是一对矛盾体,但是请把持好自己的道德底线。我不想看到你在别的玩家头上生成一枚JDAM随后轻者被踢,重者被BANN的下场!

MP的代码安全性就是反作弊,我相信没人愿意自己精心组织的一场coop以骂战收场,原因是各种莫名其妙情况发生。(ZEUS的RTE是有规则的,而hacker更本就没有道德规则可言)

想要保护好自己的MP任务不被老外黑,首先做到自己业务精通,随后了解他们是怎么黑的,才能有的放矢的保护自己的代码。

BI Dev Dwarden一直在努力的改进BE,(BattleEye)防作弊的第一步就是先防MOD,服务器有权选择允许挂载的MOD,一些低级趣味的什么超级MOD就可以一边儿呆去了。但现在的问题是始终有强大的hacker可以越过BE V2的监控,顺利绕过第一个防火墙,随即发生的情况就是他可以后台挂在MOD了,但这是服务器所不愿意看到的。



好了,接着MP开始了,hacker就可以开始行动了,而服务器目前还一无所知。其实目前能够正真称得上hacker的很少,因为首先他们自己就是MP脚本高手,大多数人都是下载MOD的菜鸟,只不过会玩MOD罢了,我们称他们为作弊者,一般性作弊者很容易就可以被BAN掉或踢掉,首先他们只会用MOD所提供的功能瞎折腾,这很快就能被admin检测到,一旦大家发现不对头那作弊者就倒霉了。
问题是hacker们,他们有自己的dev console来实时将脚本从客户端发送到服务器,接着你就碰到了这种情况“I wanna to f*** you all! B******”一排大字映入眼帘,或者是不断的发布无用代码占用你的带宽,随后所有人的网速直线下滑卡得一地。你不知道黑客会干些什么,如果你没有完整安全的监控脚本甚至无法dialog源头在哪里!



好吧,事已至此,那我们就开始高级MP编写的第三篇章——防hacker之建立安全的函数调用结构。

MP脚本编写到一定程度后考虑到各客户端网络的流畅性,你是肯定不会再直接将一大串一大串的源码直接在客户端之间进行传递,因为这么做会加大带宽的利用率,同时也会增加源码被hacker截获的风险,我们知道高手过招不是小儿科的事儿,有些好的服务器,大型的全天候服务器或RPG生活中有许多核心源码,这些源码被单独放在server的文件包内不会被客户端下载的,我们需要这些源码来给客户端发出各种指令,倘若没有好的汇编方式直接传递则很有可能被黑客们截获,到时候你的服务器被一群作弊者毁了有你好哭。

所以我们需要做什么,我们要做的就是首先就是先将写好的函数分配给每个客户端随后锁上它们,随后通过几个简单的参数调用从而随时随地的运行它们,这比活生生直接传递源码要好得多。

具体怎么做?我们把函数写入init.sqf中,客户端下载后连入游戏肯定每个人都得跑一次,好了,函数就分配好了。接着随着脚本的控制我们可以随时随地的调用某个客户端的状态,使用publicVariable和PVEH就可以了。

假设我们现在要做的是先要定义一个函数,这个【函数hallo】的作用是让【玩家A,玩家B】运行【另一个脚本C】,【脚本C】的作用比方说是blabla…,随后我们还需要一个PVEH来待命,一旦准备调用【函数hallo】PVEH就会立马把【函数hallo】发布给所有人。思路清楚后开始行动,我们看看如果我们使用的是普通的MP编写方式黑客们是如何截获我们的代码并篡改的:

  1. Fn_hallo = {
  2.     private ["_Scr","_params"];
  3.     _Scr = _this select 0;
  4.     _params = _this select 1;
  5.     call compile format ["%2 call Script_%1", _Scr, _params];
  6. };
复制代码


【函数hallo】目的是让对象运行自己的脚本,比如说有Script_前缀的脚本。(或自己名字)

  1. Script_blabla = {
  2.     private "_params";
  3.     _params = _this select 0;
  4.     hint str _params;
  5. };
复制代码


【脚本blabla】(当然也可以叫函数,无所谓,便于区分)

  1. My_publicvariable = []; "my_publicvariable" addPublicVariableEventHandler {
  2.     (_this select 1) call Fn_hallo;
  3. };
复制代码


让PVEH待命,一旦之后的my_publicvariable发布立马全客户端(包括服务器)运行fn_hallo

  1. My_publicvariable = ["blabla", ["My name is Peter"]];
  2. publicVariable "my_publicvariable";
复制代码


好了,代码发布了,Script_blabla也运行了,所有客户端也收到了,感觉不错,使用call compile format汇编之后我们等于给每个客户端运行了call {[“My name is Peter"] call Script_blabla};

但接着当高手黑客来到时,问题就出现了,看看他是怎么篡改你的代码的。

当他截获了你的变量,随后写了这么一套东西公布出去了,那你就玩完了:

  1. My_publicvariable = ["blabla;...bad_code...", ["My name is Peter"]];
  2. publicVariable "my_publicvariable";
复制代码


汇编后就成了这个样子:

  1. call {["My name is Peter"] call Script_blabla;..bad_code...};
复制代码


他就是这么顺利的“插入”了,随后就开始运行他的恶意代码了。所以作为“创建安全调用机制”的引言,请不要使用call compile format进行汇编,而是使用missionnamespace!

  1. _params call missionNamespace getVariable [format ["%1", _Scr], Script_blabla];
复制代码


这么做黑客们就没有办法插入恶意代码了!为什么?因为即使能够插入他也不能运行,那我们应该怎么写?

  1. fnc_ERROR = {
  2.     diag_log (format [
  3.         "ERROR! Call to non-existent function '%1' (Passed params: %2)",
  4.         _this select 0,
  5.         _this
  6.     ])
  7. };
  8. fnc_hallo = {
  9.     private ["_fnc","_code"];
  10.     _fnc = _this select 0;
  11.     if (_fnc != "fnc_hallo") then {
  12.         _code = missionNamespace getVariable [
  13.             format ["%1", _fnc],
  14.             fnc_ERROR
  15.         ];
  16.         if ((_this select 2) == "call") then [{
  17.             _this call _code;
  18.         },{
  19.             _nul = _this spawn _code;
  20.         }];
  21.     }
  22. };
复制代码


在这里我们始终保留与之前一样的所有脚本结构,只不过新加了一个【fnc_error】,它的目的是用于反截击,也就是说如果黑客如果打算通过你的Script_blabla脚本,修改截获的My_publicVariable变量修改其中参量并且发布他自己的一套恶意函数或代码,我们的【fnc_error】可以立即截获我们任务里原本所不应该存在的变量并且将恶意的函数名称和黑客所录入的参数记录到我们的rpt中。为什么要搞那么麻烦?那是必须的!小型的,和好友分享的MP任务没有必要搞那么复杂,但是如果你是写大型MP RPG,或是要参加MANW的MP比赛,或是搞独立服务器的,那么有必要这么做了。毕竟林子大了什么样的鸟都有,一旦当BE崩溃,新的atm躲过V2的签名认证你的MP任务就没有任何公平性可言,此时唯一能够保护自己的就是不断的rpt监控和安全的代码防御措施。

【fnc_hallo】我们也重新做了大量的调整,第一个if是要保证【fnc_hallo】不会运行自己,接下来讲下missionnamespace,我之前的教程中曾提到过,它最大的好处就在于当与getvariable配合时,MP模式下没有人能够通过修改变量后缀的方式来作弊,打个比方,如果黑客继续:

  1. My_publicvariable = ["blabla;...bad_code...", ["My name is Peter"]];
  2. publicVariable "my_publicvariable";
复制代码


来强制发布,那么汇编后就成:

  1. missionNamespace getVariable [format ["%1;…bad code…", _fnc]
复制代码


显然这是不成功的。如果黑客不准备使用后缀的方式而是直接使用他自己的一套代码,那么结果是:

  1. missionNamespace getVariable [“Bad_code”,fnc_error];
复制代码


显然bad_code这个变量根本就不存在,所以fnc_error立马开始记录。所以要搞好MP不是容易的事,至少我们要会通过diag_log监控。

安全系统完善后我们就可以放心的调用我们的脚本了:

  1. Script_blabla = {
  2.     hint (_this select 1);
  3. };
  4. My_publicvariable = []; " My_publicvariable " addPublicVariableEventHandler {
  5.     (_this select 1) call fnc_hallo;
  6. };
  7. My_publicvariable = ["Script_blabla ", "My name is Peter", "call"];
  8. publicVariable " My_publicvariable ";
复制代码


最后提醒一下,publicvariable是将内容发布到其他电脑上并让他们执行,我们自己的电脑是不会执行,因为我们自己的电脑可以直接运行代码,不必发布给自己。

全部完成后我们还差最后一道工序,就是将我们的MP框架脚本锁上,这里用到compileFinal,终极汇编需要将代码转换成字符串,所以我们还得给写好的代码加工一下:

  1. fnc_ERROR = compileFinal "
  2.     diag_log (format [
  3.         ""ERROR! Call to non-existent function '%1' (Passed params: %2)"",
  4.         _this select 0,
  5.         _this
  6.     ])
  7. ";
  8. fnc_hallo = compileFinal "
  9.     private [""_fnc"",""_code""];
  10.     _fnc = _this select 0;
  11.     if (_fnc != ""fnc_hallo"") then {
  12.         _code = missionNamespace getVariable [
  13.             format [""%1"", _fnc],
  14.             fnc_ERROR
  15.         ];
  16.         if ((_this select 2) == ""call"") then [{
  17.             _this call _code;
  18.         },{
  19.             _nul = _this spawn _code;
  20.         }];
  21.     }
  22. ";
  23. Script_blabla = compileFinal "
  24.     hint (_this select 1);
  25. ";
复制代码


(至于为什么要compilefinal?因为这样你的函数就再也无法中途被更改了,所有内容全部锁住,MP中必须得这么做,否则就会吃苦头)干的漂亮,当然如果你愿意的话,我们可以继续为我们的框架双保险,下一篇继续!《武装突袭3》——MP教程及废弃代码追加3

227楼继续教程,《武装突袭3》——MP教程及废弃代码追加3

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
发表于 2014-2-22 20:24:48 | 显示全部楼层
FFUR2007SLX2_5 发表于 2014-2-20 12:57
218楼,《武装突袭3》——MP教程及废弃代码追加

精品教程,期待版主继续更新
发表于 2014-2-22 20:24:54 | 显示全部楼层
FFUR2007SLX2_5 发表于 2014-2-20 12:57
218楼,《武装突袭3》——MP教程及废弃代码追加

精品教程,期待版主继续更新
发表于 2014-2-22 20:25:04 | 显示全部楼层
FFUR2007SLX2_5 发表于 2014-2-20 12:57
218楼,《武装突袭3》——MP教程及废弃代码追加

精品教程,期待版主继续更新
 楼主| 发表于 2014-2-23 17:22:40 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-26 11:22 编辑

227楼,《武装突袭3》——MP教程及废弃代码追加3



继续我们MP函数安全框架的教程,如果需要我们甚至可以为我们的函数加上一道铁门,具体请看:

  1. fnc_WHITELIST = compileFinal "
  2.     _this in [
  3.         ""Script_blabla"",
  4.         ""function2"",
  5.         ""function3""
  6.     ]
  7. ";
  8. fnc_ERROR = compileFinal "
  9.     diag_log (format [
  10.         ""ERROR! Call to non-existent function '%1' (Passed params: %2)"",
  11.         _this select 0,
  12.         _this
  13.     ])
  14. ";
  15. fnc_hallo = compileFinal "
  16.     private [""_fnc"",""_code""];
  17.     _fnc = _this select 0;
  18.     if (_fnc call fnc_WHITELIST) then {
  19.         _code = missionNamespace getVariable [
  20.             format [""%1"", _fnc],
  21.             fnc_ERROR
  22.         ];
  23.         if ((_this select 2) == ""call"") then [{
  24.             _this call _code;
  25.         },{
  26.             _nul = _this spawn _code;
  27.         }];
  28.     }
  29. ";
  30. Script_blabla = compileFinal "
  31.     hint (_this select 1);
  32. ";
复制代码


我们继续对所有函数compileFinal,只不过这次在开头我们添加了白名单,这样的双保险甚至使黑客没有任何办法来发布自己含有恶意代码的参数,也没有办法通过截获我们的函数名进行篡改,现在黑客们唯一能做的就是另辟蹊径。

当然许多时候我们并不是使用这种方式来直接汇编函数,因为这样太麻烦,所以我们一般都会选择专门的一个文件来集中汇编这些函数:

  1. function1 = compileFinal preprocessFileLineNumbers "myfunction1.sqf"
  2. function2 = compileFinal preprocessFileLineNumbers "myfunction2.sqf"
复制代码


compileFinal与compile唯一不同的是我们必须先汇编一次随后再能调用,如果在调用的同时去汇编函数实际上并没有给函数上锁,想这些就是有问题的调用方式:

  1. call compileFinal preprocessFileLineNumbers "myfile.sqf";
复制代码




  1. functions = compileFinal preprocessFileLineNumbers "myfile.sqf";
  2. call functions;
复制代码


MP的函数安全性框架设计,这仅仅是非常小的一部分。就好比设计这种安全方式的人也有办法反编译,代码的编写方式才得以不断进步,不断完善。有些黑客甚至连续PV空代码,破解BIS_fnc_MP或使用createUnit来进行作弊确实给服务器带来了不少麻烦,许多脚本者甚至要求BI取消客户端向服务器或其他客户端PV的功能,但这显然不可行并且会给MP编写带来极大的不便。不过从我个人推测,随着Global系即EG属性的命令不断扩建PV很有可能被取而代之,不过这仅是猜测。

了解了MP的基本编写原则后我们才能更好的理解为什么那些代码会被ban?作弊者们是如何通过这些代码作弊的?围绕着这个问题我们又可以牵涉出一大串历史性问题,接着killZone_kid,defunkt和Radioman等一群代码猿又开始了充满枪药味的讨论了,还记得在OA时代的call RE吗?很多人看到这些东西不明觉厉,甚至求教程的,我只想说当时的RE是BI的官方MP函数,现在已经被BIS_fnc_MP所代替了,当时我不会去写RE的教程,为什么?因为第一,介绍RE根本没有用,它属于整个MP编写的一部分;第二,黑客同样可以通过RE来作弊。所以RE取消了,紧接着BI逐步取消了setVehicleInit,clearVehicleInit和processInitCommands。finishMissionInit作为一个毫无用处的代码我们在此将其剔除。

还记得大量无脑玩家使用UPS城市巡逻脚本吗?很抱歉的是至少在我看来这得重新做一遍,现存的community相当多的老脚本有必要清理一遍所有之前所使用过的setVehicleInit代码段。

这里RE的作弊方法我已经不想再说了,毕竟现在是A3了,黑客通过createVehicle产生EG效应随后立马附上setVehicleInit+恶意代码+processInitCommands进行作弊,黑客可以做他任何他想做的事并且服务器很难找到到底谁干的这事,因为服务器根本就没有办法发现黑客使用过的变量。发现问题后BI立马封掉了这几个命令并且开始逐步完善PV的功能,为什么是PV?有人问PV不是照样可以作弊?但难度明显加大,黑客被发现的概率变得极高,服务器可以有效侦测含有恶意代码的PV变量,服务器又可以有权阻止使用代码的发布。所以BI便在A3使用含有PV代码的BIS_fnc_MP作为框架来代替RE升级MP环境下的安全性。

好了,现在的问题又出现了,新型黑客的出现可以直接破坏BIS_fnc_MP的结构抑或是使用createUnit来进行作弊。Biwiki不会告诉你连它也可以用来作弊但我这里不得不提一下,为什么?createUnit array中的init就是一个漏洞,现在你明白了为什么微软每天都在打补丁补漏洞?createUnit该不该被禁止?这是一个非常严肃的抉择,否则又有一大堆脚本任务彻底报废,几乎所有关于刷兵的内容都将大改,显然BI是不愿意的。

说到了这里我们要开始彻底引入BIS_fnc_MP及其家族函数了:https://community.bistudio.com/wiki/BIS_fnc_MP



相比于A2的函数库A3的可谓是做了相当大的改进,这得归功于Karel Moricky,但是自从当BIS_fnc_MP被不断攻击后,这也成了社区的一大焦点。BIS_fnc_MP作为MP框架的唯一核心,它的作用是调用个客户端函数并执行,它并不用于PV整套代码,但却允许这样的功能,这也是其成为被吐槽的对象。

首先明确一点的是BIS_fnc_MP并不支持将你自己的函数PV,就是说无论是服务器还是客户端都不行,除非你先让所有人下载你的函数或是使用已定义好的函数,官方函数或加载的ADDON。问题是黑客照样可以通过官方函数外加含有代码的参数来进行作弊或搞破坏。

比如说:
["{player setdamage 1;}","BIS_fnc_spawn",true,true] spawn BIS_fnc_MP;

这该如何解释?在这里我想在申明一下,不要试图以这篇教程学习到的能力从事违反游戏规则的任何活动,否则造成的一切后果自负!

我们先不谈这些,先来看看如何正确使用BIS_fnc_MP,其实非常简单,跟随wiki的一步步来即可,killzone_kid也解释的非常清楚,作为第三和第四个值我们可以考虑直接省略是代码看起来更简洁。

当然这里还有更高级的用法,除了wiki所列出的:
["{hint ‘hallo’;}","BIS_fnc_spawn",“var“] spawn BIS_fnc_MP; 这里我们可以使用变量名来进行限制,只有当该变量名在该客户端上为local时方可运行脚本。

["{hint ‘hallo’;}","BIS_fnc_spawn",obj] spawn BIS_fnc_MP; 这里我们可以使用实体来进行限制,只有当该实体在该客户端上为local时方可运行脚本。

["{hint ‘hallo’;}","BIS_fnc_spawn",grpName] spawn BIS_fnc_MP; 这里我们可以使用组来进行限制,只有当该客户端玩家属于改组成员时该客户端方方可运行脚本。

好了,现在是到进行MP编辑小结的时候了,MP编辑并非我们想象中的那么难,在编辑MP任务或脚本时我们必须要按照以下步骤进行:

1、        充分利用init.sqf,我们必须先将设计好的所有所需要的函数写入其中并且compilefinal进行保护。
2、        考虑是使用自己的一套PV安全函数调用框架还是选用官方的MP框架。

         如果是自己的PV框架,则需要自行设定框架结构和PVEH。框架可以保存在自己的server文件夹中不被下载作为核心控制文件。服务器对客户端的控制就是服务器不断发出PVEH的函数调用指令随后客户端运行。

         如果使用官方MP框架则可以省去自建框架的步骤,所有服务器指令则可以直接通过BIS_FNC_MP发出。

3、        细分到函数的写法,我们要用到之前所学的代码四属性,一旦函数指令被PV至个客户端。我们先考虑AL或AG问题,对于AL的被运行实体我们先判断是否local,这很简单,因为PV的函数所有人都执行一遍,所以不怕找不到owner。AG则无所谓。随后是判断作用EL还是GL,如果是EL我们则还需PV一次EL代码的作用,GL则不用。其它问题全部不用管了,JIP玩家也不必担心,如果你用的是官方MP框架它是JIP兼容的。这么一来你就再也不必为MP编辑而烦恼了。


光说不练没把戏,在此我推荐给大家killzone_kid的一款专用服务器的MP任务编辑debug console。
Debug console必须在dedicated server运行,那为什么是dedicated server?因为相比于普通服务器,dedicated server拥有更大的带宽使用率,所以大型的MP对战都使用dedicated server。这个debug console最好是能够有两台机器上进行测试这样MP的各种问题才能更加直观的表现出来。

说到dedicated server我们还有专门的参数设置,关于参数设置我会在下一篇教程中详解。



Debug console的下载地址在这里:

其作用是是可以进行客户端与服务器的变量进行监控并且分别显示个变量的locality状态。使用时先将文件夹放入MP mission中,随后确保description.ext含有#include "KKSB\KKSB.ext"头文件和init.sqf含有#include "KKSB\KKSB.sqf"。



随后我们以专用服务器的身份进入游戏,按两下esc调出debug console。我们在使用debugconsole时还有一个小贴士就是tab和f1键,它们可以帮助我们更快的输入代码。(我们在建立专用服务器时需要参数设置,这里我们使用killzone_kid的参数来设置服务器:,将所有内容放入MP mission中并运行批处理文件。



进入专用服务器后进行如上设置,如果我们需要多个玩家则需要两台机器,进入kkserver输入密码abc123,这些设置都可以在cfg中进行调整)

当我们打开debug console后在服务器栏中输入isdedicated返回true时,那么就说明我们已经进入专用服务器模式进行MP编辑了。

为什么我们需要这样的工具?因为它可以帮助我们更快的进行MP调试,事半功倍,节约时间。

下一篇我们继续教程,228楼,《武装突袭3》——MP教程4及服务器设置

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
 楼主| 发表于 2014-2-26 11:21:15 | 显示全部楼层
本帖最后由 FFUR2007SLX2_5 于 2014-2-28 15:26 编辑

228楼,《武装突袭3》——MP教程4及服务器设置



作为对kk MP debug console的补充说一下netID,ObjectFromNetId和groupFromNetId。
这个家族是截取实体单位或小组最为直观的方式,netID支持objNull和grpNull,返回id类型为科学计数法的浮点数,随后通过相对应的ObjectFromNetId和groupFromNetId获取id并返还objNull或grpNull。相对于只能够返还玩家id的getPlayerUID,ArmA3新增的家族可以返还所有objNull或grpNull的id,换言之ArmA2的getPlayerUID已经相当鸡肋甚至可以剔除了。

在kk的MP debug console中他通过本地获取玩家id随后pvServer从而使dedicated server获取玩家实体。

  1. netId objNull;
  2. ObjectFromNetId scalar;
  3. groupFromNetId scalar;
复制代码


对于我们之前提到的AddStackedEH onPlayerConnected和onPlayerDisconnected。它们的主要作用是JIP和中途退出玩家。特殊私用变量_id,_uid和_name,_uid其返还的浮点数与netId相同,_id返还的数来自#userlist,在专用服务器设置上我会讲,不过它们的具体作用还是不一样的,在init.sqf中我们同样需要将OPC和OPD分配到每个客户端上去:

  1. ["BIS_id", "onPlayerConnected", “fn_onPlayerConnected”] call BIS_fnc_addStackedEventHandler;
  2. ["BIS_id", "onPlayerDisconnected", “fn_onPlayerDisconnected”] call BIS_fnc_addStackedEventHandler;
复制代码


理清了逻辑,现在开始专用服务器设置,这里已经有一片中文教程:http://blog.sina.com.cn/s/blog_640b65f20101a7su.html

作为该教程的补充,我将对其所没介绍到的在做一下详细的梳理:

专用服务器的优势相比不用再复述,自BI提供了arma3server.exe之后我们不必再在目标栏中添加-server参数了。

首先我们先来看看专用服务器是什么:
1、        Armaholic有一些傻瓜式专用服务器加载器,不过已经相当老旧无法跟上新时代的步伐,所以我们需要手动配置服务器,其优势不言而喻;
2、        所有ArmA3服务器文件的安装全部使用steamCMD进行,使用steamCMD时我们就无需在服务器上安装steam客户端。
3、        ArmA3server.exe与绝大多数游戏不一样,它是一个空项目并且显示waiting,在这过程中没有任务加载也没有玩家进行游戏,当我们在description.ext中定义maxPlayers的数量时,它只会显示这个数。
4、        当有玩家接入你的专用服务器后,它会显示playing,任务开始加载。
5、        当专用服务器没有任何玩家后其返回waiting状态。
6、        如果说管理员想要让专用服务器在无玩家的情况下继续保持任务进行,你则需要在server.cfg中添加persistent=1; 只有在description.ext定义重生项目的任务才能允许这种模式,如果脚本控制任务结束则强制结束。
7、        服务器进行时管理员不能中途修改配置参数。
8、        任务列表的更新可以随时刷新。




开始服务器配置:
1、        手动写配置文件太麻烦,所以我们需要steamCMD批处理文件进行配置文件的生成,事半功倍。下载:
2、        我们新建一个文件夹并在这个文件夹内开始工作,新建A3_serverOne.bat(名字自定),随后在其中输入:
  1. steamcmd.exe +runscript A3_serverOne.txt
复制代码

3、        接着在该文件夹内建立A3_serverOne.txt,输入:
  1.     login USER PASS
  2. force_install_dir C:\Arma3ServerOne
  3. app_update 107410 -beta development validate
  4. exit
复制代码


4、        随后运行A3_serverOne.bat。这里我们成功设置了一个专用服务器,如果需要多个服务器,以此类推,不过不同的服务器我们需要在server.cfg中定义不同的端口。

开始server.cfg配置:
这里的许多细节内容fromz的教程已经翻译了,所以这里我就直接提供一个样板:

  1. passwordAdmin = "mypassword";
  2. steamPort=8766;
  3. steamQueryPort=27016;
  4. hostname="My Server Name";
  5. motd[]=
  6. {
  7. "Welcome to my server name",
  8. "hello world",
  9. };
  10. motdInterval=50;
  11. voteThreshold=0.25;
  12. maxPlayers=80;
  13. reportingIP="arma2pc.master.gamespy.com";
  14. logfile="myserver.log";  
  15. class Missions
  16. {
  17. class Mission_01   
  18. {
  19. template = mymission.Stratis;
  20. difficulty = "regular";   
  21. param1 =   
  22. param2 =
  23. };
  24. class Mission_02
  25. {
  26. template = anothermission.Stratis;
  27. difficulty = "veteran";
  28. param1 =   
  29. param2 =
  30. };
  31. };
  32. voteMissionPlayers=1;
  33. kickduplicate=1;
  34. equalModRequired=0;
  35. disableVoN=0;
  36. vonCodecQuality=7;
  37. timeStampFormat=full;
  38. persistent=1;
  39. verifySignatures=2;
  40. regularcheck="";
  41. localClient[]={127.0.0.1};
  42. onDifferentData = "";
复制代码
               

下面是arma3.cfg,这里我也只提供一个样板:
  1. MinBandwidth=2097152;
  2. MaxBandwidth=2097152000;
  3. MaxMsgSend = 1024;
  4. MaxSizeGuaranteed = 512;
  5. MaxSizeNonguaranteed = 256;
  6. MinErrorToSend = 0.001;
  7. MinErrorToSendNear=0.01;
  8. MaxCustomFileSize=0;
复制代码


下面是default.arma3profile,这个文件定义了游戏性内容,游戏在进行时最高fps不会超过50,如果低于15则可能任务过于复杂,PV过多或优化有问题:
  1. class Difficulties
  2. {
  3. class Recruit
  4. {
  5. class Flags
  6. {

  7. Armor=1;               
  8. FriendlyTag=1;               
  9. EnemyTag=1;               
  10. HUD=1;                       
  11. HUDPerm=1;               
  12. HUDWp=1;               
  13. HUDWpPerm=1;               
  14. WeaponCursor=1;               
  15. AutoAim=1;               
  16. AutoGuideAT=1;               
  17. 3rdPersonView=1;       
  18. ClockIndicator=1;       
  19. Map=1;                       
  20. Tracers=1;               
  21. AutoSpot=1;               
  22. UltraAI=0;               
  23. DeathMessages=1;       
  24. NetStats=1;               
  25. VonID=1;               

  26. };
  27. skillFriendly=0.34999999;
  28. skillEnemy=0.34999999;
  29. precisionFriendly=0.20;
  30. precisionEnemy=0.20;
  31. };

  32. class Regular
  33. {
  34. class Flags
  35. {
  36. Armor=1;
  37. FriendlyTag=1;
  38. EnemyTag=0;
  39. HUD=1;
  40. HUDPerm=0;
  41. HUDWp=1;
  42. HUDWpPerm=1;
  43. WeaponCursor=1;
  44. AutoAim=0;
  45. AutoGuideAT=0;
  46. 3rdPersonView=1;
  47. ClockIndicator=1;
  48. Map=1;
  49. Tracers=1;
  50. AutoSpot=0;
  51. UltraAI=0;
  52. DeathMessages=1;       
  53. NetStats=1;               
  54. VonID=1;               
  55. };
  56. skillFriendly=0.44999999;
  57. skillEnemy=0.44999999;
  58. precisionFriendly=0.35;
  59. precisionEnemy=0.35;
  60. };
  61. class Veteran
  62. {
  63. class Flags
  64. {
  65. Armor=0;
  66. FriendlyTag=0;
  67. EnemyTag=0;
  68. HUD=1;
  69. HUDPerm=0;
  70. HUDWp=1;
  71. HUDWpPerm=1;
  72. WeaponCursor=0;
  73. AutoAim=0;
  74. AutoGuideAT=0;
  75. 3rdPersonView=0;
  76. ClockIndicator=1;
  77. Map=0;
  78. Tracers=1;
  79. AutoSpot=0;
  80. UltraAI=0;
  81. DeathMessages=0;       
  82. NetStats=0;       
  83. VonID=1;               
  84. };
  85. skillFriendly=0.74999999;
  86. skillEnemy=0.74999999;
  87. precisionFriendly=0.75;
  88. precisionEnemy=0.75;
  89. };
  90. class Expert
  91. {
  92. class Flags
  93. {
  94. Armor=0;
  95. FriendlyTag=0;
  96. EnemyTag=0;
  97. HUD=1;
  98. HUDPerm=0;
  99. HUDWp=1;
  100. HUDWpPerm=1;
  101. WeaponCursor=0;
  102. AutoAim=0;
  103. AutoGuideAT=0;
  104. 3rdPersonView=0;
  105. ClockIndicator=1;
  106. Map=0;
  107. Tracers=1;
  108. AutoSpot=0;
  109. UltraAI=0;
  110. DeathMessages=0;       
  111. NetStats=0;               
  112. VonID=0;               
  113. };
  114. skillFriendly=0.94999999;
  115. skillEnemy=0.94999999;
  116. precisionFriendly=0.95;
  117. precisionEnemy=0.95;
  118. };
  119. };
  120. viewDistance=2000;
  121. terrainGrid=25;
复制代码

配置完成后我们来看看端口:
•port          2302        UDP         ArmA3 网络
•port+1      2303        UDP         Gamespy 公共服务器
•port+3      2305        UDP         VON网络
•port          8766        UDP         STEAM服务网络
•port          27016      UDP         STEAM query


最后我们就可以开始运行arma3server.exe了,当然我们使用和运行游戏相同的办法来开始专用服务器,我们需要快捷方式和开始参数:
  1. -port=2302 -name=default -config=default\server.cfg -cfg=default\Arma3.cfg -profiles=default
复制代码


开始参数的更多内容请看:https://community.bistudio.com/wiki/Arma3:_Startup_Parameters

如果说想要升级自己的专用服务器请运行我们的批处理文件。现在你的长航时专用服务器就可以开始工作了,加上fromz的无人服务器自动重启的设置和我们的MP脚本编写知识,给社区带来全新形式的MP任务从这里开始。

当然不要忘记我们之前所学的函数安全调用框架,如果仍然不放心我们则需要对任务进行监控并及时发作弊者,管理员的服务器命令在这里:https://community.bistudio.com/wiki/In_Game_Server_Commands

下一篇教程中我将对服务器的安全性做更进一步的介绍:《武装突袭3——MP全教程5及安全性》

230楼继续教程:《武装突袭3——MP全教程5及专用服务器安全性》

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入VME

x
您需要登录后才可以回帖 登录 | 加入VME

本版积分规则

小黑屋|中国虚拟军事网

GMT+8, 2024-5-3 12:24

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表