Добро пожаловать в Форум администраторов игровых серверов

Привет уважаемый пользователь, ты зашел на форум администраторов игровых серверов, здесь сможешь найти, плагины для своего сервера, расширения, серверные модели оружия, моды, и многое другое. Для того чтобы скачивать ресурсы, а также создавать темы, и писать ответы, требуется пройти регистрацию.

[SourcePawn] Вводный урок - Самые основы

xyligan

Модератор
Регистрация
06.12.2020
Сообщения
513
Реакции
40
Баллы
38
Возраст
21
Местоположение
Киев, Украина
[SourcePawn] Вводный урок - Самые основы
<= Вернуться к содержанию

Это скорее даже не урок, а монолог.
В нем я хочу рассказать о том, с какими проблемами чаще всего сталкиваются начинающие "кодеры" и дам несколько советов.

Писать постараюсь максимально просто.

Начну с самого основного - игроков.
Тут следует ввести 2 основных понятия:
  • Индекс игрока - номер слота, занимаемого игроком на сервере.
Слот номер 0 это сервер. О нем позже.​
Максимальное кол-во слотов на сервере 64.​
Значит индексы игроков от 1 до 64-х.​
Еще 1 слот - Source/GO TV.​
Я не знаю занимает он один из слотов выделенных для игроков или для него добавляется отдельный (65-й).​
В SourcePawn есть константа которая обозначает это число:​
C-подобный:
#define MAXPLAYERS                65
Да-да, это та самая MAXPLAYERS, которую вы так часто видели.​
Это ничто иное как "псевдоним" числа 65.​
Так же есть еще 1 константа:​
C-подобный:
int MaxClients;
Она всегда равна количеству слотов на сервере.​
И так закрепим что такое индекс:​
Предположим что у нас пустой сервер, без Source/GO TV, и имеет 10 слотов.​
И того MaxClients равна 10, MAXPLAYERS как и всегда равна 65.​
На сервер заходит игрок Петя, и получает индекс 1.​
После него заходит Вася, и следовательно получает индекс 2.​
За ними заходит еще 2 игрока, в итоге получаем это:​
Индекс - Ник​
1 - Петя​
2 - Вася​
3 - Коля​
4 - Алеша​

Далее Вася решает что он наигрался и выходит. Слот 2 становится свободен.​
На сервер заходит игрок Инокентий, и получает минимальный свободный индекс - 2.​
Затем сервер покидает и Коля.​
Заходит Витя и занимает 3-й слот, а за ним Валера занимая 5-й слот.​
И того имеем:​
Индекс - Ник​
1 - Петя​
2 - Инокентий​
3 - Витя​
4 - Алеша​
5 - Валера​
Всё просто. Думаю, это уже ясно.​
Тут же следует сказать о мистической переменной client, она же чудом может называться iClient, i и много других названий.​
Так вот это просто переменная, которая хранит в себе индекс игрока.​
Плагины работают с игроками по их индексу, поэтому важно что хранится в переменной client, а не как она называется (это дело вкуса (в моих уроках это iClient)).​
  • UserID игрока - уникальный номер игрока на сервере.
Как и в случае со слотом 0 это сервер.​
Но с игроками немного иначе.​
Предположим что ситуация та же:​
У нас пустой сервер, без Source/GO TV, и имеет 10 слотов.​
И того MaxClients равна 10, MAXPLAYERS как и всегда равна 65.​
На сервер заходит игрок Петя, и получает индекс 1, юзер ид так же 1.​
После него заходит Вася, и следовательно получает индекс 2, юзер ид 2.​
За ними заходит еще 2 игрока, в итоге получаем это:​
Индекс - Юзерид - Ник​
1 - 1 - Петя​
2 - 2 - Вася​
3 - 3 - Коля​
4 - 4 - Алеша​
Но вот дальше ситуация меняется.​
Вася выходит, заходит Инокентий, и получает минимальный свободный индекс - 2, а вот юзер ид у него будет 5.​
Почему так?​
Юзерид это просто уникальный номер, который присваивается каждому заходящему на сервер игроку, каждый раз увеличиваясь на 1.​
Сейчас на сервере такая картина:​
Индекс - Юзерид - Ник​
1 - 1 - Петя​
2 - 5 - Инокентий​
3 - 3 - Коля​
4 - 4 - Алеша​
Сервер покидает Коля, заходит Витя и занимает 3-й слот, а Юзерид получает 6.​
Выходит Петя, заходит Оля и занимает 1-й слот, а Юзерид получает 7.​
Заходит Валера занимая 5-й слот и Юзерид 8.​
И того имеем:​
Индекс - Юзерид - Ник​
1 - 7 - Оля​
2 - 5 - Инокентий​
3 - 6 - Витя​
4 - 4 - Алеша​
5 - 8 - Валера​
И так с каждым заходящим игроком.​
Если Коля зайдет на сервер опять (просто реконнектнится, или позже решит продолжить нагиб) - его Юзерид будет не 6 как при выходе, а уже 9, т.к. сейчас найбольший Юзерид равен 8.​
И так до тех пор пока не дойдет до 2147483647 (максимальное положительное значение int). Затем он станет равен​
–2147483648 и будет уменьшаться до 0 (но 0 не станет, после -1 станет 1) и весь круг опять заново.​
Так же на 1 он сбрасывается при выключении/перезагрузке сервера.​
Над этим особо не заморачивайтесь. Это нужно просто для того чтобы как-то на небольшие промежутки времени идентифицировать игроков.​
Это часто используется в меню, таймерах, эвентах (событиях).​
Например, в промежутке между открытием меню админу со списком игроков, до момента когда админ выберет игрока, чтобы убедится что игрок выбранный админом всё еще на сервере, а не другой игрок занявший его слот, в описание пунктов меню передаются именно юзерид, а не индексы.​
Для работы с Юзерид есть 2 функции:​
  • GetClientUserId - Получает Юзерид по индексу игрока.​
  • GetClientOfUserId - Получает индекс игрока по его Юзерид (Возвращает 0 если игрока с таким Юзерид уже нет на сервере).​
Небольшой пример:​
Допустим мы хотим воскресить игрока через минуту после его смерти.​
C-подобный:
#include <cstrike> // Чтобы иметь возможность воскрешать игроков

public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath);    // Ловим событие игры с именем player_death (смерть игрока)
    // https://wiki.alliedmods.net/Counter-Strike:_Source_Events#player_death
    // Подробнее об эвентах: http://hlmod.ru/threads/sourcepawn-urok-3-sobytija-events.36891/
}

public void Event_PlayerDeath(Event hEvent, const char[] sEvName, bool bDontBroadcast)    // Функция будет вызвана когда на сервере произойдет событие player_death
{
    // Из параметров события: https://wiki.alliedmods.net/Counter-Strike:_Source_Events#player_death
    // Мы выдим что в событии по имени userid нам передается Юзерид игрока (жертвы).
    // Но плагины же работают с игроками по их индексу.
    int iUserID = hEvent.GetInt("userid");    // Получаем в переменную iUserID Юзерид игрока
    // Теперь нам нужно получить его индекс, имея iUserID
    // Используем для этого вышеприведенную ф-ю GetClientOfUserId
    int iClient = GetClientOfUserId(iUserID);        // Получили индекс игрока из Юзерид, в переменную iClient

    // Это всё дело можно было записать сразу так:
    // int iClient = GetClientOfUserId(hEvent.GetInt("userid"));
    // Но дело в том, что Юзерид нам еще понадобится и с целью оптимизации дабы не получать из эвента его снова, получаем его в переменную

    // Давайте убедимся что игрок еще на сервере
    if(iClient != 0)    // Можно сократить до if(iClient)
    {
        // Еще можно проверить что-то, например, давайте воскрешать только тех, у кого есть любой админ-флаг
        if(GetUserAdmin(iClient) != INVALID_ADMIN_ID)
        {
            // Как это работает ?
            // Функция GetUserAdmin возвращает уникальный ID админа или INVALID_ADMIN_ID если игрок не админ.
            // Значит если её результат не равен INVALID_ADMIN_ID то игрок админ
         
            // Теперь нам нужно создать задержку в минуту перед воскрешением.
            // Сделаем это таймером:
            CreateTimer(60.0, Timer_RespawnClient, iUserID);
            // Думаю ясно что 60.0 это время, через которое будет вызвана функция Timer_RespawnClient.
            // Я предпочитаю называть обратные вызовы (каллбэки) таймеров начиная с Timer_
            // В данные таймера (то что будет передано в каллбек) передаем Юзерид игрока, который как мы помним хранится в iUserID
        }
    }
}

