Lua-скриптинг: Примеры и паттерны
На этой странице представлены полные рабочие примеры скриптов пропов FreeMinecraftModels, а также практические паттерны и лучшие практики. Каждый пример содержит пошаговое объяснение того, что он делает и почему.
Если вы новичок в скриптинге пропов, начните с раздела Начало работы. Полную информацию об API смотрите в API пропов.
Пример: Неуязвимый проп
Чему учит этот пример: Самый простой полезный скрипт -- отмена урона, чтобы проп нельзя было сломать.
Это готовый скрипт, поставляемый вместе с FreeMinecraftModels.
Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
Пошаговый разбор
-
Выбор хука --
on_left_clickсрабатывает, когда игрок бьёт (левый клик) по пропу. Внутри этоEntityDamageByEntityEventна стойке для брони, лежащей в основе пропа. -
Проверка события --
context.eventвсегда должен присутствовать в этом хуке, но проверка является хорошей практикой. -
Отмена --
context.event.cancel()отменяет событие урона, что предотвращает получение стойкой для брони урона и её уничтожение.
Использование
Добавьте в .yml-конфигурацию вашего пропа:
isEnabled: true
scripts:
- invulnerable.lua
Пример: Интерактивная дверь
Чему учит этот пример: Переключение состояния по правому клику, воспроизведение и остановка анимаций, использование context.state для отслеживания того, открыта дверь или закрыта.
Полный файл скрипта (нажмите, чтобы развернуть)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"
return {
api_version = 1,
on_spawn = function(context)
context.state.is_open = false
end,
on_left_click = function(context)
-- Make the door invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
local loc = context.prop.current_location
if context.state.is_open then
-- Close the door
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Open the door
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true
if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}
Пошаговый разбор
-
Константы на уровне файла --
OPEN_ANIMATIONиCLOSE_ANIMATIONопределены выше таблицы return. Это позволяет легко менять их для разных файлов моделей, которые могут использовать другие имена анимаций. -
Инициализация состояния --
on_spawnустанавливаетcontext.state.is_open = false. Состояние сохраняется между всеми хуками для этого экземпляра пропа. -
Неуязвимость -- Хук
on_left_clickотменяет урон, чтобы дверь не могла быть случайно сломана. -
Логика переключения --
on_right_clickпроверяетcontext.state.is_open, останавливает текущую анимацию, воспроизводит соответствующую анимацию, переключает состояние и воспроизводит звук. Вызовstop_animation()передplay_animation()обеспечивает чистые переходы. -
Звуковая обратная связь --
context.world:play_sound()воспроизводит имя перечисления Sound из Bukkit в позиции пропа. Имена звуков должны быть в формате UPPER_CASE.
Имена анимаций, такие как "open" и "close", должны совпадать с определёнными в файле модели. Если анимация не найдена, play_animation() возвращает false и ничего не происходит. Проверьте файл модели на точные имена анимаций.
Пример: Триггер приближения с частицами
Чему учит этот пример: Создание зон, наблюдение за зонами для событий входа/выхода, эффекты частиц и очистка.
Полный файл скрипта (нажмите, чтобы развернуть)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle bursts
return {
api_version = 1,
on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0
local loc = context.prop.current_location
if loc == nil then return end
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Watch for enter/leave
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end
)
-- Start a repeating particle effect at the zone boundary
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end
-- Spawn particles in a ring at the zone boundary
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS
if (tick_context.state.players_in_zone or 0) > 0 then
-- Red particles when players are inside
tick_context.world:spawn_particle("DUST", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
Пошаговый разбор
-
Константы --
ZONE_RADIUSиPARTICLE_INTERVALнаходятся на уровне файла для удобной настройки. -
Инициализация состояния --
on_spawnустанавливает все поля состояния вnil/0перед выполнением других действий. -
Создание зоны --
context.zones:create_sphere()создаёт сферическую зону с центром в пропе. Возвращаемый дескриптор — это числовой ID для дальнейшей ссылки на эту зону. -
Наблюдение за зоной --
context.zones:watch()регистрирует обратные вызовы для входа и выхода игроков. Обратные вызовы увеличивают и уменьшают счётчик, хранящийся вcontext.state. -
Цикл частиц -- Повторяющаяся задача генерирует частицы кольцом вокруг пропа каждые полсекунды. Тип частиц меняется в зависимости от того, есть ли игроки в зоне.
-
Очистка --
on_destroyотменяет повторяющуюся задачу и снимает наблюдение за зоной. Хотя оба элемента очищаются автоматически при удалении пропа, явная очистка является лучшей практикой.
Генерация большого количества частиц каждый тик может повлиять на производительность. Используйте разумный интервал (10-20 тиков) и держите количество частиц низким. Пример выше использует PARTICLE_INTERVAL = 10 (дважды в секунду) всего с 12 частицами на кольцо.
Пример: Проп, издающий звук
Чему учит этот пример: Воспроизведение звуков при взаимодействии, поведение с кулдауном через state и scheduler, предотвращение спама взаимодействий.
Полный файл скрипта (нажмите, чтобы развернуть)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Make invulnerable
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if context.state.on_cooldown then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
-- Set cooldown
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}
Пошаговый разбор
-
Паттерн кулдауна -- Поскольку скрипты пропов FMM не имеют встроенного API
context.cooldowns, как EliteMobs, пример реализует простой кулдаун с помощьюcontext.state.on_cooldownиscheduler:run_later(). Флаг устанавливается вtrueпри воспроизведении звука, а отложенная задача сбрасывает его черезCOOLDOWN_TICKS. -
Воспроизведение звука --
context.world:play_sound()принимает имя перечисления Sound из Bukkit в UPPER_CASE, координаты, громкость и высоту тона. -
Визуальная обратная связь через частицы -- Частицы нот появляются над пропом при воспроизведении звука, давая визуальную подсказку.
-
Неуязвимость -- Хук
on_left_clickотменяет урон, как обычно. -
Контекст обратного вызова планировщика -- Обратный вызов
run_laterполучаетlater_context— свежий контекст. Мы используемlater_context.state(неcontext.state) для сброса флага кулдауна. Поскольку состояние общее, оба указывают на одну и ту же таблицу — но использование параметра контекста обратного вызова является правильной привычкой.
Скрипты пропов FMM не имеют API context.cooldowns из EliteMobs. Используйте паттерн, показанный здесь: булевый флаг в context.state в сочетании с scheduler:run_later() для его сброса. Это даёт полный контроль над длительностью и поведением кулдауна.
Пример: Анимированный атмосферный проп
Чему учит этот пример: Запуск зацикленной анимации при появлении с тик-эмиттером частиц.
Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,
on_spawn = function(context)
-- Start the idle animation immediately, looping
context.prop:play_animation("idle", false, true)
-- Emit ambient particles every 40 ticks (2 seconds)
context.state.ambient_task = context.scheduler:run_repeating(0, 40, function(tick_context)
local loc = tick_context.prop.current_location
if loc == nil then return end
tick_context.world:spawn_particle(
"ENCHANT",
loc.x, loc.y + 1, loc.z,
10, 0.5, 0.5, 0.5, 0.05
)
end)
end,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
if context.state.ambient_task then
context.scheduler:cancel(context.state.ambient_task)
end
end
}
Пошаговый разбор
-
Автоматический запуск анимации --
on_spawnнемедленно воспроизводит зацикленную анимацию"idle". Значениеfalseдля blend означает, что она начинается без смешивания с предыдущей анимацией. Значениеtrueдля loop означает, что она повторяется бесконечно. -
Атмосферные частицы -- Повторяющаяся задача генерирует частицы стола зачарования над пропом каждые 2 секунды, создавая магический атмосферный эффект.
-
Очистка --
on_destroyотменяет задачу частиц.
Лучшие практики
-
Начните с маленького хука и проверьте. Напишите один
on_spawn, который отправляет сообщение в лог. Убедитесь, что он срабатывает. Затем стройте дальше. -
Держите вспомогательные функции локальными. Объявляйте хелперы вроде
local function toggle_door(context)выше таблицы return. Это держит их вне глобальной области видимости. -
Инициализируйте всё состояние в
on_spawn. Если вы читаетеcontext.state.is_openвon_right_click, но никогда не устанавливаете его вon_spawn, оно будетnil, и ваши сравнения могут работать неожиданно. -
Отменяйте повторяющиеся задачи, когда они больше не нужны. Каждый
run_repeatingдолжен иметь соответствующийcancelвon_destroy. Неотменённые задачи тратят CPU. -
Используйте свежие контексты обратных вызовов планировщика. Обратные вызовы планировщика получают свежий параметр контекста. Всегда используйте этот параметр внутри обратного вызова, а не внешний
context. -
Держите
on_game_tickлегковесным. Если вы определяете этот хук, он выполняется каждый серверный тик (20 раз в секунду). Защищайте ресурсоёмкую работу проверкой кулдауна на основе состояния. -
Делайте пропы неуязвимыми по умолчанию. Если вы не хотите, чтобы проп можно было сломать, включайте отмену урона
on_left_clickв каждый скрипт. -
Используйте UPPER_CASE для перечислений Bukkit. Имена звуков и частиц должны использовать формат констант перечислений Bukkit (например,
"FLAME", а не"flame").
Распространённые ошибки новичков
-
Использование внешнего
contextвнутри обратного вызова планировщика. Внешний контекст захватывает снимок на момент выполнения хука. Внутри обратных вызовов всегда используйте собственный параметр обратного вызова. -
Забыли отменить повторяющиеся задачи. Если вы запустили
run_repeatingвon_spawn, но ни разу не отменили, задача работает до удаления пропа. -
Не инициализировали состояние в
on_spawn. Чтениеcontext.state.xдо установки значения возвращаетnil, что может незаметно нарушить вашу логику. -
Неправильные имена анимаций. Если
play_animation("open")возвращаетfalse, имя анимации не совпадает с тем, что в файле модели. Проверьте модель на точные имена. -
Имена звуков/частиц в нижнем регистре.
"flame"не работает -- используйте"FLAME". API внутренне преобразует в UPPER_CASE для частиц, но имена перечислений Sound должны быть точными. -
Забыли
api_version = 1. Возвращаемая таблица должна содержать это поле, иначе FMM не загрузит скрипт. -
Размещение функций внутри возвращаемой таблицы, которые не являются хуками. Вспомогательные функции должны быть объявлены выше оператора
return. Только имена хуков (on_spawn,on_right_clickи т.д.) допускаются как ключи в возвращаемой таблице.
Контрольный список QC
Используйте этот контрольный список для проверки скрипта пропа перед развёртыванием:
- Файл возвращает ровно одну таблицу с
api_version = 1. - Каждое имя хука точно совпадает с записью в списке хуков.
context.eventпроверяется черезif context.event thenперед вызовомcancel().- Поля
context.stateинициализированы вon_spawn. - Каждый вызов
scheduler:run_repeating(...)имеет соответствующийscheduler:cancel(...)вon_destroy. - Обратные вызовы планировщика используют собственный параметр контекста, а не внешний
context. - Хуки
on_game_tickзащищают ресурсоёмкую работу проверкой. - Все имена методов существуют в справочнике API пропов -- никаких выдуманных алиасов.
- Имена звуков и частиц используют UPPER_CASE-имена перечислений Bukkit.
- Скрипт не вызывает блокирующих или длительных операций внутри хука или обратного вызова.
Советы по генерации с помощью ИИ
Если вы хотите, чтобы ИИ надёжно генерировал скрипты пропов, убедитесь, что промпт включает:
- Точное имя хука -- например,
on_right_click, а не «когда игрок кликает по пропу». - Имена анимаций из файла модели -- ИИ не может их угадать; предоставьте их.
- Имена перечислений Sound -- например,
"BLOCK_NOTE_BLOCK_HARP", а не «звук арфы». - Имена перечислений Particle -- например,
"FLAME", а не «частицы огня». - Должен ли проп быть неуязвимым -- если да, включите
on_left_clickсcontext.event.cancel(). - Используйте только задокументированные имена методов -- если этого нет на странице API пропов, этого не существует.
Хороший пример промпта
Напиши скрипт пропа FMM, который воспроизводит анимацию "activate" при правом клике, делает проп неуязвимым, генерирует частицы FLAME в позиции пропа при клике, воспроизводит звук BLOCK_LEVER_CLICK и имеет 2-секундный кулдаун между кликами с использованием context.state и scheduler:run_later.
Следующие шаги
- Начало работы -- структура файлов, хуки, разбор первого скрипта, шаблоны
- API пропов -- полная справка по API для всех таблиц контекста
- Устранение неполадок -- распространённые проблемы, советы по отладке