Язык описания сценариев игры 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.