メインコンテンツまでスキップ

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_ANIMATIONCLOSE_ANIMATIONはreturnテーブルの上で定義されています。これにより、異なるアニメーション名を使用する別のモデルファイルに合わせて簡単に変更できます。

  2. 状態の初期化 -- on_spawncontext.state.is_open = falseを設定します。状態はこのプロップインスタンスのすべてのフックにわたって保持されます。

  3. 無敵化 -- on_left_clickフックがダメージをキャンセルし、ドアが誤って壊されるのを防ぎます。

  4. トグルロジック -- on_right_clickcontext.state.is_openをチェックし、現在のアニメーションを停止し、適切なアニメーションを再生し、状態を反転させ、サウンドを再生します。play_animation()の前にstop_animation()を呼ぶことで、クリーンな遷移を保証します。

  5. サウンドフィードバック -- context.world:play_sound()はプロップの位置でBukkitのSoundenum名を再生します。サウンド名はUPPER_CASEのenum名でなければなりません。

アニメーション名

"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. パーティクルループ -- 繰り返しタスクが0.5秒ごとにプロップの周囲にリング状のパーティクルを生成します。ゾーン内にプレイヤーがいるかどうかでパーティクルの種類が変わります。

  6. クリーンアップ -- on_destroyは繰り返しタスクをキャンセルし、ゾーン監視を解除します。プロップが削除されると両方とも自動的にクリーンアップされますが、明示的なクリーンアップはベストプラクティスです。

パーティクルのパフォーマンス

毎ティックで大量のパーティクルを生成すると、パフォーマンスに影響を与える可能性があります。適切なインターバル(10〜20ティック)を使用し、パーティクル数を低く抑えてください。上記のサンプルではPARTICLE_INTERVAL = 10(毎秒2回)で、リングあたり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 enum名、座標、音量、ピッチを受け取ります。

  3. パーティクルフィードバック -- サウンド再生時にプロップの上にノートパーティクルが表示され、視覚的な手がかりを提供します。

  4. 無敵化 -- on_left_clickフックは通常通りダメージをキャンセルします。

  5. スケジューラーコールバックのコンテキスト -- run_laterのコールバックはlater_contextという新しいコンテキストを受け取ります。クールダウンフラグのリセットにはlater_context.statecontext.stateではなく)を使用します。状態は共有されているため、両方とも同じテーブルを指しますが、コールバックのコンテキストパラメータを使用するのが正しい習慣です。

クールダウンの実装

FMMのプロップスクリプトにはEliteMobsのcontext.cooldowns APIがありません。ここで示すパターンを使用してください: 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"アニメーションを再生します。blendのfalseは前のアニメーションからのブレンドなしで開始することを意味します。loopのtrueは無限に繰り返すことを意味します。

  2. アンビエントパーティクル -- 繰り返しタスクが2秒ごとにプロップの上にエンチャントテーブルのパーティクルを生成し、魔法のようなアンビエントエフェクトを作り出します。

  3. クリーンアップ -- on_destroyはパーティクルタスクをキャンセルします。


ベストプラクティス

  • 小さなフックから始めて確認する。 ログメッセージを送信する単一のon_spawnを書きます。発火することを確認してから、そこから構築していきます。

  • ヘルパー関数はローカルにする。 local function toggle_door(context)のようなヘルパーはreturnテーブルの上で宣言します。これによりグローバルスコープの外に保たれます。

  • すべての状態をon_spawnで初期化する。 on_right_clickcontext.state.is_openを読み取るがon_spawnで設定しない場合、nilになり比較が予期しない動作をする可能性があります。

  • 完了したら繰り返しタスクをキャンセルする。 すべてのrun_repeatingにはon_destroy内に対応するcancelが必要です。リークしたタスクはCPUを無駄にします。

  • 新しいスケジューラーコールバックコンテキストを使用する。 スケジューラーコールバックは新しいコンテキストパラメータを受け取ります。コールバック内では外側のcontextではなく、常にそのパラメータを使用してください。

  • on_game_tickを軽量に保つ。 このフックを定義すると、サーバーティックごとに実行されます(毎秒20回)。コストの高い処理は状態ベースのクールダウンチェックの背後に置いてください。

  • プロップはデフォルトで無敵にする。 プロップを破壊可能にしたい場合を除き、すべてのスクリプトにon_left_clickのダメージキャンセルを含めてください。

  • Bukkit enumにはUPPER_CASEを使用する。 サウンド名とパーティクル名はBukkit enum定数形式を使用する必要があります(例: "FLAME""flame"ではなく)。


