Игра

Игроки и сущности

Всё в игровом мире — сущности: игроки, оружие, гранаты, двери, бомба. В плагине сущность — это объект, а её данные — свойства, которые читаются и присваиваются:

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, maxSpeednumber
позиция или направлениеorigin, velocity, anglesпри чтении Vector, при записи — любые три числа
текстclassname, modelstring
да или нетnightVisionOnboolean
одно из значений движкаrenderMode, moveType, kevlarимя, см. ниже
набор флаговflags, hideHud, buttonsмассив имён, см. Флаги
другая сущностьactiveItem, weapon.playerWeapon | 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
("смотрит чужими глазами");
}
СвойствоУ когоТип: имена
renderModeEntityRenderMode: "normal", "color", "texture", "glow", "alpha", "additive"
renderFxEntityRenderFx: "none", "glowShell", "pulseSlow", "hologram", …
moveTypeEntityMoveType: "none", "walk", "fly", "toss", "noclip", "bounce", "follow", …
solidEntitySolid: "none", "trigger", "box", "slideBox", "bsp"
takeDamageEntityTakeDamage: "no", "yes", "aim"
deadFlagEntityDeadFlag: "alive", "dying", "dead", "respawnable", "discardBody"
waterLevelEntityWaterLevel: "none", "feet", "waist", "head"
waterTypeEntityContents: "empty", "water", "slime", "lava", …
fixAngleEntityFixAngle: "none", "set", "addYaw"
observerMode, observerLastModePlayerObserverMode: "none", "chaseLocked", "chaseFree", "roaming", "inEye", "mapFree", "mapChase"
kevlarPlayerArmorType: "none", "vest", "vestHelmet"
lastHitGroupPlayerHitGroup: "generic", "head", "chest", "stomach", "leftArm", … "shield"
joiningStatePlayerJoinState: "joined", "showMotd", "pickingTeam", …
openMenuPlayerGameMenu: "none", "team", "buy", "buyRifle", "radio1", …
modelNamePlayerPlayerModel: "urban", "gign", "terror", "leet", … "unassigned", "auto"
ignoreGlobalChatPlayerIgnoredChat: "none", "enemy", "all"
throwDirectionPlayerThrowDirection: "none", "forward", "backward", "grenade", …
bloodColorPlayerBloodColor: "none", "red", "yellow"
musicStatePlayerMusicState: "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-Life m_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 и остальные — это свойства, они описаны на странице Игроки и сущности. Эта страница — о том, что с игроком можно делать: найти, перевести в команду, выдать оружие, показать текст и поделиться его состоянием с другими плагинами.