Язык опи­сания сце­нари­ев иг­ры Operation Flashpoint (SQS-SQF). Га­лопом по европам.

Галопом по европам

Это при­ложе­ние нес­коль­ко от­сту­па­ет от ос­новной идеи спра­воч­ни­ка — крат­ко опи­сывать ин­те­рес­ные и тон­кие мо­мен­ты языка. Я слег­ка от­вле­кусь, и в сво­бод­ной фор­ме по­ком­менти­рую вся­кие ве­щи по по­воду ко­торых мне есть что сказать.

Некоторые ошибки начинающего скриптовальщика

Совсем ужасный код

; увеличивает число на единицу

? a == 0 : b = 1; goto "end"
? a == 1 : b = 2; goto "end"
? a == 2 : b = 3; goto "end"
? a == 3 : b = 4; goto "end"
? a == 4 : b = 5; goto "end"
? a == 5 : b = 6; goto "end"
? a == 6 : b = 7; goto "end"
? a == 7 : b = 8; goto "end"
? a == 8 : b = 9; goto "end"
? a == 9 : b = 10; goto "end"

#end

Уз­на­ете? На та­ком ко­де бы­ва­ет да­же ста­вят своё имя!

; Скрипт увеличения числа на единицу. DenVdmj (c) 2006
; Если Вам понравилася эта ИДЕЯ вы можете использовать её в своих скриптах,
; только не забудте указать МЕНЯ как АВТОРА этой ИДЕИ! Я думаю Вы поняли,
; что все ОЧЕНЬ ПРОСТО, точно также можно увеличивать на ДВОЙКУ и на ТРОЙКУ
; и на ДРУГИЕ ЧИСЛА!

? CHISLO == 0 : RESULTAT = 1; goto "KONETS"
? CHISLO == 1 : RESULTAT = 2; goto "KONETS"
? CHISLO == 2 : RESULTAT = 3; goto "KONETS"
? CHISLO == 3 : RESULTAT = 4; goto "KONETS"
? CHISLO == 4 : RESULTAT = 5; goto "KONETS"
? CHISLO == 5 : RESULTAT = 6; goto "KONETS"
? CHISLO == 6 : RESULTAT = 7; goto "KONETS"
? CHISLO == 7 : RESULTAT = 8; goto "KONETS"
? CHISLO == 8 : RESULTAT = 9; goto "KONETS"
? CHISLO == 9 : RESULTAT = 10; goto "KONETS"

#KONETS

Вам смеш­но? Мне — нис­коль­ко, ведь лю­ди дей­стви­тель­но с удо­воль­стви­ем пользуются. Я об­наглею, и, нев­зи­рая на объ­ем та­кого ко­да, рас­пи­шу его здесь пол­ностью:

_unit = _this select 0
_num = random(10)
?_num >= 9: _unit setObjectTexture [0, "\PathTo\Sign9.paa"], goto "num2"
?_num >= 8: _unit setObjectTexture [0, "\PathTo\Sign8.paa"], goto "num2"
?_num >= 7: _unit setObjectTexture [0, "\PathTo\Sign7.paa"], goto "num2"
?_num >= 6: _unit setObjectTexture [0, "\PathTo\Sign6.paa"], goto "num2"
?_num >= 5: _unit setObjectTexture [0, "\PathTo\Sign5.paa"], goto "num2"
?_num >= 4: _unit setObjectTexture [0, "\PathTo\Sign4.paa"], goto "num2"
?_num >= 3: _unit setObjectTexture [0, "\PathTo\Sign3.paa"], goto "num2"
?_num >= 2: _unit setObjectTexture [0, "\PathTo\Sign2.paa"], goto "num2"
?_num >= 1: _unit setObjectTexture [0, "\PathTo\Sign1.paa"], goto "num2"
?_num  < 1: _unit setObjectTexture [0, "\PathTo\Sign0.paa"], goto "num2"

#num2
_num = random(10)
?_num >= 9: _unit setObjectTexture [1, "\PathTo\Sign9.paa"], goto "num1"
?_num >= 8: _unit setObjectTexture [1, "\PathTo\Sign8.paa"], goto "num1"
?_num >= 7: _unit setObjectTexture [1, "\PathTo\Sign7.paa"], goto "num1"
?_num >= 6: _unit setObjectTexture [1, "\PathTo\Sign6.paa"], goto "num1"
?_num >= 5: _unit setObjectTexture [1, "\PathTo\Sign5.paa"], goto "num1"
?_num >= 4: _unit setObjectTexture [1, "\PathTo\Sign4.paa"], goto "num1"
?_num >= 3: _unit setObjectTexture [1, "\PathTo\Sign3.paa"], goto "num1"
?_num >= 2: _unit setObjectTexture [1, "\PathTo\Sign2.paa"], goto "num1"
?_num >= 1: _unit setObjectTexture [1, "\PathTo\Sign1.paa"], goto "num1"
?_num  < 1: _unit setObjectTexture [1, "\PathTo\Sign0.paa"], goto "num1"

