Roblox · Tower Defense · урок 2

Волны врагов

Враги появляются сами, идут с анимацией, не толкаются и исчезают в конце пути. Главный скрипт запускает 5 волн.

Структура

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

ServerStorage
└─ Mobs (Folder) — образцы врагов
   ├─ NormalRig (Model) — PrimaryPart = HumanoidRootPart
   │  └─ Animation (Animation) — AnimationId ходьбы
   ├─ FastRig (Model) — WalkSpeed выше
   └─ StrongRig (Model) — MaxHealth и Health выше
Workspace
├─ Grass (Folder) — вся карта
│  ├─ Grass (Part) — земля
│  ├─ Road (Folder)
│  └─ SpawnPart (Part)
├─ Waypoints (Folder)
└─ Mobs (Folder) — враги на карте
ServerScriptService
├─ PlayerCollisionGroup (Script)
└─ MainScript (Script) — волны
   └─ Mob (ModuleScript) — появление и движение врага

Шаги

  1. Анимация ходьбы (Animation Editor): 3 кадра, третий = первый, Loop, Priority = Movement, Publish → ID в Animation.AnimationId каждого врага.
  2. Анимацию публикуй с того же аккаунта (или группы), что и игру, — чужая анимация не проиграется.
  3. У каждого врага: PrimaryPart = HumanoidRootPart, части не Anchored.
  4. Model → Collision Groups: группы Mob и Player; выключи Mob–Mob, Player–Player, Mob–Player. Имена — точно как в скриптах.
  5. ModuleScript сам не работает: функции кладём в таблицу (без local), подключаем через require(путь).
  6. MovingScript и Rig из прошлого урока удали.

Скрипты

PlayerCollisionGroup

Где: ServerScriptService · Script
Зачем: Записывает все детали персонажа в группу Player.

-- игрок зашёл в игру
game.Players.PlayerAdded:Connect(function(player)
    -- появился его персонаж
    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)

Mob

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

-- хранилище с образцами врагов
local SS = game:GetService("ServerStorage")
-- таблица модуля — сюда кладём функции
local mob = {}

-- ведёт врага по маршруту
function mob.Move(enemy, map)
    -- Humanoid управляет движением
    local humanoid = enemy:WaitForChild("Humanoid")
    -- точки маршрута
    local waypoints = workspace:FindFirstChild("Waypoints")
    -- анимация ходьбы внутри врага
    local animation = enemy:FindFirstChild("Animation")
    -- загружаем её в Humanoid
    local animTrack = humanoid:LoadAnimation(animation)
    -- запускаем ходьбу
    animTrack:Play()

    -- по точкам по порядку
    for waypoint = 1, #waypoints:GetChildren() do
        -- идём к точке
        humanoid:MoveTo(waypoints[waypoint].Position)
        -- ждём, пока дойдёт
        humanoid.MoveToFinished:Wait()
        -- враг погиб по дороге — дальше не идёт
        if humanoid.Health <= 0 then return end
    -- следующая точка
    end
    -- дошёл до конца — убираем
    enemy:Destroy()
-- конец функции
end

-- создаёт quantity врагов с именем name
function mob.Spawn(name, quantity, map)
    -- ищем образец
    local mobModel = SS.Mobs:FindFirstChild(name)
    -- образец найден
    if mobModel then
        -- нужное количество раз
        for i = 1, quantity do
            -- пауза между врагами
            task.wait(0.5)
            -- копия врага
            local mobClone = mobModel:Clone()
            -- ставим на старт
            mobClone.PrimaryPart.CFrame = map.SpawnPart.CFrame
            -- кладём на карту
            mobClone.Parent = workspace.Mobs
            -- все детали врага
            for _, obj in pairs(mobClone:GetDescendants()) do
                -- только физические
                if obj:IsA("BasePart") then
                    -- группа коллизий Mob
                    obj.CollisionGroup = "Mob"
                -- конец проверки
                end
            -- конец перебора
            end
            -- враг погиб
            mobClone.Humanoid.Died:Connect(function()
                -- короткая пауза
                task.wait(0.5)
                -- убираем тело с карты
                mobClone:Destroy()
            -- конец обработчика смерти
            end)
            -- движение параллельно — не ждём, пока враг пройдёт путь
            coroutine.wrap(mob.Move)(mobClone, map)
        -- следующий враг
        end
    -- конец проверки
    end
-- конец функции
end

-- отдаём таблицу тому, кто вызвал require
return mob

MainScript

Где: ServerScriptService · Script
Зачем: 5 волн: в каждой врагов больше; новая волна — когда карта пуста.

-- подключаем модуль врагов
local mob = require(script.Mob)
-- карта
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) и убедись: