Roblox · Песочница · урок 2

Система размещения

Как в Minecraft: выбираешь блок, под курсором появляется полупрозрачный призрак по сетке, R поворачивает, клик ставит.

Структура

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

ReplicatedStorage
├─ ObjectFolder (Folder) — образцы блоков
│  └─ Wood, Stone, Glass, Fence (Model) — PrimaryPart = MainPart
│     └─ MainPart (Part) — невидимая коробка вокруг, CanCollide выкл.
└─ PlacementEvent (RemoteEvent)
Workspace
└─ Placed (Folder) — поставленные блоки
ServerScriptService
└─ PlacementServer (Script)
StarterGui
└─ PlacementGui (ScreenGui) — ResetOnSpawn выкл.
   ├─ Frame (Frame) — кнопки Wood, Stone, Glass, Fence
   └─ PlacementLocal (LocalScript)

Шаги

  1. Блоки: куб 4 × 4 × 4 из дерева, камня, стекла, забор из столбиков. Все детали Anchored.
  2. Каждый блок — в модель с тем же именем. Внутрь — невидимая «MainPart» во весь объект (CanCollide выкл.), у модели PrimaryPart = MainPart: по ней модель двигается и стоит на поверхности.
  3. ReplicatedStorage → Folder «ObjectFolder» — все блоки туда. RemoteEvent «PlacementEvent». Workspace → Folder «Placed».
  4. PlacementGui → Frame с TextButton, названными как блоки. LocalScript «PlacementLocal», ServerScriptService → Script «PlacementServer».
  5. Сетка 4: координата округляется до ближайшего кратного 4 — блоки встают ровно рядом. R — поворот на 45°, Q — отмена.
  6. В методичке подписки на R и клик добавлялись при каждом выборе блока; здесь подписки одни. Сервер проверяет имя блока и расстояние (до 40 studs) — иначе можно было ставить что угодно где угодно.

Скрипты

PlacementLocal

Где: StarterGui → PlacementGui · LocalScript
Зачем: Призрак, сетка, поворот, клик.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- клавиатура
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- куда указывает курсор
local mouse = player:GetMouse()
-- просим сервер поставить блок
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы блоков
local objects = RS:WaitForChild("ObjectFolder")
-- кнопки блоков
local frame = script.Parent:WaitForChild("Frame")
-- шаг сетки — размер блока
local GRID = 4
-- полупрозрачный блок под курсором
local preview = nil
-- какой блок ставим
local currentName = nil
-- поворот в градусах
local rotation = 0

-- закончить размещение
local function Stop()
    -- убираем призрак
    if preview then preview:Destroy() end
    -- ничего не ставим
    preview = nil
    -- блока нет
    currentName = nil
-- конец функции
end

-- начать ставить блок
local function Start(name)
    -- старый призрак убираем
    Stop()
    -- образец
    local object = objects:FindFirstChild(name)
    -- такого блока нет
    if not object then return end
    -- копия только у этого игрока
    preview = object:Clone()
    -- все детали
    for _, part in ipairs(preview:GetDescendants()) do
        -- деталь
        if part:IsA("BasePart") then
            -- полупрозрачная (MainPart так и остаётся невидимой)
            part.Transparency = math.max(part.Transparency, 0.6)
            -- сквозь
            part.CanCollide = false
            -- курсор её не замечает
            part.CanQuery = false
        -- конец проверки
        end
    -- конец перебора
    end
    -- показываем
    preview.Parent = workspace
    -- mouse.Hit смотрит сквозь призрак
    mouse.TargetFilter = preview
    -- запоминаем
    currentName = name
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- ничего не ставим
    if not preview or not preview.PrimaryPart then return end
    -- точка под курсором
    local hit = mouse.Hit.Position
    -- по сетке
    local x = math.floor(hit.X / GRID + 0.5) * GRID
    -- по сетке
    local z = math.floor(hit.Z / GRID + 0.5) * GRID
    -- стоит на поверхности, а не утоплен
    local y = hit.Y + preview.PrimaryPart.Size.Y / 2
    -- двигаем призрак
    preview:PivotTo(CFrame.new(x, y, z) * CFrame.Angles(0, math.rad(rotation), 0))
-- конец функции
end)

-- нажали клавишу — подписка одна
UIS.InputBegan:Connect(function(input, processed)
    -- печатаем в чате
    if processed then return end
    -- R
    if input.KeyCode == Enum.KeyCode.R then
        -- поворот на 45°
        rotation = (rotation + 45) % 360
    -- Q
    elseif input.KeyCode == Enum.KeyCode.Q then
        -- отмена
        Stop()
    -- клик по миру (клик по кнопке отсеян processed)
    elseif input.UserInputType == Enum.UserInputType.MouseButton1 and preview then
        -- сервер решает, можно ли
        placementEvent:FireServer(currentName, preview:GetPivot())
        -- призрак убираем
        Stop()
    -- конец проверки
    end
-- конец функции
end)

-- кнопки блоков
for _, button in ipairs(frame:GetChildren()) do
    -- только кнопки
    if button:IsA("TextButton") then
        -- имя кнопки = имя блока
        button.MouseButton1Click:Connect(function() Start(button.Name) end)
    -- конец проверки
    end
-- конец перебора
end

PlacementServer

Где: ServerScriptService · Script
Зачем: Ставит блок, если можно.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- теги
local CS = game:GetService("CollectionService")
-- клиент просит поставить
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы
local objects = RS:WaitForChild("ObjectFolder")
-- куда ставим постройки
local placed = workspace:WaitForChild("Placed")
-- дальше ставить нельзя
local MAX_DISTANCE = 40

-- клиент прислал блок и место
placementEvent.OnServerEvent:Connect(function(player, name, cf)
    -- пришёл мусор
    if typeof(name) ~= "string" or typeof(cf) ~= "CFrame" then return end
    -- образец
    local sample = objects:FindFirstChild(name)
    -- нет такого или слишком далеко
    if not sample or player:DistanceFromCharacter(cf.Position) > MAX_DISTANCE then return end
    -- копия
    local object = sample:Clone()
    -- на место
    object:PivotTo(cf)
    -- в мир — теги копируются, блок можно сломать
    object.Parent = placed
-- конец функции
end)

Проверка

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