#num1
_num = random(10)
?_num >= 9: _unit setObjectTexture [2, "\PathTo\Sign9.paa"], goto "exit"
?_num >= 8: _unit setObjectTexture [2, "\PathTo\Sign8.paa"], goto "exit"
?_num >= 7: _unit setObjectTexture [2, "\PathTo\Sign7.paa"], goto "exit"
?_num >= 6: _unit setObjectTexture [2, "\PathTo\Sign6.paa"], goto "exit"
?_num >= 5: _unit setObjectTexture [2, "\PathTo\Sign5.paa"], goto "exit"
?_num >= 4: _unit setObjectTexture [2, "\PathTo\Sign4.paa"], goto "exit"
?_num >= 3: _unit setObjectTexture [2, "\PathTo\Sign3.paa"], goto "exit"
?_num >= 2: _unit setObjectTexture [2, "\PathTo\Sign2.paa"], goto "exit"
?_num >= 1: _unit setObjectTexture [2, "\PathTo\Sign1.paa"], goto "exit"
?_num >= 0: _unit setObjectTexture [2, "\PathTo\Sign0.paa"], goto "exit"

#exit
exit

И это толь­ко для трех раз­ря­дов чис­ла! Мож­но толь­ко те­рять­ся в до­гад­ках от­ку­да бе­рут­ся та­кие «ге­ни­аль­ные» ве­щи и нас­коль­ко на­до быть за­нятым че­лове­ком, что­бы это использовать. На­шему «за­нято­му че­лове­ку» при­дет­ся, ко­неч­но, при­бег­нуть к ав­то­заме­не и ко­пипас­ту, с пос­ле­ду­ющей ду­рац­кой прав­кой, что­бы по­дог­нать это «чу­до» под свои нужды. А ведь все ре­шение для аб­со­лют­но лю­бых вход­ных па­рамет­ров (сам но­мер, име­на тек­стур и сек­ции) уме­ща­ет­ся в нес­коль­ко строк кода. И это де­ла­ет­ся один раз на всю жизнь и на лю­бые проекты.

_object =
_number =
_texturePathMask = "\Path\To\Signatures\Sign%1colorRed.paa";
_sections = [5,4,3,2,1,0]; // секции перечисленные от младшего
                           // разряда к старшему, т.е. слева направо
{
    // отбросить дробную часть
    _number = _number - _number % 1;
    // установить текстуру c цифрой, остаток деления _number на 10
    // вернёт младший десятичный разряд числа
    _object setObjectTexture [_x, format [_texturePathMask, _number % 10]];
    // сдвинуть _number на один десятичный разряд вправо
    _number = _number / 10
} foreach _sections;
И еще раз для са­мых за­нятых, в ви­де скрип­та при­нима­юще­го па­рамет­ры:
;// args: [объект, номер, маска пути/имени файла сигнатуры, массив секций]
_num = _this select 1;
{
    _num = _num - _num % 1;
    _this select 0 setObjectTexture [_x, format [_this select 2, _num % 10]];
    _num = _num / 10
} foreach (_this select 3)

Мо­жете по­уда­лять пе­рево­ды строк что­бы вы­зывать это ко­ман­дой exec.

Вот еще один со­вер­шенно ре­аль­ный об­разчик сти­ля ко­торым на­писа­но не­мало скрип­тов:

#loop
_dam=getdammage _unit
?(_dam==0) : goto "start"
?(_dam==1) : goto "end"
?(_dam<=0.1):_unit setdammage _dam+0,01
?(_dam<=0.2) && (_dam>0.1):_unit setdammage (_dam+0.02)
?(_dam<=0.3) && (_dam>0.2):_unit setdammage (_dam+0.03)
?(_dam<=0.4) && (_dam>0.3):_unit setdammage (_dam+0.04)
?(_dam<=0.5) && (_dam>0.4):_unit setdammage (_dam+0.05)
?(_dam<=0.6) && (_dam>0.5):_unit setdammage (_dam+0.06)
?(_dam<=0.7) && (_dam>0.6):_unit setdammage (_dam+0.07)
?(_dam<=0.8) && (_dam>0.7):_unit setdammage (_dam+0.08)
?(_dam<=0.9) && (_dam>0.8):_unit setdammage (_dam+0.09)
?(_dam>0.9):_unit setdammage (_dam+0.1)
~10
goto "loop"

Со­вер­шенно не ва­жен имен­но этот код — де­ло в том, что та­кой под­ход проц­ве­та­ет сре­ди на­чина­ющих скриптовать.

Ни­ког­да так не де­лай­те — при­думай­те свою су­пер-фор­му­лу здо­ровья, или че­го угод­но, что вы со­бира­етесь ре­шать по­доб­ным об­ра­зом, но пи­шите скрип­ты по человечески.

Никогда не используйте копипаст кода

Это не толь­ко уби­ва­ющая мысль ру­тина, но и час­тый ис­точник оши­бок — обыч­но Вам нуж­но из­ме­нить па­ру-трой­ку пе­ремен­ных, или еще что-то что­бы по­дог­нать его под те­кущие за­дачи, и за­быв нез­на­читель­ную де­таль Вы пот­ра­тите на вы­яс­не­ние при­чины не­кото­рое вре­мя, ко­торое мож­но бы­ло пот­ра­тить с пользой.

Выносите многократно повторяющиеся действия в подпрограммы

Вмес­то то­го что­бы пи­сать (или ко­пипас­тить) один и тот­же код де­сят­ки раз, ис­поль­зуй­те фун­кции — они имен­но для это­го и предназначены.

Разделяйте код и данные

Не рас­со­вывай­те дан­ные по скрип­там, пом­ни­те о том, что од­нажды Вы за­хоти­те из­ме­нить тот на­бор ство­лов, ко­торый до­бав­ля­ет­ся в ма­шины, за­хоти­те из­ме­нить це­ны на ору­жие в ва­шей па­лат­ке тор­говца ору­жи­ем — пом­ни­те о тех нес­час­тных ко­торые хо­тят ис­поль­зо­вать ва­ши скрип­ты с дру­гими аддонами.

Ни­ког­да не де­лай­те так:

_car addWeaponCargo ["SomeGun",1];
_car addWeaponCargo ["OtherGun",1];
_car addWeaponCargo ["SuperGun",1];
_car addWeaponCargo ["SuperPuperGun",1];
// еще с десяток раз
_car addWeaponCargo ["SuperPuperMegaGun",1]

Вы­носи­те дан­ные о ство­лах в фай­лы с дан­ны­ми:

[
   "SomeGun",
   "OtherGun",
   "SuperGun",
   "SuperPuperGun",
   "SuperPuperMegaGun"
]

И ис­поль­зуй­те

{
    _car addWeaponCargo [_x, 1]
} foreach call preprocessFile "resources.sqf"

Вы лег­ко мо­жете улуч­шить скрипт, хра­ня ма­гази­ны и про­чие от­но­сящи­еся к ору­жию дан­ные вмес­те:

[
   ["SomeGun", "SomeGunMag"],
   ["OtherGun", "OtherGunMag"],
   ["SuperGun", "SuperGunMag"],
   ["SuperPuperGun", "SuperPuperGunMag"],
   ["SuperPuperMegaGun", "SuperPuperMegaGunMag"]
]
{
    _car addWeaponCargo [_x select 0, 1];
    _car addMagazineCargo [_x select 1, 4]

} foreach call preprocessFile "resources.sqf"

Ис­поль­зуй­те та­кой под­ход да­же ес­ли ваш скрипт очень прос­той; ес­ли Вы не хо­тите ис­поль­зо­вать лиш­ние фай­лы, то хо­тя-бы вы­носи­те дан­ные в на­чало скрип­та, до лю­бого ко­да, так что­бы Ва­ши поль­зо­вате­ли смог­ли без му­чений доб­рать­ся до них.

Ваши структуры данных важнее вашего кода

Боль­ше ду­май­те над тем как хра­нить дан­ные, чем как на­писать ка­кой-то там цикл. Ес­ли Вы пра­виль­но выб­ра­ли струк­ту­ры дан­ных (увы, в офп это всег­да при­ходит­ся ими­тиро­вать на мас­си­вах, что ус­ложня­ет хо­роший вы­бор) Вы всег­да лег­ко на­пиши­те лю­бой код для ра­боты с ними.

Никогда не дублируйте данные

Нель­зя дуб­ли­ровать дан­ные, это эле­мен­тарно чре­вато час­ты­ми ошиб­ка­ми — из­ме­нив неч­то в од­ном мес­те, но за­быв внес­ти из­ме­нения в дру­гом, Вы по­лучи­те не­од­но­зач­ность, и труд­но об­на­ружи­ва­емые глюки.

Храните данные связанные по смыслу в одном месте

Ста­рай­тесь вы­делять сущ­ности, так нап­ри­мер ав­то­мат — сущ­ность, а его наз­ва­ние, вес, цвет, вкус, за­пах, сов­мести­мые с ним при­целы, ма­гази­ны ко­торые он ис­поль­зу­ет — его аттрибуты.

Это прос­то удоб­но — так как поз­во­ля­ет лег­ко об­на­ружи­вать их вза­имос­вязь, и со­от­ветс­твен­но на­ходить как все ат­три­буты не­кой сущ­ности так и на­обо­рот — при­над­лежность аттрибута.

Что­бы по­казать как ис­поль­зо­вать этот со­вет на прак­ти­ке, при­веду один прос­той пример.

Пред­став­те се­бе таб­личку:

