Программирование в Roblox Studio: Создание игр на Lua с нуля

Этот курс поможет вам освоить язык Lua и научиться создавать полноценные игры в Roblox Studio. Вы пройдете путь от базовых скриптов до разработки сложных механик, интерфейсов и визуальных эффектов, закрепляя знания на практике.

Основы программирования на Lua: переменные, функции и циклы

Приветствую! В прошлый раз мы успешно освоились в интерфейсе Roblox Studio, научились перемещать объекты и настраивать их свойства вручную. Но настоящая магия создания игр начинается тогда, когда мир оживает без вашего прямого участия. Двери, которые открываются при приближении игрока, монеты, которые исчезают при сборе, и таймеры, отсчитывающие время до конца раунда — всё это работает благодаря коду.

Сегодня мы заложим фундамент вашего пути в программировании. Мы разберем три кита, на которых держится логика любой игры на языке Lua: переменные, функции и циклы.

Переменные: память вашей игры

Представьте, что вы переезжаете в новую квартиру. Чтобы не потерять вещи, вы складываете их в картонные коробки и подписываете маркером: «Посуда», «Книги», «Одежда». В программировании переменная — это такая же подписанная коробка, только хранится в ней не одежда, а информация.

Переменные позволяют игре «запоминать» данные: сколько у игрока здоровья, как зовут его питомца или открыт ли сундук с сокровищами.

В Lua создание переменной выглядит так:

local playerName = "Hero"
local health = 100
local isAlive = true

Давайте разберем этот код:

  1. Слово local говорит движку: «Создай новую коробку прямо здесь и сейчас». Использование локальных переменных — это правило хорошего тона, которое защищает ваш код от путаницы.
  2. playerName, health, isAlive — это названия наших коробок. Мы придумываем их сами. Главное правило — название должно отражать суть того, что внутри.
  3. Знак = означает не математическое равенство, а команду «положить в коробку». Мы берем значение справа и кладем его в переменную слева.

Типы данных

В наши «коробки» можно класть разные типы информации. В Lua есть три основных типа данных, с которыми вы будете работать 90% времени:

Тип данных Название в Lua Пример Зачем нужен в игре
Число Number 10, 3.14, -5 Подсчет очков, координат, таймеров, урона.
Строка String "Привет", "Меч" Текст на экране, имена игроков, названия предметов. Обязательно пишется в кавычках.
Логический Boolean true, false Состояния: жив/мертв, день/ночь, дверь открыта/закрыта.

Программирование — это искусство управления данными. Если вы понимаете, какие данные у вас есть и как они меняются, вы уже наполовину написали игру.

Билл Гейтс

Мысленный эксперимент: Представьте, что вы создаете систему инвентаря. Какие переменные вам понадобятся для одного яблока? Вероятно, строка для названия ("Яблоко"), число для силы исцеления (15) и число для веса предмета (0.5).

Функции: действия и механизмы

Если переменные — это существительные нашего игрового мира (имя, здоровье, скорость), то функции — это глаголы (прыгнуть, получить урон, открыть инвентарь).

Функция — это изолированный блок кода, который выполняет конкретную задачу. Вы можете написать этот блок один раз, а затем вызывать его сотни раз в разных частях игры.

Аналогия из реальной жизни: автомат по продаже кофе. Вы не знаете, как именно он мелет зерна и кипятит воду. Вы просто даете ему монету и нажимаете кнопку (передаете данные), а он выдает вам стаканчик капучино (возвращает результат).

Синтаксис функции в Lua выглядит так:

local function takeDamage(currentHealth, damageAmount)
    local newHealth = currentHealth - damageAmount
    return newHealth
end

local myHealth = 100
myHealth = takeDamage(myHealth, 20)
print(myHealth) -- Выведет 80

Разберем анатомию этого механизма:

  • local function — объявление о том, что мы строим новый механизм.
  • takeDamage — имя функции.
  • (currentHealth, damageAmount) — это параметры. Те самые «монетки», которые мы бросаем в автомат. Это данные, необходимые функции для работы.
  • return — команда выдачи результата. Функция отдает нам вычисленное значение, чтобы мы могли сохранить его в переменную.

Если текущее здоровье игрока hh, а урон от ловушки dd, то новое здоровье вычисляется по формуле hnew=h−dh_{new} = h - d. В нашем примере h=100h = 100, а d=20d = 20, поэтому после выполнения функции здоровье становится равным 80.

Сократовский вопрос: Как вы думаете, что произойдет, если мы вызовем функцию takeDamage, но забудем передать ей значение урона? Автомат сломается, потому что попытается вычесть пустоту из числа. Поэтому всегда следите за тем, какие данные ожидает ваша функция.

Циклы: автоматизация рутины

Представьте, что вам нужно построить лестницу из 100 ступенек в Roblox Studio. Вы можете вручную скопировать и вставить блок 100 раз, каждый раз сдвигая его чуть выше. Это долго и скучно.

Для таких задач существуют циклы — конструкции, которые заставляют код повторяться определенное количество раз или до выполнения заданного условия.

Цикл for (для точного количества повторений)

Цикл for идеально подходит, когда вы заранее знаете, сколько раз нужно выполнить действие.

for i = 1, 5 do
    print("Создаем ступеньку номер " .. i)
end

Здесь переменная i (от слова index) — это счетчик. Он начинает с 11 и увеличивается на единицу с каждым кругом, пока не достигнет 55. Код внутри цикла выполнится ровно пять раз.

Цикл while (пока условие истинно)

Цикл while работает иначе. Он повторяет код до тех пор, пока определенное условие остается правдивым (true). Это похоже на бег на беговой дорожке: вы бежите пока у вас есть силы.

local countdown = 3

while countdown > 0 do
    print("Осталось: " .. countdown)
    countdown = countdown - 1
    task.wait(1) -- Ждем 1 секунду
end

print("Старт!")

В этом примере цикл проверяет математическое условие: countdown>0countdown > 0. Как только переменная станет равна нулю, условие превратится в ложь (false), и цикл остановится.

Критически важное правило для Roblox: Внутри цикла while всегда должна быть пауза, например task.wait(). Если вы заставите игру бесконечно считать без пауз, процессор попытается выполнить миллионы операций за долю секунды, и Roblox Studio просто зависнет.

Объединяем знания: создаем механику отравления

Давайте посмотрим, как переменные, функции и циклы работают вместе в реальном игровом сценарии. Напишем логику ядовитого болота: когда игрок наступает в него, он получает урон каждую секунду в течение 5 секунд.

-- 1. Переменные (настройки яда)
local poisonDamage = 5
local poisonDuration = 5

-- 2. Функция (механизм отравления)
local function applyPoison(playerHealth)
    print("Игрок отравлен!")

    -- 3. Цикл (постепенный урон)
    for i = 1, poisonDuration do
        playerHealth = playerHealth - poisonDamage
        print("Здоровье: " .. playerHealth)
        task.wait(1) -- Пауза в 1 секунду между тиками урона
    end

    print("Действие яда закончилось.")
    return playerHealth
end

-- Запускаем сценарий
local myHealth = 100
myHealth = applyPoison(myHealth)

Проследите за логикой: мы сохранили настройки в переменные, передали здоровье в функцию, а внутри функции использовали цикл for, чтобы отнять здоровье 5 раз с интервалом в секунду. В конце функция вернула обновленное здоровье.

Подведение итогов

Сегодня мы сделали огромный шаг от визуального редактора к написанию реальной логики:

  • Переменные хранят состояния (числа, строки, логические значения).
  • Функции группируют команды в удобные механизмы, принимающие параметры и возвращающие результат.
  • Циклы избавляют нас от рутины, повторяя действия заданное число раз (for) или пока выполняется условие (while).

Ваше комплексное задание для размышления: Подумайте, как с помощью изученных сегодня инструментов создать систему квестов. У вас должна быть переменная для хранения количества собранных волшебных камней. Вам понадобится функция collectStone(), которая будет увеличивать эту переменную. И, возможно, цикл while, который будет проверять, не достигло ли количество камней нужной отметки (например, 10), чтобы выдать игроку награду.

В следующей статье мы научимся привязывать этот код к реальным объектам в мире Roblox: заставим блоки менять цвет при касании и создавать эффекты. До встречи в студии!

Взаимодействие с объектами: свойства, события и обработка касаний

Приветствую! В прошлой статье мы разобрали фундамент языка Lua: научились сохранять данные в переменные, упаковывать действия в функции и автоматизировать рутину с помощью циклов. Но пока наш код существовал в вакууме. Игровой мир сам по себе, а скрипты — сами по себе.

Сегодня мы соединим эти два измерения. Вы узнаете, как заставить код управлять 3D-объектами, реагировать на действия игрока и создавать настоящие интерактивные механики. Мы разберем концепции свойств и событий, а также напишем классические скрипты: смертельную лаву и исчезающую монетку.

Свойства: ДНК игровых объектов

Каждый объект в Roblox Studio, будь то обычный блок (Part), источник света или сам игрок, обладает набором характеристик. В программировании они называются свойствами (Properties).

Аналогия из реальной жизни: представьте себе автомобиль. У него есть цвет (красный), состояние дверей (закрыты), скорость (0 км/ч) и масса (1500 кг). Если вы перекрасите машину, она останется той же самой машиной, но её свойство «цвет» изменится.

В Roblox Studio вы уже меняли свойства вручную через окно Properties. Теперь мы сделаем это с помощью кода. Чтобы изменить свойство объекта, нужно указать путь к этому объекту, поставить точку, написать имя свойства и присвоить ему новое значение.

-- Находим наш блок в игровом мире (Workspace)
local myBlock = game.Workspace.MagicBlock

-- Меняем его свойства
myBlock.Transparency = 0.5 -- Делаем блок полупрозрачным (от 0 до 1)
myBlock.CanCollide = false -- Отключаем физическое столкновение (игрок пройдет сквозь него)
myBlock.BrickColor = BrickColor.new("Bright red") -- Красим в красный

Основные свойства объекта Part

Свойство Тип данных Описание Пример использования
Transparency Число Прозрачность объекта от 00 (непрозрачный) до 11 (невидимый). Создание невидимых стен или исчезающих платформ.
CanCollide Логический Определяет, могут ли другие объекты проходить сквозь этот блок. Секретные двери, голограммы.
Anchored Логический Закрепляет объект в пространстве, игнорируя гравитацию. Парящие острова, зависшие в воздухе монеты.
Material Перечисление Текстура поверхности (дерево, неон, лед). Изменение внешнего вида при активации механизма.

Мысленный эксперимент: Представьте, что вы делаете игру про призрака. Какие свойства персонажа вам нужно изменить при нажатии кнопки «Стать бесплотным»? Вероятно, вам потребуется установить Transparency = 0.8 и CanCollide = false.

События: Слушатели игрового мира

Изменять свойства напрямую — это здорово, но игра должна реагировать на действия. Как скрипт узнает, что игрок наступил на мину или нажал на кнопку?

Для этого существуют события (Events).

Событие — это сигнал, который объект отправляет в систему, когда с ним что-то происходит. Вы можете привязать свою функцию к этому сигналу. Как только сигнал прозвучит, функция автоматически выполнится.

