===== Туториал: 3D/VR многопользовательский режим =====
Концепция логических машин платформы Лямбда-Мю 3 позволяет с легкостью решать задачи работы с многими пользователями. При этом клиенты могут по-разному визуализировать данные с платформы, но следуют одному протоколу. Например, 3D или VR вариант одной комнаты.
Разберем взаимодействие платформы с несколькими клиентами.
==== Пример реализации ====
Виртуализация при обучении помогает достигнуть целей обучения быстрее, дешевле, безопаснее и при необходимости удаленно. Рассмотрим проект обучения обслуживания серверов: преподаватель и учащиеся подключаются в одну виртуальную комнату - серверную. В режиме 3D или VR. Выполняют сценарии обучения или следят за другими пользователями.
==== Репозитарий ====
Файловая структура проекта представлена ниже.
Необходимые скрипты туториала находятся в репозитарии [[https://gitlab.com/lambda-mu/lm.ver3/lm3.examples/godot_multiplayer|lm3.examples/godot_multiplayer]].
Чтобы запустить платформу скачайте бинарный файл и необходимые библиотеки из репозитария [[https://gitlab.com/lambda-mu/lm.ver3/lm3.engine.ce|lm3.engine.ce]] и скопируйте в корень проекта.
==== Исходные данные ====
* Сцена серверной импортированная в Godot Engine
* Запрограммированное управление виртуальным персонажем в режиме 3D и VR
* Запрограммированный модуль связи lm3 и протокол передачи данных
==== Цель проекта ====
Разбор взаимодействия платформы Лямбда-Мю 3 с клиентами в многопользовательских 3D и VR режимах:
* авторизация
* синхронизация данных (положения в пространстве)
==== Разбор исходных данных ====
Разработка модуля связи и протокола передачи данных для Godot Engine тема отдельного туториала. Однако, для понимания необходимо отметить общую концепцию обработки данных при приеме от платформы и перед отправкой на платформу.
В Godot Engine доступны сигналы, которые могут исходить от объектов или скриптов. При разработке в паре с платформой lm3 крайне удобно воспользоватся сигналами и перенести ивентную логику.
Модуль связи lm3 (скрипт автозагрузки) выполняет следующие функции:
* подключение к серверу,
* отслеживание состояния подключения,
* получение данных, распаковка, излучение соответствующего сигнала,
* буферизация данных на отправку, запаковка, отправка данных.
Каждому объекту взаимодействующему с платформой привязывается скрипт. За каждом скриптом (объекта, сцены или автозагрузки) закрепляется имя - имя объекта. Протокол взаимодействия должен при этом быть картой <Имя Объекта> : <Данные> или <Имя Объекта> : <<Имя Атрибута> : <Данные>>. Для каждого сприпта необходимо описать обработчики прием и отправки данных.
Если придерживаться данной концепции, то при разработки больших проектов всегда можно проследить поток данных и быстро отладить код.
==== Протокол запросов и ответов ====
Ответ при подключении - информация о всех пользователях:
{ : { "type" : }, ...}
---
Ответ (всем пользователям кроме данного) при отключении:
{ : { "type" : "NA" } }
---
Запрос присвоения типа (3D / VR):
{ "clients" = { "type" : } }
Ответ (всем пользователям кроме данного):
{ : { "type" : } }
---
Запрос присвоения положения в пространстве:
{ "telemetry" = }
Ответ (всем пользователям кроме данного):
{ = }
* различается для 3D и VR
==== Файловая структура проекта ====
{{ :lm3:ce:godot_mp_files.png?direct |}}
==== Взаимосвязи ====
Объекты основной логической машины:
* TCP-server server: прием и передача данных клиенту
* DataPack packer: распаковка и запаковка данных клиенту
* Clients clients: взаимодействие клиентов
Диаграмма связей:
{{ :lm3:ce:godot_mp_lm.png?direct |}}
==== Скрипты ====
**main / start.lua**
lm3:Include("mp_lib.lua")
lm3:LoadLibrary( {Alias = "sys", FileName = "lm3system"} )
lm3:CreateLObject( { LibAlias = "sys", Type = "TCPServer", Name = "server" } )
server:SetAttr("SocCount", lmconf.MaxClientCount)
server:SetAttr("Port", lmconf.TCPVRPort)
lm3:CreateLObject( { LibAlias = "sys", Type = "DataPack", Name = "packer" } )
packer:SetAttr("BufCount", lmconf.MaxClientCount)
init_Clients("clients")
init_Telemetry("telemetry")
-- связи объектов
server:Connect("OnConnect", "clients", "DoConnect")
server:Connect("OnDisconnect", "clients", "DoDisconnect")
server:Connect("OnIncoming", "packer", "DoUnpack")
packer:Connect("OnPack", "server", "DoSend")
clients:Connect("OnSend", "packer", "DoPackTagged")
-- распределим данные
packer:AddHandler("OnUnpack", function(o, ev)
for obj_name, obj_data in pairs(ev.Data) do
if obj_name == "clients" then
for attr_name, attr_value in pairs(obj_data) do
if attr_name == "Type" then
clients:Type( { Tag = ev.Tag, Type = attr_value} )
end
end
elseif obj_name == "telemetry" then
clients:Telemetry( {Tag = ev.Tag, Data = obj_data} )
end
end
end)
**main / mp_lib.lua**
function init_Clients(_name)
local obj = lm3:CreateObject( { Type = "Base", Name = _name } )
-- структура данных о клиентах
obj.ClientList = {}
for i = 1, lmconf.MaxClientCount do
obj.ClientList[i] = {
isConnected = false,
Type = ""
}
end
-- подключение клиента
obj:AddInEvent("DoConnect", "int", function(o, ev)
lm3:Log("New client connected:"..tostring(ev))
obj.ClientList[ev].isConnected = true
obj.ClientList[ev].Type = "NA"
local sendData = { }
local needSend = false
for i = 1, lmconf.MaxClientCount do
if i ~= ev and obj.ClientList[i].isConnected == true then
sendData["Player"..tostring(i)] = { Type = obj.ClientList[i].Type }
needSend = true
end
end
if needSend == true then
obj:OnSend( { Tag = ev, Data = sendData } )
end
end)
-- отключение клиента
obj:AddInEvent("DoDisconnect", "int", function(o,ev)
lm3:Log("Client disconnected:"..tostring(ev))
obj.ClientList[ev].isConnected = false
local sendData = { }
sendData["Player"..tostring(ev)] = { Type = "NA" }
for i = 1, lmconf.MaxClientCount do
if obj.ClientList[i].isConnected == true then
obj:OnSend( { Tag = i, Data = sendData } )
end
end
end)
-- присвоение типа
obj:AddInEvent("Type", "table[Tag:int,Type:string]", function(o, ev)
if obj.ClientList[ev.Tag].isConnected == true then
obj.ClientList[ev.Tag].Type = ev.Type
local sendData = { }
sendData["Player"..tostring(ev.Tag)] = { Type = ev.Type }
-- lm3:Log("Set player type:"..lm3:DataToString(sendData))
for i = 1, lmconf.MaxClientCount do
if i ~= ev.Tag and obj.ClientList[i].isConnected == true then
obj:OnSend( { Tag = i, Data = sendData } )
end
end
end
end)
-- присвоение положения
obj:AddInEvent("Telemetry", "table[Tag:int,Data:any]", function(o, ev)
local sendData = {}
sendData["Player"..ev.Tag] = ev.Data
for i = 1, lmconf.MaxClientCount do
if i ~= ev.Tag and obj.ClientList[i].isConnected == true then
obj:OnSend( { Tag = i, Data = sendData } )
end
end
end)
obj:AddOutEvent("OnSend", "table[Tag:int,Data:any]")
return obj
end
function init_Telemetry(_name)
local obj = lm3:CreateObject( { Type = "Base", Name = _name } )
return obj
end
==== Скриншоты ====
С пультом управления:
{{ :lm3:ce:godot_mp_test1.png?direct&400 |}}
Без:
{{ :lm3:ce:godot_mp_test2.png?direct&400 |}}