_weapons =
//     gun            magazine       optic   silencer
[
     ["ICP_val",     "ICP_valmag",   "",     "integrated"],
     ["ICP_valpso",  "ICP_valmag",   "pso",  "integrated"],
     ["ICP_vss",     "ICP_vssmag",   "pso",  "integrated"],
     ["ICP_ak74",    "ICP_ak74mag",  "",     ""          ],
     ["ICP_ak74mo",  "ICP_ak74mag",  "pso",  ""          ]
]

Да­вай­те на­пишем па­ру-трой­ку по­лез­ных фун­кций для ра­боты с та­кой табличкой. В по­доб­ных таб­личках лишь один стол­бец со­дер­жит га­ран­ти­рован­но уни­каль­ные зна­чения, ес­ли это не так, то это ка­кая то дру­гая таб­личка )). Зна­чения та­кого стол­бца мож­но наз­вать клю­чами, в на­шем слу­чае это стол­бец с наз­ва­ни­ями ство­лов, пусть сле­ду­ющая фун­кция бу­дет воз­вра­щать нам ряд по клю­чу (наз­ва­нию ство­ла):

_getByKey = { // args: table, key value
    private ["_tbl", "_key", "_i"];
    _tbl = _this select 0;
    _key = _this select 2;
    _i = 0;
    while { _key != (_tbl select _i) select 0 } do { _i = _i + 1 };
    if ( _i < count _tbl ) then { _tbl select _i } else { [] }
}

Наз­на­чение та­кой фун­кции весь­ма по­нят­но — вы­зов:

([_weapons, "ICP_vss"] call _getByKey) select 1

вер­нёт наз­ва­ние ма­гази­на для вин­то­реза, а вы­зов

([_weapons, "ICP_vss"] call _getByKey) select 3

наз­ва­ние его глушителя.

Еще од­на важ­ная опе­рация — это по­луче­ние всех ря­дов по сов­павшим зна­чени­ям в оп­ре­делен­ных стол­бцах, это по­может нам в си­ту­ации ког­да мы за­хотим по­лучить все ство­лы ко­торые ис­поль­зу­ют оп­ре­делен­ный ма­газин или оп­ти­ку:

_selectByColumn = { // args: table, column, value
    private ["_tbl", "_col", "_val", "_sel"];
    _tbl = _this select 0;
    _col = _this select 1;
    _val = _this select 2;
    _sel = [];
    {
        if ( _x select _col == _val ) then { _sel set [count _sel, _x] }
    } foreach _tbl;
    _sel // return selection
}

Ок, те­перь вы­зов:

[_weapons, 1, "ICP_ak74mag"] call _selectByColumn

воз­вра­ща­ет мас­сив:

[
     ["ICP_ak74",   "ICP_ak74mag", "",    ""],
     ["ICP_ak74mo", "ICP_ak74mag", "pso", ""]
]

а вы­зов:

[_weapons, 3, "integrated"] call _selectByColumn

мас­сив:

[
     ["ICP_val",    "ICP_valmag", "",    "integrated"],
     ["ICP_valpso", "ICP_valmag", "pso", "integrated"],
     ["ICP_vss",    "ICP_vssmag", "pso", "integrated"],
]

Ана­логич­но мож­но най­ти все шум­ные ство­лы:

[_weapons, 3, ""] call _selectByColumn

все ство­лы с оп­ти­кой ПСО:

[_weapons, 2, "pso"] call _selectByColumn

или без оп­ти­чес­ко­го при­цела:

[_weapons, 2, ""] call _selectByColumn

Вы мо­жете су­щес­твен­но рас­ши­рить воз­можнос­ти _selectByColumn ес­ли из­ме­ните её так, что­бы вмес­то прос­то­го зна­чения она при­нима­ла кал­бэк фун­кцию оп­ре­деля­ющую под­хо­дит нам этот ряд или нет. Итак соз­да­дим еще од­ну фун­кцию:

_selectByCondition = { // args: table, condition
    private "_sel"; _sel = [];
    {
        if ( _x call (_this select 1) ) then { _sel set [count _sel, _x] }
    } foreach (_this select 0);
    _sel // return selection
};

Эта фун­кция сов­сем не­боль­шая, так как всю про­вер­ку мы вы­тащи­ли на­ружу, и ка­залось бы, что мож­но обой­тись сов­сем без этой фун­кции, но все же с ней зап­рос выг­ля­дит ос­мыслен­ней:

[
    _weapons,
    {
        _this select 2 == "pso" &&
        _this select 3 == "integrated"
    }
] call _selectByCondition

этот вы­зов вер­нёт та­кой мас­сив:

[
    ["ICP_valpso", "ICP_valmag", "pso", "integrated"],
    ["ICP_vss",    "ICP_vssmag", "pso", "integrated"]
]

Кста­ти, имей­те вви­ду, что воз­вра­ща­емые ря­ды — это не ко­пии ря­дов таб­ли­цы, а ссыл­ки на них. Ес­ли вы из­ме­ните зна­чения в них, то из­ме­нит­ся и са­ма таблица.

