Добро пожаловать в увлекательный мир по разработке онлайн игр.
Наш сервис поможет вам добавить в Ваши игры мультиплеер режима реального времени, при этом на чем сделана игра – совершенно не важно (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 игроков.
Такой подход снимает нагрузку с программиста игр , дает возможность делать менять поведение объектов , менять игровой мир редакторами через админ панель без изменение самой игры, делать игры с разной графикой и анимацией на базе уже созданных событий и компонентов
Что бы игроки могли отправлять и получать команды с сервера необходимо установить постоянное подключение к серверу по протоколу websocket. На нашем сайте вы можете найти исходники игр на популярных платформах где уже реализован образец как это сделать. Адрес сервера так же указан на нашем сайте
Перед тем как установить соединение WebSocket с сервером необходимо получить одноразовые токен (token
). Этот токен выдается после того как будет пройдена авторизация с логином и паролем игрока. Для этого необходимо отправить обычный запрос через интернет по протоколу http с POST с ['login'=><логин>, 'password'=><пароль>]
на адрес /server/sign/auth/
(ip самого сервера указано тут ). Примеры так же есть на нашем сайте для популярных платформ (например для Unity). Результатом запроса будет ответ http в формате JSON с :
После получения данных можно открыть соединение WebSocket на адрес (параметр HTTP_AUTHORIZATION кодированная в base6 строка id и token разделенная знаком :
для авторизации), пример для Java Script
ws://" + server + ":" + port/?HTTP_AUTHORIZATION=" + btoa(id + ':' + token)
В нашем сервисе уже заложена часть системных событий. События объединяются в группы и имею вид группа/команда. В одной группе может быть множество команд с уникальным названием. Одна из таких команд называется 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 - отвечает за открытый мир. Что бы получить все карты необходимо отправить запрос на сервер с позицией карты которую необходимо получить (желательно кешировать данный запрос)
Как уже было сказано выше – при изменении данных сущностей сервер отправляет сообщение в виде массива, где ключами является тип сущности (players, enemys, animals, objects) и вложенный массив – сами сущности которые изменились и их компоненты. Каждая сообщение имеет идентификатор сущности id . Иногда полезно описать какие поля ожидаются от сервера: пример как это сделано в C# для Unity
Изменение значений компонентов сущности – задача событий . Вы можете создать неограниченное количество компонентов в админ панели к которым необходимо написать код на языке программирования – Lua . Любой игровой процесс - это манипуляция с данными сущностями .
Наш сервис уже имеет образцы событий для популярных механик. Вы можете изменить их код или написать свои
Для того что бы события могли исполняться их нужно повесить на сущность. Для этого в админ панели предусмотрена возможность добавить свой код LUA который будет выполняться когда указанная сущность добавляется в игру. С помощью него можно добавить необходимые события. О нем пойдет речь ниже
Поля компонентов можно добавить в неограниченном количестве , как и вовсе не иметь ни одного. Однако имеются системные поля сущностей которые есть всегда и их нельзя удалить. Список этих полей:
nil
в Lua это null в других языках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)
- возвращает таблицу элементами являются части строки разбитые по разделителю 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.add('тип сущности на выбор: enemys, animals, objects', 'данные в виде таблицы') -- добавит в игровой мир новую сущность или добавит в лог ошибку если не верно введены данные. players добавлять нельзя
object.remove('поле key сущности') -- удалит сущность по аналогии с одноимённой функцией доступной уже в самой сущности
local entity = { prefab = 'goblin', x = 0, -- если координаты ниже отсутствуют или не доступны в карте - будет указана случайная доступная координата карты y = 0, z = 0 } objects.add('enemys', entity)
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
object.events.add('группа', 'событие', 'доп. параметры в виде таблицы') -- повесить на текущую сущность (или любую другую из массива objects) новое событие. параметры - таблица необязательных объявленных через админ панель доп параметров object.events.remove('название группы') -- обнулит текущее событие группы если оно есть на сущности (тк в группе только одно событие может выполняться одновременно, например движение в определенную сторону, нет нужды указывать какое именно )
{ action: событие на выполнение, может быть null (при добавлении если не указано имеет значение index что полезно когда в группе одно событие), data: данные события (опционально), command_id: если событие послал пользователь должно содержать unixTimestamp ее отправки (нужно для расчета пинг и интерполяции), time: время в формате Unixtimestamp когда закончится таймаут (остаётся при обнулении события, но можно сбросить функцией 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()
Для версии 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")); });