Roblox · RPG · урок 4

Взаимодействие: монеты и записки

Своя система взаимодействия: наводишь курсор на предмет — над ним надпись, кликаешь — монета в кошельке или открывается записка.

Структура

Синим — что добавляем на этом уроке.

Workspace
└─ MapFolder (Folder)
   ├─ Coin (Part) — Highlight, CanInteract (BoolValue, вкл.), Coin (IntValue), Sound
   └─ Note (Part) — Highlight, CanInteract, Text (StringValue), Sound
ReplicatedStorage
└─ InteractEvent (RemoteEvent)
ServerScriptService
├─ PlayerData (Script) — кошелёк «Монеты»
└─ InteractServer (Script)
StarterPlayer → StarterPlayerScripts
└─ InteractSystem (LocalScript)
StarterGui
├─ InteractGui (BillboardGui) — AlwaysOnTop, MaxDistance 60
│  └─ Object, Interact (TextLabel) — имя и действие
└─ NoteGui (ScreenGui) — ResetOnSpawn выкл.
   └─ NoteFrame (Frame) — NoteText, CloseButton; Visible выкл.

Шаги

  1. StarterGui → BillboardGui «InteractGui»: два TextLabel «Object» (имя) и «Interact» (что делать). Размеры — через Scale, AlwaysOnTop вкл., MaxDistance 60.
  2. Монета: деталь с Highlight, BoolValue «CanInteract» (Value вкл.), IntValue «Coin» (сколько монет), Sound со звоном. Сундук из прошлого урока — то же самое: CanInteract и Coin в модели.
  3. Записка: деталь с Highlight, CanInteract и StringValue «Text» — её текст.
  4. NoteGui — отдельный ScreenGui (в методичке он лежал внутри InteractGui — так ScreenGui не показывается): NoteFrame (Visible выкл.) с TextLabel «NoteText» и TextButton «CloseButton».
  5. ReplicatedStorage → RemoteEvent «InteractEvent». Монеты начисляет сервер: в методичке это делал локальный скрипт, и сервер о деньгах не знал.
  6. PlayerData — кошелёк в leaderstats, InteractServer — проверяет расстояние, начисляет и прячет монету на минуту.
  7. Объект можно навести и мышью на любую его деталь: скрипт поднимается к модели с CanInteract.

Скрипты

PlayerData

Где: ServerScriptService · Script
Зачем: Кошелёк игрока.

-- игроки
local Players = game:GetService("Players")

-- создаёт контейнер со значением
local function Value(class, name, value, parent)
    -- контейнер
    local v = Instance.new(class)
    -- имя
    v.Name = name
    -- значение
    v.Value = value
    -- на место
    v.Parent = parent
    -- отдаём
    return v
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- таблица по Tab
    local leaderstats = Instance.new("Folder")
    -- имя строго такое
    leaderstats.Name = "leaderstats"
    -- в игрока
    leaderstats.Parent = player
    -- кошелёк
    local coins = Value("IntValue", "Монеты", 0, leaderstats)
-- конец функции
end

-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end

InteractServer

Где: ServerScriptService · Script
Зачем: Монеты и сундуки: начисление и возврат.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- клик игрока по монете или сундуку
local interactEvent = RS:WaitForChild("InteractEvent")
-- дальше — не засчитываем (защита от читов)
local MAX_DISTANCE = 32
-- через сколько секунд награда вернётся
local RESPAWN = 60

-- спрятать или показать монету/сундук
local function SetVisible(item, visible)
    -- всё внутри
    local list = item:GetDescendants()
    -- и сам объект
    table.insert(list, item)
    -- каждый
    for _, obj in ipairs(list) do
        -- деталь
        if obj:IsA("BasePart") then
            -- запоминаем обычную прозрачность
            if obj:GetAttribute("BaseTransparency") == nil then obj:SetAttribute("BaseTransparency", obj.Transparency) end
            -- видна или нет
            obj.Transparency = visible and obj:GetAttribute("BaseTransparency") or 1
        -- подсветка
        elseif obj:IsA("Highlight") then
            -- вместе с объектом
            obj.Enabled = visible
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- игрок кликнул
interactEvent.OnServerEvent:Connect(function(player, item)
    -- пришло не то — игнорируем
    if typeof(item) ~= "Instance" or not item:IsDescendantOf(workspace) then return end
    -- метка доступности
    local can = item:FindFirstChild("CanInteract")
    -- сколько монет внутри
    local coin = item:FindFirstChild("Coin")
    -- уже взято или это не награда
    if not can or not can.Value or not coin then return end
    -- где объект
    local position = item:IsA("Model") and item:GetPivot().Position or item.Position
    -- далеко — не засчитываем
    if player:DistanceFromCharacter(position) > MAX_DISTANCE then return end
    -- больше не взять
    can.Value = false
    -- монеты в кошелёк
    player.leaderstats["Монеты"].Value += coin.Value
    -- звук монеты
    local sound = item:FindFirstChild("Sound")
    -- звон
    if sound then sound:Play() end
    -- исчезает
    SetVisible(item, false)
    -- ждём
    task.wait(RESPAWN)
    -- появляется снова
    SetVisible(item, true)
    -- можно взять снова
    can.Value = true
-- конец функции
end)

InteractSystem

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Надпись над объектом и клики.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- мышь: mouse.Target — деталь под курсором
local mouse = player:GetMouse()
-- интерфейс игрока
local playerGui = player:WaitForChild("PlayerGui")
-- надпись над объектом (BillboardGui)
local interactGui = playerGui:WaitForChild("InteractGui")
-- окно записки
local noteFrame = playerGui:WaitForChild("NoteGui"):WaitForChild("NoteFrame")
-- просим сервер: «взять монету/сундук»
local interactEvent = RS:WaitForChild("InteractEvent")
-- дальше 30 studs не взаимодействуем
local MAX_DISTANCE = 30

-- объект с CanInteract под курсором (деталь или её модель)
local function Target()
    -- деталь под курсором
    local obj = mouse.Target
    -- поднимаемся к родителям
    while obj and obj ~= workspace do
        -- метка «с этим можно взаимодействовать»
        local can = obj:FindFirstChild("CanInteract")
        -- нашли, и он сейчас доступен
        if can and can.Value then return obj end
        -- выше
        obj = obj.Parent
    -- конец цикла
    end
    -- ничего нет
    return nil
-- конец функции
end

-- где объект
local function PositionOf(item)
    -- у модели — центр
    if item:IsA("Model") then return item:GetPivot().Position end
    -- у детали — её позиция
    return item.Position
-- конец функции
end

-- что написать под именем
local function ActionText(item)
    -- записка
    if item:FindFirstChild("Text") then return "ЛКМ — прочитать" end
    -- монета или сундук
    return "ЛКМ — взять"
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- что под курсором
    local item = Target()
    -- есть и не слишком далеко
    if item and player:DistanceFromCharacter(PositionOf(item)) < MAX_DISTANCE * 2 then
        -- надпись над объектом
        interactGui.Adornee = item:IsA("Model") and (item.PrimaryPart or item:FindFirstChildWhichIsA("BasePart")) or item
        -- имя объекта
        interactGui.Object.Text = item.Name
        -- что можно сделать
        interactGui.Interact.Text = ActionText(item)
        -- показываем
        interactGui.Enabled = true
    -- ничего нет
    else
        -- отвязываем
        interactGui.Adornee = nil
        -- прячем
        interactGui.Enabled = false
    -- конец проверки
    end
-- конец функции
end)

-- клик левой кнопкой
mouse.Button1Down:Connect(function()
    -- что под курсором
    local item = Target()
    -- ничего или далеко
    if not item or player:DistanceFromCharacter(PositionOf(item)) > MAX_DISTANCE then return end
    -- это записка?
    local text = item:FindFirstChild("Text")
    -- да
    if text then
        -- текст записки
        noteFrame.NoteText.Text = text.Value
        -- показываем окно
        noteFrame.Visible = true
        -- шелест бумаги
        local sound = item:FindFirstChild("Sound")
        -- играем у себя
        if sound then sound:Play() end
    -- монета или сундук
    else
        -- деньги начисляет только сервер
        interactEvent:FireServer(item)
    -- конец проверки
    end
-- конец функции
end)
-- «Закрыть» записку — подписка одна
noteFrame:WaitForChild("CloseButton").MouseButton1Click:Connect(function()
    -- прячем
    noteFrame.Visible = false
-- конец функции
end)

Проверка

Запусти Play (или Run) и убедись: