Roblox · Tower Defense · урок 3

Установка и атака башен

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

Структура

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

ReplicatedStorage
├─ Towers (Folder) — образцы башен
│  └─ Tower (Model) — PrimaryPart задан, все части Anchored
└─ Events (Folder)
   └─ TowerEvent (RemoteEvent) — клиент → сервер
Workspace
└─ Towers (Folder) — поставленные башни
StarterGui
└─ TowerGui (ScreenGui)
   ├─ Tower (TextButton) — имя = имя башни
   └─ TowerPlacementPreview (LocalScript)
ServerScriptService
└─ MainScript (Script)
   ├─ Mob (ModuleScript)
   └─ Tower (ModuleScript) — атака и установка

Шаги

  1. Башни — в ReplicatedStorage: их видит и сервер, и клиент (ServerStorage клиенту недоступен).
  2. У модели башни задай PrimaryPart (нижний парт), все её части — Anchored.
  3. Collision Groups: группа Tower; выключи Tower–Player, Tower–Mob, Tower–Tower.
  4. Ставить можно только на парт с именем Grass.

Скрипты

TowerPlacementPreview

Где: StarterGui → TowerGui · LocalScript
Зачем: Превью башни у курсора, цвет по месту, R — поворот, ЛКМ — отправить место на сервер.

-- ввод: мышь и клавиатура
local UIS = game:GetService("UserInputService")
-- события каждого кадра
local RunS = game:GetService("RunService")
-- общее хранилище клиента и сервера
local RS = game:GetService("ReplicatedStorage")
-- сигнал на сервер
local towerEvent = RS.Events:WaitForChild("TowerEvent")
-- камера игрока
local camera = workspace.CurrentCamera
-- TowerGui
local gui = script.Parent
-- образцы башен
local towers = RS:WaitForChild("Towers")

-- башня-превью, пока её нет
local spawnTower = nil
-- можно ли поставить башню сюда
local canPlace = false
-- поворот превью в градусах
local rotation = 0

-- что под курсором в 3D
local function MouseRaycast(exclude)
    -- курсор на экране (2D)
    local mousePosition = UIS:GetMouseLocation()
    -- луч из камеры через курсор
    local mouseRay = camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
    -- настройки луча
    local raycastParams = RaycastParams.new()
    -- режим «не замечать объекты из списка»
    raycastParams.FilterType = Enum.RaycastFilterType.Exclude
    -- список: само превью
    raycastParams.FilterDescendantsInstances = exclude
    -- пускаем луч на 1000 studs
    local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)
    -- точка и объект попадания или nil
    return raycastResult
-- конец функции
end

-- убирает превью
local function RemoveTowerPreview()
    -- превью есть
    if spawnTower then
        -- удаляем модель
        spawnTower:Destroy()
        -- забываем её
        spawnTower = nil
        -- сбрасываем поворот
        rotation = 0
    -- конец проверки
    end
-- конец функции
end

-- создаёт превью башни name
local function AddTowerPreview(name)
    -- образец
    local tower = towers:FindFirstChild(name)
    -- нашли
    if tower then
        -- убираем прошлое превью
        RemoveTowerPreview()
        -- копия для превью
        spawnTower = tower:Clone()
        -- показываем на карте
        spawnTower.Parent = workspace.Towers
        -- все детали
        for _, obj in pairs(spawnTower:GetDescendants()) do
            -- только физические
            if obj:IsA("BasePart") then
                -- превью ни с чем не сталкивается
                obj.CollisionGroup = "Tower"
                -- «призрачный» материал
                obj.Material = Enum.Material.ForceField
            -- конец проверки
            end
        -- конец перебора
        end
    -- конец проверки
    end
-- конец функции
end

-- красит превью
local function ColorTowerPreview(color)
    -- все детали
    for _, obj in pairs(spawnTower:GetDescendants()) do
        -- только физические
        if obj:IsA("BasePart") then
            -- новый цвет
            obj.Color = color
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- нажали кнопку Tower
gui.Tower.Activated:Connect(function()
    -- показываем превью башни Tower
    AddTowerPreview("Tower")
-- конец обработчика
end)

-- нажата клавиша или кнопка мыши
UIS.InputBegan:Connect(function(input, gp)
    -- нажатие ушло в интерфейс — пропускаем
    if gp then return end
    -- сейчас показано превью
    if spawnTower then
        -- левая кнопка мыши
        if input.UserInputType == Enum.UserInputType.MouseButton1 then
            -- место подходит
            if canPlace then
                -- просим сервер поставить башню сюда
                towerEvent:FireServer(spawnTower.Name, spawnTower.PrimaryPart.CFrame)
                -- убираем превью
                RemoveTowerPreview()
            -- конец проверки места
            end
        -- клавиша R
        elseif input.KeyCode == Enum.KeyCode.R then
            -- поворот на 90°
            rotation += 90
        -- конец выбора
        end
    -- конец проверки превью
    end
-- конец обработчика
end)

-- каждый кадр
RunS.RenderStepped:Connect(function()
    -- есть превью
    if spawnTower then
        -- что под курсором (превью не считаем)
        local raycastResult = MouseRaycast({spawnTower})
        -- луч во что-то попал
        if raycastResult and raycastResult.Instance then
            -- под курсором трава
            if raycastResult.Instance.Name == "Grass" then
                -- ставить можно
                canPlace = true
                -- превью зелёное
                ColorTowerPreview(Color3.new(0, 1, 0))
            -- дорога или что-то ещё
            else
                -- ставить нельзя
                canPlace = false
                -- превью красное
                ColorTowerPreview(Color3.new(1, 0, 0))
            -- конец проверки
            end
            -- точка под курсором: X
            local x = raycastResult.Position.X
            -- Y, поднятый на полвысоты башни
            local y = raycastResult.Position.Y + (spawnTower.PrimaryPart.Size.Y / 2)
            -- Z
            local z = raycastResult.Position.Z
            -- позиция + поворот
            local cframe = CFrame.new(x, y, z) * CFrame.Angles(0, math.rad(rotation), 0)
            -- двигаем превью за курсором
            spawnTower:PivotTo(cframe)
        -- курсор в небе — луч никуда не попал
        else
            -- ставить нельзя
            canPlace = false
        -- конец проверки попадания
        end
    -- конец проверки превью
    end
-- конец обработчика кадра
end)

Tower

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Ставит башню по сигналу и раз в секунду бьёт ближайшего живого врага в радиусе 25.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- сигнал «поставь башню» от клиента
local towerEvent = RS.Events:WaitForChild("TowerEvent")

-- таблица модуля
local tower = {}

-- ищет ближайшего живого врага
local function FindNearestTarget(towerModel)
    -- радиус атаки
    local maxDistance = 25
    -- пока никого
    local nearestTarget = nil
    -- все враги на карте
    for _, target in ipairs(workspace.Mobs:GetChildren()) do
        -- мёртвых не считаем
        if target.Humanoid.Health > 0 then
            -- расстояние до башни
            local distance = (target.HumanoidRootPart.Position - towerModel.PrimaryPart.Position).Magnitude
            -- ближе лучшего найденного
            if distance < maxDistance then
                -- запоминаем врага
                nearestTarget = target
                -- новое лучшее расстояние
                maxDistance = distance
            -- конец проверки расстояния
            end
        -- конец проверки здоровья
        end
    -- конец перебора
    end
    -- ближайший враг или nil
    return nearestTarget
-- конец функции
end

-- башня стреляет, пока стоит на карте
function tower.Attack(towerModel)
    -- башня ещё на карте
    while towerModel.Parent do
        -- ищем цель
        local target = FindNearestTarget(towerModel)
        -- цель есть
        if target then
            -- наносим урон
            target.Humanoid:TakeDamage(25)
        -- конец проверки
        end
        -- перезарядка 1 секунда
        task.wait(1)
    -- следующий выстрел
    end
-- конец функции
end

-- ставит башню по сигналу от клиента
function tower.Spawn(player, name, cframe)
    -- образец башни
    local towerModel = RS.Towers:FindFirstChild(name)
    -- такая башня есть
    if towerModel then
        -- копия
        local towerClone = towerModel:Clone()
        -- ставим всю модель туда, где выбрал игрок
        towerClone:PivotTo(cframe)
        -- на карту
        towerClone.Parent = workspace.Towers
        -- все детали башни
        for _, obj in pairs(towerClone:GetDescendants()) do
            -- только физические
            if obj:IsA("BasePart") then
                -- группа коллизий Tower
                obj.CollisionGroup = "Tower"
            -- конец проверки
            end
        -- конец перебора
        end
        -- атака параллельно, скрипт не ждёт
        coroutine.wrap(tower.Attack)(towerClone)
    -- конец проверки
    end
-- конец функции
end

-- клиент прислал сигнал → ставим башню
towerEvent.OnServerEvent:Connect(tower.Spawn)

-- отдаём таблицу модуля
return tower

MainScript

Где: ServerScriptService · Script
Зачем: Добавлена строка подключения модуля Tower.

-- подключаем модуль врагов
local mob = require(script.Mob)
-- подключаем модуль башен — он сам начнёт ждать сигнал от игроков
local tower = require(script.Tower)
-- карта
local map = workspace.Grass

-- 5 волн
for wave = 1, 5 do
    -- обычные: 2, 4, 6…
    mob.Spawn("NormalRig", 2 * wave, map)
    -- быстрые
    mob.Spawn("FastRig", 2 * wave, map)
    -- сильные
    mob.Spawn("StrongRig", 2 * wave, map)
    -- ждём, пока на карте не останется врагов
    repeat task.wait(1) until #workspace.Mobs:GetChildren() == 0
-- следующая волна
end

Проверка

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