public Action Timer_RespawnClient(Handle hTimer, any iUserID) // Это будет вызвано через 60 секунд, после смерти игрока
{
    // теперь давайте получим индекс игрока, чтобы с ним можно было работать.
    int iClient = GetClientOfUserId(iUserID);        // Получили индекс игрока из Юзерид, в переменную iClient
    // Если к моменту вызова этого каллбека игрок покинул сервер то iClient будет равна 0.
    // Давайте проверим на сервере ли еще игрок:
    if(iClient)
    {
        // Если мы попали сюда то игрок еще на сервере.
        // Теперь убедимся что игрок еще мертв
        if(!IsPlayerAlive(iClient)) // Аналогично if(IsPlayerAlive(iClient) == false)
        {
            // Ну и воскрешаем его
            CS_RespawnPlayer(iClient);
            // Всё
        }
    }
 
    return Plugin_Stop; // Так нужно. Подробнее в уроке про таймеры.
}
Это не совсем рабочий код. Точнее он рабочий но с багами.​
Нужно учесть что раунд может закончится, а таймер будет продолжать идти.​
К примеру, если игрок умер, сразу же закончился раунд (через 10 сек после смерти) и игрок снова умер (через 20 сек после начала нового раунда) (менее через за 60 после прошлой смерти).​
Получается что прошло 10 сек + 20 сек = 30 сек. Игрок умирает, опять создается таймер на 60 сек. Но прошлый таймер еще жив и через 30 сек игрока возродит тот таймер, что был создан первым.​
Это уже не соответсвие заданию.​
Тут есть несколько вариантов решения этой проблемы. Наиболее простой:​
C-подобный:
#include <cstrike> // Чтобы иметь возможность воскрешать игроков

Handle g_hTimer[MAXPLAYERS+1]; // Да, та самая MAXPLAYERS, которая равна 65
// Этой строкой мы создали переменную g_hTimer в 66-ти экземплярах, т.е. 66 штук. По одной для каждого игрока с учетом сервера и Source/GO TV

public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath);    // Ловим смерть игрока
    HookEvent("player_spawn", Event_PlayerSpawn);     // Ловим спавн игрока
}

public void Event_PlayerSpawn(Event hEvent, const char[] sEvName, bool bDontBroadcast)
{
    // Игрок спавнился
    int iClient = GetClientOfUserId(hEvent.GetInt("userid")); // Получаем его индекс
    if(g_hTimer[iClient]) // Если таймер игрока не равен null
    {
        // Через [индекс] мы обращаемся к определенному элементу массива, коим является g_hTimer
        KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
        g_hTimer[iClient] = null;    // Обнуляем таймер игрока
    }
}

public void OnClientDisconnect(int iClient)
{
    // Игрок отключился от сервера
    if(g_hTimer[iClient]) // Если таймер игрока не равен null
    {
        KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
        g_hTimer[iClient] = null;    // Обнуляем таймер игрока
    }
}

public void Event_PlayerDeath(Event hEvent, const char[] sEvName, bool bDontBroadcast)
{
    // Игрок умер
    int iUserID = hEvent.GetInt("userid");
    int iClient = GetClientOfUserId(iUserID);

    if(iClient && GetUserAdmin(iClient) != INVALID_ADMIN_ID) // Немного сократим код
    {
        if(g_hTimer[iClient]) // Если таймер игрока не равен null
        {
            KillTimer(g_hTimer[iClient]); // Убиваем таймер игрока
        }
        g_hTimer[iClient] = CreateTimer(60.0, Timer_RespawnClient, iClient);
        // теперь мы можем передать не Юзерид, а индекс, т.к. в случае выхода игрока - таймер будет уничтожен
    }
}

public Action Timer_RespawnClient(Handle hTimer, any iClient) // Это будет вызвано через 60 секунд, после смерти игрока
{
    g_hTimer[iClient] = null;    // Сразу обнуляем его таймер

    if(!IsPlayerAlive(iClient))
    {
        CS_RespawnPlayer(iClient);
    }

    return Plugin_Stop;
}
С этим кажется разобрались.​
Дальше хотелось бы поговорить о цикле по игрокам.​
Некоторые события (как начало раунда, конец раунда и т.д. и т.п.) не имеют в параметрах юзерид игрока. И это логично, ибо они вызываются не для конкретного игрока, а для всех одновременно.​
Но что делать если нужно что-то сделать с каждым игроком (или каждым игроком определенной команды), к примеру в начале раунда? Выход - цикл по игрокам.​
C-подобный:
#include <sdktools> // Чтобы иметь возможность убивать игроков

