Производительность
Плагин компилируется в машинный код до того, как сервер его загрузит, поэтому работа, которую он делает сам, — циклы, арифметика, массивы, текст — идёт быстро. Время уходит на переходы между плагином и игрой — вызов в 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,
форматированный текст — собирает свои аргументы на ходу и стоит намного
дороже. В обработчике, который работает каждый кадр, их стоит считать; в
остальных местах они не важны.
Async и промисы
Плагин ждёт так же, как JavaScript: async-функции, await, Promise. Пока одна функция ждёт — ответа на веб-запрос, пару секунд, — сервер и остальной плагин продолжают работать.
Игроки и сущности
Всё в игровом мире — сущности: игроки, оружие, гранаты, двери, бомба. В плагине сущность — это объект, а её данные — свойства, которые читаются и присваиваются: