Roblox · Tower Defense · урок 6

Деньги за врагов и цена башен

У игрока кошелёк. Башня стоит денег, убитый башней враг приносит награду её владельцу.

Структура

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

ReplicatedStorage
├─ Functions (Folder)
│  └─ SpawnTowerFunction (RemoteFunction) — клиент спрашивает — сервер отвечает
└─ Towers (Folder)
   └─ Tower (Model)
      └─ Configuration (Configuration)
         └─ Price (NumberValue) — цена башни
ServerStorage
└─ Mobs (Folder)
   └─ NormalRig (Model)
      └─ Configuration (Configuration)
         └─ Reward (NumberValue) — награда за убийство
StarterGui
└─ TowerGui (ScreenGui)
   ├─ Money (TextLabel) — деньги игрока
   └─ MoneyLocal (LocalScript)
ServerScriptService
└─ PlayerController (Script) — бывший PlayerCollisionGroup

Шаги

  1. PlayerCollisionGroup переименуй в PlayerController и замени текст на скрипт ниже.
  2. Price — в Configuration каждой башни, Reward — в Configuration каждого врага.
  3. RemoteFunction, в отличие от RemoteEvent, возвращает ответ: клиент ждёт true/false.
  4. Деньги списывает и начисляет только сервер — клиент лишь спрашивает.

Скрипты

PlayerController

Где: ServerScriptService · Script
Зачем: Кошелёк Money (300) + группа коллизий персонажа.

-- игрок зашёл в игру
game.Players.PlayerAdded:Connect(function(player)
    -- кошелёк внутри игрока
    local money = Instance.new("IntValue", player)
    -- называем Money
    money.Name = "Money"
    -- стартовые деньги — хватит на первую башню
    money.Value = 300
    -- появился персонаж
    player.CharacterAdded:Connect(function(character)
        -- все его части
        for _, obj in pairs(character:GetDescendants()) do
            -- только физические
            if obj:IsA("BasePart") then
                -- группа коллизий Player
                obj.CollisionGroup = "Player"
            -- конец проверки
            end
        -- конец перебора
        end
    -- конец обработчика персонажа
    end)
-- конец обработчика игрока
end)

Tower

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Проверка денег, списание цены, награда за убитого врага.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- удаление объектов по таймеру
local Debris = game:GetService("Debris")
-- сигнал «поставь башню»
local towerEvent = RS.Events:WaitForChild("TowerEvent")
-- вопрос клиента «хватает денег?»
local spawnTowerFunction = RS.Functions:WaitForChild("SpawnTowerFunction")

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

-- ищет ближайшего живого врага в радиусе range
local function FindNearestTarget(towerModel, range)
    -- пока никого
    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 < range then
                -- запоминаем врага
                nearestTarget = target
                -- теперь ищем ещё ближе
                range = distance
            -- конец проверки расстояния
            end
        -- конец проверки здоровья
        end
    -- конец перебора
    end
    -- ближайший враг или nil
    return nearestTarget
-- конец функции
end

-- луч от башни к врагу
local function Trail(towerModel, target)
    -- новый парт-луч
    local trail = Instance.new("Part")
    -- длина луча
    local distance = (towerModel.PrimaryPart.Position - target.PrimaryPart.Position).Magnitude
    -- тонкий и длинный
    trail.Size = Vector3.new(0.1, 0.1, distance)
    -- сдвиг на полдлины, чтобы луч начинался у башни
    local offset = CFrame.new(0, 0, -distance / 2)
    -- направляем на врага
    trail.CFrame = CFrame.new(towerModel.PrimaryPart.Position, target.PrimaryPart.Position) * offset
    -- не падает
    trail.Anchored = true
    -- ни с чем не сталкивается
    trail.CanCollide = false
    -- полупрозрачный
    trail.Transparency = 0.5
    -- цвет луча этой башни
    trail.Color = towerModel.Configuration.Trail.Value
    -- светится
    trail.Material = Enum.Material.Neon
    -- показываем
    trail.Parent = workspace
    -- через 0,1 с удаляется сам
    Debris:AddItem(trail, 0.1)
-- конец функции
end

-- башня стреляет и платит владельцу за убийства
function tower.Attack(towerModel, player)
    -- характеристики башни
    local configuration = towerModel:WaitForChild("Configuration")
    -- башня ещё на карте
    while towerModel.Parent do
        -- живая цель в радиусе
        local target = FindNearestTarget(towerModel, configuration.Range.Value)
        -- цель есть
        if target then
            -- рисуем луч
            Trail(towerModel, target)
            -- наносим урон
            target.Humanoid:TakeDamage(configuration.Damage.Value)
            -- этот выстрел убил врага, игрок ещё в игре
            if target.Humanoid.Health <= 0 and player.Parent then
                -- награда за врага
                player.Money.Value += target.Configuration.Reward.Value
            -- конец проверки
            end
            -- перезарядка
            task.wait(configuration.Cooldown.Value)
        -- целей нет
        else
            -- проверим снова чуть позже
            task.wait(0.1)
        -- конец проверки
        end
    -- следующий выстрел
    end
-- конец функции
end

-- можно ли игроку поставить башню name
function tower.CanSpawn(player, name)
    -- образец башни
    local towerModel = RS.Towers:FindFirstChild(name)
    -- такая башня есть
    if towerModel then
        -- денег хватает
        if towerModel.Configuration.Price.Value <= player.Money.Value then
            -- можно
            return true
        -- денег мало
        else
            -- сообщение в Output
            warn("No money")
        -- конец проверки денег
        end
    -- башни нет
    else
        -- сообщение в Output
        warn("No tower")
    -- конец проверки
    end
    -- нельзя
    return false
-- конец функции
end

-- ставит башню, если хватает денег
function tower.Spawn(player, name, cframe)
    -- сервер сам ещё раз проверяет деньги
    if tower.CanSpawn(player, name) then
        -- копия башни
        local towerClone = RS.Towers[name]: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
        -- списываем цену
        player.Money.Value -= towerClone.Configuration.Price.Value
        -- атака параллельно
        coroutine.wrap(tower.Attack)(towerClone, player)
    -- конец проверки
    end
-- конец функции
end

-- клиент просит поставить башню
towerEvent.OnServerEvent:Connect(tower.Spawn)
-- клиент спрашивает «хватает денег?» — отвечаем
spawnTowerFunction.OnServerInvoke = tower.CanSpawn

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

TowerPlacementPreview — блок кнопок

Где: StarterGui → TowerGui · LocalScript
Зачем: Перед превью спрашивает сервер, хватает ли денег.

-- добавь к переменным в начале скрипта
local spawnTowerFunction = RS.Functions:WaitForChild("SpawnTowerFunction")

-- по каждой башне
for _, tower in pairs(towers:GetChildren()) do
    -- копия шаблона кнопки
    local button = gui.TowerFrame.Sample:Clone()
    -- характеристики башни
    local configuration = tower:WaitForChild("Configuration")
    -- имя кнопки = имя башни
    button.Name = tower.Name
    -- картинка башни
    button.Image = configuration.Image.Texture
    -- показываем копию
    button.Visible = true
    -- в список кнопок
    button.Parent = gui.TowerFrame
    -- нажали на кнопку
    button.Activated:Connect(function()
        -- спрашиваем сервер: хватает денег?
        if spawnTowerFunction:InvokeServer(tower.Name) then
            -- хватает — показываем превью
            AddTowerPreview(tower.Name)
        -- конец проверки
        end
    -- конец обработчика
    end)
-- следующая башня
end

MoneyLocal

Где: StarterGui → TowerGui · LocalScript
Зачем: Показывает деньги игрока.

-- этот игрок
local player = game.Players.LocalPlayer
-- его кошелёк (создаёт сервер)
local money = player:WaitForChild("Money")
-- надпись с деньгами
local moneyText = script.Parent.Money

-- обновляет надпись
local function UpdateMoney()
    -- «$ 300»
    moneyText.Text = "$ " .. money.Value
-- конец функции
end

-- показываем сразу
UpdateMoney()
-- и при каждом изменении
money.Changed:Connect(UpdateMoney)

Проверка

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