Повествование будет несколько сумбурное. так как оно делается пост фактум.
Начну с того, что решил переделать считалку (арифметический тренажёр) с 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