Lua 脚本:示例与模式
本页包含 FreeMinecraftModels 道具脚本的完整可运行示例,以及实用模式和最佳实践。每个示例都附有详细说明,解释其功能和原因。
如果你刚接触道具脚本,请从入门指南开始。完整的 API 详情请参阅 Prop 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,停止当前动画,播放相应的动画,翻转状态,并播放声音。在play_animation()之前调用stop_animation()确保干净的过渡。 -
声音反馈 --
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
}
逐步讲解
-
常量 --
ZONE_RADIUS和PARTICLE_INTERVAL位于文件作用域,便于调整。 -
状态初始化 --
on_spawn在执行其他操作之前,将所有状态字段设置为nil/0。 -
区域创建 --
context.zones:create_sphere()创建以道具为中心的球形区域。返回的句柄是一个数字 ID,用于后续引用该区域。 -
区域监听 --
context.zones:watch()注册玩家进入和离开的回调。回调增减存储在context.state中的计数器。 -
粒子循环 -- 重复任务每半秒在道具周围以环形生成粒子。粒子类型根据区域内是否有玩家而变化。
-
清理 --
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
}
逐步讲解
-
冷却模式 -- 由于 FMM 道具脚本没有像 EliteMobs 那样的内置
context.cooldownsAPI,此示例使用context.state.on_cooldown和scheduler:run_later()实现简单的冷却。播放声音时标记设为true,延迟任务在COOLDOWN_TICKS后将其重置。 -
声音播放 --
context.world:play_sound()接受 UPPER_CASE 的 Bukkit Sound 枚举名称、坐标、音量和音调。 -
粒子反馈 -- 声音播放时,道具上方会出现音符粒子,提供视觉提示。
-
无敌 --
on_left_click钩子照常取消伤害。 -
调度器回调上下文 --
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
}
逐步讲解
-
自动启动动画 --
on_spawn立即播放循环"idle"动画。blend 的false表示不与之前的动画混合直接开始。loop 的true表示无限重复。 -
氛围粒子 -- 重复任务每 2 秒在道具上方生成附魔台粒子,营造魔法氛围效果。
-
清理 --
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_spawn、on_right_click等)作为键。
QC 检查清单
在部署道具脚本之前,使用此检查清单进行验证:
- 文件恰好返回一个包含
api_version = 1的表。 - 每个钩子名称与钩子列表中的条目精确匹配。
- 在调用
cancel()之前,通过if context.event then检查了context.event。 context.state字段在on_spawn中初始化。- 每个
scheduler:run_repeating(...)调用在on_destroy中有对应的scheduler:cancel(...)。 - 调度器回调使用回调自身的上下文参数,而不是外部的
context。 on_game_tick钩子将开销大的工作放在检查之后。- 所有方法名称存在于 Prop API 参考中——没有编造的别名。
- 声音和粒子名称使用 UPPER_CASE 的 Bukkit 枚举名称。
- 脚本不在钩子或回调中调用任何阻塞或长时间运行的操作。
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 秒的冷却。