« ^ »
[WIP]

スキンを作る

所要時間: 約 13分

数年の間、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_gamesdevtest はここにある。独自のゲームをインストールすると、 ~/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_gamesdefault Modから拝借した。

以降では、その他の重要なファイルについて説明する。

game.conf

そのディレクトリをゲームとして認識させるためには、このファイルが必要になる。ゲームタイトルや、バージョン、作者など、ゲーム自体の設定を行う。最小の設定であれば、それほど設定する項目は多くない。以下に設定の例を示す。

title = testing
description = A sandbox game
version=0.1.0
author=TakesxiSximada
game.conf

mods/default

mods/ 配下にはModを配置する。Modは mod.confinit.lua を格納したディレクトリの形式をしている。ここでは default という名前でModを作成している。もし example というModを作りたければ、ディレクトリ名も example となる。

mods/default/mod.conf

mod.conf は、そのModの設定を記述する。

name = default
description = Next generation
author = TakesxiSximada
mods/default/mod.conf

mods/default/init.lua

init.lua は、そのModを読み込む際に読み込まれるLuaコードを記述する。

ワールド作成時又はゲームプレイ時にエラーを吐かないようにするには、音、石(default:stone)、丸石(default:cobble)、水源(default:water_source)、流水(default:water_flowing)、マップ生成に関連する設定が最低限必要になる。以下に例を示す。

local S = minetest.get_translator(minetest.get_current_modname())

default = {}

-- sound
function default.node_sound_defaults(table)
	table = table or {}
	table.footstep = table.footstep or
			{name = "", gain = 1.0}
	table.dug = table.dug or
			{name = "default_dug_node", gain = 0.25}
	table.place = table.place or
			{name = "default_place_node_hard", gain = 1.0}
	return table
end

function default.node_sound_stone_defaults(table)
	table = table or {}
	table.footstep = table.footstep or
			{name = "default_hard_footstep", gain = 0.2}
	table.dug = table.dug or
			{name = "default_hard_footstep", gain = 1.0}
	default.node_sound_defaults(table)
	return table
end

function default.node_sound_water_defaults(table)
	table = table or {}
	table.footstep = table.footstep or
			{name = "default_water_footstep", gain = 0.2}
	default.node_sound_defaults(table)
	return table
end


-- stone
minetest.register_node("default:stone", {
	description = S("Stone"),
	tiles = {"default_stone.png"},
	groups = {cracky = 3, stone = 1},
	drop = "default:cobble",
	legacy_mineral = true,
	sounds = default.node_sound_stone_defaults(),
})

minetest.register_node("default:cobble", {
	description = S("Cobblestone"),
	tiles = {"default_cobble.png"},
	is_ground_content = false,
	groups = {cracky = 3, stone = 2},
	sounds = default.node_sound_stone_defaults(),
})


-- Liquids
minetest.register_node("default:water_source", {
	description = S("Water Source"),
	drawtype = "liquid",
	waving = 3,
	tiles = {
		{
			name = "default_water_source_animated.png",
			backface_culling = false,
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 2.0,
			},
		},
		{
			name = "default_water_source_animated.png",
			backface_culling = true,
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 2.0,
			},
		},
	},
	use_texture_alpha = "blend",
	paramtype = "light",
	walkable = false,
	pointable = false,
	diggable = false,
	buildable_to = true,
	is_ground_content = false,
	drop = "",
	drowning = 1,
	liquidtype = "source",
	liquid_alternative_flowing = "default:water_flowing",
	liquid_alternative_source = "default:water_source",
	liquid_viscosity = 1,
	post_effect_color = {a = 103, r = 30, g = 60, b = 90},
	groups = {water = 3, liquid = 3, cools_lava = 1},
	sounds = default.node_sound_water_defaults(),
})

minetest.register_node("default:water_flowing", {
	description = S("Flowing Water"),
	drawtype = "flowingliquid",
	waving = 3,
	tiles = {"default_water.png"},
	special_tiles = {
		{
			name = "default_water_flowing_animated.png",
			backface_culling = false,
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 0.5,
			},
		},
		{
			name = "default_water_flowing_animated.png",
			backface_culling = true,
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 0.5,
			},
		},
	},
	use_texture_alpha = "blend",
	paramtype = "light",
	paramtype2 = "flowingliquid",
	walkable = false,
	pointable = false,
	diggable = false,
	buildable_to = true,
	is_ground_content = false,
	drop = "",
	drowning = 1,
	liquidtype = "flowing",
	liquid_alternative_flowing = "default:water_flowing",
	liquid_alternative_source = "default:water_source",
	liquid_viscosity = 1,
	post_effect_color = {a = 103, r = 30, g = 60, b = 90},
	groups = {water = 3, liquid = 3, not_in_creative_inventory = 1,
		cools_lava = 1},
	sounds = default.node_sound_water_defaults(),
})


-- mapdisgens
minetest.register_alias("mapgen_stone", "default:stone")
minetest.register_alias("mapgen_water_source", "default:water_source")
mods/default/init.lua

最小世界

ここまで整備すれば最小の構成で世界を作る事ができる。ただし、ゲーム要素は全くない。石と水だけの広大な世界が、ただただ広がっているだけだ。アイテムもモブもない。クラフトもできないし、採掘もできない。ただし命はある。落下ダメージは受けるし、溺死もする。最小世界とはそういう世界だ。

グリーンマン

最小のゲームを整備し、最小世界を作った。最小世界ではスキンも最小だ。プレイヤーは緑色の薄っぺらい物体として表現される。ぎりぎりヒトっぽい形はしているけれど、キャラクターとしてはショボすぎる。これが最小世界だ。このキャラクターを グリーンマン と呼ぶ事にする。

正面

https://res.cloudinary.com/symdon/image/upload/v1721385665/blog.symdon.info/1721384196/green-man-omote.png

背面

https://res.cloudinary.com/symdon/image/upload/v1721385665/blog.symdon.info/1721384196/green-man-ura.png

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 でプレイヤーに適応している。ゲームを読み込み直すと、だいぶ人間性を取り戻す事ができる。

https://res.cloudinary.com/symdon/image/upload/v1721391706/blog.symdon.info/1721384196/sam.png

アニメーションの仕組み

models/character.b3d は3Dモデルのデータだが、これは models/character.blend を元に生成する。 models/character.blend はBlenderという無料の3Dモデリングソフトウェアのファイルだ。

Blenderでこのファイルを開くと、モデルデータがある。アニメーションビューからアニメーションを実行すると、一連の動作を確認できる。

https://res.cloudinary.com/symdon/image/upload/v1721392883/blog.symdon.info/1721384196/character_animation.gif

コントロール

歩いたり、採掘したり、しゃがんだりといったプレイヤーの制御をコントールと呼ぶ。 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.luaplayer_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用のファイルが既にあるのでそのまま使う事にする。つまりテクスチャが作れれば、雰囲気をガラっと変える事ができるだろう。

  1. UVマップを作成する。
  2. UVマップのレイアウトをPNGとして書き出す。
  3. UVマップのレイアウトのPNGに対し描画し書き出す。
  4. 書き出した画像をテクスチャとして適応する。

だいたいこのような流れになる。手始めに雑なサイコロを作った。

https://res.cloudinary.com/symdon/image/upload/v1721477992/blog.symdon.info/1721384196/cube-krita.png

https://res.cloudinary.com/symdon/image/upload/v1721477993/blog.symdon.info/1721384196/cube-shading.png

テクスチャを作る

元々あるモデルデータを利用し、そこからUV展開しサイコロと同じ要領でテクスチャを用意しようとしたが、上手くUV展開させる事ができなかった。

分からない事が多すぎる。なぜ、こんなにも難しいのか。仕方ないので、もっと簡単な状態から始める事にする。

player_apiを観察する

キャラクタのモデルデータとその設定は player_api で実装されている。 player_api には、必ず minetest とやりとりしている部分があるはずだ。そこを観察し、段階的に上手く抽出する。

player_api/init.lua では起動時に、次のような処理が実行される。

  1. player_api の関数の実装( api.lua )を読み込む。
  2. player_api.register_model でキャラクターのモデルや設定を player_api.registered_models に保持する。 ただし、この時にモデルが反映される訳ではない。
  3. 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画像を指定している。 visualupright_sprite についてはよくわからないけれど、見た目のサイズ、当たり判定のサイズ、高さ、目線の高さを設定している。

グリーンマンを牛乳ビンにする

player:set_properties() に渡す textures の値を、お気に入りのPNG画像に差し替えれば、キャラクターの姿を変更する事ができそうだ。そこで MineBonbon公式キャラクターの牛乳ビンを投影してみる。

https://res.cloudinary.com/symdon/image/upload/v1721952439/blog.symdon.info/1721384196/player_bonbon_ab1551.png

このキャラクターは、チームでゲーム配信をやろうという話になった時、メンバーが手書きで作成したキャラクターだ。クオリティは低いが愛着があるから使い続けている。名前すら決まっていないが、便宜上「牛乳ビン」と呼ぶ事にする。

前面の画像はあったけれど、背面の画像はない。そこで画像を反転し、目と口を消す事で背面画像を作った。

https://res.cloudinary.com/symdon/image/upload/v1721952725/blog.symdon.info/1721384196/player_bonbon_back_ig9ovj.png

これらの画像を 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 と入力しエンターを押す。すると、キャラクターの姿が変化する。

https://res.cloudinary.com/symdon/image/upload/v1721953259/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-26_9.20.11_sq2t1f.png

https://res.cloudinary.com/symdon/image/upload/v1721953258/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-26_9.20.21_rbt2jq.png

とても雑な画像ではあるけれど、画像を差し替える方法は理解できた。せめて外枠の白い部分は、透過するようにした。

https://res.cloudinary.com/symdon/image/upload/v1721971487/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-26_14.23.58_ggvgkj.png

https://res.cloudinary.com/symdon/image/upload/v1721971487/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-26_14.23.25_qz6asf.png

メッシュを適応する

先程はPNG画像をテクスチャに指定し、キャラクターの見た目を変更した。その結果、平面の画像がキャラクターに投影されたが、今度が3Dモデルを投影したい。

minetestでプレイヤーに対して設定を行うには、先程と同様に player:set_properties() を使う。引数として指定する値は異なるため1つずつ確認していく。

mesh にはモデル名を指定する。これは恐らくblenderで生成したb3dファイルの名前を指定する。

texture にはテクスチャのPNGファイルの名前を指定する。前回は全面と背面の2種類を指定したが、ここではUV展開済みの画像を1つだけ指定する。

visual には文字列 "mesh" を指定する。 visual_sizestepheight については値が表わす意味に変更はない。 また、 collisionboxeye_height は指定していない。これは、おそらくモデルの中に含まれているデータなのだろう。

	local model = models[model_name]
	if model then
		player:set_properties({
			mesh = model_name,
			textures = player_data.textures or model.textures,
			visual = "mesh",
			visual_size = model.visual_size,
			stepheight = model.stepheight
		})
mods/player_api/api.lua抜粋

簡単な3DモデルをBlenderで作成する。

https://res.cloudinary.com/symdon/image/upload/v1722161617/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-28_19.13.06_sbd6ym.png

作成したモデルを、obj形式のファイルに書き出す。

https://res.cloudinary.com/symdon/image/upload/v1722161743/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-28_18.12.44_w7gwx9.png

書き出したファイルは 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,
})

これにより、キャラクターが猿になった。僕達はもしかしたら元々は猿だったのかもしれない。

https://res.cloudinary.com/symdon/image/upload/v1722161413/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-07-28_18.55.48_pusome.png

メッシュ用のテクスチャをUV展開して作り適応する

ここまでで、UV展開でサイコロのテクスチャを作った。またメッシュの適応も行った。これらを合わせて、簡単なキャラクターをデザインする。

  1. Blenderで作業

    1. モデルを作る。
    2. UV展開する。
    3. 「Export UV Layout」でPNG形式のファイルをエクスポートする。
    4. 「Export」でモデルをobj形式のファイルをエクスポートする。
  1. Kritaで作業

    1. 先程出力したPNGファイルを開く。
    2. レイヤーを追加する。
    3. 追加したレイヤーに色を塗る。
    4. 元々あったレイヤーを非表示にする。
    5. PNGファイルとしてエクスポートする。
  2. ターミナルで作業

    1. obj形式でエクスポートしたファイルをmodelsディレクトリに移動する。
    2. kritaでエクスポートしたPNGファイルをtexturesディレクトリに移動する。
  3. テキストエディタで作業

    1. モデルとテクスチャを読み込む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,
})

こんな感じの新しいキャラクターができた。

https://res.cloudinary.com/symdon/image/upload/v1722503164/blog.symdon.info/1721384196/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2024-08-01_17.56.01_kp2lzm.png


1

ボクセルゲームとは、マイクラみたいなやつのこと。