初心者によくある間違い

  • スケジューラーコールバック内で外側のcontextを使用する。 外側のcontextはフックが実行された時点のスナップショットをキャプチャします。コールバック内では常にコールバック自身のパラメータを使用してください。

  • 繰り返しタスクのキャンセルを忘れる。 on_spawnrun_repeatingを開始しても一度もキャンセルしない場合、タスクはプロップが削除されるまで実行され続けます。

  • on_spawnで状態を初期化しない。 設定前にcontext.state.xを読み取るとnilが返され、ロジックが静かに壊れる可能性があります。

  • アニメーション名の誤り。 play_animation("open")falseを返す場合、アニメーション名がモデルファイルのものと一致していません。正確な名前についてはモデルを確認してください。

  • サウンド/パーティクル名を小文字にする。 "flame"は機能しません -- "FLAME"を使用してください。APIはパーティクルについては内部的にUPPER_CASEに変換しますが、Sound enum名は正確でなければなりません。

  • api_version = 1を忘れる。 返されるテーブルにはこのフィールドが含まれている必要があります。そうでないとFMMはスクリプトを読み込みません。

  • フックではない関数を返されるテーブル内に配置する。 ヘルパー関数はreturn文の上で宣言する必要があります。返されるテーブルのキーとして許可されるのはフック名(on_spawnon_right_clickなど)のみです。


QCチェックリスト

プロップスクリプトをデプロイする前に、このチェックリストで確認してください:

  1. ファイルがapi_version = 1を持つテーブルを正確に1つ返す。
  2. すべてのフック名がフックリストのエントリと正確に一致する。
  3. cancel()を呼ぶ前にcontext.eventif context.event thenでガードしている。
  4. context.stateのフィールドがon_spawnで初期化されている。
  5. すべてのscheduler:run_repeating(...)呼び出しにon_destroy内のscheduler:cancel(...)が対応している。
  6. スケジューラーコールバックが外側のcontextではなく、コールバック自身のコンテキストパラメータを使用している。
  7. on_game_tickフックがコストの高い処理をチェックの背後に置いている。
  8. すべてのメソッド名がプロップAPIリファレンスに存在する -- 作り出したエイリアスがない。
  9. サウンドとパーティクル名がUPPER_CASEのBukkit enum名を使用している。
  10. スクリプトがフックまたはコールバック内でブロッキングまたは長時間実行される操作を呼び出していない。

AI生成のヒント

AIに信頼性の高いプロップスクリプトを生成させたい場合、プロンプトに以下を含めてください:

  • 正確なフック名 -- 例: on_right_click、「プレイヤーがプロップをクリックしたとき」ではなく。
  • モデルファイルからのアニメーション名 -- AIはこれらを推測できません。提供してください。
  • Sound enum名 -- 例: "BLOCK_NOTE_BLOCK_HARP"、「ハープの音」ではなく。
  • Particle enum名 -- 例: "FLAME"、「炎のパーティクル」ではなく。
  • プロップを無敵にすべきかどうか -- はいの場合、context.event.cancel()付きのon_left_clickを含める。
  • ドキュメントに記載されたメソッド名のみを使用する -- プロップAPIページにないものは存在しません。

良いプロンプトの例

右クリック時に「activate」アニメーションを再生し、プロップを無敵にし、クリック時にプロップの位置にFLAMEパーティクルを生成し、BLOCK_LEVER_CLICKサウンドを再生し、context.stateとscheduler:run_laterを使用してクリック間に2秒のクールダウンを持つFMMプロップスクリプトを書いてください。


次のステップ