Событийно-ориентированное программирование похоже на работу мышеловки. Пружина (функция) взведена и ждет. Она не тратит энергию просто так. Но как только мышь касается педали (событие), механизм мгновенно срабатывает.

Чтобы соединить событие и функцию, в Lua используется метод Connect (подключить).

Обработка касаний: Событие Touched

Самое популярное и важное событие в Roblox — это Touched (Коснулся). Оно срабатывает каждый раз, когда физические границы одного объекта пересекаются с границами другого.

Давайте напишем скрипт, который выводит сообщение в консоль, когда кто-то касается нашего блока. Для этого создайте обычный Part в Workspace, добавьте в него Script и напишите следующий код:

local trapBlock = script.Parent -- Обращаемся к блоку, внутри которого лежит скрипт

local function onTouch(hit)
    print("Блока коснулся объект: " .. hit.Name)
end

trapBlock.Touched:Connect(onTouch)

Разберем анатомию этого механизма:

  1. script.Parent — это способ скрипта сказать «мой родитель». Скрипт лежит внутри блока, значит, родитель — это сам блок.
  2. onTouch — наша функция-реакция.
  3. hit — это параметр, который движок Roblox автоматически передает в нашу функцию. hit содержит информацию о том, какая именно деталь коснулась нашего блока (например, левая нога игрока или брошенный мяч).
  4. trapBlock.Touched:Connect(onTouch) — мы говорим: «Когда блок trapBlock испытает событие Touched, запусти функцию onTouch».

Сократовский вопрос: Если вы бросите обычный кубик на нашу ловушку, сработает ли событие Touched? Да, сработает. Событие реагирует на любые физические объекты, а не только на игроков. И это подводит нас к следующей важной задаче.

Идентификация игрока: Поиск Humanoid

Поскольку Touched реагирует на всё подряд, нам нужно научить скрипт отличать игрока от катящегося камня.

В Roblox персонаж игрока — это не один объект, а целая модель, состоящая из рук, ног, головы и специального невидимого компонента под названием Humanoid. Именно Humanoid хранит здоровье игрока и управляет его передвижением.

Когда нога игрока (например, LeftFoot) касается блока, переменная hit указывает именно на ногу. Чтобы найти Humanoid, нам нужно подняться на уровень выше — к самой модели персонажа (hit.Parent), и поискать там.

Создадим классическую механику — блок с лавой, который наносит урон:

local lava = script.Parent
local damage = 20

local function dealDamage(hit)
    -- Ищем компонент Humanoid внутри родителя объекта, который нас коснулся
    local character = hit.Parent
    local humanoid = character:FindFirstChild("Humanoid")

    -- Если Humanoid найден, значит это живое существо
    if humanoid then
        humanoid:TakeDamage(damage) -- Наносим урон
        print("Игрок получил урон!")
    end
end

lava.Touched:Connect(dealDamage)

Метод FindFirstChild("Имя") — это безопасный способ проверить, существует ли объект. Если Humanoid есть, код внутри условия if выполнится. Если камень коснется лавы, Humanoid не будет найден, и скрипт просто проигнорирует касание.

Проблема множественных срабатываний: Debounce

Если вы протестируете скрипт лавы из предыдущего примера, вы заметите странность: игрок погибает мгновенно, хотя урон всего 2020 единиц из 100100.

Почему так происходит? Физический движок Roblox обновляется десятки раз в секунду. Пока нога игрока проходит сквозь блок лавы, событие Touched успевает сработать 10-20 раз за одну секунду. Урон наносится снова и снова.

Чтобы решить эту проблему, программисты используют паттерн Debounce (защита от дребезга).

Аналогия: турникет в метро. Когда вы прикладываете билет, турникет открывается (действие выполнено), но затем он блокируется на пару секунд, чтобы по одному билету не прошла толпа.

Реализуем турникет в коде с помощью логической переменной:

local healPad = script.Parent
local healAmount = 50
local canHeal = true -- Наш "турникет" изначально открыт

local function applyHeal(hit)
    local humanoid = hit.Parent:FindFirstChild("Humanoid")

    -- Проверяем, есть ли Humanoid И открыт ли турникет
    if humanoid and canHeal then
        canHeal = false -- Мгновенно закрываем турникет

        humanoid.Health = humanoid.Health + healAmount
        healPad.BrickColor = BrickColor.new("Black") -- Меняем цвет для визуальной отдачи

        task.wait(3) -- Ждем 3 секунды (перезарядка)

        healPad.BrickColor = BrickColor.new("Bright green") -- Возвращаем цвет
        canHeal = true -- Снова открываем турникет
    end
end

healPad.Touched:Connect(applyHeal)

Теперь, как только игрок касается лечебной платформы, переменная canHeal становится false. Все последующие касания в течение следующих 3 секунд будут проигнорированы, потому что условие if humanoid and canHeal не выполнится.

Подведение итогов

Сегодня мы оживили наш игровой мир:

  • Свойства позволяют менять физические и визуальные характеристики объектов прямо во время игры.
  • События (такие как Touched) служат триггерами, которые запускают наши функции в ответ на изменения в мире.
  • Метод FindFirstChild помогает безопасно определять, с чем именно взаимодействует наш объект.
  • Паттерн Debounce критически важен для контроля частоты срабатывания событий.

Ваше комплексное задание для размышления: Подумайте, как объединить знания из этой и прошлой статьи для создания монетки. Монетка должна реагировать на касание (Touched), проверять, игрок ли это (FindFirstChild), увеличивать счетчик в переменной, а затем уничтожаться (метод Destroy()), чтобы её нельзя было собрать дважды.

