Игроки и сущности
Всё в игровом мире — сущности: игроки, оружие, гранаты, двери, бомба. В плагине сущность — это объект, а её данные — свойства, которые читаются и присваиваются:
server.addCommand("/low", ({ player }) => lowGravity(player));
function lowGravity(player: Player) {
player.gravity = 0.5;
player.renderMode = "additive";
console.log(player.classname); // "player"
const here = player.origin; // Vector: here.x, here.distanceTo(...)
const nade = Entity.find({ classname: "grenade", owner: player });
if (nade) nade.velocity = [0.0, 0.0, 300.0];
}
Классов три:
Entity— любая сущность;Player— игрок: всё, что есть уEntity, плюс данные и действия самого игрока (Игроки);Weapon— оружие: всё, что есть уEntity, плюс обойма, владелец и тайминги.
Игрока даёт событие или команда, к которой он относится (event.player), или
server.players; сущность — событие, Entity.find или Entity.create.
new Player(id) и new Entity(id) оборачивают уже известный индекс.
Свойства
У Entity есть все поля, которые движок хранит для сущности, у Player —
все члены игрового класса игрока, у Weapon — все члены оружия. Тип свойства — тип его данных:
| Данные | Свойство | Тип |
|---|---|---|
| число | gravity, money, maxSpeed | number |
| позиция или направление | origin, velocity, angles | при чтении Vector, при записи — любые три числа |
| текст | classname, model | string |
| да или нет | nightVisionOn | boolean |
| одно из значений движка | renderMode, moveType, kevlar | имя, см. ниже |
| набор флагов | flags, hideHud, buttons | массив имён, см. Флаги |
| другая сущность | activeItem, weapon.player | Weapon | null, Player | null |
Редактор показывает их все. Если навести на свойство, подсказка скажет, что
это такое, — «Деньги игрока», — с единицами, диапазоном и обычными
значениями, а последней строкой назовёт его так, как его зовут в Pawn
(Pawn: `CBasePlayer::m_iAccount` ), — на случай, если вы знаете поле по
имени из Pawn.
Имена — это имена из Pawn в camelCase и без префикса — var_rendermode —
renderMode, m_iHideHUD — hideHud, — или слово игрока там, где у игры
своё: m_iAccount — money, m_flVelocityModifier — slowdown.
Число, записанное в целочисленное поле, теряет дробную часть:
player.money = 99.9 записывает 99.
Что видит игрок с v0.2
Здоровье, броню, поле зрения и скрытые части HUD игра сама отправляет игроку, когда они меняются. Некоторые поля его игра узнаёт только из сообщения, и запись такого поля отправляет ему сообщение, как это делает игра:
| Свойство | Что он сразу видит |
|---|---|
money | новую сумму на HUD, с миганием |
kevlar | шлем на значке брони или его отсутствие |
flashlightBattery | заряд фонарика |
hasNightVision, nightVisionOn | прибор в меню покупки; ночное видение на экране или его выключение |
hasDefuser | набор на модели, его значок на HUD, набор в меню покупки |
player.money += 500;
Сообщение уходит так же, как сообщения самой игры, поэтому его слышит
слушатель сообщения money, а без ReAPI — и addMoney, который узнаёт о деньгах
из этого сообщения.
Свободные поля
iuser1–iuser4, fuser1–fuser4, vuser1–vuser4 и euser1–euser4 —
запасные поля, в которых плагин может хранить свои значения на любой
сущности, кроме игрока. У игрока часть из них занимает игра: iuser1–iuser3 — наблюдение, iuser4 перезаписывается каждый кадр, fuser2 и fuser3 —
движение. Подсказка каждого говорит, какое для чего. То, что плагин помнит об
игроке, место в его собственном объекте на игрока или в
общем поле игрока.
Имена вместо чисел
Поле, в котором лежит одно из фиксированных значений движка, принимает и отдаёт имя:
function hide(player: Player, box: Entity) {
box.renderMode = "additive";
box.moveType = "noclip";
box.solid = "trigger";
player.kevlar = "vestHelmet";
if (player.waterLevel == "head") console.log(`${player.name} под водой`);
if (player.deadFlag != "alive") return;
if (player.observerMode == "inEye") console.log("смотрит чужими глазами");
}
| Свойство | У кого | Тип: имена |
|---|---|---|
renderMode | Entity | RenderMode: "normal", "color", "texture", "glow", "alpha", "additive" |
renderFx | Entity | RenderFx: "none", "glowShell", "pulseSlow", "hologram", … |
moveType | Entity | MoveType: "none", "walk", "fly", "toss", "noclip", "bounce", "follow", … |
solid | Entity | Solid: "none", "trigger", "box", "slideBox", "bsp" |
takeDamage | Entity | TakeDamage: "no", "yes", "aim" |
deadFlag | Entity | DeadFlag: "alive", "dying", "dead", "respawnable", "discardBody" |
waterLevel | Entity | WaterLevel: "none", "feet", "waist", "head" |
waterType | Entity | Contents: "empty", "water", "slime", "lava", … |
fixAngle | Entity | FixAngle: "none", "set", "addYaw" |
observerMode, observerLastMode | Player | ObserverMode: "none", "chaseLocked", "chaseFree", "roaming", "inEye", "mapFree", "mapChase" |
kevlar | Player | ArmorType: "none", "vest", "vestHelmet" |
lastHitGroup | Player | HitGroup: "generic", "head", "chest", "stomach", "leftArm", … "shield" |
joiningState | Player | JoinState: "joined", "showMotd", "pickingTeam", … |
openMenu | Player | GameMenu: "none", "team", "buy", "buyRifle", "radio1", … |
modelName | Player | PlayerModel: "urban", "gign", "terror", "leet", … "unassigned", "auto" |
ignoreGlobalChat | Player | IgnoredChat: "none", "enemy", "all" |
throwDirection | Player | ThrowDirection: "none", "forward", "backward", "grenade", … |
bloodColor | Player | BloodColor: "none", "red", "yellow" |
musicState | Player | MusicState: "silent", "calm", "intense" |
Редактор подсказывает имена и не пропускает опечатку, а подсказка объясняет,
что значит каждое. Типы экспортируются из @amxts/core — для параметра своей
функции: function glow(player: Player, fx: RenderFx).
observerMode — это iuser1 игрока, прочитанный по имени; сам iuser1
остаётся числом. Присваивание режима переключает камеру так же, как игра,
когда игрок выбирает режим сам: player.observerMode = "inEye" находит, за
кем ему наблюдать, — observerTarget, если за ним ещё можно, — а если не за
кем, включает "roaming"; режим, который он просил, становится
observerLastMode, а название режима появляется у него на экране.
Присваивание режима, в котором он уже есть, ничего не меняет. "none" только
очищает поле: наблюдение заканчивает игра, когда он появляется.
Значение, для которого нет имени, — его записал другой плагин или мод, — читается как
"unknown", а присваивание "unknown" оставляет поле как было.
Само число можно прочитать нативом: get_entvar(player.id, var_rendermode).Векторы — Vector
function launch(player: Player, target: Player) {
const here = player.origin; // Vector
const distance = here.distanceTo(target.origin); // в юнитах
const up = new Vector(0.0, 0.0, 1.0);
player.velocity = here.subtract(target.origin).normalize().scale(400).add(up.scale(200));
console.log(`${here.x} ${here[0]} ${distance}`); // x и [0] - одно и то же число
}
Vector — это три числа, x, y, z, с обычными операциями:
| Метод | Даёт |
|---|---|
add(v), subtract(v) | новый вектор |
scale(n) | новый вектор в n раз длиннее |
dot(v) | число |
distanceTo(v) | расстояние до v |
magnitude() | длину вектора |
normalize() | новый вектор длиной в один юнит; нулевой остаётся нулевым |
add, subtract, dot и distanceTo принимают любые три числа, в том числе
литерал: here.add([0, 0, 64]). Свойство, которое принимает вектор, тоже
принимает литерал: player.velocity = [0, 0, 300].
Vector — массив из трёх чисел, поэтому v[0] читает x, а натив, который
принимает Float:v[3], принимает Vector. По той же причине его длина —
magnitude(): v.length — длина массива, и она равна 3.
Каждое чтение векторного свойства — player.origin, player.velocity — это
новый Vector, а его создание стоит времени. В коде, который
работает каждый кадр, держите один Vector и пусть свойство записывается в
него своим методом get, который его и возвращает:
const origin = new Vector(); // один раз
const velocity = new Vector();
function onFrame(player: Player) {
player.getOrigin(origin); // те же числа, что player.origin, без нового Vector
player.getVelocity(velocity);
}
Поиск, создание, удаление
function cleanUp(player: Player) {
const targets = Entity.findAll({ classname: "info_target" });
const near = Entity.findAll({ near: player.origin, radius: 200 });
const mine = Entity.findAll({ classname: "grenade", owner: player });
const bomb = Entity.find({ model: "models/w_c4.mdl" }); // Entity | null
const box = Entity.create("info_target"); // Entity | null
if (box) box.origin = player.origin;
for (const target of targets) target.remove();
console.log(`рядом ${near.length}, гранат ${mine.length}, бомба: ${bomb != null}`);
}
Entity.findAll(filter) возвращает все подходящие сущности, Entity.find —
первую из них или null. Все поля фильтра необязательны, и сущность должна
подходить под все заданные:
| Поле | Подходит |
|---|---|
classname | имя класса: "grenade", "weaponbox", "func_door" |
model | модель: "models/w_c4.mdl" |
owner | чья она: бросивший гранату, несущий оружие |
near, radius | сущности не дальше radius юнитов от точки near |
Без фильтра findAll() — все сущности на карте.
Entity.create(classname) создаёт сущность или возвращает null, если движок
не смог. entity.remove() удаляет её в конце текущего кадра — так игра
удаляет свои сущности, — поэтому это безопасно даже изнутри её собственного
touch или think. До конца кадра findAll её ещё находит.
const boxes: Entity[] = [];
function clearBoxes() {
for (const box of boxes) {
if (box.exists) box.remove(); // пропускает ту, которую игра с тех пор удалила
}
}
entity.exists — true, пока сущность есть в мире: false, когда движок её
освободил, для ушедшего игрока и для 0 — это «нет сущности». Сущности,
сохранённой на потом — в массиве, в таймере, — к тому времени может уже не
быть; exists — способ это спросить.
const box = Entity.create("info_target");
if (box) {
box.model = "models/myplugin/box.mdl"; // прекэширована через server.precache
box.setSize([-16, -16, 0], [16, 16, 32]);
}
Присваивание entity.model ставит модель так, как это делает игра:
modelIndex и размер следуют за ней. Модель должна быть прекэширована
(звуки и прекэш). entity.setSize(mins, maxs)
задаёт габариты, по которым сущность сталкивается, углы — относительно
origin; габариты, у которых mins хоть по одной оси больше maxs, не
ставятся, а в консоль пишется ошибка. Запись mins или maxs меняет числа, и
только.
Оружие
server.addCommand("/unload", ({ player }) => unloadAll(player));
function unloadAll(player: Player) {
const weapon = player.activeItem; // Weapon | null: оружие в руках
if (weapon && weapon.kind == "knife") return;
for (const item of player.items) unload(item);
}
function unload(weapon: Weapon) {
weapon.clip = 0;
const holder = weapon.player; // Player | null: кто его несёт
if (holder) console.log(`${holder.name}: ${weapon.kind} разряжен`);
}
weapon.kind— какое это оружие, именем:"knife","ak47","awp","hegrenade","flashbang", … (типWeaponKind; неизвестное оружие —"none").weapon.kindId— то же самое игровым числом.weapon.classname— имя оружия в том виде, в каком его принимают методы игрока,WeaponName, например"weapon_ak47":player.give(weapon.classname),player.getAmmo(weapon.classname).player.activeItem,player.lastItemиplayer.activeItemSent— этоWeaponилиnull;weapon.player—Player, который его несёт, илиnull, если оружие лежит на земле;weapon.next— следующее оружие в том же слоте инвентаря. Они только для чтения.player.items— всё оружие, которое несёт игрок, вместе с гранатами; одно из них —find:player.items.find(item => item.classname == "weapon_ak47"),null, если такого нет.weapon.owner— поле владельца у движка, индекс сущности; того, кто несёт оружие, даётweapon.player.- Собственные члены оружия теряют префикс из Pawn:
m_Weapon_iClip— этоclip,m_Weapon_flNextPrimaryAttack—nextPrimaryAttack.
Основное об игроке
name, health, armor, frags, deaths, team, ip, steamId,
isAlive, isConnected и isBot работают на любом сервере, с ReAPI и без.
health игрока — целое число, и 0 или меньше его убивает. У любой другой
сущности health тоже есть — число, возможно дробное: box.health = 50 —
разбиваемая ломается, когда урон доводит его до 0.
player.fov — поле зрения игрока в градусах, то, по которому игра
приближает прицел: 90 — обычное, 40 и 10 — в снайперский прицел, а
player.fov = 110 показывает ему больше. Игра ставит своё при появлении,
когда он достаёт оружие и когда приближает прицел, так что плагин, который
держит обзор шире, ставит его снова в эти моменты.
Остальное, что умеет игрок, — команды, действия, HUD — на странице Игроки.
Поля, которые не свойства
У части игровых данных свойства нет. Такие члены читаются и пишутся
нативами ReAPI get_member и set_member — каждый тем, что в нём лежит
(вызов нативов), — а на сервере
без ReAPI нативами fakemeta get_ent_data и set_ent_data, которые называют
класс и член: get_ent_data(player.id, "CBasePlayer", "m_rgAmmo", 5):
- члены-массивы, например патроны по типам
m_rgAmmo, — с индексом элемента после члена (само оружие — этоplayer.items); var_controllerиvar_blending— черезget_entvarиset_entvar;- члены, которые игра держит не для игрока Counter-Strike: ИИ монстра
(
m_Activity,m_IdealActivity,m_MonsterState,m_IdealMonsterState,m_afConditions,m_afMemory,m_vecEnemyLKP,m_HackedGunPos,m_hTargetEnt), имя команды Half-Lifem_szTeamName(сторона — этоplayer.team) и тип брониvar_armortype(броня — этоplayer.armorиplayer.kevlar), карьера Condition Zero (m_bInCareerGame,m_fCareerRoundMenuTime,m_iCareerMatchWins,m_fCareerMatchMenuTime,m_iRoundWinDifference), номера голосовых сообщений (m_msgPlayerVoiceMask,m_msgRequestState), события выстрела оружия (m_Weapon_usFireGlock18,m_Weapon_usFireFamas) и последний выстрел пистолетаm_Weapon_flLastFire(у любого оружия этоweapon.lastFireTime).
Текстовый член — такое же свойство, как остальные: player.animExtension —
набор анимаций, с которым модель игрока держит оружие, например "knife"
(m_szAnimExtention).
autoSwitchWeapon и shotgunReloadStage — простые числа, те же, что в Pawn.
Производительность
Плагин компилируется в машинный код до того, как сервер его загрузит, поэтому работа, которую он делает сам, — циклы, арифметика, массивы, текст — идёт быстро. Время уходит на переходы между плагином и игрой — вызов в AMX Mod X, событие по пути к обработчику — и на память, которую нагруженная функция просит снова и снова. На этой странице — куда уходит время и привычки, которые из этого следуют.
Игроки: действия
Player — это игрок на сервере. Его даёт событие или команда, к которой он относится (event.player, player в обработчике команды), а server.players перечисляет всех. Игровые данные игрока — gravity, origin, hideHud и остальные — это свойства, они описаны на странице Игроки и сущности. Эта страница — о том, что с игроком можно делать: найти, перевести в команду, выдать оружие, показать текст и поделиться его состоянием с другими плагинами.