Luanti(旧Minetest)向けの鬼ごっこ用のModを作った
はじめに
誰が誰を殴った!
最近はよくLuanti(旧Minetest)内で複数人で遊んでいるのだが、口頭でルールを決めて鬼ごっこのようなミニゲームをしていると、だいたいこうなる。ゲーム内で独自のミニゲームをする時、みんながルールを守るというのは案外難しい。それなら、システム側でルールを強制すればいい。そこで、鬼ごっこModを自作することにした。
ルール
ルールはシンプルにした。
- 鬼は「札」というアイテムを持つ
- 札を手に持った状態で別のプレイヤーをパンチすると、札が相手に渡る
- パンチしてもダメージは入らない
- 鬼になったプレイヤーは赤く光り、10秒間動けなくなる
では作っていく。
札アイテムを作る
まず minetest.register_tool で札を登録した。
minetest.register_tool("oni_tag:fuda", {
description = "鬼の札\n(パンチで相手に渡す)",
inventory_image = "oni_tag_fuda.png",
on_use = function(itemstack, user, pointed_thing)
return itemstack
end,
})右クリックでは何もしない。アクションはすべてパンチ(左クリック)で起こす。
パンチを横取りする
minetest.register_on_punchplayer を使うと、プレイヤーが殴られた瞬間に割り込める。戻り値に true を返すとダメージがキャンセルされる。
minetest.register_on_punchplayer(function(player, hitter, ...)
if not hitter or not hitter:is_player() then
return
end
if hitter:get_wielded_item():get_name() ~= "oni_tag:fuda" then
return
end
-- ダメージをキャンセルして札を移動させる
...
return true
end)
最初は hitter:get_inventory():contains_item("main", "oni_tag:fuda") でインベントリ全体を確認していたが、これでは「インベントリに入れておくだけ」でも発動してしまう。手に持って殴ったときだけ反応させたいので get_wielded_item():get_name() に切り替えた。
赤いパーティクルをまとわせる
鬼になったことを見た目でわかるようにするために minetest.add_particlespawner を使った。 attached にプレイヤーオブジェクトを渡すと、パーティクルがプレイヤーに追従する。
local id = minetest.add_particlespawner({
amount = 20,
time = 0, -- 0 で永続
attached = player,
minpos = {x = -0.6, y = -0.1, z = -0.6},
maxpos = {x = 0.6, y = 2.0, z = 0.6},
minvel = {x = -0.3, y = 0.2, z = -0.3},
maxvel = {x = 0.3, y = 0.8, z = 0.3},
minacc = {x = 0, y = 0.1, z = 0},
maxacc = {x = 0, y = 0.2, z = 0},
minexptime = 0.4,
maxexptime = 0.9,
minsize = 0.8,
maxsize = 1.2,
collisiondetection = false,
color = "#FF1100FF",
})
collisiondetection = false にしておかないとパーティクルが地面に当たって処理が重くなる。 amount と maxsize を大きくしすぎると負荷が上がるので、見た目と負荷のバランスで amount = 20 、 maxsize = 1.2 に落ち着いた。
ネームタグも赤くした。
player:set_nametag_attributes({
color = {r = 255, g = 80, b = 80, a = 255},
})鬼になったら10秒間動けなくする
set_physics_override でプレイヤーの移動速度とジャンプを0にできる。
player:set_physics_override({speed = 0, jump = 0})
ただし古いバージョンのLuantiではこれが効かない場合がある。そこで register_globalstep を使って毎フレームプレイヤーの座標を強制リセットする方法を併用した。
minetest.register_globalstep(function(dtime)
for name, pos in pairs(frozen_positions) do
local p = minetest.get_player_by_name(name)
if p then
p:set_pos(pos)
end
end
end)
停止開始時にその座標を frozen_positions に記録しておき、毎フレーム set_pos で戻し続ける。新旧どちらのバージョンでも動く。
10秒後に解除するのは minetest.after に任せる。
minetest.after(10, function()
local p = minetest.get_player_by_name(name)
if p and frozen_players[name] then
frozen_players[name] = nil
frozen_positions[name] = nil
p:set_physics_override({speed = 1, jump = 1})
end
end)タッチ成功時に前の鬼の停止を解除し忘れていた
鬼がタッチに成功して札を渡したとき、前の鬼のパーティクルとネームタグは解除していたが、停止(freeze)を解除していなかった。タッチされた側は新しく10秒停止するのに、タッチした側も残り時間のあいだ動けないままになってしまっていた。
clear_oni_effect の中に unfreeze_player を入れず、パンチ処理の中で明示的に呼ぶようにした。
if oni_name then
local prev_oni = minetest.get_player_by_name(oni_name)
if prev_oni then
clear_oni_effect(prev_oni)
unfreeze_player(prev_oni) -- これを忘れていた
end
endチャットコマンド
ゲームの開始・終了・確認を操作するコマンドを3つ用意した。
| コマンド | 説明 |
|---|---|
/oni_start [プレイヤー名] |
鬼ごっこを開始し、最初の鬼を指定する |
/oni_stop |
鬼ごっこを終了する |
/oni_who |
現在の鬼を確認する |
完成したコード
-- oni_tag/init.lua
local oni_name = nil
local particle_spawners = {}
local frozen_players = {}
local frozen_positions = {}
minetest.register_globalstep(function(dtime)
for name, pos in pairs(frozen_positions) do
local p = minetest.get_player_by_name(name)
if p then p:set_pos(pos) end
end
end)
local function freeze_player(player)
local name = player:get_player_name()
frozen_players[name] = true
frozen_positions[name] = player:get_pos()
player:set_physics_override({speed = 0, jump = 0})
minetest.chat_send_player(name, "※ 10秒間動けません!")
minetest.after(10, function()
local p = minetest.get_player_by_name(name)
if p and frozen_players[name] then
frozen_players[name] = nil
frozen_positions[name] = nil
p:set_physics_override({speed = 1, jump = 1})
minetest.chat_send_player(name, "※ 動けるようになりました!")
end
end)
end
local function unfreeze_player(player)
local name = player:get_player_name()
if frozen_players[name] then
frozen_players[name] = nil
frozen_positions[name] = nil
player:set_physics_override({speed = 1, jump = 1})
end
end
local function apply_oni_effect(player)
local name = player:get_player_name()
if particle_spawners[name] then
minetest.delete_particlespawner(particle_spawners[name])
end
local id = minetest.add_particlespawner({
amount = 20,
time = 0,
attached = player,
minpos = {x = -0.6, y = -0.1, z = -0.6},
maxpos = {x = 0.6, y = 2.0, z = 0.6},
minvel = {x = -0.3, y = 0.2, z = -0.3},
maxvel = {x = 0.3, y = 0.8, z = 0.3},
minacc = {x = 0, y = 0.1, z = 0},
maxacc = {x = 0, y = 0.2, z = 0},
minexptime = 0.4,
maxexptime = 0.9,
minsize = 0.8,
maxsize = 1.2,
collisiondetection = false,
color = "#FF1100FF",
})
particle_spawners[name] = id
player:set_nametag_attributes({color = {r = 255, g = 80, b = 80, a = 255}})
freeze_player(player)
end
local function clear_oni_effect(player)
local name = player:get_player_name()
if particle_spawners[name] then
minetest.delete_particlespawner(particle_spawners[name])
particle_spawners[name] = nil
end
player:set_nametag_attributes({color = {r = 255, g = 255, b = 255, a = 255}})
end
minetest.register_tool("oni_tag:fuda", {
description = "鬼の札\n(パンチで相手に渡す)",
inventory_image = "oni_tag_fuda.png",
on_use = function(itemstack, user, pointed_thing)
return itemstack
end,
})
minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch,
tool_capabilities, dir, damage)
if not hitter or not hitter:is_player() then return end
if hitter:get_wielded_item():get_name() ~= "oni_tag:fuda" then return end
local hitter_name = hitter:get_player_name()
local player_name = player:get_player_name()
if hitter_name == player_name then return end
if oni_name then
local prev_oni = minetest.get_player_by_name(oni_name)
if prev_oni then
clear_oni_effect(prev_oni)
unfreeze_player(prev_oni)
end
end
hitter:get_inventory():remove_item("main", "oni_tag:fuda 1")
player:get_inventory():add_item("main", "oni_tag:fuda 1")
oni_name = player_name
apply_oni_effect(player)
minetest.chat_send_all("*** " .. player_name .. " が鬼になりました! ***")
return true
end)
minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
if particle_spawners[name] then
minetest.delete_particlespawner(particle_spawners[name])
particle_spawners[name] = nil
end
unfreeze_player(player)
if oni_name == name then
oni_name = nil
minetest.chat_send_all("鬼がいなくなりました。")
end
end)
minetest.register_chatcommand("oni_start", {
description = "鬼ごっこを開始する",
privs = {interact = true},
func = function(caller, param)
local target_name = (param ~= "" and param or caller)
local target = minetest.get_player_by_name(target_name)
if not target then
return false, "プレイヤー '" .. target_name .. "' が見つかりません"
end
if oni_name then
local prev = minetest.get_player_by_name(oni_name)
if prev then
clear_oni_effect(prev)
unfreeze_player(prev)
prev:get_inventory():remove_item("main", "oni_tag:fuda 1")
end
end
target:get_inventory():add_item("main", "oni_tag:fuda 1")
oni_name = target_name
apply_oni_effect(target)
minetest.chat_send_all("*** 鬼ごっこスタート! " .. target_name .. " が鬼です! ***")
return true, "開始しました"
end,
})
minetest.register_chatcommand("oni_stop", {
description = "鬼ごっこを終了する",
privs = {interact = true},
func = function(caller, param)
if not oni_name then
return false, "現在、鬼ごっこは開始されていません"
end
local prev = minetest.get_player_by_name(oni_name)
if prev then
clear_oni_effect(prev)
unfreeze_player(prev)
prev:get_inventory():remove_item("main", "oni_tag:fuda 1")
end
oni_name = nil
minetest.chat_send_all("*** 鬼ごっこを終了しました ***")
return true, "終了しました"
end,
})
minetest.register_chatcommand("oni_who", {
description = "現在の鬼を確認する",
privs = {},
func = function(caller, param)
if not oni_name then return true, "現在、鬼はいません" end
return true, "現在の鬼: " .. oni_name
end,
})まとめ
Modを作ってみた感触としては、LuantiのModに関連するAPIは思ったよりシンプルだった。パンチの横取りもダメージのキャンセルも1つの関数を登録するだけで済む。インベントリの操作も remove_item と add_item で直感的に書ける。
ただ、動作確認をしながら進めると細かいバグが出てたりもした。「インベントリ全体ではなく手持ちで判定する」「タッチ成功時に前の鬼の停止も解除する」といった点は、実際に複数人で動かしてみないと気づきにくい。
移動停止については set_physics_override だけでは旧バージョンで動かないことがあるため、 register_globalstep で座標を固定する方法を併用した。こういった互換性の問題はドキュメントだけではわからないので、実際に試してみることが大事だと思った。
おかげで最近は「誰が誰を殴った」という話にならない。Modに聞けば一発でわかる。