В следующей статье мы выйдем за пределы серверных скриптов и познакомимся с пользовательским интерфейсом (GUI). Вы узнаете, как выводить количество собранных монет прямо на экран игрока. Увидимся!

Пользовательский интерфейс: создание кнопок, меню и текстовых панелей

Пользовательский интерфейс: создание кнопок, меню и текстовых панелей

В прошлых уроках мы вдохнули жизнь в 3D-мир: научились управлять свойствами объектов и заставили их реагировать на касания с помощью события Touched. Игрок уже может прыгать по исчезающим платформам и получать урон от лавы. Но как сообщить ему, сколько у него осталось здоровья? Как дать возможность выбрать уровень или купить предмет?

Для этого нам нужно выйти за пределы трехмерного пространства и обратиться напрямую к экрану игрока. Сегодня мы разберем GUI (Graphical User Interface — графический пользовательский интерфейс) и научимся создавать интерактивные меню.

Анатомия интерфейса: от холста до кнопки

Создание интерфейса в Roblox Studio похоже на работу художника. Вы не можете просто бросить краску в воздух — вам нужен мольберт, холст и рамка.

В окне Explorer есть специальная папка StarterGui. Это наш «мольберт». Все элементы интерфейса, которые вы поместите сюда, будут автоматически скопированы на экран каждого игрока, зашедшего в игру.

Однако напрямую в StarterGui кнопки класть нельзя. Иерархия строится по строгим правилам:

  1. ScreenGui (Холст). Это невидимый слой, который накладывается поверх 3D-мира. Без него ни один элемент не отобразится на экране.
  2. Frame (Рамка/Панель). Контейнер для группировки элементов. Представьте себе окно инвентаря — это большой Frame, внутри которого лежат ячейки.
  3. UI-элементы (Краски). Видимые компоненты: текст, изображения и кнопки.

Основные видимые элементы

Элемент Назначение Пример использования
TextLabel Отображение статического текста. Счетчик монет, имя локации, подсказка на экране.
TextButton Кликабельная кнопка с текстом. Кнопка «Играть», «Купить», «Закрыть меню».
ImageLabel Отображение картинок. Иконка здоровья, логотип игры, аватар персонажа.
ImageButton Кликабельная картинка. Иконка рюкзака, при нажатии на которую открывается инвентарь.

Мысленный эксперимент: Вы хотите создать всплывающее окно, которое сообщает игроку о победе. В нем должен быть фон, надпись «Вы победили!» и кнопка «В главное меню». Как выстроится иерархия? Правильный ответ: StarterGui →\rightarrow ScreenGui →\rightarrow Frame (фон) →\rightarrow внутри Frame лежат TextLabel (надпись) и TextButton (кнопка).

Математика позиционирования: UDim2

Когда вы двигаете блок в 3D-мире, вы используете координаты XX, YY и ZZ. На плоском экране координаты всего две: XX (по горизонтали) и YY (по вертикали).

Но экраны у игроков разные. У одного — огромный монитор компьютера, у другого — маленький экран смартфона. Если вы скажете кнопке «отступи 500 пикселей слева», на мониторе она будет в центре, а на телефоне улетит за пределы экрана.

Для решения этой проблемы в Roblox используется система координат UDim2. Она состоит из четырех чисел: {Scale X, Offset X}, {Scale Y, Offset Y}.

  • Scale (Масштаб) — это процент от размера экрана, от 00 до 11. Значение 0.50.5 означает ровно половину экрана. Если ширина экрана 19201920 пикселей, то 1920×0.5=9601920 \times 0.5 = 960 пикселей.
  • Offset (Смещение) — это точное количество пикселей.

Чтобы ваш интерфейс не ломался на разных устройствах, всегда старайтесь использовать Scale для размеров и позиционирования, а Offset оставляйте равным нулю.

Клиент против Сервера: появление LocalScript

До этого момента мы писали код в обычных Script. Они выполняются на сервере — мощном компьютере Roblox, который просчитывает физику и правила игры для всех игроков одновременно.

Но интерфейс — это личное пространство игрока. Если вы откроете свой инвентарь, другие игроки не должны увидеть его на своих экранах. Поэтому для работы с GUI используется LocalScript (Локальный скрипт).

LocalScript выполняется на клиенте — то есть на устройстве самого игрока (на его телефоне или ПК).

Разница критически важна:

  • Обычный Script (Сервер): Управляет сменой дня и ночи, здоровьем босса, спавном монет. То, что общее для всех.
  • LocalScript (Клиент): Управляет нажатиями кнопок на экране, движениями камеры, анимацией интерфейса. То, что индивидуально.

Сократовский вопрос: Что произойдет, если вы попытаетесь обработать нажатие кнопки меню через обычный серверный Script? В лучшем случае код сработает с задержкой из-за пинга. В худшем — когда один игрок нажмет «Закрыть меню», меню закроется у всех игроков на сервере одновременно.

Оживляем кнопку: событие MouseButton1Click

Давайте создадим простое главное меню. Добавьте в StarterGui →\rightarrow ScreenGui →\rightarrow Frame. Растяните Frame на весь экран. Внутрь Frame поместите TextButton и назовите её PlayButton.

Теперь нам нужно, чтобы при нажатии на кнопку меню исчезало, позволяя игроку начать игру. Для этого добавьте LocalScript внутрь вашей кнопки PlayButton.

В 3D-мире мы использовали событие Touched. Для кнопок интерфейса главное событие — MouseButton1Click (Клик левой кнопкой мыши или тап по экрану телефона).

-- Находим саму кнопку, в которой лежит скрипт
local button = script.Parent

