Основы архитектуры: Модульность и организация кода с помощью ModuleScripts
Основы архитектуры: Модульность и организация кода с помощью ModuleScripts
Добро пожаловать в курс «Архитектура и разработка масштабных скриптов на Roblox Lua». Вы здесь, потому что задались вопросом: «Как написать хороший и большой скрипт?». Ответ может вас удивить: секрет написания большого скрипта заключается в том, чтобы не писать большие скрипты.
В этой первой статье мы разберем фундамент любой качественной разработки на Roblox — модульность. Мы научимся разбивать гигантские задачи на маленькие, управляемые части, используя ModuleScript.
Проблема «Спагетти-кода»
Представьте, что вы строите огромный город. Если вы попытаетесь построить все здания, дороги, электросети и канализацию, используя один гигантский чертеж, где все линии наложены друг на друга, вы сойдете с ума. То же самое происходит и в программировании.
Многие новички создают один Script (или LocalScript) и пишут в него всё: обработку нажатий, сохранение данных, анимацию интерфейса и логику магазина. Когда такой файл разрастается до 2000+ строк, он превращается в так называемый спагетти-код.
Основные проблемы монолитного (единого) скрипта:
- Нечитаемость: Найти нужную функцию занимает вечность.
- Сложность отладки: Если что-то ломается, трудно понять, какая часть кода виновата.
- Дублирование: Если вам нужна одна и та же формула в двух разных местах, вам приходится копировать и вставлять код. Если формула изменится, придется менять её везде.
- Конфликты: При командной разработке два программиста не могут эффективно работать в одном файле одновременно.
Что такое ModuleScript?
В Roblox существует специальный тип скрипта — ModuleScript. В отличие от обычных Script (серверных) и LocalScript (клиентских), модульный скрипт не запускается автоматически.
ModuleScript — это контейнер для кода, который можно использовать (импортировать) в других скриптах. Он выполняется только тогда, когда его «запрашивают» с помощью функции require().
Анатомия модуля
Стандартный ModuleScript выглядит так:
-- Создаем пустую таблицу, которая будет содержать наши функции и переменные
local MyModule = {}
-- Добавляем переменную в таблицу
MyModule.SomeValue = 10
-- Добавляем функцию в таблицу
function MyModule.SayHello()
print("Привет из модуля!")
end
-- Обязательно возвращаем таблицу в конце
return MyModule
Ключевые моменты:
- Таблица: Модуль обычно возвращает таблицу, наполненную функциями или данными.
- Return: Модуль обязан возвращать ровно одно значение (обычно это таблица, но может быть и функция, и даже число).
Как использовать require()
Чтобы использовать код из ModuleScript в обычном скрипте, мы используем функцию require(путь_к_модулю).
Предположим, наш модуль лежит в ServerScriptService и называется MathUtils.
-- Обычный Script в ServerScriptService
local ServerScriptService = game:GetService("ServerScriptService")
-- Загружаем модуль
local MathUtils = require(ServerScriptService.MathUtils)
-- Теперь мы можем использовать функции из модуля
MathUtils.SayHello() -- Выведет: Привет из модуля!
print(MathUtils.SomeValue) -- Выведет: 10
Принцип Singleton (Одиночка)
Это критически важный концепт в Roblox Lua. Когда вы вызываете require() для модуля в первый раз, Roblox выполняет код внутри модуля и запоминает (кеширует) то, что модуль вернул.
Все последующие вызовы require() для этого же модуля (на той же стороне: сервер или клиент) не будут выполнять код модуля заново. Они просто вернут ту же самую таблицу, которая была создана в первый раз.
Это позволяет использовать модули как хранилища состояния.
-- ModuleScript "GameConfig"
local Config = {}
Config.IsGameRunning = false
return Config
Если Скрипт А изменит Config.IsGameRunning = true, то Скрипт Б, подключив этот же модуль, увидит, что значение уже равно true.
Организация кода: Разделяй и властвуй
Чтобы написать «большой и хороший скрипт», нужно разбить его на слои ответственности. Давайте рассмотрим пример простой системы выдачи монет за убийство монстров.
Плохой подход (Монолит)
Один скрипт в ServerScriptService, который делает всё:
-- BAD CODE: MonolithScript
game.Players.PlayerAdded:Connect(function(player)
-- Создание папки leaderstats
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = stats
-- Логика монстров прямо тут
workspace.Monster.Humanoid.Died:Connect(function()
coins.Value = coins.Value + 10
print("Монстр умер, монеты выданы")
end)
end)
Почему это плохо? Если вы захотите добавить магазин, вам придется лезть в этот же скрипт. Если вы захотите сохранять данные (DataStore), код станет еще длиннее.
Хороший подход (Модульность)
Мы разделим логику на три части:
- DataManager (Модуль): Отвечает только за работу с данными игрока.
- MonsterLogic (Скрипт): Отвечает за поведение монстров.
- Main (Скрипт): Инициализирует игру.
1. ModuleScript: DataManager (в ServerScriptService.Modules)
local DataManager = {}
local playersData = {} -- Локальная таблица для хранения данных в памяти
function DataManager.InitPlayer(player)
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = stats
playersData[player.UserId] = coins
end
function DataManager.AddCoins(player, amount)
local coins = playersData[player.UserId]
if coins then
coins.Value = coins.Value + amount
end
end
return DataManager
2. Script: MonsterHandler (в ServerScriptService)
local ServerScriptService = game:GetService("ServerScriptService")
-- Подключаем наш модуль
local DataManager = require(ServerScriptService.Modules.DataManager)
-- Представим, что у нас есть папка с монстрами
for _, monster in pairs(workspace.Monsters:GetChildren()) do
local humanoid = monster:FindFirstChild("Humanoid")
if humanoid then
humanoid.Died:Connect(function()
-- Находим игрока, который убил (упрощенно)
-- В реальной игре здесь была бы логика определения убийцы
local killer = game.Players:GetPlayers()[1]
if killer then
-- Мы просто просим менеджер добавить монеты
-- Нам не важно, КАК он это делает
DataManager.AddCoins(killer, 10)
end
end)
end
end
3. Script: MainLoader (в ServerScriptService)
local ServerScriptService = game:GetService("ServerScriptService")
local DataManager = require(ServerScriptService.Modules.DataManager)
game.Players.PlayerAdded:Connect(function(player)
DataManager.InitPlayer(player)
end)
Преимущества такой структуры
- Инкапсуляция: Скрипту монстров не нужно знать, как создаются
leaderstatsили как они называются. Он просто говорит: «Дай денег». Если завтра вы решите хранить деньги не вIntValue, а в SQL-базе, вы измените толькоDataManager. Скрипт монстров трогать не придется. - Повторное использование: Если вы добавите магазин, он тоже просто вызовет
require(DataManager)и сможет проверять баланс или списывать деньги.
Где хранить модули?
Правильная организация файлов в Explorer — залог успеха.
1. ServerScriptService
Здесь хранятся модули, которые содержат секретную логику или работают с защищенными сервисами (DataStoreService, HTTPService). Клиент (игрок) не видит и не может получить доступ к этим модулям.
- Примеры: Менеджер базы данных, админ-панель, античит, генерация карты.
2. ReplicatedStorage
Здесь хранятся модули, которые нужны и серверу, и клиенту. Это «общая зона».
- Примеры: Конфигурации оружия (урон, скорострельность), математические утилиты, модули для анимации интерфейса, сетевые протоколы.
Важно: Никогда не храните пароли, ключи API или логику проверки покупок в
ReplicatedStorage. Эксплойтеры могут скопировать любой модуль оттуда.
Принцип DRY (Don't Repeat Yourself)
«Не повторяйся» — это золотое правило программирования. Если вы видите, что копируете один и тот же кусок кода в два разных скрипта — остановитесь. Выделите этот код в ModuleScript.
Пример: У вас есть формула расчета опыта для уровня. Она нужна на сервере (чтобы выдать уровень) и на клиенте (чтобы показать прогресс-бар).
Вместо того чтобы писать формулу дважды, создайте модуль LevelCalculator в ReplicatedStorage:
-- ReplicatedStorage.LevelCalculator
local LevelCalculator = {}
function LevelCalculator.GetExpForLevel(level)
return 100 * (level ^ 1.5)
end
return LevelCalculator
Теперь и сервер, и клиент используют единый источник истины.
Заключение
Написание «большого скрипта» начинается с отказа от написания больших файлов. Ваша задача как архитектора — разбивать систему на маленькие, независимые кирпичики (модули), которые общаются друг с другом через понятные интерфейсы (функции).
В следующих статьях мы углубимся в объектно-ориентированное программирование (ООП) на Lua и узнаем, как создавать собственные классы с помощью метатаблиц, что выведет вашу архитектуру на новый уровень.