Перейти к основному содержимому

Lua-скриптинг: Примеры и паттерны

На этой странице представлены полные рабочие примеры скриптов пропов FreeMinecraftModels, а также практические паттерны и лучшие практики. Каждый пример содержит пошаговое объяснение того, что он делает и почему.

Если вы новичок в скриптинге пропов, начните с раздела Начало работы. Полную информацию об API смотрите в API пропов.


Пример: Неуязвимый проп

Чему учит этот пример: Самый простой полезный скрипт -- отмена урона, чтобы проп нельзя было сломать.

Это готовый скрипт, поставляемый вместе с FreeMinecraftModels.

Полный файл скрипта (нажмите, чтобы развернуть)
return {
api_version = 1,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}

Пошаговый разбор

  1. Выбор хука -- on_left_click срабатывает, когда игрок бьёт (левый клик) по пропу. Внутри это EntityDamageByEntityEvent на стойке для брони, лежащей в основе пропа.

  2. Проверка события -- context.event всегда должен присутствовать в этом хуке, но проверка является хорошей практикой.

  3. Отмена -- 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
}

Пошаговый разбор

  1. Константы на уровне файла -- OPEN_ANIMATION и CLOSE_ANIMATION определены выше таблицы return. Это позволяет легко менять их для разных файлов моделей, которые могут использовать другие имена анимаций.

  2. Инициализация состояния -- on_spawn устанавливает context.state.is_open = false. Состояние сохраняется между всеми хуками для этого экземпляра пропа.

  3. Неуязвимость -- Хук on_left_click отменяет урон, чтобы дверь не могла быть случайно сломана.

  4. Логика переключения -- on_right_click проверяет context.state.is_open, останавливает текущую анимацию, воспроизводит соответствующую анимацию, переключает состояние и воспроизводит звук. Вызов stop_animation() перед play_animation() обеспечивает чистые переходы.

  5. Звуковая обратная связь -- 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
}

Пошаговый разбор

  1. Константы -- ZONE_RADIUS и PARTICLE_INTERVAL находятся на уровне файла для удобной настройки.

  2. Инициализация состояния -- on_spawn устанавливает все поля состояния в nil / 0 перед выполнением других действий.

  3. Создание зоны -- context.zones:create_sphere() создаёт сферическую зону с центром в пропе. Возвращаемый дескриптор — это числовой ID для дальнейшей ссылки на эту зону.

  4. Наблюдение за зоной -- context.zones:watch() регистрирует обратные вызовы для входа и выхода игроков. Обратные вызовы увеличивают и уменьшают счётчик, хранящийся в context.state.

  5. Цикл частиц -- Повторяющаяся задача генерирует частицы кольцом вокруг пропа каждые полсекунды. Тип частиц меняется в зависимости от того, есть ли игроки в зоне.

  6. Очистка -- 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
}

Пошаговый разбор

  1. Паттерн кулдауна -- Поскольку скрипты пропов FMM не имеют встроенного API context.cooldowns, как EliteMobs, пример реализует простой кулдаун с помощью context.state.on_cooldown и scheduler:run_later(). Флаг устанавливается в true при воспроизведении звука, а отложенная задача сбрасывает его через COOLDOWN_TICKS.

  2. Воспроизведение звука -- context.world:play_sound() принимает имя перечисления Sound из Bukkit в UPPER_CASE, координаты, громкость и высоту тона.

  3. Визуальная обратная связь через частицы -- Частицы нот появляются над пропом при воспроизведении звука, давая визуальную подсказку.

  4. Неуязвимость -- Хук on_left_click отменяет урон, как обычно.

  5. Контекст обратного вызова планировщика -- Обратный вызов 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
}

Пошаговый разбор

  1. Автоматический запуск анимации -- on_spawn немедленно воспроизводит зацикленную анимацию "idle". Значение false для blend означает, что она начинается без смешивания с предыдущей анимацией. Значение true для loop означает, что она повторяется бесконечно.

  2. Атмосферные частицы -- Повторяющаяся задача генерирует частицы стола зачарования над пропом каждые 2 секунды, создавая магический атмосферный эффект.

  3. Очистка -- 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

Используйте этот контрольный список для проверки скрипта пропа перед развёртыванием:

  1. Файл возвращает ровно одну таблицу с api_version = 1.
  2. Каждое имя хука точно совпадает с записью в списке хуков.
  3. context.event проверяется через if context.event then перед вызовом cancel().
  4. Поля context.state инициализированы в on_spawn.
  5. Каждый вызов scheduler:run_repeating(...) имеет соответствующий scheduler:cancel(...) в on_destroy.
  6. Обратные вызовы планировщика используют собственный параметр контекста, а не внешний context.
  7. Хуки on_game_tick защищают ресурсоёмкую работу проверкой.
  8. Все имена методов существуют в справочнике API пропов -- никаких выдуманных алиасов.
  9. Имена звуков и частиц используют UPPER_CASE-имена перечислений Bukkit.
  10. Скрипт не вызывает блокирующих или длительных операций внутри хука или обратного вызова.

Советы по генерации с помощью ИИ

Если вы хотите, чтобы ИИ надёжно генерировал скрипты пропов, убедитесь, что промпт включает:

  • Точное имя хука -- например, 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.


Следующие шаги