-- Находим родительскую рамку (наше меню), чтобы потом её скрыть
local menuFrame = button.Parent

-- Создаем функцию, которая будет выполнена при нажатии
local function startGame()
    -- Меняем свойство Visible (Видимость) у рамки на false
    menuFrame.Visible = false
    print("Игрок нажал кнопку 'Играть'!")
end

-- Подключаем функцию к событию клика
button.MouseButton1Click:Connect(startGame)

Разберем механику:

  1. script.Parent указывает на кнопку.
  2. button.Parent поднимается на уровень выше — к Frame.
  3. Свойство Visible принимает логическое значение (true или false). Отключая видимость Frame, мы автоматически прячем и всё, что лежит внутри него (включая саму кнопку).

Динамический текст: обновление информации

Кнопки позволяют игроку отправлять команды игре. А как игре отправить информацию игроку? Для этого используются текстовые панели (TextLabel).

Представьте, что вы хотите сделать счетчик кликов. Рядом с кнопкой создайте TextLabel и назовите его ClickDisplay.

Изменим наш LocalScript так, чтобы он не закрывал меню, а считал нажатия:

local button = script.Parent
-- Ищем TextLabel в той же папке (Frame), где лежит кнопка
local display = button.Parent:FindFirstChild("ClickDisplay")

local clicks = 0 -- Переменная для хранения количества кликов

local function addClick()
    clicks = clicks + 1 -- Увеличиваем счетчик на 1

    -- Обновляем свойство Text у текстовой панели
    -- Используем оператор .. для склеивания строки и числа
    display.Text = "Кликов: " .. clicks
end

button.MouseButton1Click:Connect(addClick)

Здесь мы используем свойство Text. Обратите внимание на оператор .. (две точки). В языке Lua он используется для конкатенации (склеивания) строк. Мы берем статичный текст "Кликов: " и приклеиваем к нему текущее значение переменной clicks. Если clicks равно 55, на экране появится надпись Кликов: 5.

Визуальный отклик: наведение мыши

Хороший интерфейс всегда дает игроку понять, что элемент интерактивен. Кнопка должна реагировать, когда мышь просто проходит над ней.

У кнопок есть два полезных события:

  • MouseEnter — срабатывает, когда курсор заходит на территорию кнопки.
  • MouseLeave — срабатывает, когда курсор покидает её.

Добавим немного интерактива в наш скрипт:

local button = script.Parent

-- Запоминаем оригинальный цвет кнопки
local defaultColor = button.BackgroundColor3
-- Создаем новый цвет для эффекта наведения (RGB формат)
local hoverColor = Color3.fromRGB(200, 200, 200)

local function onHover()
    button.BackgroundColor3 = hoverColor
end

local function onLeave()
    button.BackgroundColor3 = defaultColor
end

button.MouseEnter:Connect(onHover)
button.MouseLeave:Connect(onLeave)

Теперь, когда игрок наведет курсор на кнопку, она станет светло-серой, а когда уберет — вернет свой изначальный цвет. Это базовый принцип создания отзывчивого UI (User Interface).

Подведение итогов

Сегодня мы освоили важнейший слой разработки — взаимодействие с игроком через экран:

  • Интерфейс строится по иерархии: StarterGui →\rightarrow ScreenGui →\rightarrow Frame →\rightarrow Элементы.
  • Для позиционирования и изменения размеров используется система UDim2, где Scale отвечает за проценты от экрана, а Offset — за точные пиксели.
  • Код для интерфейса пишется исключительно в LocalScript, так как UI существует локально на устройстве каждого игрока.
  • Событие MouseButton1Click позволяет запускать функции при нажатии на TextButton или ImageButton.

Комплексное задание для размышления: Представьте, что вы делаете игру-кликер. У вас есть кнопка, при нажатии на которую игрок получает 11 монету. Текст на экране обновляется. Но как сделать так, чтобы за эти монеты можно было купить улучшение (например, +5+5 монет за клик)? Подумайте, какие переменные вам понадобятся и как условие if поможет проверить, хватает ли у игрока денег на покупку при нажатии на кнопку магазина.

В следующей статье мы свяжем наши знания воедино. Вы узнаете, как заставить LocalScript (интерфейс) общаться с серверным Script (игровым миром) с помощью технологии RemoteEvents. Это позволит нам создать полноценный магазин, где нажатие кнопки на экране будет выдавать реальный предмет в руки персонажу.

Игровые механики: программирование инвентаря и системы квестов

Игровые механики: программирование инвентаря и системы квестов

Представьте ситуацию: вы создали потрясающее меню магазина. Игрок нажимает на красивую кнопку «Купить меч», скрипт обрабатывает клик, меню закрывается... а в руках у персонажа ничего не появляется.

В прошлой статье мы научились создавать пользовательский интерфейс (UI) и работать с LocalScript. Но интерфейс — это лишь красивая витрина. Чтобы игра по-настоящему ожила, нам нужно связать нажатия кнопок на экране с реальными изменениями в 3D-мире. Сегодня мы создадим систему инвентаря и базовую механику квестов, объединив всё, что изучили ранее.

Проблема доверия: Клиент, Сервер и RemoteEvents

Прежде чем писать код инвентаря, нам нужно решить фундаментальную проблему многопользовательских игр — проблему доверия.

Как мы помним, LocalScript работает на устройстве игрока (на Клиенте). Игрок может легко взломать свой клиент с помощью сторонних программ.

Мысленный эксперимент: Что произойдет, если мы напишем код выдачи золота прямо в LocalScript кнопки? Хакер сможет искусственно отправить сигнал о нажатии этой кнопки миллион раз за секунду. Если клиент сам решает, сколько у него денег, хакер мгновенно станет самым богатым в игре.

Поэтому в Roblox действует золотое правило: Сервер не доверяет Клиенту.

Сервер (где работают обычные Script) — это строгий судья. Только он имеет право изменять здоровье, выдавать предметы или начислять опыт. Но как кнопке на Клиенте попросить Сервер выдать предмет?

Для этого используется специальный объект — RemoteEvent (Удаленное событие).

RemoteEvent — это защищенный канал связи, мост между устройством игрока и сервером игры.

Аналогия из реальной жизни

Представьте, что вы пришли в ресторан.

  • Вы — это Клиент (LocalScript).
  • Меню в ваших руках — это UI.
  • Кухня, где готовят еду — это Сервер (Script).

Вы не можете просто зайти на кухню и взять стейк (это нарушение безопасности). Вы подзываете официанта и делаете заказ. Официант — это и есть RemoteEvent. Он передает вашу просьбу на кухню. Повар (Сервер) проверяет, есть ли у вас деньги и есть ли продукты, и только потом выдает блюдо.

Как это выглядит в коде

  1. Создайте объект RemoteEvent в папке ReplicatedStorage (это общая зона, которую видят и Клиент, и Сервер). Назовите его BuyItemEvent.
  2. В LocalScript вашей кнопки напишите:
local button = script.Parent
-- Находим наш "официанта" в общей папке
local replicatedStorage = game:GetService("ReplicatedStorage")
local buyEvent = replicatedStorage:WaitForChild("BuyItemEvent")

local function onButtonClick()
    -- Отправляем сигнал на сервер: "Я хочу купить Меч!"
    buyEvent:FireServer("Sword")
end

button.MouseButton1Click:Connect(onButtonClick)
  1. В обычном Script на сервере (например, в ServerScriptService) мы принимаем этот сигнал:
local replicatedStorage = game:GetService("ReplicatedStorage")
local buyEvent = replicatedStorage:WaitForChild("BuyItemEvent")

-- Функция, которая сработает, когда придет сигнал от клиента
local function processPurchase(player, itemName)
    print(player.Name .. " хочет купить " .. itemName)
    -- Здесь сервер проверит деньги и выдаст предмет
end

-- Подключаем функцию к событию
buyEvent.OnServerEvent:Connect(processPurchase)

Обратите внимание: когда клиент вызывает FireServer, сервер автоматически первым параметром получает ссылку на самого игрока (player). Сервер всегда точно знает, кто именно отправил запрос.

Таблицы (Tables): бездонный рюкзак игрока

Теперь, когда мы умеем безопасно общаться с сервером, нам нужно где-то хранить предметы игрока.

До сих пор мы использовали переменные для хранения одного значения: local coins = 10. Но инвентарь — это множество предметов. Создавать отдельную переменную для каждого предмета (local hasSword, local hasShield, local appleCount) — неэффективно.

В языке Lua для хранения наборов данных используются Таблицы (Tables).

Таблица — это универсальный контейнер, который может хранить внутри себя сколько угодно других переменных, списков или даже других таблиц.

Представьте таблицу как шкаф с подписанными ящиками. Название ящика называется Ключом (Key), а то, что лежит внутри — Значением (Value).

Давайте создадим простую систему инвентаря на сервере:

-- Создаем пустую таблицу для инвентаря
local inventory = {}

-- Функция добавления предмета
local function addItem(itemName)
    -- Проверяем, есть ли уже такой предмет в таблице
    if inventory[itemName] then
        -- Если есть, увеличиваем количество на 1
        inventory[itemName] = inventory[itemName] + 1
    else
        -- Если нет, создаем новую запись со значением 1
        inventory[itemName] = 1
    end

    print("Теперь у вас " .. inventory[itemName] .. " шт. предмета: " .. itemName)
end

-- Тестируем механику
addItem("Apple")  -- Выведет: Теперь у вас 1 шт. предмета: Apple
addItem("Apple")  -- Выведет: Теперь у вас 2 шт. предмета: Apple
addItem("Sword")  -- Выведет: Теперь у вас 1 шт. предмета: Sword

В этом примере itemName (например, "Apple") выступает в роли ключа. Мы обращаемся к ящику с надписью "Apple" с помощью квадратных скобок: inventory["Apple"].

Система квестов: отслеживание прогресса

Имея инвентарь, мы можем легко создать систему квестов. Квест — это, по сути, математическое условие.

Например, NPC (неигровой персонаж) просит вас собрать 5 яблок. В терминах программирования это звучит так: нужно проверять инвентарь игрока до тех пор, пока значение по ключу "Apple" не станет больше или равно пяти (apples≥5apples \geq 5).

Давайте напишем скрипт для детали, разбросанной по карте (яблока). Когда игрок касается яблока, оно исчезает, добавляется в инвентарь, и сервер проверяет прогресс квеста.

local applePart = script.Parent
local questGoal = 5 -- Цель квеста

-- Временный инвентарь для примера (в реальной игре он хранится отдельно для каждого игрока)
local playerApples = 0

local function onTouch(otherPart)
    local character = otherPart.Parent
    local humanoid = character:FindFirstChild("Humanoid")

    if humanoid then
        -- Увеличиваем счетчик
        playerApples = playerApples + 1
        print("Собрано яблок: " .. playerApples .. " / " .. questGoal)

        -- Проверяем условие квеста
        if playerApples >= questGoal then
            print("Квест выполнен! Вы получили награду.")
            -- Здесь можно выдать золото или проиграть победный звук
        end

        -- Удаляем яблоко из мира
        applePart:Destroy()
    end
end

applePart.Touched:Connect(onTouch)

