Повествование будет несколько сумбурное. так как оно делается пост фактум.
Начну с того, что решил переделать считалку (арифметический тренажёр) с Excel-евского макроса под Roblox. И вот тут выяснилось — что пользовательский интерфейс, это совсем не то же самое что серверное приложение. Как и то, что один игрок, это совсем не то же самое что мультиплеер.
По большому счёту можно конечно извращаться и через сервер менять пользовательский интерфейс (что в большинстве я и делал), но в итоге спотыкаемся на том, что действия пользователя ну никак не отследить сервером.
И тут на меня снизошло прозрение в виде LocalScript, с которыми ранее уже знакомились. Они то как раз работают исключительно на стороне пользователя! И тут вторая подножка — они не видят что творится на сервере!
Долго извращался и так и эдак, чтобы остановиться на каком нибудь приемлемом варианте, но в итоге «выкрутился» через одну функцию — удалённый запуск функции на сервере по запросу с клиента с параметром. И… вместо передачи клиенту переменные, операцию и результата сверки.. передаю ничего! Вывожу ему на экран пример в готовом виде и ожидаю вызова серверной функции с параметров в котором и есть ответ набранный пользаком.
https://developer.roblox.com/en-us/articles/Remote-Functions-and-Events/index.html
Причём на сам клиент я ничего не получаю, а с клиента получаю исключительно введённые подряд циферки (через LocalScript).
Скрипт, отслеживающий нажатия клавиш, ниже:
local ReplicatedStorage = game:GetService("ReplicatedStorage") local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest") print("кейлогер запущен") local txt="" local res -- коды клавиш тут -- https://developer.roblox.com/en-us/api-reference/property/InputObject/KeyCode function onKeyPress(inputObject, gameProcessedEvent) -- print(inputObject.UserInputType) -- Enum.UserInputType.Keyboard -- клавиатура if inputObject.KeyCode == Enum.KeyCode.Return or inputObject.KeyCode == Enum.KeyCode.KeypadEnter then script.Parent.Parent.PlayerGui.Main.Primer.Text="" -- передаём значение на сервер, если оно не нулевое!!! --print("Enter was pressed") res = remoteFunction:InvokeServer(txt) -- print("ответ сервера:",res) txt="" elseif inputObject.KeyCode == Enum.KeyCode.Backspace or inputObject.KeyCode == Enum.KeyCode.Delete then -- стираем полностью ввод --print("Backspace was pressed") txt="" else -- добавляем цифру к введёному local code = inputObject.KeyCode if code.Value>47 and code.Value < 58 then -- print("Нажата клавиша: ", code) local num=code.Value-48 --print(num, code.Name, code.Value) txt=txt .. num end if code.Value>255 and code.Value < 266 then -- print("Нажата клавиша кеймпада: ", code) local num=code.Value-256 --print(num, code.Name, code.Value) txt=txt .. num end end print(txt) script.Parent.Parent.PlayerGui.Main.Result.Text=txt end game:GetService("UserInputService").InputBegan:connect(onKeyPress)
Но, чтобы он работал нам нужно:
- создать удалённую функцию в ReplicatedStorage:
- и скрипт на сервере который эту функцию будет отрабатывать:
local Players = game:GetService("Players") -- берём ссылку на список игроков local ReplicatedStorage = game:GetService("ReplicatedStorage") local remoteFunction = ReplicatedStorage:WaitForChild("RemoteFunctionTest") local ws = game.Workspace local me = script.Parent -- Create a new part and return it local function createPart(player,txt) print("От игрока:" .. player.Name .. " получено: " .. txt) -- ищем территорию игрока и вписываем туда ответ local children = workspace:GetChildren() -- ищем детей Workspace for i, child in ipairs(children) do -- перебираем if child.ClassName == "Model" then -- ищем что это модель if child:FindFirstChild("Rocket") ~= nil then -- и это модель пространства if child.Owner.Value == player.Name then -- кем то занято child.VAL.sO.Value = tonumber(txt) -- ws.Main.VAL.sO.Value = tonumber(txt) end end end end return "received..." end -- Bind the "createPart()" function to the remote function's "OnServerInvoke" callback remoteFunction.OnServerInvoke = createPart
Это и есть три основных момента про функцию клиент серверной ориентации. Т.е. запрос клиентом выполнения функции на сервере с заданным параметром.
И, если честно, на этом мои беды не закончились! Но, пораскинув мозгами, я таки выкрутился кучкой переменных и исключительно серверными скриптами.
Кстати, на этот раз я обошёлся без команд игроков (teams) и проверку осуществлял исключительно по имени текущего игрока и содержимому переменной Owner модели текущей территории. Например, Script преграды через которую может пройти только хозяин территории, выглядит так:
trap = script.Parent -- работаем с родителем скрипта trap.Touched:Connect(function(Part) -- вызываем событие, когда касаемся столбика local player = game.Players:GetPlayerFromCharacter(Part.Parent) -- сохраняем информацию об игроке if player ~= nil then if trap.Parent:FindFirstChild("Owner").Value == Part.Parent.Name then -- для хозяина забор проходимый trap.CanCollide = false wait(1) -- на полсекунды проходимый trap.CanCollide = true end end end) -- конец function
Ещё один момент был в том, чтобы инициализировать повторный вычислительный процесс. На сей раз я сделал полную копию сохранённой в ServerStorage модели территории, поместил её на место текущей и.. выполнил всё что касается заклеймления данной территории текущим игроком. Вот финальный кусочек скрипта:
player.PlayerGui.Main.Timer.Text = "Prepare for restart..." wait(5) local original = game.ReplicatedStorage.Main -- Сохраняем в переменную оригинальный объект local par=me.Parent -- сохраняем привязку local pos=me:GetPrimaryPartCFrame() -- сохраняем текущую позицию local copy = original:Clone() -- создаём копию объекта copy.Parent = par -- указываем значение родителя у копии copy.Name = me.Name -- сохраняем имя зоны copy.Owner.Value = me.Owner.Value -- сохраняем привязку зоны -- восстанавливаем Табло copy.Tablo.SurfaceGui.TextLabel.Text=me.Owner.Value copy.Tablo.Transparency = 0 -- выставляем прозрачность copy.Tablo.SurfaceGui.Frame1.Visible=true -- восстанавливаем положение зоны copy:SetPrimaryPartCFrame(pos) -- уничтожаем текущую - СЕБЯ me:Destroy()
Ну, тут могу только прокомментировать, что вместо MoveTo использована SetPrimaryPartCFrame, т.к. MoveTo, как выяснено было ранее, работает не совсем корректно в случае наложения объектов друг на друга, а именно помещает новый над старым.
Как всегда, в конце ссылка на сам проект, где можно протестировать всё описанное в деле:
https://www.roblox.com/games/4789977549/Math-Rocket