数年の間、minetestというゲームで遊んでいる。これは、C++で実装されたボクセルゲーム1で、Luaで拡張していく事ができる。この拡張はmodと呼ばれる。これまで、ゲームサーバをGoogle Cloud上にホスティングしたり、小さなmodをいくつか実装したりした。その中で自分だけのスキンを作りたいと思うようになった。そこで今回は、これについて調べる事にした。
スキン
スキンとは、キャラクターの外観の事だ。多くのゲームでは、このスキンを変更できるようにしており、お気に入りの顔、髪、肌、服などの要素を設定できるようになっている。ゲームによっては、それらを組み合わせたり、顔のパーツ(目、鼻、口など)も組み合せられるようになっていたりする。
スキン、自分で作れたらとても楽しいと思うんだ。
スキン用Mod
MinetestのModは、https://content.minetest.net/に集められている。その中にはスキン用のModもある。
skinsdbはスキンを変更するMod、3d_armorはスキンと装備を含んでいる。また、minetestのゲームの一種であるVoxelLibreには、独自の完成度の高いスキンの実装を持っている。
最小のゲーム
まず、Minetestというのはゲームエンジンであって、ゲームそのものではない。だから何もない状態でゲームをプレイする事はできない。とはいえ初期状態のMinetestには、minetest_gameとdevtestという2つのゲームが用意されている。このどちらかのゲームを使い、ワールドを新規作成してゲームをプレイする事ができる。
minetest_game
は、それなりにゲームとして成立するぐらいに整備された小さなゲームだ。通常は、ここから始める人が多いだろう。 devtest
は、開発者向けの実験場として整備されている。その代わり、スキンなどは整備されていない。
ただし、どちらも最小のゲームという訳ではない。
Modは各ゲームにインストールする形となる。そして、各ゲームは、Minetestのアプリケーションデータの中に含まれている。
macOSの場合 /Applications/minetest.app/Contents/Resources/games/
配下に初期状態のゲームが格納されている。 minetest_games
や devtest
はここにある。独自のゲームをインストールすると、 ~/Library/Application Support/minetest/games/
配下に格納される。
ゲームのディレクトリ構成は以下のようになる。
YOUR_GAME_NAME
|-- game.conf
|-- menu
| |-- background.png
| `-- icon.png
`-- mods
`-- default
|-- init.lua
|-- mod.conf
`-- textures
|-- default_cobble.png
|-- default_stone.png
|-- default_water.png
|-- default_water_flowing_animated.png
`-- default_water_source_animated.png
各ファイル及びディレクトリは以下のような役割を持っている。
ファイル/ディレクトリ名 | 役割 |
---|---|
game.conf | ゲームの設定を記述ファイル。 |
menu/ | ゲーム選択のメニュー画面用ディレクトリ。 |
menu/background.png | ゲーム選択のメニューの背景画像。 |
menu/icon.png | ゲーム選択のメニューに表示するアイコン画像。 |
mods/ | Mod格納用ディレクトリ。 |
mods/default/ | 初期Mod。 default という名前である必要はない。 |
mods/default/init.lua | 初期Mod用ファイル。中身は後述する。 |
mods/default/mod.conf | 初期Mod用設定ファイル。中身は後述する |
mods/default/textures/ | テクスチャを格納するディレクトリ。 |
mods/default/textures/default_cobble.png | 丸石用テクスチャ。 |
mods/default/textures/default_stone.png | 石用テクスチャ。 |
mods/default/textures/default_water.png | 水用テクスチャ。 |
mods/default/textures/default_water_flowing_animated.png | 流水用テクスチャ(アニメーション用)。 |
mods/default/textures/default_water_source_animated.png | 水源用テクスチャ(アニメーション用)。 |
メニューに表示する画像ファイルは、適当に好きな画像をPNG形式で使えばいい。ファイル名に意味があるため、ファイル名は同じにする必要がある。 mods/default/textures/
配下にあるテクスチャは minetest_games
の default
Modから拝借した。
以降では、その他の重要なファイルについて説明する。
game.conf
そのディレクトリをゲームとして認識させるためには、このファイルが必要になる。ゲームタイトルや、バージョン、作者など、ゲーム自体の設定を行う。最小の設定であれば、それほど設定する項目は多くない。以下に設定の例を示す。
mods/default
mods/
配下にはModを配置する。Modは mod.conf
と init.lua
を格納したディレクトリの形式をしている。ここでは default
という名前でModを作成している。もし example
というModを作りたければ、ディレクトリ名も example
となる。
mods/default/mod.conf
mod.conf
は、そのModの設定を記述する。
mods/default/init.lua
init.lua
は、そのModを読み込む際に読み込まれるLuaコードを記述する。
ワールド作成時又はゲームプレイ時にエラーを吐かないようにするには、音、石(default:stone
)、丸石(default:cobble
)、水源(default:water_source
)、流水(default:water_flowing
)、マップ生成に関連する設定が最低限必要になる。以下に例を示す。
最小世界
ここまで整備すれば最小の構成で世界を作る事ができる。ただし、ゲーム要素は全くない。石と水だけの広大な世界が、ただただ広がっているだけだ。アイテムもモブもない。クラフトもできないし、採掘もできない。ただし命はある。落下ダメージは受けるし、溺死もする。最小世界とはそういう世界だ。
グリーンマン
最小のゲームを整備し、最小世界を作った。最小世界ではスキンも最小だ。プレイヤーは緑色の薄っぺらい物体として表現される。ぎりぎりヒトっぽい形はしているけれど、キャラクターとしてはショボすぎる。これが最小世界だ。このキャラクターを グリーンマン と呼ぶ事にする。
正面
背面
minetest_game
にはそれなりのスキンが用意されているためグリーンマンは登場しないが、 devtest
にはスキンがないためグリーンマンが登場する。人間の本質とは、グリーンマンという事なのだろう。
受肉
僕達はグリーンマンに対し、感情移入したり人間性を感じ取れるだろうか。もしかしたら、そういう感情を抱く事ができるかもしれないけれど、やはりあまりに記号的で限度を越えている。せめて顔があって服を着ているぐらいには人間っぽくなっていて欲しい。
人間性の移植
minetest_games
のModである player_api
には、そこそこ人間を感じられる程度のスキンが用意されている。そのスキンのデータを、先程の最小世界に移植し、人間性を取り戻すと共に、スキンとは何かについて学ぶ事にする。
player_api
は以下のようなディレクトリ構成となっている。
mods/player_api
|-- README.txt
|-- api.lua
|-- init.lua
|-- license.txt
|-- mod.conf
|-- models
| |-- character.b3d
| |-- character.blend
| `-- character.png
`-- textures
|-- player.png
`-- player_back.png
このModは根本的な要素を構成するModであるため、このディレクトリを丸ごとコピーすれば良い。 api.lua
には外部向けに実装されたAPIが用意されている。ここで用意されている処理は、Minetestというゲームエンジンを操作する。そして models/
ディレクトリには、プレイヤーの外観(character.png)と3Dモデル(character.b3d)が含まれている。これを init.lua
でプレイヤーに適応している。ゲームを読み込み直すと、だいぶ人間性を取り戻す事ができる。
アニメーションの仕組み
models/character.b3d
は3Dモデルのデータだが、これは models/character.blend
を元に生成する。 models/character.blend
はBlenderという無料の3Dモデリングソフトウェアのファイルだ。
Blenderでこのファイルを開くと、モデルデータがある。アニメーションビューからアニメーションを実行すると、一連の動作を確認できる。
コントロール
歩いたり、採掘したり、しゃがんだりといったプレイヤーの制御をコントールと呼ぶ。 player:get_player_control()
を使うと、このコントロールを取得できる。
player:get_player_control()
Minetestでは minetest/src/script/lua_api/l_object.cpp
で次のようなコントロールが用意されている。
- up
- down
- left
- right
- jump
- aux1
- sneak
- dig
- placep
またModとの互換性を維持するためのフィールドも用意されている。ただし、これはレガシーとした扱われているため、新しいコードを実装する際には使用しない方がよさそうだ。
- LMB
- RMB
- zoom
player_api
では player_api/api.lua
で player_api.globalstep
を定義し、この関数を minetest.register_globalstep()
で登録している。
minetest.register_globalstep(function(...)
player_api.globalstep(...)
end)
player_api.globalstep
は、minetestによって呼び出される。
function player_api.globalstep()
for _, player in ipairs(minetest.get_connected_players()) do
local name = player:get_player_name()
local player_data = players[name]
local model = player_data and models[player_data.model]
if model and not player_attached[name] then
local controls = player:get_player_control()
local animation_speed_mod = model.animation_speed or 30
-- Determine if the player is sneaking, and reduce animation speed if so
if controls.sneak then
animation_speed_mod = animation_speed_mod / 2
end
-- Apply animations based on what the player is doing
if player:get_hp() == 0 then
player_set_animation(player, "lay")
elseif controls.up or controls.down or controls.left or controls.right then
if controls.LMB or controls.RMB then
player_set_animation(player, "walk_mine", animation_speed_mod)
else
player_set_animation(player, "walk", animation_speed_mod)
end
elseif controls.LMB or controls.RMB then
player_set_animation(player, "mine", animation_speed_mod)
else
player_set_animation(player, "stand", animation_speed_mod)
end
end
end
end
この中で player_set_animation
を呼び出してアニメーションを更新している。
player_api.register_model("character.b3d", {
animation_speed = 30,
textures = {"character.png"},
animations = {
-- Standard animations.
stand = {x = 0, y = 79},
lay = {x = 162, y = 166, eye_height = 0.3, override_local = true,
collisionbox = {-0.6, 0.0, -0.6, 0.6, 0.3, 0.6}},
walk = {x = 168, y = 187},
mine = {x = 189, y = 198},
walk_mine = {x = 200, y = 219},
sit = {x = 81, y = 160, eye_height = 0.8, override_local = true,
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.0, 0.3}}
},
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.7, 0.3},
stepheight = 0.6,
eye_height = 1.47,
})
アニメーションはモデルの登録時に同時に登録している。その時に、xに開始フレーム、yに終了フレームを指定する。
この関数は player_api
の関数なので、Minetest自信の機能を呼び出している所は別にある。
テクスチャを貼る練習
スキンは形状とテクスチャを作るという事になる。通常のキャラクタのモデルは、Blender用のファイルが既にあるのでそのまま使う事にする。つまりテクスチャが作れれば、雰囲気をガラっと変える事ができるだろう。
- UVマップを作成する。
- UVマップのレイアウトをPNGとして書き出す。
- UVマップのレイアウトのPNGに対し描画し書き出す。
- 書き出した画像をテクスチャとして適応する。
だいたいこのような流れになる。手始めに雑なサイコロを作った。
テクスチャを作る
元々あるモデルデータを利用し、そこからUV展開しサイコロと同じ要領でテクスチャを用意しようとしたが、上手くUV展開させる事ができなかった。
分からない事が多すぎる。なぜ、こんなにも難しいのか。仕方ないので、もっと簡単な状態から始める事にする。
player_apiを観察する
キャラクタのモデルデータとその設定は player_api
で実装されている。 player_api
には、必ず minetest
とやりとりしている部分があるはずだ。そこを観察し、段階的に上手く抽出する。
player_api/init.lua
では起動時に、次のような処理が実行される。
player_api
の関数の実装(api.lua
)を読み込む。player_api.register_model
でキャラクターのモデルや設定をplayer_api.registered_models
に保持する。 ただし、この時にモデルが反映される訳ではない。minetest.register_on_joinplayer
によって、プレイヤーが参加した時に呼び出されるコールバック関数を登録する。 このコールバック関数の中でモデルを反映する関数(player_api.set_model
) が呼び出される。
player_api.set_model()
によってモデルを反映しているが、これもまた player_api
で実装されている。この関数の中で呼び出されている player:set_properties()
が本質的な部分だ。 player
はコールバック関数の呼出元(つまりminetest本体)が渡してくるプレイヤーのオブジェクトであり、C++で実装されている。ここまで分かれば太刀打ちできる。
グリーンマンの反映
モデルのデータがないなどの場合、 player_api
はスキンをグリーンマンに縮退する。実際にには、以下のコードによってグリーンマンの設定を反映している。
player:set_properties({
textures = {"player.png", "player_back.png"},
visual = "upright_sprite",
visual_size = {x = 1, y = 2},
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.75, 0.3},
stepheight = 0.6,
eye_height = 1.625,
})
textures
に表用と裏用のPNG画像を指定している。 visual
の upright_sprite
についてはよくわからないけれど、見た目のサイズ、当たり判定のサイズ、高さ、目線の高さを設定している。
グリーンマンを牛乳ビンにする
player:set_properties()
に渡す textures
の値を、お気に入りのPNG画像に差し替えれば、キャラクターの姿を変更する事ができそうだ。そこで MineBonbon公式キャラクターの牛乳ビンを投影してみる。
このキャラクターは、チームでゲーム配信をやろうという話になった時、メンバーが手書きで作成したキャラクターだ。クオリティは低いが愛着があるから使い続けている。名前すら決まっていないが、便宜上「牛乳ビン」と呼ぶ事にする。
前面の画像はあったけれど、背面の画像はない。そこで画像を反転し、目と口を消す事で背面画像を作った。
これらの画像を mods/default/textures
に配置し、 mods/default/init.lua
に次のようなコードを追加した。
minetest.register_chatcommand("bonbon", {
func = function (name, param)
local player = minetest.get_player_by_name(name)
player:set_properties({
textures = {"player_bonbon.png", "player_bonbon_back.png"},
visual = "upright_sprite",
visual_size = {x = 1, y = 2},
collisionbox = {-0.3, 0.0, -0.3, 0.3, 1.75, 0.3},
stepheight = 0.6,
eye_height = 1.625,
})
end,
})
minetest.register_chatcommand()
は、チャットコマンドを登録する。第1引数にコマンド、第2引数にコマンドによって実行される処理を渡す。
ここでは bonbon
というコマンドを定義し、そのコマンドが実行されると現在のプレイヤーを取得し、そのプレイヤーのテクスチャを牛乳ビンに変更する。コマンドを打つ時は T
でテキストチャットを開き、 /bonbon
と入力しエンターを押す。すると、キャラクターの姿が変化する。
とても雑な画像ではあるけれど、画像を差し替える方法は理解できた。せめて外枠の白い部分は、透過するようにした。
メッシュを適応する
先程はPNG画像をテクスチャに指定し、キャラクターの見た目を変更した。その結果、平面の画像がキャラクターに投影されたが、今度が3Dモデルを投影したい。
minetestでプレイヤーに対して設定を行うには、先程と同様に player:set_properties()
を使う。引数として指定する値は異なるため1つずつ確認していく。
mesh
にはモデル名を指定する。これは恐らくblenderで生成したb3dファイルの名前を指定する。
texture
にはテクスチャのPNGファイルの名前を指定する。前回は全面と背面の2種類を指定したが、ここではUV展開済みの画像を1つだけ指定する。
visual
には文字列 "mesh"
を指定する。
visual_size
、 stepheight
については値が表わす意味に変更はない。
また、 collisionbox
、 eye_height
は指定していない。これは、おそらくモデルの中に含まれているデータなのだろう。
簡単な3DモデルをBlenderで作成する。
作成したモデルを、obj形式のファイルに書き出す。
書き出したファイルは mods/defaults/models/
配下に配置した。
そしてこのファイルを読み込むようなコマンドを作成する。
minetest.register_chatcommand("monkey", {
func = function (name, param)
local player = minetest.get_player_by_name(name)
player:set_properties({
mesh = "monkey.obj",
textures = {"player_bonbon.png"},
visual = "mesh",
visual_size = {x = 5, y = 5},
stepheight = 0.6,
})
minetest.log("error", "Okay: apply bonbon2!!")
end,
})
これにより、キャラクターが猿になった。僕達はもしかしたら元々は猿だったのかもしれない。
メッシュ用のテクスチャをUV展開して作り適応する
ここまでで、UV展開でサイコロのテクスチャを作った。またメッシュの適応も行った。これらを合わせて、簡単なキャラクターをデザインする。
Blenderで作業
- モデルを作る。
- UV展開する。
- 「Export UV Layout」でPNG形式のファイルをエクスポートする。
- 「Export」でモデルをobj形式のファイルをエクスポートする。
Kritaで作業
- 先程出力したPNGファイルを開く。
- レイヤーを追加する。
- 追加したレイヤーに色を塗る。
- 元々あったレイヤーを非表示にする。
- PNGファイルとしてエクスポートする。
ターミナルで作業
- obj形式でエクスポートしたファイルをmodelsディレクトリに移動する。
- kritaでエクスポートしたPNGファイルをtexturesディレクトリに移動する。
テキストエディタで作業
- モデルとテクスチャを読み込むLuaを書く。
Luaの拡張は以下のようなコードとなる。ここでは /newcube
とするとモデルとテクスチャが適応されるようにした。
minetest.register_chatcommand("newcube", {
func = function (name, param)
local player = minetest.get_player_by_name(name)
player:set_properties({
mesh = "newcube.obj",
textures = {"newcube.png"},
visual = "mesh",
visual_size = {x = 5, y = 5},
stepheight = 0.6,
})
minetest.log("error", "Okay: apply bonbon2!!")
end,
})
こんな感じの新しいキャラクターができた。
ボクセルゲームとは、マイクラみたいなやつのこと。