public void OnPluginStart()
{
    HookEvent("round_end", Event_RoundEnd);    // Ловим конец раунда
    // https://wiki.alliedmods.net/Generic_Source_Events#round_end
}

public void Event_RoundEnd(Event hEvent, const char[] sEvName, bool bDontBroadcast)
{
    // Раунд закончился
    // Давайте к примеру убьем всех выживших в проигравшей команде, за то, что они не выполнили задания (не спасли заложников, не заложили/обезвредили бомбу)
    // Получим победителя.
    // В параметрах события мы видим: winner    winner team/user id
    // Это значит что по ключу winner мы можем получить индекс победившей команды:
    // 2 - террористы
    // 3 - контр-террористы
    // 1 - ничья
    int iWinner = hEvent.GetInt("winner"); // Получаем индекс победившей команды в переменную iWinner

    if(iWinner < 2)    // Если ничья. Наверное можно проверить просто if(iWinner == 1), но лучше предохраниться
    {
        // В случае ничьей не будем делать ничего.
        // Для этого просто выйдем из функции
        return;
        // Всё что после return не будет выполнено
    }

    // Сюда мы дойдем в случае если раунд завершился не ничьей

    // и так, у нас есть индекс победившей команды.
    // Теперь пройдемся по всем игрокам
    for(int i = 1; i <= MaxClients; ++i)
    // тут мы создали переменную i и присвоили ей значение 1 т.к. с него начинаются индексы игроков.
    // Мы помним что индекс игрока это число от 1 до максимального кол-ва слотов, которым и является MaxClients
    // этот цикл пройдется по всем числам от 1 до MaxClients (включительно)
    {
        // Тут начинается тело цикла
        // Сейчас i равно 1, в следущей итерации будет равно 2, и так дальше пока не станет равно MaxClients
        // Теперь нам нужно убедиться что текущий слот занят игроком
        if(IsClientInGame(i)) // Это очень важная операция и всегда обязательно должна быть первой в условии
        {
            // Проверкой выше мы убедились что i (индекс игрока) занят игроком, и с ним можно работать.
            // т.е. сейчас i это тот же iClient что был в предыдущих примерах.
            // Теперь нам нужно узнать что игрок жив и он в проигравшей команде:
            // GetClientTeam получает индекс команды игрока
            if(GetClientTeam(i) != iWinner && IsPlayerAlive(i))
            // GetClientTeam(i) != iWinner это "Индекс команды игрока не равен индексу победившей команды"
            {
                // Игроку не повезло, он в команде лузеров
                // Валим его
                ForcePlayerSuicide(i); // Убиваем игрока
            }
        }
        // Тут заканчивается тело цикла происходит инкремент i (те самые ++i, прописанные внутри for)
        // i становится на 1 больше и всё тело цикла выполняется с начала с новым значением i
    }
}

Думаю тут тоже не должно возникнуть сложностей.​
Просто всегда делайте так:​
C-подобный:
for(int i = 1; i <= MaxClients; ++i)
{
    if(IsClientInGame(i))
    {
        // Вот здесь вы точно значете что игрок в игре и можете с ним работать
        // Ваш код...
    }
}

О сущностях (entity, edict)
Сущности - это как бы объекты на сервере. Кнопки, триггреры, оружия, зоны закупки, точки спавнов, сами игроки, свет и прочее - это всё сущности (далее просто entity). Даже сам сервер это entity.​
Максимальное количество entity - 2048.​
0 - Сервер​
От 1 до MaxClients (включительно) - игроки​
От MaxClients до 2048 - entity.​
Работа с ними похожа на работу с игроками.​
Так же можно проверять валиден ли индекс сущности: IsValidEntity / IsValidEdict​
С ними можно проворачивать туже же фишку как с юзер ид игроков. Только у сущностей это называется ссылкой.​
  • EntIndexToEntRef - получает ссылку на сущность из её индекса
  • EntRefToEntIndex - получает индекс сущности из её ссылки. В случае провала, вернет INVALID_ENT_REFERENCE (0xFFFFFFFF). В API написано что лучше проверять на не равенство именной это константе, а не -1.
    В Wiki сказано что все функции принимающие как аргумент индекс сущности, могут принимать вместо него ссылку.
    С сущностями нужно работать осторожно т.к. у каждой есть свои свойства и нюансы работы с ними.
    Большинство из них уничтожаются сервером в конце раунда. Подробную информация о сущности можно найти здесь Category:Source Base Entities - Valve Developer Community
    Каждая сущность имеет свой класс (например: дверь (func_door, func_door_rotating), триггер (trigger_multiple), оружие (weapon_*), проп (prop_dynamic, prop_physics) и другие).
Как придет вдохновение еще дополню здесь.

Немного о строках.

Строка - это массив символов. Это значит что помимо обращения к строке, вы можете обращаться к определенным символам в ней.
Конец строки - это нулевой символ.
Т.е. плагин видит строку только до тех пор, пока не встречает в ней нулевой символ. Не '0', а '\0'.
Например:
C-подобный:
char szMyString[64] = "моя первая строка!";
Размер строки szMyString равен 64. Это значит что в неё можно записать максимально 64 символа (вместе с нулевым)
Длина строки равна 18. Нулевой символ не считается.
Есть несколько функций для работы со строками:
C-подобный:
sizeof(szMyString)
П
олучает размер массива (в данном случае строки). Это оператор пре-процессора. Это значит что эта функция вызывается еще на этапе компиляции плагина.
Т.е. компилятор заменит sizeof(szMyString) на strlen(szMyString) - Получает длину строки, т.е. кол-во символов до нулевого. В данном случае вернет 18

Очистка строки.
Я встречал много способов очистки строк:
  1. C-подобный:
    strcopy(szMyString, sizeof(szMyString), "");
  2. C-подобный:
    Format(szMyString, sizeof(szMyString), "");
  3. C-подобный:
    szMyString = "";
  4. C-подобный:
    szMyString[0] = '\0';
  5. C-подобный:
    szMyString[0] = 0;
Хоть их результат одинаковый скорость разная.
Мне больше всего нравится последний вариант (он идентичен предпоследнему). Хоть выглядит сложно, но на самом деле всё очень просто если понимать что происходит.
Мы знаем что конец строки там, где стоит нулевой символ.
Следовательно если 1-й символ строки нулевой - она пустая.
Индексы массивов начинаются с 0, а значит что 1-й символ имеет индекс 0.
Т.е. обращение к первому символу строки выглядит так: szMyString[0]
Так вот если присвоть 1-му символу строки значение 0, то строка будет считаться пустой.
C-подобный:
szMyString[0] = 0; // То же что и '\0'
Исходя из утверждений выше, так же можно и проверять пуста ли строка:
C-подобный:
if(szMyString[0] != 0) // Оно же if(szMyString[0] != '\0')
{
    // Строка не пустая
}
Или более сокращенный вариант:
C-подобный:
if(szMyString[0])
{
    // Строка не пустая
}
Это наиболее быстрые и простые способы очистки и провери строк (не вызывают внешних функций самого SM)

Если вам нужно образать строку после определенного символа можно просто установить его в 0.
C-подобный:
char szMyString[] = "Часть1|Часть2"; // Размер строки компилятор определит сам, т.к. скобки пустые. Это работает только если задано начальное значение
Что можно сделать с этой строкой?
Получить только "Часть1":
C-подобный:
int index = FindCharInString(szMyString, '|');
if(index != -1)
{
    szMyString[index] = 0;
}
Теперь с пояснениями:
C-подобный:
// Ф-я FindCharInString ищет символ в строке и возвращает его индекс. Если символ не найден вернет -1
int index = FindCharInString(szMyString, '|'); // Ищем в строке szMyString символ | и заносим результат в index
// Обратите внимание, что ф-я возвращает индекс первого вхождения символа в строку.
// Это значит что если в строке есть несколько таких символов то она вернет индекс первого
// Если нужно искать символ с конца строки то нужно использовать: FindCharInString(szMyString, '|', true)
// При этом индекс символа будет всё ровно с начала строки
if(index != -1) // Убеждаемся что символ найден
{
    // В даном случае index равен 6
    szMyString[index] = 0; // Устанаваливаем элемент с индексом записанным в index значение 0
    // Мы ж помним что в index записан индекс символа | т.е. 6
    // После всех этих действий szMyString будет равна "Часть1"
}
Получим только "Часть2":
C-подобный:
int index = FindCharInString(szMyString, '|');
if(index != -1)
{
    // До этого момента всё тоже самое.
    // А дальше происходит магия
    strcopy(szMyString, sizeof(szMyString), szMyString[index+1]);
    // Мы копируем в szMyString, её же но пропуская index символов.
}
Как же так?
Дело в том что компилятор понимает какой тип данных требует ф-я.
Если ей нужна строка то szMyString[index] будет передана с пропуском первых index-х символов. В нашем случае 6 (index)+1 (сам символ |) = 7. И того остается только "Часть2"
Если же ей нужен только 1 символ то будет передан только 7-й символ (т.к. с 0-я индексация) т.е. szMyString[7] = 'Ч'.

Это немного сложно по началу, но если разобраться то всё просто.
Вот пример:
Очень часто бывает нужно узнать что у игрока некоторое оружие в руках
C-подобный:
char szWeapon[32];
GetClientWeapon(iClient, szWeapon, sizeof(szWeapon)); // Получаем текущее оружие у игрока
// В szWeapon получили к примеру "weapon_deagle"
// Вы хотите узнать не "weapon_ak47" ли это
// Мы видим что начало "weapon_" у всех оружий совпадает, давайте пропустим его
if(StrEqual(szWeapon[7], "ak47")) // Мы пропускаем "weapon_" и сравниваем только "deagle" и "ak47". Таким образом сэкономив 7 итераций цикла сравнения
{
    // Это ak47
}

UPD #1
Немного о типах их преобразованиях.
Часто нужно конвертировать строку в число и наоборот, а так же числа в другие числовые типы (целые в дробные/логические и наоборот).
Как же это сделать?
Вот список ф-й, который помогут вам в этом:
  • StringToInt - Преобразует строку в целое число.
C-подобный:
char szValue[] = "457";

int iValue = StringToInt(szValue);
// iValue равно 457
Если в ф-ю передать не валидную строку (например просто текст или дробное число) - вернет 0.​
  • StringToIntEx - Аналогично StringToInt но вместо возврата результата, записывает его по указанному адресу.
C-подобный:
char szValue[] = "864";
int iValue;

StringToIntEx(szValue, iValue);
// iValue равно 864
C-подобный:
int iValue = 984864;

char szValue[64];
IntToString(iValue, szValue, sizeof(szValue);
// szValue равно "984864"
Что касается строк - этого хватит для решения большинства задач.​
Для того чтобы говорить о типах следует ввести 2 понятия:​
  • Преобразование в тип - наглядный пример со строками выше.
Это правильное преобразование значения одного типа к соответствующему значению другого типа.​
Например:​
C-подобный:
int iValue = 353;

float fValue = float(iValue);
// fValue равно 353.0
Здесь ф-я float преобразует целое число в дробное.​
Обратное преобразование:​
  • RoundFloat - Окгругляет дробное к ближайшему целому.
C-подобный:
float fValue = 65.33;

int iValue = RoundFloat(fValue);

// iValue равно 65 т.к. 65.33 ближе к 65 чем 66
  • RoundToCeil - Окгругляет дробное к ближайшему целому в большую сторону.
C-подобный:
float fValue = 65.33;

int iValue = RoundToCeil(fValue);

// iValue равно 66
[php]
[*][URL='https://sm.alliedmods.net/new-api/float/RoundToFloor']RoundToFloor[/URL] - Окгругляет дробное к ближайшему целому в меньшую сторону.
[php]

float fValue = 65.76;

int iValue = RoundToFloor(fValue);

// iValue равно 65
  • RoundToNearest - Окгругляет дробное к целому по стандарту IEEE.
  • RoundToZero - Окгругляет дробное до ближайшего целого числа до нуля (дословный перевод).
 

Shyper

Участник
Зарегистрированный
Регистрация
22.02.2021
Сообщения
14
Реакции
1
Баллы
8
Здравствуйте. Хотел бы телепортировать игроков на определенные координаты...
Как записать координаты в эту функцию? Например, [175, 157, 25]
TeleportEntity(tid, Координаты..., NULL_VECTOR, NULL_VECTOR);
 

inklesspen

Участник
Зарегистрированный
Регистрация
28.11.2020
Сообщения
8
Реакции
5
Баллы
13
Здравствуйте. Хотел бы телепортировать игроков на определенные координаты...
Как записать координаты в эту функцию? Например, [175, 157, 25]
TeleportEntity(tid, Координаты..., NULL_VECTOR, NULL_VECTOR);
Код:
TeleportEntity(tid, view_as<float>({175.0, 157.0, 25.0}, NULL_VECTOR, NULL_VECTOR);

view_as<float> нужен из-за того, что такие прямые массивы объявляются как int[], однако пишем числа именно с точкой, иначе и данные будут как int, когда как функция принимает массив float-данных
 
shape1
shape2
shape3
shape4
shape7
shape8
Верх