Roblox · Приключение · урок 7
Итог курса на одной странице: полная структура приключения и финальные версии всех скриптов. Удобно сверить свою игру или собрать её заново.
Синим — что добавляем на этом уроке.
Workspace — StreamingEnabled выкл. ├─ Elder (Model) — Head → Dialog, ProximityPrompt; QuestGiver (Мешки с песком) ├─ Hunter (Model) — Head → ProximityPrompt; QuestGiver (Уничтожение) ├─ Trader (Model) — Head → ProximityPrompt; TraderScript ├─ Sandbags (Folder) — Sandbag → ProximityPrompt, QuestItem ├─ Enemies (Folder) — Goblin → EnemyScript ├─ Lights (Folder) — фонари └─ Hints (Folder) — Hint → Text ReplicatedStorage └─ Events (Folder) — CompleteEvent, CancelEvent, ShopEvent, BuyFunction ServerStorage ├─ Drop (Part) — DropScript └─ Shop (Folder) — HealingPotion, InvisiblePotion (Price) ServerScriptService └─ QuestSystem, EnemyManager, BuyScript, DayNight (Script) StarterPack └─ Sword (Tool) — Handle, SwordScript StarterGui — все ResetOnSpawn выкл. ├─ QuestGui (ScreenGui) — QuestFrame, DoneLabel, QuestLocal ├─ ShopGui (ScreenGui) — ShopFrame → Items, Message, CloseButton; ShopLocal └─ HintGui (ScreenGui) — HintLabel, HintLocal
Где: ServerScriptService · Script
Зачем: Монеты и квесты.
-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- папка событий
local events = RS:WaitForChild("Events")
-- квест выполнен — сообщить игроку
local completeEvent = events:WaitForChild("CompleteEvent")
-- игрок отменил квест
local cancelEvent = events:WaitForChild("CancelEvent")
-- создаёт контейнер со значением
local function Value(parent, class, name, value)
-- контейнер
local v = Instance.new(class)
-- имя
v.Name = name
-- значение
v.Value = value
-- на место
v.Parent = parent
-- отдаём
return v
-- конец функции
end
-- у игрока нет квеста
local function ClearQuest(quest)
-- сначала цель: теперь сброс прогресса ничего не засчитает
quest.Objective.Value = 0
-- прогресс
quest.Progress.Value = 0
-- название пустое = квеста нет
quest.Title.Value = ""
-- описание
quest.Description.Value = ""
-- награда
quest.Reward.Value = 0
-- конец функции
end
-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
-- таблица по Tab
local leaderstats = Instance.new("Folder")
-- имя строго такое
leaderstats.Name = "leaderstats"
-- в игрока
leaderstats.Parent = player
-- монеты
local coins = Value(leaderstats, "IntValue", "Coins", 0)
-- текущий квест игрока
local quest = Instance.new("Folder")
-- имя
quest.Name = "QuestInfo"
-- в игрока — клиент тоже видит
quest.Parent = player
-- название
Value(quest, "StringValue", "Title", "")
-- что сделать
Value(quest, "StringValue", "Description", "")
-- сколько нужно
Value(quest, "IntValue", "Objective", 0)
-- сколько уже
local progress = Value(quest, "IntValue", "Progress", 0)
-- награда в монетах
Value(quest, "IntValue", "Reward", 0)
-- прогресс изменился
progress.Changed:Connect(function(value)
-- цель
local objective = quest.Objective.Value
-- квеста нет или ещё не всё
if objective <= 0 or value < objective then return end
-- запоминаем награду
local reward = quest.Reward.Value
-- и название
local title = quest.Title.Value
-- квест закрыт — второй раз награда не выдастся
ClearQuest(quest)
-- награда
coins.Value += reward
-- сообщение на экране
completeEvent:FireClient(player, title, reward)
-- конец функции
end)
-- конец функции
end)
-- кнопка «Отменить»
cancelEvent.OnServerEvent:Connect(function(player)
-- квест игрока
local quest = player:FindFirstChild("QuestInfo")
-- очищаем
if quest then ClearQuest(quest) end
-- конец функции
end)Где: Workspace → Elder / Hunter · Script
Зачем: Выдаёт квест.
-- персонаж, который даёт квест
local npc = script.Parent
-- «Взять задание»
local prompt = npc:WaitForChild("Head"):WaitForChild("ProximityPrompt")
-- игрок нажал E
prompt.Triggered:Connect(function(player)
-- квест игрока
local quest = player:FindFirstChild("QuestInfo")
-- уже есть другой квест
if not quest or quest.Title.Value ~= "" then return end
-- название — из значений под скриптом
quest.Title.Value = script.Title.Value
-- описание
quest.Description.Value = script.Description.Value
-- награда
quest.Reward.Value = script.Reward.Value
-- с нуля
quest.Progress.Value = 0
-- цель — последней: квест начался
quest.Objective.Value = script.Objective.Value
-- конец функции
end)Где: Workspace → Sandbags → Sandbag · Script
Зачем: Мешок для квеста.
-- мешок с песком
local item = script.Parent
-- «Взять»
local prompt = item:WaitForChild("ProximityPrompt")
-- для какого квеста — как Title у квестодателя
local QUEST = "Мешки с песком"
-- через сколько секунд мешок вернётся
local RESPAWN = 60
-- игрок нажал E
prompt.Triggered:Connect(function(player)
-- квест игрока
local quest = player:FindFirstChild("QuestInfo")
-- мешки нужны только с этим квестом
if not quest or quest.Title.Value ~= QUEST then return end
-- +1 к прогрессу
quest.Progress.Value += 1
-- второй раз не взять
prompt.Enabled = false
-- мешок пропал
item.Transparency = 1
-- и не мешает
item.CanCollide = false
-- ждём
task.wait(RESPAWN)
-- вернулся
item.Transparency = 0
-- снова твёрдый
item.CanCollide = true
-- можно взять
prompt.Enabled = true
-- конец функции
end)Где: StarterGui → QuestGui · LocalScript
Зачем: Окно квеста.
-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- его квест
local quest = player:WaitForChild("QuestInfo")
-- события
local events = RS:WaitForChild("Events")
-- квест выполнен
local completeEvent = events:WaitForChild("CompleteEvent")
-- отмена
local cancelEvent = events:WaitForChild("CancelEvent")
-- QuestGui
local gui = script.Parent
-- окно квеста
local frame = gui:WaitForChild("QuestFrame")
-- надпись «Выполнено»
local doneLabel = gui:WaitForChild("DoneLabel")
-- обновить окно
local function Update()
-- квест есть — окно видно
frame.Visible = quest.Title.Value ~= ""
-- название
frame.Title.Text = quest.Title.Value
-- описание
frame.Description.Text = quest.Description.Value
-- прогресс: 2 / 5
frame.Progress.Text = quest.Progress.Value .. " / " .. quest.Objective.Value
-- конец функции
end
-- все значения квеста
for _, value in ipairs(quest:GetChildren()) do
-- изменилось — обновляем
value.Changed:Connect(Update)
-- конец перебора
end
-- сразу при входе
Update()
-- «Отменить»
frame.CancelButton.MouseButton1Click:Connect(function()
-- просим сервер
cancelEvent:FireServer()
-- конец функции
end)
-- квест выполнен
completeEvent.OnClientEvent:Connect(function(title, reward)
-- текст
doneLabel.Text = "Квест «" .. title .. "» выполнен! +" .. reward .. " монет"
-- показываем
doneLabel.Visible = true
-- 3 секунды
task.wait(3)
-- прячем
doneLabel.Visible = false
-- конец функции
end)Где: Workspace → Enemies → Goblin · Script
Зачем: Противник.
-- игроки
local Players = game:GetService("Players")
-- модель противника
local enemy = script.Parent
-- Humanoid
local humanoid = enemy:WaitForChild("Humanoid")
-- центр
local root = enemy:WaitForChild("HumanoidRootPart")
-- Animator
local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
-- видит игроков ближе 40 studs
local SIGHT = 40
-- бьёт ближе 5 studs
local ATTACK_RANGE = 5
-- урон
local DAMAGE = 10
-- секунд между ударами
local COOLDOWN = 1
-- ходьба
local walkAnim = Instance.new("Animation")
-- стандартная R15
walkAnim.AnimationId = "rbxassetid://507777826"
-- удар
local attackAnim = Instance.new("Animation")
-- стандартный взмах R15
attackAnim.AnimationId = "rbxassetid://522635514"
-- загружаем
local walkTrack = animator:LoadAnimation(walkAnim)
-- по кругу
walkTrack.Looped = true
-- загружаем
local attackTrack = animator:LoadAnimation(attackAnim)
-- поверх ходьбы
attackTrack.Priority = Enum.AnimationPriority.Action
-- куда возвращаться
local home = root.Position
-- время прошлого удара
local lastAttack = 0
-- физику считает сервер
pcall(function() root:SetNetworkOwner(nil) end)
-- скорость изменилась
humanoid.Running:Connect(function(speed)
-- пошёл
if speed > 0.5 and not walkTrack.IsPlaying then
-- шагаем
walkTrack:Play()
-- встал
elseif speed <= 0.5 and walkTrack.IsPlaying then
-- стоим
walkTrack:Stop()
-- конец проверки
end
-- конец функции
end)
-- ближайший живой видимый игрок
local function FindTarget()
-- пока никого
local best, bestRoot, bestDistance = nil, nil, SIGHT
-- все игроки
for _, player in ipairs(Players:GetPlayers()) do
-- персонаж
local character = player.Character
-- его Humanoid
local target = character and character:FindFirstChildOfClass("Humanoid")
-- его центр
local targetRoot = character and character:FindFirstChild("HumanoidRootPart")
-- жив и не невидим
if target and targetRoot and target.Health > 0 and not character:GetAttribute("Invisible") then
-- расстояние
local distance = (targetRoot.Position - root.Position).Magnitude
-- ближе прошлого
if distance < bestDistance then
-- запоминаем
best, bestRoot, bestDistance = target, targetRoot, distance
-- конец проверки
end
-- конец проверки
end
-- конец перебора
end
-- цель, её центр, расстояние
return best, bestRoot, bestDistance
-- конец функции
end
-- пока жив
while humanoid.Health > 0 do
-- ищем цель
local target, targetRoot, distance = FindTarget()
-- нашли
if target then
-- идём к ней
humanoid:MoveTo(targetRoot.Position)
-- рядом и удар готов
if distance <= ATTACK_RANGE and os.clock() - lastAttack >= COOLDOWN then
-- запоминаем время
lastAttack = os.clock()
-- взмах
attackTrack:Play()
-- урон (ForceField защищает)
target:TakeDamage(DAMAGE)
-- конец проверки
end
-- никого рядом
else
-- возвращаемся на место
humanoid:MoveTo(home)
-- конец проверки
end
-- 5 раз в секунду
task.wait(0.2)
-- конец цикла
endГде: ServerScriptService · Script
Зачем: Возрождение и награды.
-- здесь лежит награда Drop
local SS = game:GetService("ServerStorage")
-- все противники
local enemies = workspace:WaitForChild("Enemies")
-- через сколько секунд противник вернётся
local RESPAWN_TIME = 10
-- монет за победу
local KILL_REWARD = 3
-- квест, которому засчитываются победы
local KILL_QUEST = "Уничтожение"
-- следим за одним противником
local function Setup(enemy)
-- копия целого — для возрождения
local sample = enemy:Clone()
-- где стоял
local spot = enemy:GetPivot()
-- Humanoid
local humanoid = enemy:WaitForChild("Humanoid")
-- противник погиб
humanoid.Died:Connect(function()
-- метка от меча
local tag = humanoid:FindFirstChild("creator")
-- кто победил
local player = tag and tag.Value
-- игрок ещё в игре
if player and player.Parent then
-- таблица
local stats = player:FindFirstChild("leaderstats")
-- монеты
if stats then stats.Coins.Value += KILL_REWARD end
-- квест
local quest = player:FindFirstChild("QuestInfo")
-- взят квест на противников
if quest and quest.Title.Value == KILL_QUEST then
-- +1
quest.Progress.Value += 1
-- конец проверки
end
-- конец проверки
end
-- мешочек с монетами
local drop = SS:FindFirstChild("Drop")
-- есть
if drop then
-- копия
local copy = drop:Clone()
-- там, где упал противник
copy:PivotTo(CFrame.new(enemy:GetPivot().Position))
-- в мир
copy.Parent = workspace
-- конец проверки
end
-- ждём
task.wait(RESPAWN_TIME)
-- убираем тело
enemy:Destroy()
-- новый противник
local new = sample:Clone()
-- на старое место
new:PivotTo(spot)
-- в игру — его EnemyScript запустится сам
new.Parent = enemies
-- следим и за ним
Setup(new)
-- конец функции
end)
-- конец функции
end
-- все противники
for _, enemy in ipairs(enemies:GetChildren()) do
-- следим
Setup(enemy)
-- конец перебора
endГде: ServerStorage → Drop · Script
Зачем: Мешочек с монетами.
-- игроки
local Players = game:GetService("Players")
-- мешочек
local drop = script.Parent
-- сколько монет внутри
local COINS = 5
-- через сколько секунд исчезнет
local LIFETIME = 30
-- уже подобран
local taken = false
-- касание
drop.Touched:Connect(function(hit)
-- уже взяли
if taken then return end
-- игрок?
local player = Players:GetPlayerFromCharacter(hit.Parent)
-- его таблица
local stats = player and player:FindFirstChild("leaderstats")
-- не игрок
if not stats then return end
-- больше не берётся
taken = true
-- монеты
stats.Coins.Value += COINS
-- мешочек пропал
drop:Destroy()
-- конец функции
end)
-- ждём
task.wait(LIFETIME)
-- никто не взял — убираем
if drop.Parent then drop:Destroy() endГде: StarterPack → Sword · Script
Зачем: Меч.
-- игроки
local Players = game:GetService("Players")
-- удаляет объекты через время
local Debris = game:GetService("Debris")
-- меч
local tool = script.Parent
-- клинок
local handle = tool:WaitForChild("Handle")
-- урон за удар
local DAMAGE = 25
-- длина взмаха
local COOLDOWN = 0.6
-- анимация взмаха
local slash = Instance.new("Animation")
-- стандартная R15
slash.AnimationId = "rbxassetid://522635514"
-- идёт взмах
local swinging = false
-- кого уже ударили этим взмахом
local hitList = {}
-- клик мышью
tool.Activated:Connect(function()
-- взмах ещё идёт
if swinging then return end
-- владелец
local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
-- погибший не бьёт
if not humanoid or humanoid.Health <= 0 then return end
-- начали
swinging = true
-- новый взмах — список пуст
table.clear(hitList)
-- анимации владельца
local animator = humanoid:FindFirstChildOfClass("Animator")
-- взмах
if animator then animator:LoadAnimation(slash):Play() end
-- ждём
task.wait(COOLDOWN)
-- можно снова
swinging = false
-- конец функции
end)
-- клинок коснулся
handle.Touched:Connect(function(hit)
-- бьём только во время взмаха
if not swinging then return end
-- модель, которой принадлежит деталь
local model = hit:FindFirstAncestorOfClass("Model")
-- её Humanoid
local target = model and model:FindFirstChildOfClass("Humanoid")
-- не цель, сам себя или уже ударили
if not target or target.Health <= 0 or model == tool.Parent or hitList[target] then return end
-- игроков не бьём — только противников
if Players:GetPlayerFromCharacter(model) then return end
-- этим взмахом больше не бьём
hitList[target] = true
-- прошлая метка
local old = target:FindFirstChild("creator")
-- убираем
if old then old:Destroy() end
-- метка «кто ударил»
local tag = Instance.new("ObjectValue")
-- стандартное имя метки в Roblox
tag.Name = "creator"
-- игрок с мечом
tag.Value = Players:GetPlayerFromCharacter(tool.Parent)
-- в Humanoid противника
tag.Parent = target
-- через 2 секунды метка исчезнет
Debris:AddItem(tag, 2)
-- урон
target:TakeDamage(DAMAGE)
-- конец функции
end)Где: ServerStorage → Shop → HealingPotion · Script
Зачем: Лечение.
-- удаляет объекты через время
local Debris = game:GetService("Debris")
-- зелье
local tool = script.Parent
-- сколько лечит
local HEAL = 50
-- клик — выпить
tool.Activated:Connect(function()
-- кто пьёт
local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
-- погибшим не помочь
if not humanoid or humanoid.Health <= 0 then return end
-- +50, но не больше максимума
humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
-- звук глотка
local sound = tool.Handle:FindFirstChild("Drink")
-- звук есть
if sound and humanoid.RootPart then
-- переносим в игрока — иначе пропадёт вместе с зельем
sound.Parent = humanoid.RootPart
-- буль
sound:Play()
-- потом удалим
Debris:AddItem(sound, 3)
-- конец проверки
end
-- зелье выпито
tool:Destroy()
-- конец функции
end)Где: ServerStorage → Shop → InvisiblePotion · Script
Зачем: Невидимость.
-- здесь скрипт доработает после того, как зелье пропадёт из рук
local SSS = game:GetService("ServerScriptService")
-- зелье
local tool = script.Parent
-- секунд невидимости
local DURATION = 5
-- уже выпито
local used = false
-- плавно меняет прозрачность
local function Fade(saved, toInvisible)
-- 10 шагов
for i = 1, 10 do
-- доля невидимости
local k = toInvisible and i / 10 or 1 - i / 10
-- каждая деталь
for obj, base in pairs(saved) do
-- от своей прозрачности к 1
if obj.Parent then obj.Transparency = base + (1 - base) * k end
-- конец перебора
end
-- полсекунды на всё
task.wait(0.05)
-- конец цикла
end
-- конец функции
end
-- клик — выпить
tool.Activated:Connect(function()
-- уже
if used then return end
-- кто пьёт
local character = tool.Parent
-- Humanoid
local humanoid = character:FindFirstChildOfClass("Humanoid")
-- погибший не пьёт
if not humanoid or humanoid.Health <= 0 then return end
-- выпито
used = true
-- убираем из рук, но скрипт продолжает работать
tool.Parent = SSS
-- деталь → её прозрачность
local saved = {}
-- всё в персонаже
for _, obj in ipairs(character:GetDescendants()) do
-- видимые детали и лицо
if (obj:IsA("BasePart") or obj:IsA("Decal")) and obj.Name ~= "HumanoidRootPart" then
-- запоминаем
saved[obj] = obj.Transparency
-- конец проверки
end
-- конец перебора
end
-- противники перестают видеть
character:SetAttribute("Invisible", true)
-- исчезаем
Fade(saved, true)
-- ждём
task.wait(DURATION)
-- появляемся
Fade(saved, false)
-- снова виден
character:SetAttribute("Invisible", nil)
-- теперь зелье можно удалить
tool:Destroy()
-- конец функции
end)Где: Workspace → Trader · Script
Зачем: Открывает магазин.
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- открыть магазин
local shopEvent = RS:WaitForChild("Events"):WaitForChild("ShopEvent")
-- «Торговать»
local prompt = script.Parent:WaitForChild("Head"):WaitForChild("ProximityPrompt")
-- игрок нажал E
prompt.Triggered:Connect(function(player)
-- только у него откроется окно
shopEvent:FireClient(player)
-- конец функции
end)Где: StarterGui → ShopGui · LocalScript
Зачем: Окно магазина.
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- события
local events = RS:WaitForChild("Events")
-- открыть
local shopEvent = events:WaitForChild("ShopEvent")
-- купить и получить ответ
local buyFunction = events:WaitForChild("BuyFunction")
-- окно
local frame = script.Parent:WaitForChild("ShopFrame")
-- ответ магазина
local message = frame:WaitForChild("Message")
-- торговец позвал
shopEvent.OnClientEvent:Connect(function()
-- без старых сообщений
message.Text = ""
-- открываем
frame.Visible = true
-- конец функции
end)
-- «Закрыть» — подписываемся один раз
frame:WaitForChild("CloseButton").MouseButton1Click:Connect(function()
-- закрываем
frame.Visible = false
-- конец функции
end)
-- кнопки товаров
for _, button in ipairs(frame:WaitForChild("Items"):GetChildren()) do
-- только кнопки
if button:IsA("TextButton") then
-- нажатие
button.MouseButton1Click:Connect(function()
-- имя кнопки = имя товара; ждём ответ
local _, text = buyFunction:InvokeServer(button.Name)
-- «Куплено» или «Не хватает монет»
message.Text = text
-- конец функции
end)
-- конец проверки
end
-- конец перебора
endГде: ServerScriptService · Script
Зачем: Покупка.
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- товары лежат здесь — клиент их не видит и не подменит
local SS = game:GetService("ServerStorage")
-- покупка
local buyFunction = RS:WaitForChild("Events"):WaitForChild("BuyFunction")
-- товары
local shop = SS:WaitForChild("Shop")
-- клиент хочет купить
buyFunction.OnServerInvoke = function(player, itemName)
-- пришло не имя
if typeof(itemName) ~= "string" then return false, "Ошибка" end
-- товар
local item = shop:FindFirstChild(itemName)
-- его цена
local price = item and item:FindFirstChild("Price")
-- монеты игрока
local stats = player:FindFirstChild("leaderstats")
-- рюкзак
local backpack = player:FindFirstChild("Backpack")
-- что-то не так
if not price or not stats or not backpack then return false, "Такого товара нет" end
-- денег мало
if stats.Coins.Value < price.Value then
-- ответ
return false, "Не хватает монет: нужно " .. price.Value
-- конец проверки
end
-- списываем
stats.Coins.Value -= price.Value
-- товар в рюкзак
item:Clone().Parent = backpack
-- ответ
return true, "Куплено!"
-- конец функции
endГде: ServerScriptService · Script
Зачем: День и ночь.
-- освещение
local Lighting = game:GetService("Lighting")
-- фонари
local lights = workspace:WaitForChild("Lights")
-- часов игрового времени за секунду: сутки = 8 минут
local STEP = 0.05
-- бесконечно
while true do
-- время идёт; после 24 — снова 0
Lighting.ClockTime = (Lighting.ClockTime + STEP) % 24
-- с 18 до 6 — ночь
local night = Lighting.ClockTime >= 18 or Lighting.ClockTime < 6
-- всё в фонарях
for _, light in ipairs(lights:GetDescendants()) do
-- ночью свет горит
if light:IsA("Light") then light.Enabled = night end
-- конец перебора
end
-- секунда
task.wait(1)
-- конец цикла
endГде: StarterGui → HintGui · LocalScript
Зачем: Подсказки.
-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- зоны подсказок
local hints = workspace:WaitForChild("Hints")
-- надпись
local label = script.Parent:WaitForChild("HintLabel")
-- секунд на экране
local SHOW_TIME = 4
-- какие подсказки уже были
local shown = {}
-- следим за одной зоной
local function Watch(trigger)
-- касание
trigger.Touched:Connect(function(hit)
-- уже было или это не мы
if shown[trigger] or hit.Parent ~= player.Character then return end
-- StringValue с текстом
local text = trigger:FindFirstChild("Text")
-- текста нет
if not text then return end
-- больше не показываем
shown[trigger] = true
-- текст
label.Text = text.Value
-- показываем
label.Visible = true
-- через 4 секунды
task.delay(SHOW_TIME, function()
-- прячем, если не появилась другая
if label.Text == text.Value then label.Visible = false end
-- конец функции
end)
-- конец функции
end)
-- конец функции
end
-- все зоны
for _, trigger in ipairs(hints:GetChildren()) do Watch(trigger) end
-- и новые
hints.ChildAdded:Connect(Watch)Запусти Play (или Run) и убедись: