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) — атака и установка
Где: 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)Где: 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Где: 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) и убедись: