Roblox · Хоррор · урок 7

Общий: вся игра целиком

Итог курса на одной странице: полная структура хоррора и финальные версии всех скриптов. Удобно сверить свою игру или собрать её заново.

Структура

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

Lighting — ClockTime 0, туман, Atmosphere, ColorCorrection, Bloom, Blur
Workspace — StreamingEnabled выкл.
├─ Corridor (Model) — пол, стены, потолок, лампы
├─ Eyes (Model)
├─ EnvironmentTrigger (Part) — Footsteps, EnvironmentTrigger
├─ ShadowTrigger (Part) — ShadowTrigger
├─ JumpscareTrigger (Part)
├─ MonsterSpawn (Part)
├─ MonsterTrigger (Part) — MonsterTrigger
├─ SafeZone (Part) — PointLight, LightTrigger
└─ Dust (Part) — ParticleEmitter
ServerStorage
├─ Shadow (Model) — Sound
└─ Monster (Model) — MonsterAnim
SoundService — Scream, Ambient
ServerScriptService
└─ ScreamScript (Script)
StarterPlayer → StarterCharacterScripts
└─ RunScript, CrouchScript (LocalScript) — RunAnim, CrouchAnim
StarterGui
└─ JumpscareGui (ScreenGui) — Face, Scream, JumpscareLocal

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Триггеры — невидимые детали: Transparency 1, CanCollide выкл., Anchored. JumpscareTrigger лежит прямо в Workspace.
  3. Образцы (Shadow, Monster) — в ServerStorage: на карте их нет, пока скрипт не достанет копию.
  4. Звуки, которые должны слышать все, — в SoundService или в деталях Workspace; из ServerScriptService звук не слышен.
  5. Все SoundId и картинка скримера пустые — вставь свои из Toolbox.
  6. Workspace → StreamingEnabled выкл., StarterPlayer → EnableMouseLockOption выкл.

Скрипты

RunScript

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Бег на Shift.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- скрипт лежит в персонаже
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- проигрывает анимации
local animator = humanoid:WaitForChild("Animator")
-- скорость бега
local RUN_SPEED = 25
-- обычная скорость
local WALK_SPEED = 16

-- Animation внутри скрипта
local runAnim = script:WaitForChild("RunAnim")
-- дорожка анимации (если есть своя)
local runTrack = nil
-- ID вставлен
if runAnim.AnimationId ~= "" then
    -- загружаем
    runTrack = animator:LoadAnimation(runAnim)
    -- важнее обычной ходьбы
    runTrack.Priority = Enum.AnimationPriority.Movement
    -- по кругу
    runTrack.Looped = true
-- конец проверки
end
-- сейчас бежим?
local running = false

-- включает и выключает анимацию бега
local function UpdateAnim()
    -- своей анимации нет — бежим стандартной
    if not runTrack then return end
    -- персонаж двигается
    local moving = humanoid.MoveDirection.Magnitude > 0
    -- бежим, а анимация не играет
    if running and moving and not runTrack.IsPlaying then
        -- включаем плавно
        runTrack:Play(0.2)
    -- остановились
    elseif (not running or not moving) and runTrack.IsPlaying then
        -- выключаем
        runTrack:Stop(0.2)
    -- конец проверки
    end
-- конец функции
end

-- начать или закончить бег
local function SetRunning(value)
    -- погибшие не бегают
    if humanoid.Health <= 0 then return end
    -- сидим? (ставит скрипт приседания)
    local crouching = character:GetAttribute("Crouching")
    -- сидя не бегаем
    running = value and not crouching
    -- скоростью сидя управляет другой скрипт
    if not crouching then
        -- бег или шаг
        humanoid.WalkSpeed = running and RUN_SPEED or WALK_SPEED
    -- конец проверки
    end
    -- анимация
    UpdateAnim()
-- конец функции
end

-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- левый Shift, и не в чате
    if not processed and input.KeyCode == Enum.KeyCode.LeftShift then
        -- бежим
        SetRunning(true)
    -- конец проверки
    end
-- конец функции
end)
-- отпустили клавишу
UIS.InputEnded:Connect(function(input)
    -- это Shift
    if input.KeyCode == Enum.KeyCode.LeftShift then
        -- идём шагом
        SetRunning(false)
    -- конец проверки
    end
-- конец функции
end)
-- встали или пошли — обновляем анимацию
humanoid:GetPropertyChangedSignal("MoveDirection"):Connect(UpdateAnim)
-- присели во время бега
character:GetAttributeChangedSignal("Crouching"):Connect(function()
    -- бег прекращается
    if character:GetAttribute("Crouching") then SetRunning(false) end
-- конец функции
end)

