Вводная

Добро пожаловать в увлекательный мир по разработке онлайн игр.

Наш сервис поможет вам добавить в Ваши игры мультиплеер режима реального времени, при этом на чем сделана игра – совершенно не важно (Unity, Unreal Engine , Godot , Phaser2d и др).

Мы стремимся сделать пользование сервисом легким и удобным, справочные материалы так же доступны в админ панели на каждой странице по ссылке Помощь (в правом верхнем углу)


Всего в игровом мире существуют несколько типов объектов : игроки (Players), враги (Enemys), животные/npc (Animals) и объекты (Object). Такие объекты называются сущности (Entitys)

Сервис работает по принципу REST API – вы отправляете TCP команды на сервер в формате JSON, они ставятся в очередь и выполнятся по очереди вместе с командами других сущностей. Эти команды называются события (Events) и содержат игровую логику

Каждое событие должно иметь таймаут. Принято считать что человеческий глаз воспринимает 0.03 секунды скорость объекта (30 FPS - кадров в секунду), например можно сделать паузу между событием - движение 0.2 секунды, проигрывая анимацию на клиенте как персонаж делает шаг за это время, разу высылая повторную команду на следующий шаг (она подставится в очередь за первой и пока анимация играет отработает и пакет успеет дойти до сервера и обратной, подробнее ниже в разделе интерполяция )

События благодаря своей логике меняют координаты и вспомогательные поля сущностей . Такие поля называются компоненты (Components).

Компонентным полем могут быть жизни, мана, опыт и более сложные (например, книга заклинаний, группирующая несколько дополнительных полей).

Когда в игровом мире меняются значений полей сущности – сервер отправляет массив сущностей с полями которые изменились в формате JSON всем подключенным игрокам

Наш сервис предоставляет панель администрирования, где вы сами можете добавить необходимые именно Вашей игре компоненты, события (с программным), так же дает возможность отредактировать значения полей сущностей, добавить игровые карты, смотреть фаилы логирования, добавлять музыку и многое другое

Каждая карта в игре – отдельный сервер. На одной карте могут играть свыше 500 игроков.

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

Часть 1 . WebSocket

Что бы игроки могли отправлять и получать команды с сервера необходимо установить постоянное подключение к серверу по протоколу websocket. На нашем сайте вы можете найти исходники игр на популярных платформах где уже реализован образец как это сделать. Адрес сервера так же указан на нашем сайте


Часть 2. Авторизация по HTTP

Перед тем как установить соединение WebSocket с сервером необходимо получить одноразовые токен (token). Этот токен выдается после того как будет пройдена авторизация с логином и паролем игрока. Для этого необходимо отправить обычный запрос через интернет по протоколу http с POST с ['login'=><логин>, 'password'=><пароль>] на адрес /server/sign/auth/ (ip самого сервера указано тут ). Примеры так же есть на нашем сайте для популярных платформ (например для Unity). Результатом запроса будет ответ http в формате JSON с :

  • Token – одноразовый токен для установки websoket соединения
  • Port - Порт сервера для карты на которой сейчас Ваш игрок
  • Id – уникальный идентификатор игрока

После получения данных можно открыть соединение WebSocket на адрес (параметр HTTP_AUTHORIZATION кодированная в base6 строка id и token разделенная знаком : для авторизации), пример для Java Script

ws://" + server + ":" + port/?HTTP_AUTHORIZATION=" + btoa(id + ':' + token)
После установки соединения будет выслан результат запроса из ч 3 - массив игрового мира (мир целиком можно запрашивать повторно, тк после в процессе игры на клиент по WebSocket будут приходить только изменения).

Часть 3. Получение игрового мира

В нашем сервисе уже заложена часть системных событий. События объединяются в группы и имею вид группа/команда. В одной группе может быть множество команд с уникальным названием. Одна из таких команд называется load/index – результат этой команды будет отправлен сразу после авторизации.

Результатом исполнения этой команды будет массив в формате json всех сущностей со всеми полями и сервер начнет высылать только изменения сущностей и отправлять новые если они будут появляться в процессе игр.


Формат ответа следующий:

{
    "action": "load/index",
    "players": [{
        "prefab": "Player",
        "side": "down",
        "action": "move",
        "sort": 0,
        "x": -153,
        "y": 56,
        "z": 0,
        "speed": 3,
        "id": 24,
        "key": "players_24",
        "type": "players",
        "map_id": 4,
        "created": "2022-11-10 18:53:53",
        "components": {
            "hp": 5,
            ...
        },
        "sides": {
             "center": номер главной карты, 
             "left": номер карты слева, 
              ...
        },
        "events": {
            "save": {
                "time": 1670069589.9,
                "action": "index",
                "data": null,
                "command_id": null
            },
           ...
        },
    }],
    "enemys": [{
        ...
    }],
    "animals": [{
        ...
    }],
    "objects": [{
        ...
    }]
}


После данную команду можно высылать повторно при необходимом обновлении игрового мира - актуально для браузерных игр при переключении игроком по вкладкам в браузере (рекомендуется запретить отправку и получение иных данных до момента получения ответа).

 Поле sides - отвечает за открытый мир. Что бы получить все карты необходимо отправить запрос на сервер с позицией карты которую необходимо получить (желательно кешировать данный запрос)

Часть 4. Обновление данных в клиенте игры

Как уже было сказано выше – при изменении данных сущностей сервер отправляет сообщение в виде массива, где ключами является тип сущности (players, enemys, animals, objects) и вложенный массив – сами сущности которые изменились и их компоненты. Каждая сообщение имеет идентификатор сущности id . Иногда полезно описать какие поля ожидаются от сервера: пример как это сделано в C# для Unity

Изменение значений компонентов сущности – задача событий . Вы можете создать неограниченное количество компонентов в админ панели к которым необходимо написать код на языке программирования – Lua . Любой игровой процесс - это манипуляция с данными сущностями .

Наш сервис уже имеет образцы событий для популярных механик. Вы можете изменить их код или написать свои

Часть 5. События по умолчанию

Для того что бы события могли исполняться их нужно повесить на сущность. Для этого в админ панели предусмотрена возможность добавить свой код LUA который будет выполняться когда указанная сущность добавляется в игру. С помощью него можно добавить необходимые события. О нем пойдет речь ниже

Часть 6. Системные поля сущностей

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

  • id – идентификатор сущности в базе данных (может совпадать с id других сущностей)
  • type – тип сущности (players, enemys, animals, objects)
  • key – уникальный текстовый идентификатор сущности на карте. Выглядит как id_тип сущности
  • map_id – текущая карта. Если в процессе игры она меняется – объект удаляется из игры, приходит ответ от сервера с action = remove на данную сущность
  • prefab – текстовое поле. Рекомендуется использовать как название графического пакета сущности
  • x – координата по оси х
  • y – координата по оси y
  • z – координата по оси z
  • sort – сортировка объекта (в некоторых случаях аналог z)
  • side - сторона в которую смотри наш персонаж (полезно связывать с другими игровыми действиями которые будут выполнятся в указанную сторону, например - атака)
  • action – текстовое поле. Рекомендуется использовать как название текущей анимации при разработки серверных игровых механик (например при движении игрока или при отталкивании придут лишь новые координаты, поле action уточнить каким образом они были сменены, а с полем side можно определить направление)
  • components – массив компонентов. Все компоненты являются элементами этого массива. Где ключ – название компонента. Редактируется через админ панель
  • events – массив событий сущности на находящихся исполнении. Полезно для анимации в игре. 
  • login – имя пользователя игрока. Только у сущности - игроки

Часть 7. Админ панель - сервера

Первое что нужно знать и о чем пойдет информация ниже - раздел Сервера. Каждая карта - это отдельный сервер. Можно переходить с сервера на сервер как с локации на локацию меняя системное поле map_id сущности (о котором шла речь в части 6)  через код LUA (о котором пойдет речь в следующем разделе). Важным в этом разделе  - это логи и ошибки. Если что то было сделано не так в коде который вы добавляете вы сможете найти и отладить через этот радел. 



Второй важной функцией являются логи производительности. Всегда замеряйте производительность ДО и ПОСЛЕ добавления любого кода тк неправильно написанный код может существенно затормозить вашу игру. В данных логах указаны скорость работы событий в миллисекундах (1 секунда = 1000 миллисекунд) и количество кадров сервера (хоть сервер - не визуальная составляющая у него так же есть кадр за который отрабатывает серверная логика обработчик всех сущностей на карте, их события, рассылка игрокам данных) FPS (перевод: кадров в секунду) . Максимальный показатель FPS способный выдать сервер вы можете найти на странице API однако считается что хорошим показателем в играх жанра action является цифра 60 FPS а минимальным порогом для анимационных игр 30FPS


Часть 8. Добавление пользовательского кода Lua 

Наш сервис предоставляет функционал управления игровым миром и функционал мультиплеера, разработчику игр не нужно думать о том как передаются пакеты, рассылаются между другими игроками. Однако что бы описать саму игровую логику , действие NPC и возможные команды игрока с их функциями - нужно прибегнуть к добавлению кода в наш сервис (тк игры разные, все уникальны и создавать самим все варианты команд пользователей всех типов игр невозможно). Таким кодом является LUA (хорошая статья по изучению Lua за 15 минут).

Мы не будем останавливаться в этом руководстве на описании синтаксиса языка, а остановимся на необходимых данных и примерах. Условимся о следующем:

  • Используется версия Lua 5.1
  • таблицы в Lua - это массив / объект в других языках
  • nil в Lua это null в других языках
Запрещены следующие функции LUA из соображения безопасности:

  • dofile(), loadfile() и пакет io
  • package, включая require() и module()
  • load() и loadstring()
  • os кроме os.clock(), os.date(), os.difftime() и os.time()
  • debug кроме debug.traceback()
  • string.dump

Изменены следующие функции
  • print(text) в LUA теперь сбрасывает информацию в лог доступный в раздел Сервера
  • pcall() и xpcall() не может отловить определённые ошибки, особенно ошибки времени ожидания.
  • tostring() не включает адреса указателей.
  • string.match() была исправлена для ограничения глубины рекурсии и периодической проверки времени ожидания.
  • math.random() и math.randomseed() заменяются версиями, которые не разделяют состояние
  • __pairsи __ipairs bo LUA 5.2 - поддерживаются
  • поддерживается реализация мета тега __next (если он указан для таблицы - используется он для цикла как и тег __pairs)

Добавлены следующие глобальные функции
  • error(text) - сбрасывает в лог ошибки (доступен в разделе Сервера) с трассировкой в каком месте кода LUA вызван 
  • microtime() - для более точного получения UnixTimestamp времени секунд с микросекундами (4й знак после точки)
  • keys(table) - возвращает таблицу ключей таблицы
  • split(string, separator) - возвращает таблицу элементами являются части строки разбитые по разделителю 

Lua код можно добавить в следующих случаях:

1. Изменение значения компонента сущности (триггер) . Возврата значений не требуется однако если код вернет return - значение будет записано в логи ошибок сервера указанной локации (может быть полезно для отладки)


2. Добавления событий (они же пользовательские команды если сделать их публичными) . В нашем сервисе есть встроенные в сервер события. Они написаны на другом языке (PHP) но очень схожем для того что бы их переопределить при необходимости. Возврата значений так же не требуется.

Внимательный взгляд может заметить что в этом событие есть параметр life и если бы событие было бы публичным - пользователь зная куда и какие отправляются команды . минуя логику клиентского приложения (самой игры, не сервера) отправить на регенерацию произвольное число жизней. Однако это не публичное событие и его можно добавить только из админ панели сервера


3. Код зарабатываемый когда сущность заходит в игру (меняется в соответствующем разделе сущности) . В основном что бы повесить события на сущность, можно манипулировать использую if - else конструкцию из данных сущности object



4. Описания таймаута группы событий (может быть фиксированным числом, может иметь некую логику зависящую от события или значения компонентов и окружающего мира но по итогу ожидается так же цифра что берется при каждом исполнении событии группы).




Часть 9. Доступ ко всем сущностям локации через LUA

Для того что бы получить все доступные сущности на странице доступна глобальная таблица (так называются массивы в LUA ) objects - в не находятся под таблицы , каждый ключ которой это системное поле key (ключ) сущности - конкретная сущность. Вы можете получить ее циклом перебора таблицы objects стандартными методами LUA pairs и next, а так же обратившись напрямую по objects[ключ], однако value - это всегда копия объекта и если это таблица все его свойства - будут ссылками. Пример

for key, value in pairs(objects) do
	value = nil						    -- не сработает тк value - копия
	objects[key] = nil					-- сработает, уничтожит объект
	objects[key].remove()				-- сработает, копии value доступны все функции влияющие на оригинал
	objects[key].position()             -- сработает, вернет координаты в виде строки:  x,y,z
    
    if objects[key].type == 'players' then    -- только игрокам можно отправлять пакеты с данными
        objects[key].send({key = 'value'})    -- отправлять можно только таблицы . есть поддержка вложенных подтаблиц. при изменений сущностей всем игрокам отправятся изменения автоматически
	end
				
	value.x = 160						    -- сработает, x - это ссылка на свойство таблицы оригинала
	objects[key].x = 160					-- аналогично
	objects[key]['x'] = 160					-- идентично записи выше для LUA 

	for key2, component in pairs(v['components']) do	
		component = 1					            -- не сработает. это уже не ссылка а копия свойства оригинальной таблицы		
        value['components'][key] =  1			    -- сработает тк мы обращаемся к копии таблицы свойство которое ссылается на оригинал
	end
end

Так же сам objects имеет следующие встроенные функции

objects.add('тип сущности на выбор: enemys, animals, objects', 'данные в виде таблицы')   -- добавит в игровой мир новую сущность или добавит в лог ошибку если не верно введены данные. players добавлять нельзя
object.remove('поле key сущности')                                                        -- удалит сущность по аналогии с одноимённой функцией доступной уже в самой сущности		  


Поля каждой конкретной сущности описаны в ч.6 . Пример добавления на карту сущности:

local entity = 
{
	prefab = 'goblin',
	x = 0,                -- если координаты ниже отсутствуют или не доступны в карте - будет указана случайная доступная координата карты
	y = 0,
	z = 0
}

objects.add('enemys', entity)   

По результату команды выше в клиент вернется массив новой сущности в формате JSON . В клиенте вы можете держать весь игровой мир в неком массиве (зависит на какой платформе сделана ваша игра) и при поступлении новой сущности отсутствующей ранее - добить ее на игровой экран (после полученные данные уже существующей сущности могут только обновлять тк далее будут приходить только ее изменения за исключением вызова события load/index - оно вернет все сущности карты целиком по новой)



Помимо глобальной таблицы objects для кода LUA из части 8 пунктов 1,2,3 доступна таблица object (текущая сущность на которой выполняется код LUA).
Поля кроме components, events, parameters (тк это вложенные таблицы и о них пойдет речь ниже) можно менять просто присваивая новые значения (цифры и текст).Так же имеются дополнительные функции работы с сущностью

Пример:

for k,object in pairs(objects) do
	object.x=160					        -- присвоим значение x (аналогично если написать v['x']=160)    
	local position = object .position()     -- вернет координаты в виде строки:  x,y,z	
	object  = nil					        -- не даст эффекта тк object является копией таблицы хоть и содержит свойства в виде ссылок на оригинальную	
	object.remove()      		            -- удалит объект (аналогично если написать objects[k] = nil)
end

При изменении любых данных любой сущности всем игрокам на карте отправятся изменения. Если вы не хотите отправлять другим (только себе) часть компонентов укажите это в админ панели




Часть 10. Работа с компонентами сущности

В каждой сущность как было описано выше в ч.6 есть таблица components. Доступна для перебора значений стандартными методами LUA pairs и next. Отсутствуют встроенные функции тк нельзя добавить или удалить без админ панели новые компоненты, а значения существующих можно менять обычным присваиванием значений того типа который указан в админ панели а так же nil . Если в админ панели указан тип поля JSON - вы задаете его значение в JSON (это может быть многомерный массив в тч.) но в игре он превратится в таблицу (тк JSON это текстовое представление массива, они же называются таблицами в LUA )



Часть 11. Работа с событиями сущности

В каждой сущность как было описано выше в ч.6 есть таблица events. Доступна для перебора значений стандартными методами LUA pairs и next где ключ - название группы события, содержит функции функции:

object.events.add('группа', 'событие', 'доп. параметры в виде таблицы')   -- повесить на текущую сущность (или любую другую из массива objects) новое событие. параметры - таблица необязательных объявленных через админ панель доп параметров
object.events.remove('название группы')                                   -- обнулит текущее событие группы если оно есть на сущности (тк в группе только одно событие может выполняться одновременно, например движение в определенную сторону, нет нужды указывать какое именно )

Каждое конкретное событие имеет таблицу следующей структуры:

{
   action:	     событие на выполнение, может быть null (при добавлении если не указано имеет значение index что полезно когда в группе одно событие),
   data:             данные события (опционально),
   command_id:	     если событие послал пользователь должно содержать unixTimestamp ее отправки (нужно для расчета пинг и интерполяции),
   time:			время в формате Unixtimestamp когда закончится таймаут (остаётся при обнулении события, но можно сбросить функцией resetTime)
}	

С указанными полями разрешено работать напрямую меня (без вспомогательных функций remove и resetTime), пример

for key, value in pairs(objects) do
   for event_group, event in pairs(v['events']) do	     -- по умолчанию в цикле будут ВСЕ доступные события (тк иногда нужно получить таймаут события которое еще не было создано через add)
      if event.action ~=nil                              -- так мы отфильтруем только события которые в данный момент помещены на сущности в очереди на исполнение
         event = nil                                     -- не сработает, тк event это копия таблицы свойства которой являются ссылками на оригинал (подробнее в части 9)
         event.remove()                                  -- сработает, более вызваться не будет, но останется в массиве v['events'] и обнулит поле event.action, event.data и event.command_id
         event.time = nil					             -- вызовет ошибку - только целое или дробное число доступно
         event.time = 0                                  -- вызовется на следующем кадре тк 0 меньше любого UnixTimestam		event.action =  nil			                    -- вызовет ошибку - только строки доступны и только существующие события группы событий
         event.action = 'exist_event'                    -- сработает но обнулит поле event.data и event.command_id если уже было в группе событие на исполнение в очереди
         event.command_id = 123                          -- вызовет ошибку - нельзя менять UnixTimestamp который формируется в клиентской части игры и отправляется на сервер 
         event.data = nil                                -- вызовет ошибку - только таблицы доступны. для обнуления параметров установите {} (если предполагаются данные для события - будут применены стандартные значения или вызовется ошибка ) 	end
      end
   end
end


И доступные функции 
object.events['название группы события'].remove()                          -- аналог прямого обнуления (object.events[название группы события]  = nil   )  обнулит поля action, data, command_id
object.events['название группы события'].timeout()                         -- вернет сколько секунд (целое или с плавающей точкой число) длится таймаут события (может быть дробным, код описывающий сколько и по каким критериям делать паузу между событиями задается в админ панели кодом LUA). По умолчанию 1 секунда
object.events['название группы события'].resetTime(seconds)                -- сбросит таймаут на текущее время. поле seconds - секунды (опционально. может быть дробным числом) 	


Пример :

objects['players_24'].events.add('resurrect', 'index', {'life':2});       -- добавим событие воскрешение с доп. параметром life = 2 (если они добавлены в админ панели как на скриншоте ч 8 п 2)
objects['players_24'].events['resurrect'].resetTimeout();                 -- следующая регенерация будет с момента воскрешение (сбросим время)
objects['players_24'].events['move']  = nil                               -- если мы куда то шли то в момент воскрешения  - остановимся, аналог object.events['move'].remove()	

Событие помещается в очередь и вызывается как только придет время таймаута с последнего вызова в данной группе события (или сразу если добавлено впервые). Если событие публичное - его можно вызвать из клиента. Если внутри кода LUA событие оно не добавлено вновь через add - более оно выполняться не будет. Только первое добавленное событие выполняется сразу (в следующем кадре), все остальные - когда поле event.time будет равен текущему UnixTimestamp (для сравнение с текущим используйте встроенную глобальную функцию microtime() что вернет серверное UnixTimestamp. Стандартные функции времени LUA такие как os.time - доступны но возвращают UnixTimestamp меньшей точности, до 1 знака после запятой)

События добавляются через админ панель. Обязательно должны содержать код lua в админ панели который описывает что оно должно делать с объектом или окружающим миром вокруг.
После срабатывания события оно удаляется с сущности (но в самом коде события можно повесить его снова).

В одной группе событий может быть несколько событий с уникальными именами, они имеют общий таймаут (который настраивается на группу), примеры






Часть 12. Интеграция Unity WebGl с панелью отладки

Для версии WebGL Unity добавьте/измените шаблон добавив следующий код после существующего в шаблоне кода then((unityInstance) => {

		  // для отладки в админке 
		  setInterval(() => 
		  {
			if(window.parent.document.querySelector('#map_id') && window.parent.document.querySelector('#map_id').value)
			{
				window.parent.document.querySelector('#perfomance').innerHTML = '';
				fetch('/server/api/log_perfomance/'+window.parent.document.querySelector('#map_id').value).then((response) => response.text()).then((data) => window.parent.document.querySelector('#perfomance').innerHTML = data);
			}
		  }, 10000);
		  
		  window.parent.document.querySelector('#unity-api-container').addEventListener('submit', function(e) 
		  {
			e.preventDefault();
			const data = new FormData(e.target);
			let value = Object.fromEntries(data.entries());
			value.command_id = Date.now();
			unityInstance.SendMessage('Camera', 'Api', JSON.stringify(value));
		  });

		  // не работае нормально потеря фокуса в unity webgl поэтому ставим заплатку
		  window.parent.document.addEventListener('click', function(e) 
		  {
			unityInstance.SendMessage('Camera', 'Focus', +(e.target.id == "unity-canvas"));
		  });
	
И установите в настройках этот шаблон по умолчанию




Что нового?


Статьи про разработку продукта

Intro Image

В этой части серии статей про разработку сервера для реалтайм игр я расскажу вам о показателе сетевого устройства которое значительно влияет на количество запросов, которое способен обработать ваш игровой сервер.

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

В конце статьи будет приложена видео версия.

Читать далее

Кабинет

Игры