跳到主要内容

Lua 脚本:示例与模式

本页包含 FreeMinecraftModels 道具脚本的完整可运行示例,以及实用模式和最佳实践。每个示例都附有详细说明,解释其功能和原因。

如果你刚接触道具脚本,请从入门指南开始。完整的 API 详情请参阅 Prop 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_ANIMATIONCLOSE_ANIMATION 定义在 return 表的上方。这样可以方便地针对使用不同动画名称的不同模型文件进行修改。

  2. 状态初始化 -- on_spawn 设置 context.state.is_open = false。状态在此道具实例的所有钩子之间保持。

  3. 无敌 -- on_left_click 钩子取消伤害,防止门被意外破坏。

  4. 切换逻辑 -- on_right_click 检查 context.state.is_open,停止当前动画,播放相应的动画,翻转状态,并播放声音。在 play_animation() 之前调用 stop_animation() 确保干净的过渡。

  5. 声音反馈 -- context.world:play_sound() 在道具位置播放 Bukkit Sound 枚举名称。声音名称必须使用 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_RADIUSPARTICLE_INTERVAL 位于文件作用域,便于调整。

  2. 状态初始化 -- on_spawn 在执行其他操作之前,将所有状态字段设置为 nil / 0

  3. 区域创建 -- context.zones:create_sphere() 创建以道具为中心的球形区域。返回的句柄是一个数字 ID,用于后续引用该区域。

  4. 区域监听 -- context.zones:watch() 注册玩家进入和离开的回调。回调增减存储在 context.state 中的计数器。

  5. 粒子循环 -- 重复任务每半秒在道具周围以环形生成粒子。粒子类型根据区域内是否有玩家而变化。

  6. 清理 -- on_destroy 取消重复任务并取消区域监听。虽然在道具被移除时两者都会自动清理,但显式清理是最佳实践。

粒子性能

每个 tick 生成大量粒子可能影响性能。使用合理的间隔(10-20 tick),并保持粒子数量较低。上面的示例使用 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 道具脚本没有像 EliteMobs 那样的内置 context.cooldowns API,此示例使用 context.state.on_cooldownscheduler:run_later() 实现简单的冷却。播放声音时标记设为 true,延迟任务在 COOLDOWN_TICKS 后将其重置。

  2. 声音播放 -- context.world:play_sound() 接受 UPPER_CASE 的 Bukkit Sound 枚举名称、坐标、音量和音调。

  3. 粒子反馈 -- 声音播放时,道具上方会出现音符粒子,提供视觉提示。

  4. 无敌 -- on_left_click 钩子照常取消伤害。

  5. 调度器回调上下文 -- run_later 回调接收 later_context,一个新的上下文。我们使用 later_context.state(而不是 context.state)来重置冷却标记。由于状态是共享的,两者指向同一个表——但使用回调的上下文参数是正确的习惯。

冷却实现

FMM 道具脚本没有 EliteMobs 的 context.cooldowns API。请使用此处展示的模式:在 context.state 中使用布尔标记,配合 scheduler:run_later() 进行重置。这让你可以完全控制冷却持续时间和行为。


示例:动画氛围道具

本示例教你: 生成时启动循环动画,配合基于 tick 的粒子发射器。

完整脚本文件(点击展开)
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" 动画。blend 的 false 表示不与之前的动画混合直接开始。loop 的 true 表示无限重复。

  2. 氛围粒子 -- 重复任务每 2 秒在道具上方生成附魔台粒子,营造魔法氛围效果。

  3. 清理 -- on_destroy 取消粒子任务。


