Основы

Производительность

Плагин компилируется в машинный код до того, как сервер его загрузит, поэтому работа, которую он делает сам, — циклы, арифметика, массивы, текст — идёт быстро. Время уходит на переходы между плагином и игрой — вызов в AMX Mod X, событие по пути к обработчику — и на память, которую нагруженная функция просит снова и снова. На этой странице — куда уходит время и привычки, которые из этого следуют.

Куда уходит время

  • Ваш собственный код быстрый. Циклы, арифметика с целыми и дробными числами, массивы и словари работают как машинный код.
  • Вызов стоит дороже, чем код вокруг него. Натив и вызов вашей собственной маленькой функции, в том числе [] массива, стоят больше, чем арифметика рядом с ними. Несколько сотен за кадр незаметны; код, который каждый кадр читает игру и мало что с этим делает, тратит время на вызовы.
  • Свойства игрока, оружия или сущности читаются там, где их хранит игра, без натива посередине. Свойство-вектор при каждом чтении создаёт новый Vector; get<Поле>(target) записывает его в тот, что держите вы.
  • Дороже всего стоит событие: путь до обработчика занимает намного больше времени, чем вызов. Событие раз в раунд, на убийство или на команду не заметно; событие каждый кадр для каждого игрока стоит посчитать.
  • Памятью управляют за вас: то, что создала функция, освобождается, когда на него никто не ссылается, а стоит именно создание.
  • Плагин, который собрал amxts build, не ведёт запись вызовов, которые привели к ошибке, — она замедляла бы нагруженный плагин: он называет ошибку и её место, а всю запись ведёт amxts dev (ошибки).

Как писать быстрый плагин

Сначала фасад, потом нативы

player.money и другие свойства игрока, оружия или сущности читают и пишут память игры напрямую. Нативы полей ReAPI — get_member, get_entvar и подобные — принимают хвост ..., который вызов из TypeScript собирает на ходу, поэтому они стоят намного дороже свойства:

// так
if (
player
.
money
>= price)
player
.
money
-= price;
// не так if (get_member(
player
.
id
, m_iAccount) >= price) set_member(
player
.
id
, m_iAccount, get_member(
player
.
id
, m_iAccount) - price);

Простой натив — is_user_alive, user_slap — дешёвый: вызывайте его смело там, где у фасада нет своего (нативы).

Слушайте нужный класс

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

// так: игрок касается оружия на земле, и больше ничего
game
.
addEventListener
("touch", onPickup, {
toucher
: "player",
touched
: "weaponbox" });
// не так: каждое касание каждой сущности, каждый кадр, чтобы найти те же
game
.
addEventListener
("touch", (
event
) => {
if (
event
.
toucher
.
classname
!= "player" ||
event
.
touched
.
classname
!= "weaponbox") return;
onPickup(
event
);
});

classname делает то же для события об одном классе сущностей (один класс сущностей).

Частые события должны быть лёгкими

Некоторые события идут всё время: "frame" каждый кадр сервера, preThink и postThink каждый кадр для каждого игрока, touch, а на каждый выстрел — shoot, traceAttack и primaryAttack. На полном сервере preThink приходит десятки тысяч раз в секунду, и каждый раз платит за дорогу до обработчика. Делайте там лишь то, что должно случиться в этот момент, а то, что может подождать, отдайте таймеру:

// так: десять раз в секунду, для всех сразу
setInterval
(() => {
for (const
player
of
server
.
players
) checkSpeed(
player
);
}, 100); // не так: та же проверка в каждом кадре каждого игрока
game
.
addEventListener
("preThink", (
event
) => checkSpeed(
event
.
player
));

Не создавайте объекты в нагруженном цикле

Каждый новый объект стоит времени, чтобы его создать и потом освободить, а Vector или текст — больше многих. Поле-вектор вроде player.origin при каждом чтении создаёт новый Vector, как и каждое event.player. Там, где код работает каждый кадр, держите свой Vector и пусть поле записывается в него:

// так: два Vector на весь плагин
const 
origin
= new
Vector
();
const
velocity
= new
Vector
();
function track(
player
: Player) {
player
.
getOrigin
(
origin
);
player
.
getVelocity
(
velocity
);
const
speed
=
Math
.
sqrt
(
velocity
.
x
*
velocity
.
x
+
velocity
.
y
*
velocity
.
y
);
} // не так: два новых Vector для каждого игрока в каждом кадре function track(
player
: Player) {
const
speed
=
Math
.
sqrt
(
player
.
velocity
.
x
*
player
.
velocity
.
x
+
player
.
velocity
.
y
*
player
.
velocity
.
y
);
}

С текстом так же: собирайте строку HUD, когда меняется то, что она показывает, а не при каждой отправке.

server.players читайте один раз

Каждое чтение server.players заново спрашивает сервер и создаёт новый список новых игроков, и чем больше игроков, тем это дороже. Прочитайте его один раз и пользуйтесь им:

// так: один список на весь цикл
const 
players
=
server
.
players
;
for (const
player
of
players
) {
const
enemies
=
players
.
filter
(
other
=>
other
.
team
!=
player
.
team
);
warn(
player
,
enemies
);
} // не так: новый список для каждого игрока for (const
player
of
server
.
players
) {
const
enemies
=
server
.
players
.
filter
(
other
=>
other
.
team
!=
player
.
team
);
warn(
player
,
enemies
);
}

События, а не опрос

Таймер, который спрашивает каждого игрока, не случилось ли что-то, делает эту работу каждый раз, даже когда ничего не случилось. Событие приходит один раз, когда это случается:

// так: игра сама говорит, когда игрок появился
game
.
addEventListener
("spawn", (
event
) => giveKit(
event
.
player
), {
post
: true });
// не так: десять раз в секунду спрашивать каждого игрока, жив ли он уже
setInterval
(() => {
for (const
player
of
server
.
players
) {
if (
player
.
isAlive
&& !equipped.has(
player
.
id
)) giveKit(
player
);
} }, 100);

Ждать — это await

Веб-запрос через fetch или useFetch идёт рядом с игрой: сервер продолжает работать, пока запрос ждёт, а ответ приходит в одном из следующих кадров. Цикл, который крутится, пока что-то не изменится, наоборот, держит весь сервер — ждите через await, таймер или событие (async). Файлы читаются и пишутся в собственном потоке сервера, и промис-формы @amxts/core/fs тоже: читайте файл один раз, при запуске плагина, а не в обработчике.

Переход в Pawn

Вызов натива — AMX Mod X, модуля, другого плагина — дешёвый. Натив с хвостом ... — get_entvar, get_member, ExecuteForward, форматированный текст — собирает свои аргументы на ходу и стоит намного дороже. В обработчике, который работает каждый кадр, их стоит считать; в остальных местах они не важны.