Все эти фун­кции мож­но ис­поль­зо­вать с лю­бым ко­личес­твом са­мых раз­ных та­ких табличек. Ис­поль­зуй­те их, или при­думай­те свои, бо­лее мощ­ные и удоб­ные, пусть дан­ные ва­шего про­ек­та хра­нять­ся грамотно.

Вы все еще ис­поль­зу­ете жут­кие на­боры из нес­коль­ких де­сят­ков

? _ствол == "стволнэйм" : _оптика = true; exit
или что-то в этом ду­хе? Тог­да мы идем к Вам!

SQS vs SQF?

На­вер­ное Вы за­мети­ли — что­бы иметь воз­можность вы­пол­нить эти со­веты, Вам нуж­но пи­сать ис­поль­зуя sqf. Ес­ли Вы счи­та­ете, что мо­жете лег­ко и кра­сиво пи­сать sqs, что Вас не бу­дет сму­щать не­воз­можность удоб­но соз­да­вать под­прог­раммы, и Вы лег­ко вру­ба­етесь в наг­ро­маж­де­ние не­понят­но ку­да ве­дущих го­уту — чтож, от­прав­ляй­тесь ко­дить в асм, Вы один из тех ге­ни­ев, что пред­по­чита­ют низ­ко­уров­не­вые языки. Не выш­ло? Тог­да не об­ма­нывай­те се­бя и учи­те sqf, то что Вам сей­час ка­жет­ся прос­тым, прос­то лишь до тех пор по­ка Ва­ши скрип­ты не тре­бу­ют большего.

Глобальные имена, чем это плохо и как с этим бороться

Трудности с глобальными именами

Пер­вое что Вы са­ми мо­жете за­метить — это то, что при сов­мес­тном ис­поль­зо­вании скрип­тов, на­писан­ных раз­ны­ми людь­ми, не­кото­рые гло­баль­ные име­на мо­гут слу­чай­но совпасть. Так же на­до пом­нить, что гло­баль­ное прос­траство имен в иг­ре по­мимо пе­ремен­ных вклю­ча­ет все име­на юни­тов, триг­ге­ров, а так­же име­на команд.

Ес­ли Ваш скрипт сов­сем прост, то не­об­хо­димость для его ра­боты гло­баль­ной пе­ремен­ной — это не­поз­во­литель­ная рос­кошь, и нап­расное за­мусо­рива­ние об­ще­го прос­траства имен.

Ес­ли то что вы пи­шите — слож­ная скрип­то­вая сис­те­ма, то есть и бо­лее серь­ез­ные при­чины ста­рать­ся мень­ше ис­поль­зо­вать гло­баль­ные имена. Лю­бой скрипт ис­поль­зу­ющий не­кото­рый гло­баль­ный ре­сурс ста­новит­ся за­виси­мым от него. Это зна­чит, что ес­ли на не­кото­ром эта­пе вы за­хоти­те из­ме­нить спо­соб хра­нения не­кото­рых та­ких дан­ных, Вам при­дет­ся пе­репи­сывать все заново. Ес­ли та­ких за­цепок по гло­баль­ным име­нам (ху­же — по струк­ту­рам дан­ных) бу­дет слиш­ком мно­го, то все они прос­то за­цемен­ти­ру­ют про­ект, его очень труд­но бу­дет рас­ши­рить не прев­ра­тив в му­сор­ное ме­сиво ко­да и дан­ных, и прак­ти­чес­ки не­воз­можно модифицировать.

Mangled-Style

Это не ре­шение са­мой проб­ле­мы, но са­мый прос­той и эфек­тивный спо­соб не до­пус­тить бар­да­ка и пу­тани­цы с гло­баль­ны­ми именами. Нес­мотря не за­ум­ное наз­ва­ние все очень прос­то — при­думай­те схе­му для всех имен про­ек­та и да­вай­те име­на толь­ко в со­от­вет­свии с ней. Дру­гими сло­вами, пусть ва­ши име­на бу­дут слож­но­сос­тавны­ми (та­кими как это сло­во) и бу­дут от­ра­жать не толь­ко своё пред­назна­чение, но и при­над­лежность (мо­ду, про­ек­ту, ад­до­ну, скрип­то­вой сис­те­ме, её мо­дулю и да­лее по нис­хо­дящей).

До­пус­тим наш про­ект на­зыва­ет­ся «My Super Puper Modification» сок­ра­щен­но MSPMOD, этот про­ект ре­али­зу­ет, по­мимо про­чего, сис­те­му (прос­то нес­коль­ко скрип­тов) от­сле­жива­ния сос­то­яния иг­ро­ка и име­ет со­от­ветс­тву­ещее наз­ва­ние — TracePlayer — тог­да все гло­баль­ные пе­ремен­ные ис­поль­зу­емые этим мо­дулем бу­дут иметь по­доб­ные име­на:

MSPMOD_TracePlayer_currentVehicle
MSPMOD_TracePlayer_usedVehicle
MSPMOD_TracePlayer_usedWeapons
MSPMOD_TracePlayer_killedEnemy

Мож­но ис­поль­зо­вать в ка­чес­тве скле­ива­ющих сим­во­лов двой­ное под­черки­вание «__».

Переменные передаваемые по ссылке от родительского скрипта дочерним

Час­то нес­коль­ким па­рал­лель­но вы­пол­ня­ющим­ся скрип­там ну­жен дос­туп к од­но­му на­бору дан­ных, и это по­буж­да­ет нас хра­нить эти дан­ные в гло­баль­ной переменной. Од­на­ко ес­ли су­щес­тву­ет воз­можность стар­то­вать на­ши скрип­ты из не­кото­рого об­ще­го мес­та — дру­гого «ро­дитель­ско­го» скрип­та, то впол­не мож­но обой­тись ло­каль­ны­ми переменными.

// массив с именами скриптов
_processes = [];

// массив с любыми данными
_sharedData = [];

// каждый из запущенных процессов получит доступ к _sharedData
{ _sharedData exec _x } foreach _processes;

Та­ким об­ра­зом мас­сив _sharedData ста­новит­ся об­щей об­ластью дан­ных как для те­куще­го так и для до­чер­них процессов. Пом­ни­те об этом спо­собе, и ес­ли в Ва­шем слу­чае он мо­жет за­менить гло­баль­ные пе­ремен­ные — ис­поль­зуй­те его.

Библиотеки возвращающие список определяемых функций

Фун­кции то­же име­ют име­на, и их иног­да то­же хо­чет­ся сде­лать гло­баль­ны­ми, что­бы не тас­кать их по де­сять раз из фай­ла preprocessFile'ом. Мож­но да­вать им пре­фикс в ду­хе «globFunc» или прос­то «func», в та­ком слу­чае удоб­но рас­по­ложить все фун­кции в од­ном фай­ле:

#define arg(x) = (_this select x)

// [getpos unit, circlePosition, circleRadius] call funcInCircle

funcInCircle = {
    #define mDeltaX ((_ppos select 0) - (_cpos select 0))
    #define mDeltaY ((_ppos select 1) - (_cpos select 1))
    #define mRadius (arg(2))
    private ["_ppos","_cpos"];
    _ppos = arg(0);
    _cpos = arg(1);
    mRadius ^ 2 > mDeltaX ^ 2 + mDeltaY ^ 2
};

// [getpos unit1, getpos unit2] call funcGetDirToPos

funcGetDirToPos = {
    private ["_p1","_p2","_dx","_dy"];
    _p1 = arg(0);
    _p2 = arg(1);
    _dx = (_p1 select 0) - (_p2 select 0);
    _dy = (_p1 select 1) - (_p2 select 1);
    if ( _dx == 0 && _dy == 0 ) then {
        -360 // for calling side: error if negative
    } else {
        ( 180 + (_dx atan2 _dy) ) % 360
    }
};

// и так далее...

Но раз уж мы взя­лись бо­роть­ся с гло­баль­ны­ми име­нами, то ре­шим, что не все фун­кции бы­ва­ют нуж­ны час­то, и иног­да про­ще заг­ру­зить один та­кой на­бор фун­кций:

// файл library.libname.sqf
_library_libname =
{
    _funcInCircle = {
        ...
    };

    _funcGetDirToPos = {
        ...
    };

    _funcNearestFromList = {
        ...
    };
};

["_funcInCircle", "_funcGetDirToPos", "_funcNearestFromList"]

При та­ком под­хо­де пе­ремен­ные ви­да _library_libname всег­да со­от­ветс­тву­ют наз­ва­нию фай­ла биб­ли­от­ки "library.libname.sqf", и под­клю­чение биб­ли­отеч­ных фун­кций выг­ля­дит так:

private "_library_libname";
private call preprocessFile "library.libname.sqf";
call _library_libname;

Но в дей­стви­тель­нос­ти, это удоб­ней толь­ко тог­да, ког­да мы не хо­тим ис­поль­зо­вать гло­баль­ные функции. Мож­но зна­читель­но по­высить удобс­тво ис­поль­зуя ком­про­мис­сный ва­ри­ант:

MSPMOD_LIBRARY_LIBNAME_EXPORT =
[
    "_mspmod_library_libname",
    "_funcInCircle",
    "_funcGetDirToPos",
    "_funcNearestFromList"
];

MSPMOD_LIBRARY_LIBNAME =
{
    _funcInCircle = {
        ...
    };

    _funcGetDirToPos = {
        ...
    };

    _funcNearestFromList = {
        ...
    };
};

Мы лишь од­нажды вы­зыва­ем ини­ци­али­зацию та­кой биб­ли­оте­ки, не­пос­редс­твен­но при стар­те про­ек­та:

call preprocessFile "my.lib.sqf"

И всег­да мо­жем по­лучить в те­кущий кон­текст оп­ре­делен­ные в ней фун­кции в два дви­жения:

private MSPMOD_LIBRARY_LIBNAME_EXPORT;
call MSPMOD_LIBRARY_LIBNAME;

В та­ком слу­чае мы сок­ра­ща­ем ко­личес­тво гло­баль­ных имен до двух на каж­дую биб­ли­оте­ку, и вов­се ни­чего не тас­ка­ем раз за ра­зом из фай­ла — это де­ла­ет­ся толь­ко однажды. И ес­ли бы весь код зак­лю­чен­ный в фи­гур­ные скоб­ки «{}» тран­сли­ровал­ся в про­межу­точ­ное пред­став­ле­ние, то та­кой спо­соб был бы прак­ти­чес­ки столь же быс­трым как и ва­ри­ант с гло­баль­ны­ми функциями. Мож­но «эк­спор­ти­ровать» и толь­ко нуж­ные фун­кции:

private ["_funcInCircle", "_funcNearestFromList"];
call MSPMOD_LIBRARY_LIBNAME;

Нуж­но толь­ко пом­нить, что ес­ли в те­кущем кон­тек­сте уже есть име­на сов­па­да­ющие с име­нами оп­ре­делён­ны­ми в биб­ли­оте­ке, они бу­дут пе­реза­писа­ны вы­зовом MSPMOD_LIBRARY_LIBNAME.

Ну и мож­но ска­зать, что нес­мотря на, мяг­ко го­воря, сво­еоб­разность этих за­бав­ных ме­тодов, они иног­да дей­стви­тель­но об­легча­ют жизнь.

На что уходит время?

Од­но вре­мя я ин­те­ресо­вал­ся ско­ростью ра­боты скрип­тов, с целью при­кинуть пу­ти оптимизации. Од­на­ко, бо­лее или ме­нее точ­но оп­ре­делить ско­рость ра­боты скрип­та не пред­став­ля­лось воз­можным, так как у нас нет дос­ту­па к сис­темно­му времени. Од­на­ко ког­да по­явил­ся NewFlash от Grey, я вос­поль­зо­вал­ся воз­можностью за­мерять вре­мя зат­ра­чива­емое на вы­пол­не­ние раз­ных тестов. Я не рас­ска­жу сей­час о всех ин­те­рес­ных мо­мен­тах, ска­жу лишь о глав­ном на мой взгляд.

Пос­ле мно­жес­тва тес­тов, ста­новит­ся оче­вид­ным, что ль­ви­ная часть вре­мени зат­ра­чива­ет­ся на пос­то­ян­ный парсинг. При­чем за­час­тую пов­то­рый пар­синг од­но­го и то­го же кода. Как-то я пы­тал­ся соп­ти­мизи­ровать про­вер­ку дис­танции меж­ду дву­мя точ­ка­ми на плос­кости:

00 funcNearestFromList = {
01     private ["_nearest", "_mindistq", "_distq", "_pos", "_px", "_py"];
02     // Квадрат минимальной найденной дистанции
03     _mindistq = if (count _this > 2) then {(arg(2)) ^ 2} else {1e+10};
04     // Найденный объект
05     _nearest = objNull;
06     // X и Y координаты заданной позиции
07     _px = arg(0) select 0;
08     _py = arg(0) select 1;
09     // Для всех переданных объектов (два умножения на итерацию)
10     {
11         // Позиция очередного объекта (_pos) и квадрат
12         // расстояния от объекта до заданной позиции (_distq)
13         _pos = getpos _x;
14         _distq = (_px - (_pos select 0)) ^ 2 + (_py - (_pos select 1)) ^ 2;
15         // Если текущий объект ближе предыдущего
16         if ( _mindistq > _distq ) then {
17             // то обновить данные
18             _mindistq = _distq;
19             _nearest = _x
20         }
21     } foreach arg(1);
22     // Вернуть найденный объект
23     _nearest
24 };

Здесь из­бе­га­ет­ся дос­та­точ­но тя­желая опер­ция на­хож­де­ния квад­ратно­го кор­ня, и, в лю­бом дру­гом язы­ке, да­же ин­тер­пре­тиру­емом, это да­ёт не­кото­рый, иног­да ощу­тимый, при­рост скорости. Од­на­ко, я был удив­лен оба­ружив, что род­ной distance, ра­бота­ет в сред­нем в 12 раз быс­трее три­над­ца­той и че­тыр­надца­той строк это­го примера. И это при том, что distance ра­бота­ет в трех из­ме­рени­ях и ес­тес­твен­но ис­поль­зу­ет sqrt. Оба ва­ри­ан­та за­пус­ка­лись во вло­жен­ных цик­лах вы­пол­ня­ющих­ся нес­коль­ко ты­сяч раз.

Да­же лиш­ние скоб­ки ска­зыва­лись на быс­тро­дей­ствии, это бы­ла очень нез­на­читель­ная раз­ни­ца, но она ста­биль­но пов­то­рялась от тес­та к тесту.

Ос­новной вы­вод ко­торый мож­но сде­лать из это­го фак­та — не оп­ти­мизи­руй­те на низ­ком уров­не, оп­ти­мизи­руй­те сам под­ход, алгоритм.

Сортировка

Дей­стви­тель­но, сор­ти­ров­ка ред­ко бы­ва­ет нуж­на, но все-та­ки мне встре­чались па­ру раз скрип­ты ис­поль­зу­ющие её. Это бы­ла, ес­тес­твен­но, bubble sort. её от­ли­читель­ная осо­бен­ность — это ужас­ная за­виси­мость ско­рос­ти от объ­ема сор­ти­ру­емых дан­ных, ко­торая вы­ража­ет­ся как N². Ког­да мне по­надо­бил­ся сор­ти­рован­ный вы­вод строк в ди­ало­ги я ре­шил подс­тра­ховать­ся и ис­поль­зо­вать что-то бо­лее быстрое. (Срав­не­ние строк — это еще од­на проб­ле­ма, ко­торую, впро­чем мож­но ре­шить для не­кот­рых слу­ча­ев) Быс­трая сор­ти­ров­ка Хо­ара очень пло­хо ре­али­зу­ет­ся средс­тва­ми sqf так как тре­бу­ет ре­кур­сии, и мо­им вы­бором ста­ла heap sort, столь же быс­трая и бо­лее удоб­ная в реализации.

Пос­коль­ку это не са­мый из­вес­тный ал­го­ритм, я при­вожу его здесь:

/*
    Сортировка на пирамиде, O(n log n)
    модифицирует переданный массив
*/

private ["_sift", "_arr", "_max", "_i", "_l", "_u", "_c", "_tmp"];

#define incSwapCounter _swapCounter = _swapCounter + 1
#define sift(a,b)  _l=(a); _u=(b); call _sift

_sift = {
    _tmp = _arr select _l;
    while {
        _c = _l+_l+1;
        if (_c <= _u) then {
            if (_c < _u) then {
                if ( _arr select _c+1 > _arr select _c ) then {
                    _c = _c + 1;
                };
            };
            if (_tmp < _arr select _c) then {
                _arr set [_l, _arr select _c];
                _l = _c;
                incSwapCounter;
                true
            }
        }
    } do {};
    _arr set [_l, _tmp];
};

_arr = _this;
_max = count _arr - 1;

_i = _max/2 call {_this - _this % 1};

while { _i >= 0 } do { sift(_i,_max); _i = _i-1 };

_i = _max;

while { _i > 0 } do {

    _tmp = _arr select 0;
    _arr set [0, _arr select _i];
    _arr set [_i, _tmp];
    incSwapCounter;

    sift(0,_i-1); _i = _i-1
};

_arr

Сле­ду­ющая таб­ли­ца по­казы­ва­ет ско­рость ра­боты сор­ти­ровок на пи­рами­де и пу­зырь­ка­ми (ко­личес­тво пе­рес­та­новок и вре­мя ра­боты (clock из NewFlash)):

количество сортируемых элементов: 10
sort\bubbleSort.sqf:    swap: 25      time: 0
sort\hsort.v2.4.sqf:    swap: 27      time: 0.0159912

количество сортируемых элементов: 50
sort\bubbleSort.sqf:    swap: 574     time: 0.0939941
sort\hsort.v2.4.sqf:    swap: 242     time: 0.0469971

количество сортируемых элементов: 100
sort\bubbleSort.sqf:    swap: 2724    time: 0.360001
sort\hsort.v2.4.sqf:    swap:  582    time: 0.108994

количество сортируемых элементов: 200
sort\bubbleSort.sqf:    swap: 10253   time: 1.39102
sort\hsort.v2.4.sqf:    swap:  1347   time: 0.218994

количество сортируемых элементов: 300
sort\bubbleSort.sqf:    swap: 22880   time: 3.25
sort\hsort.v2.4.sqf:    swap:  2209   time: 0.390015

количество сортируемых элементов: 400
sort\bubbleSort.sqf:    swap: 37962   time: 5.59402
sort\hsort.v2.4.sqf:    swap:  3120   time: 0.547028

количество сортируемых элементов: 500
sort\bubbleSort.sqf:    swap: 63920   time: 9.172
sort\hsort.v2.4.sqf:    swap:  4020   time: 0.625

Ко­неч­но heap sort со­дер­жит боль­ше ко­да, но от­рыв про­ис­хо­дит уже на мас­си­ве в 50 эле­мен­тов (или чуть рань­ше). Ес­ли бы код вы­пол­нялся быс­трее, а срав­не­ние об­хо­дилось бы до­роже, то heap sort выг­ля­дела бы еще привлекательнее.

Код срав­ни­ва­емых сор­ти­ровок и скрипт тес­та ле­жит в архиве.

Copyright © , 2006–2014.