最佳实践

  • 从小钩子开始并验证。 写一个只发送日志消息的 on_spawn。确认它触发了,然后在此基础上构建。

  • 保持辅助函数为局部。 在 return 表上方声明辅助函数,如 local function toggle_door(context)。这使它们不会进入全局作用域。

  • on_spawn 中初始化所有状态。 如果你在 on_right_click 中读取 context.state.is_open 但从未在 on_spawn 中设置它,它将是 nil,你的比较可能会产生意外行为。

  • 完成后取消重复任务。 每个 run_repeating 都应在 on_destroy 中有对应的 cancel。泄露的任务会浪费 CPU。

  • 使用调度器回调的新上下文。 调度器回调接收新的上下文参数。始终在回调内使用该参数,而不是外部的 context

  • 保持 on_game_tick 轻量。 如果你定义了此钩子,它每个服务器 tick 都会运行(每秒 20 次)。将开销大的工作放在基于状态的冷却检查之后。

  • 默认使道具无敌。 除非你希望道具可以被破坏,否则在每个脚本中都包含 on_left_click 的伤害取消。

  • Bukkit 枚举使用 UPPER_CASE。 声音名称和粒子名称必须使用 Bukkit 枚举常量格式(例如 "FLAME",而不是 "flame")。


常见新手错误

  • 在调度器回调中使用外部 context 外部上下文捕获的是钩子运行时的快照。在回调内部,始终使用回调自身的参数。

  • 忘记取消重复任务。 如果你在 on_spawn 中启动了 run_repeating 但从未取消它,任务会一直运行直到道具被移除。

  • 未在 on_spawn 中初始化状态。 在设置之前读取 context.state.x 会返回 nil,这可能会无声地破坏你的逻辑。

  • 错误的动画名称。 如果 play_animation("open") 返回 false,说明动画名称与模型文件中的不匹配。检查模型以确认准确的名称。

  • 声音/粒子名称使用小写。 "flame" 不起作用——使用 "FLAME"。API 内部会将粒子转换为 UPPER_CASE,但 Sound 枚举名称必须精确匹配。

  • 忘记 api_version = 1 返回的表必须包含此字段,否则 FMM 将不会加载该脚本。

  • 在返回的表中放置非钩子函数。 辅助函数必须在 return 语句上方声明。返回表中只允许使用钩子名称(on_spawnon_right_click 等)作为键。


QC 检查清单

在部署道具脚本之前,使用此检查清单进行验证:

  1. 文件恰好返回一个包含 api_version = 1 的表。
  2. 每个钩子名称与钩子列表中的条目精确匹配。
  3. 在调用 cancel() 之前,通过 if context.event then 检查了 context.event
  4. context.state 字段在 on_spawn 中初始化。
  5. 每个 scheduler:run_repeating(...) 调用在 on_destroy 中有对应的 scheduler:cancel(...)
  6. 调度器回调使用回调自身的上下文参数,而不是外部的 context
  7. on_game_tick 钩子将开销大的工作放在检查之后。
  8. 所有方法名称存在于 Prop API 参考中——没有编造的别名。
  9. 声音和粒子名称使用 UPPER_CASE 的 Bukkit 枚举名称。
  10. 脚本不在钩子或回调中调用任何阻塞或长时间运行的操作。

AI 生成提示

如果你想让 AI 可靠地生成道具脚本,请确保提示词中包含:

  • 准确的钩子名称 -- 例如 on_right_click,而不是"当玩家点击道具时"。
  • 来自模型文件的动画名称 -- AI 无法猜测这些名称;请提供它们。
  • Sound 枚举名称 -- 例如 "BLOCK_NOTE_BLOCK_HARP",而不是"竖琴声"。
  • Particle 枚举名称 -- 例如 "FLAME",而不是"火焰粒子"。
  • 道具是否应该无敌 -- 如果是,请包含带有 context.event.cancel()on_left_click
  • 只使用文档中记录的方法名称 -- 如果不在 Prop API 页面上,它就不存在。

良好的提示词示例

编写一个 FMM 道具脚本,右键点击时播放 "activate" 动画,使道具无敌,点击时在道具位置生成 FLAME 粒子,播放 BLOCK_LEVER_CLICK 声音,并使用 context.state 和 scheduler:run_later 实现点击之间 2 秒的冷却。


下一步

  • 入门指南 -- 文件结构、钩子、首个脚本讲解、模板
  • Prop API -- 所有上下文表的完整 API 参考
  • 故障排除 -- 常见问题、调试技巧