Разбор логики

  1. Мы используем событие Touched, которое изучили во второй статье.
  2. Мы проверяем наличие Humanoid, чтобы убедиться, что яблока коснулся именно игрок, а не катящийся камень.
  3. Мы используем математический оператор сравнения >= (больше или равно). Почему не строгое равенство ==?

Сократовский вопрос: Представьте, что игрок нашел баг и смог подобрать два яблока одновременно, когда у него уже было 4. Его счетчик стал равен 6. Если бы мы написали if playerApples == 5, квест бы никогда не завершился! Использование >= защищает игру от подобных логических ошибок.

Подведение итогов

Сегодня мы совершили огромный скачок от разрозненных скриптов к полноценной игровой архитектуре:

  • Мы узнали, что Клиент отвечает за визуал и интерфейс, а Сервер — за логику, безопасность и данные.
  • Мы научились передавать сигналы от интерфейса в игровой мир с помощью RemoteEvents.
  • Мы освоили Таблицы (Tables) — мощнейший инструмент языка Lua для создания инвентаря, хранения характеристик и списков.
  • Мы объединили события касания, переменные и условные конструкции для создания базовой механики Квестов.

Комплексное задание для размышления

Попробуйте спроектировать в уме (или на бумаге) полную цепочку действий для механики «Покупка зелья лечения».

  1. Игрок нажимает кнопку в UI.
  2. Какой скрипт это фиксирует?
  3. Как сигнал передается на сервер?
  4. Что сервер должен проверить в таблице инвентаря игрока перед тем, как выдать зелье?
  5. Как сервер должен изменить здоровье (Humanoid.Health), если игрок решит использовать это зелье?

В следующих уроках мы добавим нашей игре зрелищности: научимся работать с анимациями персонажей и визуальными эффектами (VFX), чтобы получение уровня или удар мечом выглядели по-настоящему эпично!

Анимация и визуальные эффекты: частицы, свет и движения персонажа

Анимация и визуальные эффекты: частицы, свет и движения персонажа

В прошлой статье мы проделали огромную работу: научили Сервер и Клиент безопасно общаться друг с другом, создали систему инвентаря на основе таблиц и написали логику для сбора квестов. Наша игра стала умной. Но пока что она выглядит... немного «деревянной».

Представьте: вы подбираете последний предмет для сложного квеста, счетчик достигает нужной цифры, и в консоли появляется надпись «Квест выполнен». И всё. Никаких фанфар, никаких вспышек. Игрок даже не поймет, что произошло что-то важное.

Сегодня мы превратим сухой код в захватывающий игровой опыт. Мы добавим игре «сочности» (game feel): научимся анимировать персонажей, плавно перемещать объекты и создавать ослепительные визуальные эффекты (VFX).

Движение — это жизнь: Анимация персонажей

В Roblox Studio есть встроенный инструмент — Animation Editor (Редактор анимаций). Он позволяет создавать собственные движения для персонажей: танцы, удары мечом, сальто или уникальную походку.

Как работает анимация?

Анимация в 3D-графике — это последовательность ключевых кадров (Keyframes). Вы не рисуете каждое движение. Вы задаете начальную позу (кадр 1) и конечную позу (кадр 10), а компьютер сам плавно вычисляет все промежуточные положения.

Аналогия из жизни: вспомните блокноты с рисунками в уголках страниц. Если быстро перелистывать страницы, статичные картинки сливаются в движущийся мультфильм. Анимация в игре работает точно так же.

Чтобы использовать анимацию в коде, нужно выполнить три шага:

  1. Создать анимацию в Animation Editor и нажать Export (Экспорт).
  2. Получить уникальный номер — Animation ID.
  3. Написать скрипт, который загрузит этот ID в персонажа и запустит его.

Программируем удар мечом

Давайте напишем скрипт, который заставит персонажа проиграть анимацию при клике мышкой. Для этого нам понадобится компонент Animator, который находится внутри Humanoid каждого игрока.

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()

-- Создаем объект анимации и вставляем наш ID
local slashAnimation = Instance.new("Animation")
slashAnimation.AnimationId = "rbxassetid://ВАШ_ID_АНИМАЦИИ" -- Замените на свой ID

-- Переменная для хранения загруженного трека
local loadedAnimTrack = nil

local function playSlash()
    local character = player.Character
    if character then
        local humanoid = character:FindFirstChild("Humanoid")
        local animator = humanoid:FindFirstChild("Animator")

        if animator then
            -- Загружаем анимацию в аниматор (делаем это один раз)
            if not loadedAnimTrack then
                loadedAnimTrack = animator:LoadAnimation(slashAnimation)
            end

            -- Запускаем анимацию
            loadedAnimTrack:Play()
        end
    end
end

-- Привязываем функцию к клику левой кнопкой мыши
mouse.Button1Down:Connect(playSlash)

Сократовский вопрос: Почему мы сохраняем загруженную анимацию в переменную loadedAnimTrack и проверяем if not loadedAnimTrack then, вместо того чтобы загружать её заново при каждом клике? Размышление: Загрузка данных требует ресурсов компьютера. Если игрок будет быстро кликать 10 раз в секунду, постоянная загрузка одной и той же анимации вызовет зависания (лаги). Мы загружаем её один раз и просто перематываем/запускаем снова.

TweenService: Искусство плавных переходов

Анимации отлично подходят для персонажей с суставами. Но что, если нам нужно плавно открыть тяжелую каменную дверь, поднять платформу-лифт или заставить кнопку в меню увеличиться при наведении мыши?

Для этого используется TweenService (Сервис интерполяции).

Интерполяция (Tweening) — это математический процесс плавного изменения значения от точки А до точки Б за заданное время.

Если мы напишем part.Position = Vector3.new(0, 10, 0), деталь мгновенно телепортируется. Это неестественно. С помощью TweenService мы говорим движку: «Перемести эту деталь в новую точку за 3 секунды, начни медленно, а в конце ускорься».

Анатомия Tween-анимации

Чтобы запустить Tween, нам нужны три вещи:

  1. Объект, который мы хотим изменить (например, дверь).
  2. TweenInfo — настройки «поездки» (сколько времени займет, какой стиль движения).
  3. Goal (Цель) — таблица со свойствами, к которым мы стремимся.

Давайте напишем скрипт для плавно поднимающейся платформы:

local TweenService = game:GetService("TweenService")
local platform = script.Parent

-- 1. Настраиваем параметры времени и стиля
local tweenInfo = TweenInfo.new(
    2.5, -- Длительность: 2.5 секунды
    Enum.EasingStyle.Sine, -- Стиль: плавное начало и конец
    Enum.EasingDirection.Out, -- Направление стиля
    -1, -- Количество повторений (-1 означает бесконечно)
    true, -- Возвращаться ли обратно (эффект маятника)
    0.5 -- Задержка перед повтором: 0.5 секунды
)

-- 2. Задаем цель (поднять на 15 единиц вверх по оси Y)
local targetPosition = platform.Position + Vector3.new(0, 15, 0)
local goal = {
    Position = targetPosition,
    Color = Color3.new(0, 1, 0) -- Заодно плавно изменим цвет на зеленый!
}

-- 3. Создаем и запускаем Tween
local elevatorTween = TweenService:Create(platform, tweenInfo, goal)
elevatorTween:Play()

Математически TweenService вычисляет текущее значение по формуле, похожей на V=A+(B−A)×tV = A + (B - A) \times t, где AA — старт, BB — финиш, а tt — процент времени (от 0 до 1). Но благодаря EasingStyle это время может течь нелинейно (например, с эффектом пружины).

Визуальные эффекты (VFX): Свет и Частицы

Движение добавлено, теперь займемся зрелищностью. В Roblox есть два главных инструмента для создания магии: Свет и Частицы.

Источники света

Вы можете добавить внутрь любой детали объекты PointLight (свет во все стороны, как лампочка), SpotLight (направленный свет, как фонарик) или SurfaceLight (свет от плоскости, как экран телевизора).

У света есть ключевые свойства:

  • Brightness — яркость.
  • Range — дальность освещения.
  • Color — цвет лучей.

ParticleEmitter (Генератор частиц)

ParticleEmitter — это объект, который непрерывно выбрасывает 2D-картинки (искры, дым, огонь, магические руны) в 3D-пространство.

Представьте хлопушку с конфетти. ParticleEmitter — это сама хлопушка, а вылетающие бумажки — это частицы. Вы можете настроить их гравитацию, скорость, размер и цвет.

Объединяем всё: Эпичное завершение квеста

Давайте вернемся к нашему скрипту сбора яблок из прошлой статьи. Сделаем так, чтобы при сборе последнего яблока происходил магический взрыв частиц и вспышка света!

Для этого заранее добавьте в деталь яблока ParticleEmitter (назовите его MagicSparkles) и PointLight (назовите Glow). Отключите их в свойствах (Enabled = false).

local applePart = script.Parent
local sparkles = applePart:WaitForChild("MagicSparkles")
local glow = applePart:WaitForChild("Glow")

local questGoal = 5
local playerApples = 4 -- Представим, что у игрока уже 4 яблока

local function onTouch(otherPart)
    local humanoid = otherPart.Parent:FindFirstChild("Humanoid")

    if humanoid then
        playerApples = playerApples + 1

        if playerApples >= questGoal then
            print("Квест выполнен!")

            -- Включаем свет
            glow.Enabled = true
            glow.Brightness = 5

            -- Выбрасываем 50 частиц мгновенно (эффект взрыва)
            sparkles:Emit(50)

            -- Ждем 1 секунду, чтобы игрок насладился эффектом
            task.wait(1)
        end

        -- Удаляем яблоко
        applePart:Destroy()
    end
end

applePart.Touched:Connect(onTouch)

Обратите внимание на метод :Emit(50). Обычно ParticleEmitter выпускает частицы постепенно (например, 10 в секунду). Но метод Emit() заставляет его выстрелить заданным количеством частиц мгновенно, что идеально подходит для взрывов, ударов или получения уровня.

Подведение итогов

Сегодня мы вдохнули жизнь в нашу игру:

  • Мы узнали, как загружать и воспроизводить Анимации через Animator, делая персонажей подвижными.
  • Мы освоили TweenService — мощнейший инструмент для плавного изменения любых числовых свойств (позиции, размера, цвета).
  • Мы научились работать с VFX, используя источники света и ParticleEmitter для создания эмоционального отклика у игрока.

Комплексное задание для размышления

Представьте, что вы создаете сундук с сокровищами. Попробуйте объединить знания из последних трех статей, чтобы описать логику его работы:

  1. Игрок подходит к сундуку и нажимает кнопку «E» на экране (UI + LocalScript).
  2. Сигнал отправляется на сервер (RemoteEvent).
  3. Сервер проверяет, есть ли у игрока ключ в инвентаре (Таблицы).
  4. Если ключ есть, крышка сундука плавно откидывается назад (TweenService).
  5. Из открытого сундука вырывается золотой свет (PointLight) и разлетаются монетки (ParticleEmitter:Emit).

В следующей, заключительной статье нашего курса, мы соберем все эти механики воедино и поговорим о публикации игры, монетизации и о том, как привлечь первых игроков в ваш собственный виртуальный мир!