CrouchScript

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Присед на C.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- анимации
local animator = humanoid:WaitForChild("Animator")
-- скорость сидя
local CROUCH_SPEED = 6
-- обычная скорость
local WALK_SPEED = 16

-- Animation внутри скрипта
local crouchAnim = script:WaitForChild("CrouchAnim")
-- дорожка анимации
local track = nil
-- своя анимация есть
if crouchAnim.AnimationId ~= "" then
    -- загружаем
    track = animator:LoadAnimation(crouchAnim)
    -- поверх ходьбы и стояния
    track.Priority = Enum.AnimationPriority.Action
    -- по кругу
    track.Looped = true
-- конец проверки
end
-- сидим?
local crouching = false

-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- нужна C и не в чате
    if processed or input.KeyCode ~= Enum.KeyCode.C then return end
    -- погибшие не приседают
    if humanoid.Health <= 0 then return end
    -- C переключает: сесть / встать
    crouching = not crouching
    -- сообщаем скрипту бега
    character:SetAttribute("Crouching", crouching)
    -- сели
    if crouching then
        -- медленно
        humanoid.WalkSpeed = CROUCH_SPEED
        -- камера ниже — видно и без анимации
        humanoid.CameraOffset = Vector3.new(0, -1.5, 0)
        -- анимация приседа
        if track then track:Play(0.2) end
    -- встали
    else
        -- обычная скорость
        humanoid.WalkSpeed = WALK_SPEED
        -- камера на место
        humanoid.CameraOffset = Vector3.zero
        -- выключаем анимацию
        if track then track:Stop(0.2) end
    -- конец проверки
    end
-- конец функции
end)

ShadowTrigger

Где: Workspace → ShadowTrigger · Script
Зачем: Тень.

-- здесь лежит тень
local SS = game:GetService("ServerStorage")
-- игроки
local Players = game:GetService("Players")
-- невидимая зона
local trigger = script.Parent
-- сколько секунд тень стоит
local SHOW_TIME = 5
-- срабатывает один раз за игру
local used = false

-- касание зоны
trigger.Touched:Connect(function(hit)
    -- уже было или это не игрок
    if used or not Players:GetPlayerFromCharacter(hit.Parent) then return end
    -- больше не срабатывает
    used = true
    -- копия тени — на том месте, где её построили
    local shadow = SS:WaitForChild("Shadow"):Clone()
    -- появляется
    shadow.Parent = workspace
    -- звук внутри тени
    local sound = shadow:FindFirstChildWhichIsA("Sound", true)
    -- шёпот
    if sound then sound:Play() end
    -- стоит
    task.wait(SHOW_TIME)
    -- исчезает
    shadow:Destroy()
-- конец функции
end)

ScreamScript

Где: ServerScriptService · Script
Зачем: Крик вдалеке.

-- звуки отсюда слышат все игроки
local SoundService = game:GetService("SoundService")
-- крик
local scream = SoundService:WaitForChild("Scream")
-- не раньше чем через 45 секунд
local MIN_TIME = 45
-- и не позже чем через 90
local MAX_TIME = 90

-- бесконечно
while true do
    -- случайная пауза — нельзя привыкнуть
    task.wait(math.random(MIN_TIME, MAX_TIME))
    -- крик где-то вдалеке
    scream:Play()
-- конец цикла
end

EnvironmentTrigger

Где: Workspace → EnvironmentTrigger · Script
Зачем: Глаза и шаги.

-- игроки
local Players = game:GetService("Players")
-- зона
local trigger = script.Parent
-- глаза в темноте
local eyes = workspace:WaitForChild("Eyes")
-- шаги
local footsteps = trigger:WaitForChild("Footsteps")
-- один раз
local used = false

-- касание
trigger.Touched:Connect(function(hit)
    -- уже было или не игрок
    if used or not Players:GetPlayerFromCharacter(hit.Parent) then return end
    -- больше не срабатывает
    used = true
    -- глаза пропали
    eyes:Destroy()
    -- и кто-то пошёл
    footsteps:Play()
-- конец функции
end)

JumpscareLocal

Где: StarterGui → JumpscareGui · LocalScript
Зачем: Скример.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- JumpscareGui
local gui = script.Parent
-- звук скримера
local scream = gui:WaitForChild("Scream")
-- зона
local trigger = workspace:WaitForChild("JumpscareTrigger")
-- сколько секунд картинка на экране
local SHOW_TIME = 1.5
-- каждый игрок пугается один раз
local used = false

-- касание зоны
trigger.Touched:Connect(function(hit)
    -- уже было или это не мы
    if used or hit.Parent ~= player.Character then return end
    -- больше не срабатывает
    used = true
    -- картинка на весь экран
    gui.Enabled = true
    -- крик
    scream:Play()
    -- держим
    task.wait(SHOW_TIME)
    -- прячем, но не удаляем
    gui.Enabled = false
-- конец функции
end)

MonsterAnim

Где: ServerStorage → Monster · Script
Зачем: Ходьба монстра.

-- модель монстра
local monster = script.Parent
-- Humanoid
local humanoid = monster:WaitForChild("Humanoid")
-- Animator (создаём, если нет)
local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
-- стандартная ходьба R15; для R6 — 180426354
local WALK_ID = "rbxassetid://507777826"

-- анимация
local walk = Instance.new("Animation")
-- какая
walk.AnimationId = WALK_ID
-- загружаем
local track = animator:LoadAnimation(walk)
-- по кругу
track.Looped = true

-- скорость монстра изменилась
humanoid.Running:Connect(function(speed)
    -- пошёл
    if speed > 0.5 and not track.IsPlaying then
        -- шагаем
        track:Play()
    -- остановился
    elseif speed <= 0.5 and track.IsPlaying then
        -- стоим
        track:Stop()
    -- конец проверки
    end
-- конец функции
end)

MonsterTrigger

Где: Workspace → MonsterTrigger · Script
Зачем: Погоня.

-- образец монстра
local SS = game:GetService("ServerStorage")
-- игроки
local Players = game:GetService("Players")
-- зона
local trigger = script.Parent
-- где появляется монстр
local spawnPoint = workspace:WaitForChild("MonsterSpawn")
-- ближе — поймал
local KILL_DISTANCE = 5
-- монстр уже гонится
local active = false

-- погоня за игроком
local function Chase(monster, player)
    -- Humanoid монстра
    local mHumanoid = monster:WaitForChild("Humanoid")
    -- центр монстра
    local mRoot = monster:WaitForChild("HumanoidRootPart")
    -- движение считает сервер — без рывков
    mRoot:SetNetworkOwner(nil)
    -- пока монстр в игре
    while monster.Parent and mHumanoid.Health > 0 do
        -- персонаж цели
        local character = player.Character
        -- её Humanoid
        local humanoid = character and character:FindFirstChildOfClass("Humanoid")
        -- её центр
        local root = character and character:FindFirstChild("HumanoidRootPart")
        -- цель погибла или ушла — конец погони
        if not humanoid or not root or humanoid.Health <= 0 then break end
        -- бежим к игроку
        mHumanoid:MoveTo(root.Position)
        -- догнал
        if (root.Position - mRoot.Position).Magnitude <= KILL_DISTANCE then
            -- поймал
            humanoid.Health = 0
            -- конец погони
            break
        -- конец проверки
        end
        -- 10 раз в секунду
        task.wait(0.1)
    -- конец цикла
    end
    -- монстр ещё секунду-две стоит
    task.wait(2)
    -- и исчезает
    monster:Destroy()
-- конец функции
end

-- касание зоны
trigger.Touched:Connect(function(hit)
    -- монстр уже бежит
    if active then return end
    -- кто коснулся
    local player = Players:GetPlayerFromCharacter(hit.Parent)
    -- образец
    local sample = SS:FindFirstChild("Monster")
    -- не игрок или монстра нет
    if not player or not sample then return end
    -- занято
    active = true
    -- копия
    local monster = sample:Clone()
    -- на точку появления
    monster:PivotTo(spawnPoint.CFrame)
    -- в игру
    monster.Parent = workspace
    -- гонимся (ждём, пока закончится)
    Chase(monster, player)
    -- можно снова
    active = false
-- конец функции
end)

LightTrigger

Где: Workspace → SafeZone · Script
Зачем: Свет прогоняет монстра.

-- игроки
local Players = game:GetService("Players")
-- светлая зона
local trigger = script.Parent

-- касание
trigger.Touched:Connect(function(hit)
    -- только игрок
    if not Players:GetPlayerFromCharacter(hit.Parent) then return end
    -- монстр в игре?
    local monster = workspace:FindFirstChild("Monster")
    -- свет его прогоняет — погоня закончится
    if monster then monster:Destroy() end
-- конец функции
end)

Проверка

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

Собрать всю игру одной командой