これは Minetest というゲーム(とゲームエンジン)でのゲームのつくり方というか、ゲームの構造を学ぶための全くのビギナー向けの本の日本語訳です。
Minetest とは簡単に言うと、ある程度までのスペックのコンピューター資源で問題なく動作するマインクラフトのようななにかです。このなにかについて、作り始めた人物へのインタビューがありますので、興味があればどうぞ読んでみてください。
この本自体は Lua 言語の知識を要しますが、プログラムについての知識が無くても学習できるように丁寧にアドバイスしていると思います。
Lua 言語について補足的に学習するための参考としては、こちらをご覧ください。
Minetest Modding Book
Book written by rubenwardy
with editing by Shara
License: CC-BY-SA 3.0
[rubenwardyに対してドネーションはいかがでしょうか?]
© 2014-20 | Helpful? Consider donating to support my work.
Front Cover
Minetest Modding Book
by rubenwardy
with editing by Shara
Introduction
Minetest は Lua スクリプトを使用して modding サポートを提供します。この本は、基本から始めて、独自の mods を作成する方法を教えることを目的としています。各章は API の特定の部分に焦点を当てており、すぐに独自の mods を作成できるようになります。
サイトからこの本を読むこともできますし、htmlファイルをダウンロードして読むこともできます。
Feedback and Contributions
間違いに気づきましたか、それともフィードバックを送りたいですか?ぜひ連絡してください。
- GitLabのIsuueを作成します。
- フォーラムトピックに投稿します。
- 私に連絡してください
- コントリビュートしたいですか?READMEをお読みください。
1 - Getting Started
Introduction
mods を作るためには mod のフォルダーの基本構造を理解する必要があります。
What are Games and Mods?
Minetest の力は、独自のボクセルグラフィックス、ボクセルアルゴリズム、または派手なネットワークコードを作成しなくても、ゲームを簡単に開発できることです。
Minetest では、ゲームは、ゲームのコンテンツと動作を提供するために連携して機能するモジュールのコレクションです。一般に mod として知られているモジュールは、スクリプトとリソースのコレクションです。 1 つの mod だけを使用してゲームを作成することは可能ですが、ゲームの一部を他の部分とは独立して調整および交換するのが容易でないため、これが行われることはめったにありません。
ゲームの外に Mod を配布することも可能です。その場合、それらはより伝統的な意味での MOD 、つまり Mod です。これらの mod は、ゲームの機能を調整または拡張します。
ゲームに含まれる mod とサードパーティの mod はどちらも同じ API を使用します。
この本の内容は Minetest API の主要部分をカバーし、ゲーム開発者と modders に適用できます。
Where are mods stored?
各 mod には、Lua コード、テクスチャ、モデル、およびサウンドが配置される独自のディレクトリがあります。 Minetest は、さまざまなロケーションを mod の存在をチェックします。これらの場所は、一般に mod load paths と呼ばれます。
特定のワールド/セーブゲームについて、 3 つの mod の場所が順番にチェックされます。:
- Game mods これらは、world が実行しているゲームを形成する mods です。例:
minetest/games/minetest_game/mods/
、/usr/share/minetest/games/minetest/
- Global mods 、ほとんどの場合 mods がインストールされる場所。疑わしい場合は、ここに配置してください。例:
minetest/mods/
- World mods 、特定のワールドに固有の mods を保存する場所。例:
minetest/worlds/world/worldmods/
Minetest は、上記の順序で場所を確認します。以前に見つかったものと同じ名前の mod が見つかった場合、前の mod の代わりに後の mod がロードされます。これは、global mod のロケーションに同じ名前の mod を配置することで、game mod をオーバーライド(上書き)できることを意味します。
各 mod ロードパスの実際の場所は、使用しているオペレーティングシステムと、Minetest のインストール方法によって異なります。
- Windows:
- ポータブルビルドの場合、例:あなたが .zip ファイルから、抽出したディレクトリに移動します。
games
、mods
およびworlds
ディレクトリを調べてください。 - インストールされているビルド、つまり setup.exe からの場合、
C:\\Minetest
またはC:\\Games\Minetest
を調べます。 - GNU / Linux:
- システム全体のインストールについては、
~/.minetest
を調べてください。これ~
は、ユーザーのホームディレクトリを意味し、ドット(.
)で始まるファイルとディレクトリは非表示になっていることに注意してください。 - ポータブルインストールの場合は、ビルドディレクトリを確認してください。
- Flatpakのインストールについては、
~/.var/app/net.minetest.Minetest/.minetest/mods/
をご覧ください 。 - mac OS
-
~/Library/Application Support/minetest/
を見てください。~
は、ユーザーのホームを意味することに注意してください。すなわち:/Users/USERNAME/
Mod Directory
mod name は mod を参照するために使用されます。各 mod には一意の(他に同名が存在しない)名前を付ける必要があります。 mod 名には、文字、数字、およびアンダースコアを含めることができます。 mod の名前から、 mod の機能を理解できることが好ましく、 mod のコンポーネントを含むディレクトリは、 mod 名と同じ名前である必要があります。 mod 名が使用可能かどうかを確認するには、content.minetest.netで検索してみてください。
mymod
├── init.lua (required) - Runs when the game loads.
├── mod.conf (recommended) - Contains description and dependencies.
├── textures (optional)
│ └── ... any textures or images
├── sounds (optional)
│ └── ... any sounds
└── ... any other files or directories
ゲームのロード時に実行するには、mod で init.lua ファイルのみが必要です。
ただし、 mod.conf が推奨されています。また、 mod の機能によっては、他のコンポーネントが必要になる場合があります。
Dependencies
依存関係(dependencies)は、ロードしようとする mod がそれ自体の前に別の mod をロードする必要がある場合に発生します。 1 つの mod で、別の mod のコード、アイテム、またはその他のリソースを使用できるようにする必要がある場合があるのです。
依存関係には、 2 種類があります。ハード依存関係とオプション依存関係です。どちらも、最初に mod をロードする必要があります。依存している mod が利用できない場合、ハード依存関係があると mod のロードに失敗しますが、オプション依存関係の場合は、有効になる機能が少なくなる可能性があります。
オプション依存関係は、オプションで別の mod をサポートする場合に役立ちます。ユーザーが両方の mod を同時に使用したい場合は、追加のコンテンツを有効にすることができます。
依存関係は mod.conf にリストされている必要があります。
mod.conf
このファイルは、mod の名前、説明、その他の情報を含む mod メタデータに使用されます。例:
name = mymod
description = Adds foo, bar, and bo.
depends = modone, modtwo
optional_depends = modthree
depends.txt
Minetest の 0.4.x バージョンとの互換性のために、mod.conf で依存関係を指定するだけでなく、すべての依存関係をリストする depends.txt ファイルを提供する必要があります。
modone
modtwo
modthree?
各 mod 名は 1 行に 1 つづつあり、名前の後に疑問符が付いた mod 名はオプション依存関係です。
Mod Packs
mod は Mod Packs にグループ化でき、複数の mod をパッケージ化して一緒に移動できます。プレーヤーに複数の Mod を提供したいが、それぞれを個別にダウンロードさせたくない場合に便利です。
modpack1
├── modpack.lua (required) - signals that this is a mod pack
├── mod1
│ └── ... mod files
└── mymod (optional)
└── ... mod files
modpack は geme ではないことに注意してください。game には独自の組織構造があり、これについてはゲームの章で説明します。
Example
これらすべてをまとめた例を次に示します。
Mod Folder
mymod
├── textures
│ └── mymod_node.png files
├── depends.txt
├── init.lua
└── mod.conf
depends.txt
default
init.lua
print("This file will be run at load time!")
minetest.register_node("mymod:node", {
description = "This is a node",
tiles = {"mymod_node.png"},
groups = {cracky = 1}
})
mod.conf
name = mymod
descriptions = Adds a node
depends = default
この mod の名前は「 mymod 」です。 init.lua 、 mod.conf 、 depends.txt の 3 つのテキストファイルがあります。
スクリプトはメッセージを出力してからノードを登録します。これについては次の章で説明します。
単一の依存関係、 default mod があります。これは通常、 Minetest Game にあります。
また、textures/ にはノードのテクスチャがあります。
2 - Lua Scripting
Introduction
この章では、Lua でのスクリプト作成、必要なツールについて説明し、おそらく役立つと思われるいくつかのテクニックについて説明します。
Code Editors
Lua でスクリプトを作成するには、コードのハイライト表示を備えたコードエディターで十分です。コードのハイライト表示は、意味に応じて、単語や文字にさまざまな色を付けて表示させます。これにより、間違いを見つけることができます。
function ctf.post(team,msg)
if not ctf.team(team) then
return false
end
if not ctf.team(team).log then
ctf.team(team).log = {}
end
table.insert(ctf.team(team).log,1,msg)
ctf.save()
return true
end
たとえば、上記のスニペットのキーワードは、 if 、then 、end 、return などでハイライト表示されています。 table.insert は、デフォルトで Lua に付属している関数です。
これは、 Lua に適した一般的なエディターのリストです。もちろん、他のエディターも利用できます。
Coding in Lua
Program Flow
プログラムは、次々に実行される一連のコマンドです。これらのコマンドを「ステートメント」と呼びます。プログラムフローは、これらのステートメントが実行される方法です。さまざまなタイプのフローを使用すると、コマンドのセットをスキップまたはジャンプできます。フローには主に 3 つのタイプがあります。
- シーケンス (Sequence):スキップせずに、ステートメントを次々に実行するだけです。
- 選択 (Selection):条件に応じてシーケンスをスキップします。
- 反復 (Iteration):繰り返し、ループします。条件が満たされるまで、同じステートメントを実行し続けます。
では、 Lua のステートメントはどのように見えますか?
local a = 2 -- Set 'a' to 2
local b = 2 -- Set 'b' to 2
local result = a + b -- Set 'result' to a + b, which is 4
a = a + 10
print("Sum is "..result)
おっと、そこで何が起こりましたか?
a、b、および result は 変数 です。ローカル変数は、local キーワードを使用して宣言され、初期値が与えられます。ローカルは スコープ(scope) と呼ばれる非常に重要な概念の一部であるため、少し説明します。
=
は アサイン 、ですから result = a + b
は「 result 」 に 「 a + b 」を割り当てます。「 result 」変数で見られるように、変数名は数学とは異なり、1文字より長くなる可能性があります。 Lua では大文字と小文字が区別されることにも注意してください。 A は a とは異なる変数です。
Variable Types
変数は次のタイプのいずれかのみであり、割り当て後にタイプを変更できます。変数が nil または nil 以外の単一の型のみであることを確認することをお勧めします。
タイプ | 説明 | 例 |
---|---|---|
Nil | 初期化されていません。変数は空で、値がありません |
local A 、 D = nil
|
Number | 整数または10進数。 | local A = 4 |
String | テキストの一部 | local D = "one two three" |
Boolean | True or False |
local is_true = false 、 local E = (1 == 1)
|
Table | Lists | 以下に説明します |
Function | 実行できます。入力が必要な場合があり、値を返す場合があります | local result = func(1, 2, 3) |
Arithmetic Operators
網羅的なリストではありません。考えられるすべての演算子が含まれているわけではありません。
シンボル | 目的 | 例 |
---|---|---|
A + B | 加算 | 2 + 2 = 4 |
A-B | 減算 | 2-10 = -8 |
A * B | 乗算 | 2 * 2 = 4 |
A / B | 分割 | 100/50 = 2 |
A ^ B | 累乗 | 2 ^ 2 = 2^2 = 4 |
A .. B | 文字列を結合する | "foo".."bar" = "foobar" |
Selection
最も基本的な選択は if ステートメントです。次のようになります。
local random_number = math.random(1, 100) -- Between 1 and 100.
if random_number > 50 then
print("Woohoo!")
else
print("No!")
end
この例では、1から100までの乱数が生成されます。次に、「 Woohoo! 」と出力されます。その数が50より大きい場合は、「 No! 」と出力されます。
local random_number = math.random(1, 100) -- Between 1 and 100.
if random_number > 50 then
print("Woohoo!")
else
print("No!")
end
「>」から他に何が離れますか?
Logical Operators
シンボル | 目的 | 例 |
---|---|---|
A == B | 等しい | 1 == 1(true)、1 == 2(false) |
A 〜 = B | 等しくない | 1〜 = 1(false)、1〜 = 2(true) |
A > B | 大なり記号 | 5> 2(true)、1> 2(false)、1> 1(false) |
A < B | 未満 | 1 <3(true)、3 <1(false)、1 <1(false) |
A >= B | 以上かイコール | 5> = 5(true)、5> = 3(true)、5> = 6(false) |
A <= B | 以下かイコール | 3 <= 6(true)、3 <= 3(true) |
A and B | And(両方ともtrueでなければなりません) | (2> 1)and(1 == 1)(true)、(2> 3)and(1 == 1)(false) |
A or B | または。一方または両方がtureでなければなりません。 | (2> 1)or(1 == 2)(true)、(2> 4)or(1 == 3)(false) |
not A | not true | not(1 == 2)(true)、not(1 == 1)(false) |
これにはすべての可能な演算子が含まれているわけではなく、次のように演算子を組み合わせることができます。
if not A and B then
print("Yay!")
end
A が false で B が true の場合「 Yay! 」をプリントします 。
論理演算子と算術演算子はまったく同じように機能します。どちらも入力を受け入れ、保存可能な値を返します。
local A = 5
local is_equal = (A == 5)
if is_equal then
print("Is equal!")
end
Programming
プログラミングとは、項目のリストを並べ替えるなどの問題を解決し、それをコンピューターが理解できる手順に変換するアクションです。
プログラミングの論理的なプロセスを教えることは、この本の範囲を超えています。ただし、次の Web サイトは、これを開発するのに非常に役立ちます。
- Codecademyは、「コーディング」を学ぶための最良のリソースの1つであり、インタラクティブなチュートリアル体験を提供します。
-
Scratchは、プログラミングに必要な問題解決手法を学び、絶対的な基礎から始めるときに優れたリソースです。
Scratch は、子供たちにプログラミング方法を教えるように設計されており、本格的なプログラミング言語ではありません。
Local and Global Scope
変数がローカルであるかグローバルであるかによって、変数の書き込み、または読み取りが可能な場所が決まります。ローカル変数には、変数が定義されている場所からのみアクセスできます。ここではいくつかの例を示します。
-- Accessible from within this script file
local one = 1
function myfunc()
-- Accessible from within this function
local two = one + one
if two == one then
-- Accessible from within this if statement
local three = one + two
end
end
一方、グローバル変数には、スクリプトファイルのどこからでも、他の mod からもアクセスできます。
my_global_variable = "blah"
function one()
my_global_variable = "three"
end
print(my_global_variable) -- Output: "blah"
one()
print(my_global_variable) -- Output: "three"
Locals should be used as much as possible
Lua はデフォルトでグローバルです(他のほとんどのプログラミング言語とは異なります)。
ローカル変数は、そのように識別する必要があります(つまりローカル変数を使う場合は明示的に宣言します)。
function one()
foo = "bar"
end
function two()
print(dump(foo)) -- Output: "bar"
end
one()
two()
dump() は、任意の変数を文字列に変換できる関数であり、プログラマーはそれが何であるかを確認できます。foo 変数は、文字列であることを示す引用符を含めて、「 bar 」として出力されます。
これはずさんなコーディングであり、Minetest は実際にこれについて警告します:
Assignment to undeclared global 'foo' inside function at init.lua:2
これを修正するには、「ローカル」を使用します。
function one()
local foo = "bar"
end
function two()
print(dump(foo)) -- Output: nil
end
one()
two()
nil
は初期化されていないことを意味することに注意してください。変数に値がまだ割り当てられていないか、存在しないか、初期化されていません(つまり、 nil
に設定されています)。
関数についても同じことが言えます。関数は特別なタイプの変数であり、他の mod が同じ名前の関数を持つ可能性があるため、ローカルにする必要があります。
local function foo(bar)
return bar * 2
end
API テーブルを使用して、次のように他の mod が関数を呼び出せるようにする必要があります。
mymod = {}
function mymod.foo(bar)
return "foo" .. bar
end
-- In another mod, or script:
mymod.foo("foobar")
Including other Lua Scripts
mod に他のLuaスクリプトを含めるための推奨される方法は、dofile を使用することです。
dofile(minetest.get_modpath("modname") .. "/script.lua")
スクリプトは値を返すことができます。これは、プライベートローカル変数を共有するのに役立ちます。
-- script.lua
return "Hello world!"
-- init.lua
local ret = dofile(minetest.get_modpath("modname") .. "/script.lua")
print(ret) -- Hello world!
後の章では、mod のコードを分割する方法について詳しく説明します。ここではシンプルなアプローチとして、 nodes.lua 、crafts.lua 、craftitems.lua などのさまざまなタイプのものに対して別々のファイルを用意します。
3 - Nodes, Items, and Crafting
Introduction
新しいノードとクラフトアイテムの登録、およびクラフトレシピの作成は、多くの mod の基本的要件です。
- What are Nodes and Items?
- Registering Items
- Registering a basic node
- Actions and Callbacks
- Crafting
- Groups
- Tools, Capabilities, and Dig Types
What are Nodes and Items?
ノード、クラフトアイテム、ツールはすべてアイテムです。アイテムは、通常のゲームプレイでは不可能な場合でも、インベントリ内で見つけることができるものです。
ノードは、世界に配置されているか、または発見することができるアイテムです。世界のすべての位置空間は、たった1つのノードで占められている必要があります-一見空白の位置空間は通常、エアノードです。
クラフトアイテムは配置できず、インベントリにあるか、ワールド内でドロップされたアイテムとしてのみ発見できます。
ツールには着用する機能があり、通常はデフォルトではない掘削する機能があります。将来的には、クラフトアイテムとツールの区別がかなり人工的なものであるため、クラフトアイテムとツールが1つのタイプのアイテムに統合される可能性があります。
Registering Items
アイテム定義は、アイテム名と定義テーブルで構成されます。定義テーブルには、アイテムの動作に影響を与える属性が含まれています。
minetest.register_craftitem("modname:itemname", {
description = "My Special Item",
inventory_image = "modname_itemname.png"
})
Item Names and Aliases
すべてのアイテムには、参照するために使用されるアイテム名があります。次の形式である必要があります。
modname:itemname
modname は、アイテムが登録されている mod の名前であり、 itemname はアイテム自体の名前です。アイテム名は、アイテムが何であるかに関連している必要があり、まだ登録できません。
アイテムには、その名前を指すエイリアスを含めることもできます。エイリアスは、擬似的なアイテム名です。エンジンはエイリアスの出現をアイテム名であるように扱います。これには、主に 2 つの一般的な用途があります。
- 削除されたアイテムの名前を別の名前に変更します。 corrective コードなしでアイテムが mod から削除された場合、world とノードインベントリに不明なノードが存在するようになる可能性があります。削除ノードを取得できる場合は、取得できないノードへのエイリアシングを回避することが重要です。
- ショートカットを追加します。
/giveme dirt
より簡単です/giveme default:dirt
。
エイリアスの登録は非常に簡単です。引数の順序を覚える良い方法はfrom → to
、fromがエイリアスで、toがターゲットであるということです。
minetest.register_alias("dirt", "default:dirt")
mod はアイテム名を直接処理する前にエイリアスを解決する必要があります。エンジンはこれを行いません。ただし、これはすごく簡単です。
itemname = minetest.registered_aliases[itemname] or itemname
Textures
テクスチャ(のファイル)は、modname_itemname.png
という命名形式で textures/
フォルダに配置する必要があります。
JPEG テクスチャはサポートされていますが、透明度はサポートされておらず、一般に低解像度では品質が低くなります。多くの場合、 PNG 形式を使用することをお勧めします。
Minetest のテクスチャは通常 16x16 ピクセルです。これらは任意の解像度にすることができますが、2 のオーダー、たとえば 16 、32 、64 、または 128 にすることをお勧めします。これは、古いデバイスでは他の解像度が正しくサポートされておらず、パフォーマンスが低下する可能性があるためです。
Registering a basic node
minetest.register_node("mymod:diamond", {
description = "Alien Diamond",
tiles = {"mymod_diamond.png"},
is_ground_content = true,
groups = {cracky=3, stone=1}
})
tiles
プロパティは、ノードが使用するテクスチャ名のテーブルです。テクスチャが 1 つしかない場合、このテクスチャは6面すべての面で使用されます。側面ごとに異なるテクスチャを与えるには、 6 つのテクスチャの名前を次の順序で指定します。
up (+Y), down (-Y), right (+X), left (-X), back (+Z), front (-Z).
(+Y, -Y, +X, -X, +Z, -Z)
3D コンピュータグラフィックスの慣例と同様に、 Minetest では +Y が上向きであることを忘れないでください。
minetest.register_node("mymod:diamond", {
description = "Alien Diamond",
tiles = {
"mymod_diamond_up.png", -- y+
"mymod_diamond_down.png", -- y-
"mymod_diamond_right.png", -- x+
"mymod_diamond_left.png", -- x-
"mymod_diamond_back.png", -- z+
"mymod_diamond_front.png", -- z-
},
is_ground_content = true,
groups = {cracky = 3},
drop = "mymod:diamond_fragments"
-- ^ Rather than dropping diamond, drop mymod:diamond_fragments
})
_ground_content 属性を使用すると、石の上に洞窟を生成できます。これは、マップの生成中に地下に配置される可能性のあるノードにとって不可欠です。エリア内の他のすべてのノードが生成された後、洞窟は world から切り取られます。
Actions and Callbacks
Minetest は、コールバックベースの modding デザインを多用しています。コールバックをアイテム定義テーブルに配置して、さまざまな異なるユーザーイベントに応答できるようにすることができます。
on_use
デフォルトでは、プレーヤーがアイテムを左クリックすると、 use コールバックがトリガーされます。 use コールバックがあると、アイテムがノードの掘削に使用されるのを防ぐことができます。 use コールバックの一般的な使用法の1つは、food(食品)です。
minetest.register_craftitem("mymod:mudpie", {
description = "Alien Mud Pie",
inventory_image = "myfood_mudpie.png",
on_use = minetest.item_eat(20),
})
minetest.item_eat 関数に提供される数値は、この food (食品)が消費されたときに回復したヒットポイントの数です。プレイヤーが持っている各ハートアイコンは、 2 つのヒットポイントの価値があります。プレーヤーは通常、最大10個のハートを持つことができます。これは、 20 ヒットポイントに相当します。ヒットポイントは整数( whole number )である必要はありません。小数にすることができます。
minetest.item_eat() は、関数を返す関数であり、on_use コールバックとして設定します。これは、上記のコードがほぼ次のようになっていることを意味します。
minetest.register_craftitem("mymod:mudpie", {
description = "Alien Mud Pie",
inventory_image = "myfood_mudpie.png",
on_use = function(...)
return minetest.do_item_eat(20, nil, ...)
end,
})
関数を返すことだけで item_eat がどのように機能するかを理解することで、カスタムサウンドの再生させるなど、より複雑な動作を行うように変更することができます。
Crafting
type
プロパティによって示される、利用可能ないくつかのタイプのクラフトレシピがあります。
- shaped - 材料は正しい位置になければなりません。
- shapeless - 材料がどこにあるかは関係ありません。適切な量があるだけです。
- cooking - 使用するかまど( furnacae )のレシピ。
- fuel - かまどで燃焼できるアイテムを定義します。
- tool_repair - ツールで修復できるアイテムを定義します。
クラフトレシピはアイテムではないため、アイテム名を使用して一意に識別しません。
Shaped
Shoped したレシピは、材料が機能するためには正しい Shope またはパターンである必要があります。以下の例では、クラフトが機能するために、フラグメントは椅子のようなパターンである必要があります。
minetest.register_craft({
type = "shaped",
output = "mymod:diamond_chair 99",
recipe = {
{"mymod:diamond_fragments", "", ""},
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""},
{"mymod:diamond_fragments", "mymod:diamond_fragments", ""}
}
})
注意すべき点の1つは、右側の空白の列です。これは、図形の右側に空の列が必要であることを意味します。そうでない場合、これは機能しません。この空の列が必要ない場合は、次のように空の文字列を省略できます。
minetest.register_craft({
output = "mymod:diamond_chair 99",
recipe = {
{"mymod:diamond_fragments", "" },
{"mymod:diamond_fragments", "mymod:diamond_fragments"},
{"mymod:diamond_fragments", "mymod:diamond_fragments"}
}
})
Shaped はデフォルトのクラフトタイプであるため、タイプフィールドは実際に shaped クラフトには必要ありません。
Shapeless
Shapeless レシピは、材料がどこに配置されていても、そこにあるだけで問題にならない場合に使用されるレシピの一種です。
minetest.register_craft({
type = "shapeless",
output = "mymod:diamond 3",
recipe = {
"mymod:diamond_fragments",
"mymod:diamond_fragments",
"mymod:diamond_fragments",
},
})
Cooking and Fuel
「クッキング」タイプのレシピは、クラフティンググリッドでは作成されませんが、かまどでされるか、または mods で見つかる可能性あります。
minetest.register_craft({
type = "cooking",
output = "mymod:diamond_fragments",
recipe = "default:coalblock",
cooktime = 10,
})
コードの唯一の本当の違いは、レシピがテーブル内({中括弧の間})にあるのと比較して、単一のアイテムにすぎないことです。また、アイテムの cooking にかかる時間を定義するオプションの「 cooktime 」パラメーターもあります。これが設定されていない場合、デフォルトで 3 になります。
上記のレシピは、石炭ブロックが入力スロットにあり、その下に何らかの形の燃料がある場合に機能します。10秒後にダイヤモンドの破片ができます!
このタイプは、mods から炉やその他の調理器具で何を燃やすことができるかを定義するため、cooking タイプの付属品です。
minetest.register_craft({
type = "fuel",
recipe = "mymod:diamond",
burntime = 300,
})
他のレシピのような出力はありませんが、燃料として持続する時間を秒単位で定義する燃焼時間( burn time )があります。だから、ダイヤモンドは 300 秒間の燃料として良いです!
Groups
アイテムは多くのグループのメンバーになることができ、グループは多くのメンバーを持つことができます。グループは groups
定義テーブルのプロパティを使用して定義され、関連付けられた値があります。
groups = {cracky = 3, wood = 1}
グループを使用する理由はいくつかあります。まず、グループは、掘削タイプや可燃性( flammability )などのプロパティを説明するために使用されます。次に、アイテム名の代わりにグループをクラフトレシピで使用して、グループ内の任意のアイテムを使用できるようにすることができます。
minetest.register_craft({
type = "shapeless",
output = "mymod:diamond_thing 3",
recipe = {"group:wood", "mymod:diamond"}
})
Tools, Capabilities, and Dig Types
Dig types は、さまざまなツールで掘削したときのノードの強度を定義するために使用されるグループです。関連付けられた値が高いdigタイプのグループは、ノードの切断がより簡単かつ迅速であることを意味します。複数の dig タイプを組み合わせて、複数のタイプのツールをより効率的に使用できるようにすることができます。Dig type のないノードは、どのツールでも掘削できません。
グループ | 最高のツール | 説明 |
---|---|---|
crumbly | spade | 土、砂 |
cracky | pickaxe (つるはし) | タフな(しかしもろい)石のようなもの |
snappy | どれか | 細かい工具を使用して切断できます。 例:葉、小植物、ワイヤー、金属板 |
choppy | axe (斧) | 鋭い力で切ることができます。例:木、木の板 |
fleshy | sword (剣) | 動物やプレイヤーのような生き物。 これは、打撃時の血液への影響を示唆している可能性があります。 |
explody | ? | 特に爆発しやすい |
oddly_breakable_by_hand | どれか | 松明など-非常に素早く掘ります |
すべてのツールにはツール機能があります。機能には、サポートされている Dig type タイプのリストと、掘削時間( dig times )や摩耗量など、各タイプに関連するプロパティが含まれます。ツールは、タイプごとに最大サポート硬度を持つこともできます。これにより、弱いツールが硬いノードを掘るのを防ぐことができます。ツールがすべての Dig type を機能に含めることは非常に一般的であり、あまり適切でないものは非常に非効率的な特性を持っています。プレイヤーが現在使用しているアイテムに明示的なツール機能がない場合は、代わりに現在のハンドの機能が使用されます。
minetest.register_tool("mymod:tool", {
description = "My Tool",
inventory_image = "mymod_tool.png",
tool_capabilities = {
full_punch_interval = 1.5,
max_drop_level = 1,
groupcaps = {
crumbly = {
maxlevel = 2,
uses = 20,
times = { [1]=1.60, [2]=1.20, [3]=0.80 }
},
},
damage_groups = {fleshy=2},
},
})
Groupcaps は、ノードを掘るためにサポートされている掘りタイプのリストです。ダメージグループは、ツールがオブジェクトにダメージを与える方法を制御するためのものです。これについては、オブジェクト、プレーヤー、エンティティの章で後述します。
4 - Creating Textures
Introduction
テクスチャを作成して最適化できることは、Minetest 用に開発するときに非常に役立つスキルです。ピクセルアートテクスチャの操作に関連する多くのテクニックがあり、これらのテクニックを理解すると、作成するテクスチャの品質が大幅に向上します。
優れたピクセルアートを作成するための詳細なアプローチはこの本の範囲外であり、代わりに最も関連性のある基本的なテクニックのみを取り上げます。ピクセルアートをより詳細にカバーする、利用可能な多くの優れたオンラインチュートリアルがあります。
Techniques
Using the Pencil
鉛筆ツールは、ほとんどのエディターで使用できます。最小サイズに設定すると、画像の他の部分を変更せずに、一度に1つのピクセルを編集できます。ピクセルを1つずつ操作することで、意図しないぼかしを発生させることなく、クリアでシャープなテクスチャを作成できます。また、高レベルの精度と制御を提供します。
Tiling
ノードに使用されるテクスチャは、通常、タイル状に設計する必要があります。これは、同じテクスチャを持つ複数のノードを一緒に配置すると、エッジが正しく整列することを意味します。
エッジを正しく一致させないと、結果は見た目がはるかに悪くなります。
Transparency
透明度は、ほぼすべてのクラフトアイテムとガラスなどの一部のノードのテクスチャを作成するときに重要です。すべてのエディタが透明度をサポートしているわけではないため、作成するテクスチャに適したエディタを選択してください。
Editors
MS Paint
MS ペイントは、基本的なテクスチャデザインに役立つシンプルなエディタです。ただし、透過性はサポートされていません。これは通常、ノードの側面のテクスチャを作成する場合は問題になりませんが、テクスチャに透明度が必要な場合は、別のエディタを選択する必要があります。
GIMP
GIMP は Minetest コミュニティで一般的に使用されています。その機能の多くがすぐには明らかにならないため、学習曲線はかなり高くなります。
GIMP を使用する場合、鉛筆ツールはツールボックスから選択できます。
消しゴムツールの [ハードエッジ] チェックボックスを選択することもお勧めします。
5 - Node Drawtypes
Node Drawtypes
Introduction
ノードを描画する方法は、 drawtype と呼ばれます。利用可能な drawtype はたくさんあります。 drawtype の動作は、ノードタイプ定義にプロパティを提供することで制御できます。これらのプロパティは、このノードのすべてのインスタンスで修正されています。と呼ばれるものを使用して、ノードごとにいくつかのプロパティを制御することができます param2
。
前の章では、ノードとアイテムの概念が紹介されましたが、ノードの完全な定義は示されていませんでした。 Minetest の世界は、位置の 3D グリッドです。各位置はノードと呼ばれ、ノードタイプ(名前)と 2 つのパラメーター( param1 と param2 )で構成されます。この関数minetest.register_node
は、実際にはノードを登録しないという点で少し誤解を招きます。新しい type のノードを登録します。
ノードパラメータは、ノードが個別にレンダリングされる方法を制御するために使用されます。 param1
は、ノードのライティングを格納するために使用され、 param2
の意味は、ノードタイプ定義の paramtype2
プロパティによって異なります。
- Cubic Nodes: Normal and Allfaces
- Glasslike Nodes
- Airlike Nodes
- Lighting and Sunlight Propagation
- Liquid Nodes
- Node Boxes
- Mesh Nodes
- Signlike Nodes
- Plantlike Nodes
- Firelike Nodes
- More Drawtypes
Cubic Nodes: Normal and Allfaces
Normal Drawtype
通常の drawtype は、通常、立方体ノードをレンダリングするために使用されます。通常のノードの側がソリッド側に接している場合、その側はレンダリングされないため、パフォーマンスが大幅に向上します。
対照的に、 allfaces drawtype は、ソリッドノードに対して上向きの場合でも内側をレンダリングします。これは、リーフノードなど、側面が部分的に透明なノードに適しています。 allfaces_optional drawtype を使用して、ユーザーが遅い描画をオプトアウトできるようにすることができます。その場合、通常のノードのように動作します。
minetest.register_node("mymod:diamond", {
description = "Alien Diamond",
tiles = {"mymod_diamond.png"},
groups = {cracky = 3},
})
minetest.register_node("default:leaves", {
description = "Leaves",
drawtype = "allfaces_optional",
tiles = {"default_leaves.png"}
})
注:通常の drawtype はデフォルトの drawtype であるため、明示的に指定する必要はありません。
Glasslike Nodes
ガラス状ノードと通常ノードの違いは、ガラス状ノードを通常ノードの隣に配置しても、通常ノードの側面が非表示にならないことです。ガラスのようなノードは透明になる傾向があるため、これは便利です。通常の drawtype を使用すると、 world を透視することができます。
Glasslike's Edges
minetest.register_node("default:obsidian_glass", {
description = "Obsidian Glass",
drawtype = "glasslike",
tiles = {"default_obsidian_glass.png"},
paramtype = "light",
is_ground_content = false,
sunlight_propagates = true,
sounds = default.node_sound_glass_defaults(),
groups = {cracky=3,oddly_breakable_by_hand=3},
})
Glasslike_Framed
ノードのエッジが個々のノードではなく、3D 効果で全体を一周します。
Glasslike_Framed's Edges
glasslike_framed_optional drawtype を使用して、ユーザーがフレーム付きの外観にオプトインできるようにすることができます。
minetest.register_node("default:glass", {
description = "Glass",
drawtype = "glasslike_framed",
tiles = {"default_glass.png", "default_glass_detail.png"},
inventory_image = minetest.inventorycube("default_glass.png"),
paramtype = "light",
sunlight_propagates = true, -- Sunlight can shine through block
groups = {cracky = 3, oddly_breakable_by_hand = 3},
sounds = default.node_sound_glass_defaults()
})
Airlike Nodes
これらのノードはレンダリングされないため、テクスチャはありません。
minetest.register_node("myair:air", {
description = "MyAir (you hacker you!)",
drawtype = "airlike",
paramtype = "light",
sunlight_propagates = true,
walkable = false, -- Would make the player collide with the air node
pointable = false, -- You can't select the node
diggable = false, -- You can't dig the node
buildable_to = true, -- Nodes can be replace this node.
-- (you can place a node and remove the air node
-- that used to be there)
air_equivalent = true,
drop = "",
groups = {not_in_creative_inventory=1}
})
Lighting and Sunlight Propagation
ノードのライティングは param1 に保存されます。ノードの側面をシェーディングする方法を理解するために、隣接ノードのライト値が使用されます。このため、ソリッドノードは光を遮断するため、光の値がありません。
デフォルトでは、ノードタイプでは、どのノードインスタンスにもライトを保存できません。通常、ガラスや空気などの一部のノードが光を通過できることが望ましいです。これを行うには、定義する必要のある 2 つのプロパティがあります。
paramtype = "light",
sunlight_propagates = true,
最初の行は、param1が実際に光レベルを保存することを意味します。
2 行目は、太陽光が値を減少させることなくこのノードを通過する必要があることを意味します。
Liquid Nodes
Liquid Drawtype
液体の種類ごとに、 2 つのノード定義が必要です。1つは液体ソース用で、もう1つは流れる液体用です。
-- Some properties have been removed as they are beyond
-- the scope of this chapter.
minetest.register_node("default:water_source", {
drawtype = "liquid",
paramtype = "light",
inventory_image = minetest.inventorycube("default_water.png"),
-- ^ this is required to stop the inventory image from being animated
tiles = {
{
name = "default_water_source_animated.png",
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 2.0
}
}
},
special_tiles = {
-- New-style water source material (mostly unused)
{
name = "default_water_source_animated.png",
animation = {type = "vertical_frames", aspect_w = 16,
aspect_h = 16, length = 2.0},
backface_culling = false,
}
},
--
-- Behavior
--
walkable = false, -- The player falls through
pointable = false, -- The player can't highlight it
diggable = false, -- The player can't dig it
buildable_to = true, -- Nodes can be replace this node
alpha = 160,
--
-- Liquid Properties
--
drowning = 1,
liquidtype = "source",
liquid_alternative_flowing = "default:water_flowing",
-- ^ when the liquid is flowing
liquid_alternative_source = "default:water_source",
-- ^ when the liquid is a source
liquid_viscosity = WATER_VISC,
-- ^ how fast
liquid_range = 8,
-- ^ how far
post_effect_color = {a=64, r=100, g=100, b=200},
-- ^ colour of screen when the player is submerged
})
フローノードの定義は似ていますが、名前とアニメーションが異なります。完全な例については、 minetest_game のデフォルト mod の default:water_flowing を参照してください。
Node Boxes
Nodebox drawtype
ノードボックスを使用すると、立方体ではなく、必要な数の直方体で作成されたノードを作成できます。
minetest.register_node("stairs:stair_stone", {
drawtype = "nodebox",
paramtype = "light",
node_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
{-0.5, 0, 0, 0.5, 0.5, 0.5},
},
}
})
最も重要な部分はノードボックステーブルです。
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
{-0.5, 0, 0, 0.5, 0.5, 0.5}
各行は直方体であり、結合されて1つのノードになります。最初の 3 つの数字は、左下隅の-0.5から0.5までの座標であり、最後の 3 つの数字は反対側の角です。それらは X, Y, Z の形式であり、Yは上です。
NodeBoxEditor)を使用して、エッジをドラッグすることでノードボックスを作成できます。これは、手動で行うよりも視覚的です。
Wallmounted Node Boxes
トーチのように床、壁、または天井に配置するときに、異なる node box が必要になる場合があります。
minetest.register_node("default:sign_wall", {
drawtype = "nodebox",
node_box = {
type = "wallmounted",
-- Ceiling
wall_top = {
{-0.4375, 0.4375, -0.3125, 0.4375, 0.5, 0.3125}
},
-- Floor
wall_bottom = {
{-0.4375, -0.5, -0.3125, 0.4375, -0.4375, 0.3125}
},
-- Wall
wall_side = {
{-0.5, -0.3125, -0.4375, -0.4375, 0.3125, 0.4375}
}
},
})
Mesh Nodes
node box は一般的に作成が簡単ですが、直方体のみで構成できるという制限があります。node box は最適化されていません。完全に非表示になっている場合でも、内面はレンダリングされます。
面はメッシュ上の平らな面です。内面は、 2 つの異なるノードボックスの面が重なるときに発生し、ノードボックスモデルの一部が非表示になりますが、レンダリングされたままになります。
メッシュノードは次のように登録できます。
minetest.register_node("mymod:meshy", {
drawtype = "mesh",
-- Holds the texture for each "material"
tiles = {
"mymod_meshy.png"
},
-- Path to the mesh
mesh = "mymod_meshy.b3d",
})
メッシュが models
ディレクトリで使用可能であることを確認してください。ほとんどの場合、メッシュは mod のフォルダーにあるはずですが、依存している別の mod によって提供されるメッシュを共有することは問題ありません。たとえば、より多くのタイプの家具を追加する mod は、基本的な furniture mod によって提供されるモデルを共有したい場合があります。
Signlike Nodes
Signlike nodes はフラットノードであり、他のノードの側面に取り付けることができます。
この drawtype の名前にもかかわらず、サインは実際にはサインライクを使用する傾向はありませんが、代わりに nodebox
drawtype を使用して 3D 効果を提供します。しかし、 signlike
drawtype は、一般的にはしごで使用されています。
minetest.register_node("default:ladder_wood", {
drawtype = "signlike",
tiles = {"default_ladder_wood.png"},
-- Required: store the rotation in param2
paramtype2 = "wallmounted",
selection_box = {
type = "wallmounted",
},
})
Plantlike Nodes
Plantlike Drawtype
植物のようなノードは、 X のようなパターンでタイルを描画します。
minetest.register_node("default:papyrus", {
drawtype = "plantlike",
-- Only one texture used
tiles = {"default_papyrus.png"},
selection_box = {
type = "fixed",
fixed = {-6 / 16, -0.5, -6 / 16, 6 / 16, 0.5, 6 / 16},
},
})
Firelike Nodes
Firelike は、壁や天井に「しがみつく」ように設計されていることを除けば、Plantlike に似ています。
minetest.register_node("mymod:clingere", {
drawtype = "firelike",
-- Only one texture used
tiles = { "mymod:clinger" },
})
More Drawtypes
これは包括的なリストではありません。次のような他のタイプがあります。
- Fencelike
- Plantlike rooted - 水の中の植物のため
- Raillike - カートトラックのため
- Torchlike - 2D 壁 / 床 / 天井ノード用。 Minetest Game のトーチは、実際にはメッシュノードの 2 つの異なるノード定義を使用します (default:torch and default:torch_wall) 。
いつものように、完全なリストについては Lua APIのドキュメントをお読みください。
6 - ItemStacks and Inventories
ItemStacks and Inventories
Introduction
この章では、プレーヤーインベントリ、ノードインベントリ、またはデタッチインベントリなどの、インベントリの使用方法と操作方法を学習します。
- What are ItemStacks and Inventories?
- ItemStacks
- Inventory Locations
- Lists
- Modifying Inventories and ItemStacks
- Wear
- Lua Tables
What are ItemStacks and Inventories?
ItemStack は、インベントリ内の単一セルの背後にあるデータです。
inventory は inventory lists コレクションです、それぞれが ItemStacks の 2D グリッドです。 inventory lists は、インベントリのコンテキストでは単に lists と呼ばれます。インベントリのポイントは、プレーヤーとノードに最大で1つのインベントリしかない場合に、複数のグリッドを許可することです。
ItemStacks
ItemStack には、名前( name )、カウント( count )、摩耗( wear )、メタデータ( metadata )の 4 つのコンポーネントがあります。
アイテム名は、登録済みアイテムのアイテム名、エイリアス、または不明なアイテム名の場合があります。不明なアイテムは、ユーザーが mod をアンインストールする場合、または mod がエイリアスの登録などの予防措置なしにアイテムを削除する場合によく見られます。
print(stack:get_name())
stack:set_name("default:dirt")
if not stack:is_known() then
print("Is an unknown item!")
end
カウントは常に 0 以上になります。通常のゲームプレイでは、カウントはアイテムの最大スタックサイズを超えないようにする必要があります - stack_max
。ただし、管理コマンド( admin commands )とバグのある mod を使用すると、スタックが最大サイズを超える場合があります。
print(stack:get_stack_max())
ItemStack は空にすることができ、その場合、カウントは0になります。
print(stack:get_count())
stack:set_count(10)
ItemStack は、 ItemStack 関数を使用して複数の方法で構築できます。
ItemStack() -- name="", count=0
ItemStack("default:pick_stone") -- count=1
ItemStack("default:stone 30")
ItemStack({ name = "default:wood", count = 10 })
アイテムメタデータは、アイテムに関するデータの無制限の Key-Value ストアです。 Key-Value は、名前(キーと呼ばれる)を使用してデータ(値と呼ばれる)にアクセスすることを意味します。一部のキーには特別な意味があります。たとえば、description
スタックごとのアイテムの説明に使用されます。これについては、メタデータとストレージの章で詳しく説明します。
Inventory Locations
インベントリロケーションは、インベントリがどこに、どのように保存されるかです。インベントリロケーションには、プレーヤー、ノード、およびデタッチの 3 つのタイプがあります。インベントリは 1 つのロケーションに直接関連付けられています。インベントリが更新されると、すぐに更新されます。
ノードインベントリは、チェストなどの特定のノードの位置に関連しています。ノードはノードメタデータに保存されているため、ロードする必要があります。
local inv = minetest.get_inventory({ type="node", pos={x=1, y=2, z=3} })
上記は、一般にInvRefと呼ばれるインベントリ参照を取得します。ノードインベントリ参照は、ノードインベントリを操作するために使用されます。
参照とは、データが実際にはそのオブジェクト内に格納されていないことを意味しますが、オブジェクトは代わりにデータをインプレースで直接更新します。
ノードインベントリ参照の場所は、次のように見つけることができます。
local location = inv:get_location()
プレーヤーのインベントリは、同様に、またはプレーヤーの参照を使用して取得できます。プレイヤーはインベントリにアクセスするためにオンラインである必要があります。
local inv = minetest.get_inventory({ type="player", name="player1" })
-- or
local inv = player:get_inventory()
デタッチされたインベントリは、プレーヤーまたはノードから独立しているインベントリです。切り離されたノードインベントリも、再起動しても保存されません。
local inv = minetest.get_inventory({
type="detached", name="inventory_name" })
他のタイプのインベントリとは異なり、アクセスする前に、まずデタッチされたインベントリを作成する必要があります。
minetest.create_detached_inventory("inventory_name")
create_detached_inventory 関数は 3 つの引数を受け入れますが、最初の引数(インベントリ名)のみが必要です。 2 番目の引数は、コールバックのテーブルを取ります。これは、プレーヤーがインベントリと対話する方法を制御するために使用できます。
-- Input only detached inventory
minetest.create_detached_inventory("inventory_name", {
allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
return count -- allow moving
end,
allow_put = function(inv, listname, index, stack, player)
return stack:get_count() -- allow putting
end,
allow_take = function(inv, listname, index, stack, player)
return -1 -- don't allow taking
end,
on_put = function(inv, listname, index, stack, player)
minetest.chat_send_all(player:get_player_name() ..
" gave " .. stack:to_string() ..
" to the donation chest from " .. minetest.pos_to_string(player:get_pos()))
end,
})
パーミッションコールバック(つまり、 allow_
で始まるもの)は、転送するアイテムの数を返します。転送を完全に防ぐために -1 が使用されます。
アクションコールバック( on
で始まる)には戻り値がありません。
Lists
インベントリリストは、複数のグリッドを1つの場所に保存できるようにするために使用される概念です。これは、メインインベントリやクラフトスロットなど、すべてのゲームに共通のリストが多数あるため、プレイヤーにとって特に便利です。
Size and Width
リストには、グリッド内のセルの総数であるサイズと、エンジン内でのみ使用される幅があります。ウィンドウの背後にあるコードが使用する幅を決定するため、ウィンドウにインベントリを描画する場合、リストの幅は使用されません。
if inv:set_size("main", 32) then
inv:set_width("main", 8)
print("size: " .. inv:get_size("main"))
print("width: " .. inv:get_width("main"))
else
print("Error! Invalid itemname or size to set_size()")
end
set_size
ではリスト名またはサイズが無効な場合は失敗し、 false を返します。たとえば、新しいサイズが小さすぎて、ノードインベントリ内の現在のすべてのアイテムを収めることができない場合があります。
Checking Contents
is_empty
ではリストにアイテムが含まれているかどうかを確認するために使用できます。
if inv:is_empty("main") then
print("The list is empty!")
end
contains_item
ではリストに特定のアイテムが含まれているかどうかを確認するために使用できます。
if inv:contains_item("main", "default:stone") then
print("I've found some stone!")
end
Modifying Inventories and ItemStacks
Adding to a List
add_item
ではリストにアイテムを追加します(この場合 "main"
)。以下の例では、最大スタックサイズも尊重されます。
local stack = ItemStack("default:stone 99")
local leftover = inv:add_item("main", stack)
if leftover:get_count() > 0 then
print("Inventory is full! " ..
leftover:get_count() .. " items weren't added")
end
Taking Items
リストからアイテムを削除するには:
local taken = inv:remove_item("main", stack)
print("Took " .. taken:get_count())
Manipulating Stacks
最初に取得することで、個々のスタックを変更できます :
local stack = inv:get_stack(listname, 0)
次に、プロパティを設定するか、以下を尊重するメソッドを使用して、それらを変更します stack_size
:
local stack = ItemStack("default:stone 50")
local to_add = ItemStack("default:stone 100")
local leftover = stack:add_item(to_add)
local taken = stack:take_item(19)
print("Could not add" .. leftover:get_count() .. " of the items.")
-- ^ will be 51
print("Have " .. stack:get_count() .. " items")
-- ^ will be 80
-- min(50+100, stack_max) - 19 = 80
-- where stack_max = 99
add_item
では ItemStack にアイテムを追加し、追加できなかったアイテムを返します。
take_item
ではアイテムの数まで要しますが、それより少なくなる場合があり、取得したスタックを返します。
最後に、アイテムスタックを設定します。
inv:set_stack(listname, 0, stack)
Wear
ツールには摩耗があります。摩耗はプログレスバーを示し、完全に摩耗するとツールが壊れます。摩耗は 65535 のうちの数です。高いほど、ツールは摩耗します。
摩耗は add_wear()
、 get_wear()
と set_wear(wear)
を使用して操作することができます。
local stack = ItemStack("default:pick_mese")
local max_uses = 10
-- This is done automatically when you use a tool that digs things
-- It increases the wear of an item by one use.
stack:add_wear(65535 / (max_uses - 1))
ノードを掘るとき、ツールの摩耗の量は、掘られるノードに依存する可能性があります。したがって、 max_uses は、何を掘っているかによって異なります。
Lua Tables
ItemStacks と Inventory は、テーブルとの間で変換できます。これは、コピーおよび一括操作に役立ちます。
-- Entire inventory
local data = inv1:get_lists()
inv2:set_lists(data)
-- One list
local listdata = inv1:get_list("main")
inv2:set_list("main", listdata)
get_lists()
によって返されるリストのテーブルは次の形式になります。
{
list_one = {
ItemStack,
ItemStack,
ItemStack,
ItemStack,
-- inv:get_size("list_one") elements
},
list_two = {
ItemStack,
ItemStack,
ItemStack,
ItemStack,
-- inv:get_size("list_two") elements
}
}
get_list()
は ItemStack のリストとして単一のリストを返します。
注意すべき重要な点の1つは、上記のsetメソッドはリストのサイズを変更しないということです。これは、リストを空のテーブルに設定することでリストをクリアでき、サイズが減少しないことを意味します。
inv:set_list("main", {})
7 - Basic Map Operations
Introduction
この章では、マップ上で基本的なアクションを実行する方法を学習します。
Map Structure
Minetest マップは MapBlock に分割され、各 MapBlock はサイズ16の立方体です。プレイヤーがマップ内を移動すると、MapBlock が作成、ロード、およびアンロードされます。まだロードされていないマップの領域は、無視できるノード、つまり通過できない選択できないプレースホルダーノードでいっぱいです。空のスペースは、通り抜けることができる目に見えないノードである空気ノードで満たされています。
ロードされたマップブロックは、アクティブブロックと呼ばれることがよくあります。アクティブブロックは、 mods またはプレーヤーからの読み取りまたは書き込みが可能で、アクティブエンティティがあります。エンジンは、液体物理学の実行など、マップ上での操作も実行します。
MapBlocks は、ワールドデータベースからロードするか、生成することができます。 MapBlocks は、デフォルトで最大値 31000 に設定されているマップ生成制限( mapgen_limit
)まで生成されます。ただし、既存の MapBlock は、生成制限の範囲外でワールドデータベースからロードできます。
Reading
Reading Nodes
位置が決まったら、地図から読みだすことができます。
local node = minetest.get_node({ x = 1, y = 3, z = 4 })
print(dump(node)) --> { name=.., param1=.., param2=.. }
位置が小数の場合、それを含むノードに丸められます。この関数は常にノード情報を含むテーブルを返します。
-
name
- ノード名。エリアがアンロードされるときに無視されます。 -
param1
- ノード定義を参照してください。これは一般的に軽いでしょう。 -
param2
- ノード定義を参照してください。
ブロックが非アクティブの場合、関数は含まれているブロックをロードせず、代わりに name
が ignore
のテーブルを返すことに注意してください。代わりに minetest.get_node_or_nil
を使用すると、ignore
という名前のテーブルではなく nil
が返されます。 ただし、それでもブロックは読み込まれません。 ブロックに実際に ignore
が含まれている場合でも、これは ignore
を返す可能性があります。 これは、マップ生成制限で定義されているように、マップの端( mapgen_limit
)の近くで発生します。
Finding Nodes
Minetestは、一般的なマップアクションを高速化するための多数のヘルパー関数を提供します。これらの中で最も一般的に使用されるのは、ノードを見つけるためです。
たとえば、メセの近くでよりよく育つ特定の種類の植物を作りたいとしましょう。近くのメセノードを検索し、それに応じて成長率を調整する必要があります。
local grow_speed = 1
local node_pos = minetest.find_node_near(pos, 5, { "default:mese" })
if node_pos then
minetest.chat_send_all("Node found at: " .. dump(node_pos))
grow_speed = 2
end
たとえば、近くに mese が多いほど成長率が上がるとしましょう。次に、エリア内の複数のノードを見つけることができる関数を使用する必要があります。
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
local pos_list =
minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })
local grow_speed = 1 + #pos_list
上記のコードは、面積に基づいてチェックするのに対し find_node_near
、範囲に基づいてチェックするため、私たちが望むことを完全には実行しません。これを修正するには、残念ながら、手動で範囲を確認する必要があります。
local pos1 = vector.subtract(pos, { x = 5, y = 5, z = 5 })
local pos2 = vector.add(pos, { x = 5, y = 5, z = 5 })
local pos_list =
minetest.find_nodes_in_area(pos1, pos2, { "default:mese" })
local grow_speed = 1
for i=1, #pos_list do
local delta = vector.subtract(pos_list[i], pos)
if delta.x*delta.x + delta.y*delta.y <= 5*5 then
grow_speed = grow_speed + 1
end
end
これで、範囲内の mese ノードに基づいてコードが grow_speed
に正しく増加します。実際の距離を取得するために位置を二乗するのではなく、位置からの距離の二乗を比較した方法に注意してください。これは、コンピューターが平方根の計算コストを高くするのを可能な限り回避する必要があるためです。
find_nodes_with_meta
、 find_nodes_in_area_under_air
など、上記の 2 つの関数にはさらに多くのバリエーションがあり、これらは同様に機能し、他の状況で役立ちます。
Writing
Writing Nodes
set_node
マップへの書き込みに使用できます。 set_node を呼び出すたびに、ライティングが再計算されます。つまり、多数のノードでは set_node がかなり遅くなります。
minetest.set_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
local node = minetest.get_node({ x = 1, y = 3, z = 4 })
print(node.name) --> default:mese
set_node は、関連するメタデータまたはインベントリをその位置から削除します。これは、すべての状況で望ましいわけではありません。特に、 1 つの概念ノードを表すために複数のノード定義を使用している場合はそうです。
この例は、ファーネスノードです。概念的には 1 つのノードと考えていますが、実際には 2 つです。
次のように、メタデータやインベントリを削除せずにノードを設定できます。
minetest.swap_node({ x = 1, y = 3, z = 4 }, { name = "default:mese" })
Removing Nodes
ノードは常に存在する必要があります。ノードを削除するには、位置を air
に設定します。
次の 2 行は両方ともノードを削除し、両方とも同一です。
minetest.remove_node(pos)
minetest.set_node(pos, { name = "air" })
実際、remove_node は、名前が air の set_node を呼び出します。
Loading Blocks
minetest.emerge_area
を使用してマップブロックをロードできます。 出現領域は非同期です。つまり、ブロックはすぐには読み込まれません。代わりに、それらは将来すぐにロードされ、コールバックは毎回呼び出されます。
-- Load a 20x20x20 area
local halfsize = { x = 10, y = 10, z = 10 }
local pos1 = vector.subtract(pos, halfsize)
local pos2 = vector.add (pos, halfsize)
local context = {} -- persist data between callback calls
minetest.emerge_area(pos1, pos2, emerge_callback, context)
Minetest は、ブロックをロードするたびに、進行状況情報とともに emerge_callback
を呼び出します。
local function emerge_callback(pos, action,
num_calls_remaining, context)
-- On first call, record number of blocks
if not context.total_blocks then
context.total_blocks = num_calls_remaining + 1
context.loaded_blocks = 0
end
-- Increment number of blocks loaded
context.loaded_blocks = context.loaded_blocks + 1
-- Send progress message
if context.total_blocks == context.loaded_blocks then
minetest.chat_send_all("Finished loading blocks!")
end
local perc = 100 * context.loaded_blocks / context.total_blocks
local msg = string.format("Loading blocks %d/%d (%.2f%%)",
context.loaded_blocks, context.total_blocks, perc)
minetest.chat_send_all(msg)
end
end
これはブロックをロードする唯一の方法ではありません。LVM を使用すると、包含ブロック( the encompassed blocks )が同期的にロードされます。
Deleting Blocks
delete_blocks を使用して、マップブロックの範囲を削除できます。
-- Delete a 20x20x20 area
local halfsize = { x = 10, y = 10, z = 10 }
local pos1 = vector.subtract(pos, halfsize)
local pos2 = vector.add (pos, halfsize)
minetest.delete_area(pos1, pos2)
これにより、そのエリア内のすべてのマップブロックが包括的に削除されます。 これは、一部のノードがエリア境界とオーバーラップするマップブロック上にあるため、エリア外で削除されることを意味します。
Introduction
特定のノードで関数を定期的に実行することは、一般的なタスクです。 Minetest は、これを行う 2 つの方法を提供します。アクティブブロック修飾子( ABM )とノードタイマーです。
ABM は、ロードされたすべての MapBlock をスキャンして、基準に一致するノードを探します。草など、世界で頻繁に見られるノードに最適です。CPU のオーバーヘッドは高くなりますが、メモリとストレージのオーバーヘッドは低くなります。
かまどやマシンなど、一般的でないノードやすでにメタデータを使用しているノードの場合は、代わりにノードタイマーを使用する必要があります。 ノードタイマーは、各 MapBlock で保留中のタイマーを追跡し、期限切れになったときに実行することで機能します。 つまり、タイマーは、一致するものを見つけるためにロードされたすべてのノードを検索する必要はありませんが、代わりに、保留中のタイマーを追跡するためにわずかに多くのメモリとストレージを必要とします。
Node Timers
ノードタイマーは、単一のノードに直接関連付けられています。 NodeTimerRef オブジェクトを取得することで、ノードタイマーを管理できます。
local timer = minetest.get_node_timer(pos)
timer:start(10.5) -- in seconds
ステータスを確認したり、タイマーを停止したりすることもできます。
if timer:is_started() then
print("The timer is running, and has " .. timer:get_timeout() .. "s remaining!")
print(timer:get_elapsed() .. "s has elapsed.")
end
timer:stop()
ノードタイマーがアップすると、ノードの定義テーブルの on_timer
メソッドが呼び出されます。このメソッドは、ノードの位置という1つのパラメーターのみを取ります。
minetest.register_node("autodoors:door_open", {
on_timer = function(pos)
minetest.set_node(pos, { name = "autodoors:door" })
return false
end
})
on_timer
で true を返すと、タイマーが同じ間隔で再度実行されます。
タイマーの制限に気付いたかもしれません。最適化の理由から、ノードタイプごとに1つのタイプのタイマーのみを実行でき、ノードごとに1つのタイマーのみを実行できます。
Active Block Modifiers
この章では、エイリアングラスは水の近くに現れる可能性のあるタイプのグラスです。
minetest.register_node("aliens:grass", {
description = "Alien Grass",
light_source = 3, -- The node radiates light. Min 0, max 14
tiles = {"aliens_grass.png"},
groups = {choppy=1},
on_use = minetest.item_eat(20)
})
minetest.register_abm({
nodenames = {"default:dirt_with_grass"},
neighbors = {"default:water_source", "default:water_flowing"},
interval = 10.0, -- Run every 10 seconds
chance = 50, -- Select every 1 in 50 nodes
action = function(pos, node, active_object_count,
active_object_count_wider)
local pos = {x = pos.x, y = pos.y + 1, z = pos.z}
minetest.set_node(pos, {name = "aliens:grass"})
end
})
この ABM は 10 秒ごとに実行され、一致するノードごとに、 50 分の1の確率で実行されます。 ABM がノード上で実行されている場合、エイリアングラスノードがその上に配置されます。 これにより、以前にその位置にあったノードが削除されることに注意してください。 これを防ぐには、 minetest.get_node を使用して、草のためのスペースがあることを確認するチェックを含める必要があります。
ネイバーの指定はオプションです。 複数のネイバーを指定する場合、要件を満たすために存在する必要があるのはそのうちの1つだけです。
チャンスの指定もオプションです。 チャンスを指定しない場合、ABM は他の条件が満たされたときに常に実行されます。
Your Turn
- midas touch: 5秒ごとに100回に1回の確率で水を金のブロックに変えます。
- Decay : 水が隣にあるとき、木を土に変えます。
- Burnin' : すべてのエアノードに火をつけます。(ヒント:「 air 」および「 fire:basic_flame 」)。警告:ゲームがクラッシュします。
9 - Storage and Metadata
Introduction
この章では、データを保存する方法を学習します。
Metadata
What is Metadata?
Minetest では、メタデータはカスタムデータを何かに添付するために使用される Key-Value ストアです。メタデータを使用して、ノード、プレーヤー、または ItemStack に対する情報を格納できます。
各タイプのメタデータは、まったく同じAPIを使用します。メタデータは値を文字列として格納しますが、他のプリミティブ型を変換して格納する方法はいくつかあります。
メタデータの一部のキーには、特別な意味がある場合があります。 たとえば、ノードメタデータの infotext
は、十字線を使用してノードにカーソルを合わせたときに表示されるツールチップを格納するために使用されます。 他の mod との競合を避けるために、キーには標準の名前空間規則 modname:keyname
を使用する必要があります。 例外は owner
として保存される所有者名などの従来のデータです。
メタデータはデータに関するデータです。ノードのタイプやスタックの数などのデータ自体はメタデータではありません。
Obtaining a Metadata Object
ノードの位置がわかっている場合は、そのメタデータを取得できます。
local meta = minetest.get_meta({ x = 1, y = 2, z = 3 })
Player および ItemStack メタデータは、 get_meta()
を使用して取得されます。
local pmeta = player:get_meta()
local imeta = stack:get_meta()
Reading and Writing
ほとんどの場合、 get <type>()
メソッドと set <type>()
メソッドを使用してメタの読み取りと書き込みを行います。メタデータは文字列を格納するため、文字列メソッドは直接値を設定して取得します。
print(meta:get_string("foo")) --> ""
meta:set_string("foo", "bar")
print(meta:get_string("foo")) --> "bar"
キーが存在しない場合、入力されたすべてのゲッターはニュートラルなデフォルト値を返します。 " "
や 0
など。 get()
を使用して、文字列または nil を返すことができます。
メタデータは参照であるため、変更はすべてソースに自動的に更新されます。 ただし、 ItemStack は参照ではないため、インベントリ内の itemstack を更新する必要があります。
型指定されていないゲッターとセッターは、文字列との間で変換されます :
print(meta:get_int("count")) --> 0
meta:set_int("count", 3)
print(meta:get_int("count")) --> 3
print(meta:get_string("count")) --> "3"
Special Keys
infotext
はノードメタデータで使用され、十字線をノード上に置いたときにツールチップを表示します。 これは、ノードの所有権またはステータスを表示するときに役立ちます。
description
は ItemStack メタデータで使用され、インベントリ内のスタックにカーソルを合わせたときに説明を上書きします。 minetest.colorize()
でエンコードすることで色を使用できます。
owner
は、アイテムまたはノードを所有するプレーヤーのユーザー名を保存するために使用される共通のキーです。
Storing Tables
テーブルは、保存する前に文字列に変換する必要があります。 Minetest は、これを行うために Lua と JSON の 2 つの形式を提供します。
Lua メソッドははるかに高速で、Lua がテーブルに使用する形式と一致する傾向がありますが、JSON はより標準的な形式であり、構造が優れており、別のプログラムと情報を交換する必要がある場合に適しています。
local data = { username = "player1", score = 1234 }
meta:set_string("foo", minetest.serialize(data))
data = minetest.deserialize(minetest:get_string("foo"))
Private Metadata
ノードメタデータのエントリはプライベートとしてマークでき、クライアントに送信されません。 プライベートとしてマークされていないエントリは、クライアントに送信されます。
meta:set_string("secret", "asd34dn")
meta:mark_as_private("secret")
Lua Tables
to_table
と from_table
を使用して Lua テーブルとの間で変換できます :
local tmp = meta:to_table()
tmp.foo = "bar"
meta:from_table(tmp)
Mod Storage
Mod ストレージは、技術的にはメタデータではありませんが、メタデータとまったく同じ API を使用します。 Mod ストレージは Mod ごとに、ロード時にのみどの Mod がそれを要求しているかを知るために取得できます。
local storage = minetest.get_mod_storage()
メタデータと同じようにストレージを操作できるようになりました :
storage:set_string("foo", "bar")
Databases
mod がサーバーで使用される可能性が高く、大量のデータを保存する場合は、データベースの保存方法を提供することをお勧めします。 データの保存方法と使用場所を分離して、これをオプションにする必要があります。
local backend
if use_database then
backend =
dofile(minetest.get_modpath("mymod") .. "/backend_sqlite.lua")
else
backend =
dofile(minetest.get_modpath("mymod") .. "/backend_storage.lua")
end
backend.get_foo("a")
backend.set_foo("a", { score = 3 })
backend_storage.lua ファイルには、 mod ストレージの実装が含まれている必要があります :
local storage = minetest.get_mod_storage()
local backend = {}
function backend.set_foo(key, value)
storage:set_string(key, minetest.serialize(value))
end
function backend.get_foo(key, value)
return minetest.deserialize(storage:get_string(key))
end
return backend
backend_sqlite も同様のことを行いますが、mod ストレージの代わりに Lualsqlite3 ライブラリを使用します。
SQLite などのデータベースを使用するには、安全でない環境を使用する必要があります。安全でない環境とは、ユーザーが明示的にホワイトリストに登録した Mod のみが利用できるテーブルであり、悪意のある Mod が利用できる場合に悪用される可能性のある Lua API の制限の少ないコピーが含まれています。安全でない環境については、セキュリティ 章で詳しく説明します。
local ie = minetest.request_insecure_environment()
assert(ie, "Please add mymod to secure.trusted_mods in the settings")
local _sql = ie.require("lsqlite3")
-- Prevent other mods from using the global sqlite3 library
if sqlite3 then
sqlite3 = nil
end
SQL または、lsqlite3 library の使い方を説明するのはこの本の範疇を超えています。
Deciding Which to Use
使用するメソッドのタイプは、データの内容、フォーマット方法、およびデータの大きさによって異なります。ガイドラインとして、小さいデータは最大 10K 、中程度のデータは最大 10MB 、大きいデータはそれを超える任意のサイズです。
ノードメタデータは、ノード関連のデータを保存する必要がある場合に適しています。中程度のデータを非公開にすると、かなり効率的に保存できます。
アイテムのメタデータは、クライアントへの送信を回避することができないため、少量のデータ以外のものを格納するために使用しないでください。スタックが移動されるか、Lua からアクセスされるたびに、データもコピーされます。
Mod ストレージは中程度のデータには適していますが、大きなデータの書き込みは非効率的である可能性があります。保存のたびにすべてのデータを書き出す必要がないように、大きなデータにはデータベースを使用することをお勧めします。
安全でない環境にアクセスするために mod をホワイトリストに登録する必要があるため、データベースはサーバーでのみ実行可能です。大規模なデータセットに最適です。
Your Turn
- 5回パンチすると消えるノードを作ります。(ノード定義の
on_punch
とminetest.set_node
を使います)
10 - Objects, Players, and Entities
Introduction
この章では、オブジェクトを操作する方法と、オブジェクトを定義する方法を学習します。
- What are Objects, Players, and Entities?
- Position and Velocity
- Object Properties
- Entities
- Attachments
- Your Turn
What are Objects, Players, and Entities?
プレーヤーとエンティティはどちらもオブジェクトのタイプです。オブジェクトは、ノードグリッドとは独立して移動できるものであり、速度やスケールなどのプロパティがあります。オブジェクトはアイテムではなく、独自の個別の登録システムがあります。
プレイヤーとエンティティの間にはいくつかの違いがあります。最大のものは、プレイヤーがプレイヤーによって制御されるのに対し、エンティティは mod によって制御されることです。これは、プレイヤーの速度を mod で設定できないことを意味します。プレイヤーはクライアント側であり、エンティティはサーバー側です。もう1つの違いは、プレーヤーによってマップブロックが読み込まれるのに対し、エンティティは保存されて非アクティブになることです。
この区別は、後で説明するように、エンティティが Lua エンティティと呼ばれるテーブルを使用して制御されるという事実によって混乱しています。
Position and Velocity
get_pos
と set_pos
は、エンティティの位置を取得および設定できるようにするために存在します。
local object = minetest.get_player_by_name("bob")
local pos = object:get_pos()
object:set_pos({ x = pos.x, y = pos.y + 1, z = pos.z })
set_pos
は、アニメーションなしですぐに位置を設定します。 オブジェクトを新しい位置にスムーズにアニメーション化する場合は、 move_to
を使用する必要があります。 残念ながら、これはエンティティに対してのみ機能します。
object:move_to({ x = pos.x, y = pos.y + 1, z = pos.z })
エンティティを処理するときに考慮すべき重要なことは、ネットワークの遅延です。 理想的な世界では、エンティティの動きに関するメッセージは、正しい順序で、送信方法と同様の間隔ですぐに届きます。 ただし、シングルプレイヤーでない限り、これは理想的な世界ではありません。 メッセージが届くまでしばらく時間がかかります。 位置メッセージが順不同で到着する可能性があり、現在の既知の位置より古い位置に移動するポイントがないため、一部の set_pos
呼び出しがスキップされます。 動きの間隔が同じでない場合があり、アニメーションに使用するのが困難になります。
これらすべての結果、クライアントはサーバーに対してさまざまなことを認識します。これは、注意する必要があることです。
Object Properties
オブジェクトプロパティは、オブジェクトをレンダリングして処理する方法をクライアントに指示するために使用されます。 定義上、プロパティはエンジンが使用するためのものであるため、カスタムプロパティを定義することはできません。
ノードとは異なり、オブジェクトは設定された外観ではなく動的な外観を持っています。プロパティを更新することで、オブジェクトの外観をいつでも変更できます。
object:set_properties({
visual = "mesh",
mesh = "character.b3d",
textures = {"character_texture.png"},
visual_size = {x=1, y=1},
})
更新されたプロパティは、範囲内のすべてのプレイヤーに送信されます。これは、プレイヤーごとにスキンが異なるなど、大量のバリエーションを非常に安価に入手するのに非常に便利です。
次のセクションに示すように、エンティティはその定義で提供される初期プロパティを持つことができます。ただし、デフォルトのプレーヤープロパティはエンジンで定義されているため、新しく参加したプレーヤーのプロパティを設定するには、 on_joinplayer
で set_properties()
を使用する必要があります。
Entities
エンティティには、アイテム定義テーブルに似た定義テーブルがあります。このテーブルには、コールバックメソッド、初期オブジェクトプロパティ、およびカスタムメンバーを含めることができます。
ただし、エンティティは1つの非常に重要な点でアイテムとは異なります。エンティティが出現すると(つまり、ロードまたは作成されると)、メタテーブルを使用して定義テーブルから継承するそのエンティティの新しいテーブルが作成されます。
この新しいテーブルは、一般に Lua エンティティテーブルと呼ばれます。
メタテーブルは、Lua 言語の重要な部分であるため、知っておく必要のある重要な Lua 機能です。
素人の言葉で言えば、メタテーブルを使用すると、特定の Lua 構文を使用するときにテーブルがどのように動作するかを制御できます。メタテーブルの最も一般的な使用法は、別のテーブルをプロトタイプとして使用する機能です。現在のテーブルに存在しない場合は、デフォルトで他のテーブルのプロパティとメソッドが使用されます。
テーブル A のメンバー X にアクセスするとします。テーブル A にそのメンバーがある場合、通常どおりに返されます。ただし、テーブルにそのメンバーがなくても、メタテーブルの可能性があるテーブル B がある場合は、テーブル B がそのメンバーを持っているかどうかを確認します。
local MyEntity = {
initial_properties = {
hp_max = 1,
physical = true,
collide_with_objects = false,
collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
visual = "wielditem",
visual_size = {x = 0.4, y = 0.4},
textures = {""},
spritediv = {x = 1, y = 1},
initial_sprite_basepos = {x = 0, y = 0},
},
message = "Default message",
}
function MyEntity:set_message(msg)
self.message = msg
end
エンティティが出現すると、そのタイプテーブルからすべてをコピーすることにより、エンティティ用のテーブルが作成されます。このテーブルは、その特定のエンティティの変数を格納するために使用できます。
local entity = object:get_luaentity()
local object = entity.object
print("entity is at " .. minetest.pos_to_string(object:get_pos()))
エンティティで使用できるコールバックは多数あります。完全なリストはlua_api.txtにあります。
function MyEntity:on_step(dtime)
local pos = self.object:get_pos()
local pos_down = vector.subtract(pos, vector.new(0, 1, 0))
local delta
if minetest.get_node(pos_down).name == "air" then
delta = vector.new(0, -1, 0)
elseif minetest.get_node(pos).name == "air" then
delta = vector.new(0, 0, 1)
else
delta = vector.new(0, 1, 0)
end
delta = vector.multiply(delta, dtime)
self.object:move_to(vector.add(pos, delta))
end
function MyEntity:on_punch(hitter)
minetest.chat_send_player(hitter:get_player_name(), self.message)
end
ここで、このエンティティをスポーンして使用すると、エンティティが非アクティブになってから再びアクティブになると、メッセージが忘れられることに気付くでしょう。これは、メッセージが保存されていないためです。 Minetest では、エンティティテーブルにすべてを保存するのではなく、保存方法を制御できます。 Staticdata は、保存する必要のあるすべてのカスタム情報を含む文字列です。
function MyEntity:get_staticdata()
return minetest.write_json({
message = self.message,
})
end
function MyEntity:on_activate(staticdata, dtime_s)
if staticdata ~= "" and staticdata ~= nil then
local data = minetest.parse_json(staticdata) or {}
self:set_message(data.message)
end
end
Minetest は、いつでも何度でも get_staticdata()
を呼び出すことができます。これは、 Minetest が MapBlock が非アクティブになるのを待たずに保存するためです。これにより、データが失われる可能性があります。 MapBlock は約18秒ごとに保存されるため、 get_staticdata()
が呼び出される間隔も同様であることに注意してください。
一方、 on_activate()
は、 MapBlock がアクティブになるか、エンティティの生成によってエンティティがアクティブになったときにのみ呼び出されます。これは、 staticdata が空である可能性があることを意味します。
最後に、適切な名前の register_entity
を使用して型テーブルを登録する必要があります。
minetest.register_entity("mymod:entity", MyEntity)
エンティティは、次のような mod によって生成できます。
local pos = { x = 1, y = 2, z = 3 }
local obj = minetest.add_entity(pos, "mymod:entity", nil)
3 番目のパラメーターは初期静的データです。メッセージを設定するには、エンティティテーブルメソッドを使用できます。
obj:get_luaentity():set_message("hello!")
give privilegeを持つプレイヤーは、 chat コマンドを使用してエンティティを生成できます。
/spawnentity mymod:entity
Attachments
アタッチされたオブジェクトは、親(アタッチされているオブジェクト)が移動すると移動します。アタッチされたオブジェクトは、親の子であると言われます。オブジェクトには無制限の数の子を含めることができますが、親は多くても 1 つです。
child:set_attach(parent, bone, position, rotation)
オブジェクトの get_pos()
は、オブジェクトがアタッチされているかどうかに関係なく、常にオブジェクトのグローバル位置を返します。 set_attach
は相対的な位置を取りますが、期待どおりではありません。アタッチメントの位置は、 10 倍に拡大された親の原点を基準にしています。
したがって、 0,5,0
は、親の原点の半分上のノードになります。
⚠ 度とラジアン |
---|
アタッチメントの回転は度で設定されますが、オブジェクトの回転は ラジアンです。必ず正しい角度測定に変換してください。 |
アニメーションのある 3D モデルの場合、ボーン引数を使用してエンティティをボーンにアタッチします。 3D アニメーションは、スケルトンに基づいています。モデル内のボーンのネットワークで、各ボーンに位置と回転を指定して、モデルを変更したり、腕を動かしたりできます。ボーンにアタッチすることは、キャラクターに何かを持たせたい場合に便利です。
obj:set_attach(player,
"Arm_Right", -- default bone
{x=0.2, y=6.5, z=3}, -- default position
{x=-100, y=225, z=90}) -- default rotation
Your Turn
- ノードとエンティティを組み合わせて風車を作成します。
- 選択したmobを作成します(エンティティ API のみを使用し、他の Mod は使用しません)。
11 - Privileges
Introduction
特権は、しばしば略して priv と呼ばれ、プレーヤーに特定のアクションを実行する能力を与えます。サーバーの所有者は、各プレーヤーが持つ能力を制御するための特権を付与および取り消すことができます。
- When to use Privileges
- Declaring Privileges
- Checking for Privileges
- Getting and Setting Privileges
- Adding Privileges to basic_privs
When to use Privileges
特権はプレイヤーに何かをする能力を与えるべきです。クラスまたはステータスを示すための特権ではありません。
Good Privileges:
- interact
- shout
- noclip
- fly
- kick
- ban
- vote
- worldedit
- area_admin - admin functions of one mod is ok
Bad Privileges:
- moderator
- admin
- elf
- dwarf
Declaring Privileges
register_privilege
を使用して、新しい特権を宣言します。
minetest.register_privilege("vote", {
description = "Can vote on issues",
give_to_singleplayer = true
})
give_to_singleplayer
は、指定されていない場合、デフォルトで true に設定されるため、上記の定義では実際には必要ありません。
Checking for Privileges
プレーヤーが必要なすべての特権を持っているかどうかをすばやく確認するには:
local has, missing = minetest.check_player_privs(player_or_name, {
interact = true,
vote = true })
この例では、プレーヤーが必要なすべての特権を持っている場合、 has
は true です。 has
が false の場合、 missing
には欠落している特権の Key-Value テーブルが含まれます。
local has, missing = minetest.check_player_privs(name, {
interact = true,
vote = true })
if has then
print("Player has all privs!")
else
print("Player is missing privs: " .. dump(missing))
end
不足している権限を確認する必要がない場合は、 check_player_privs
を直接 if ステートメントに入れることができます。
if not minetest.check_player_privs(name, { interact=true }) then
return false, "You need interact for this!"
end
Getting and Setting Privileges
プレーヤーがオンラインであるかどうかに関係なく、プレーヤーの特権にアクセスまたは変更できます。
local privs = minetest.get_player_privs(name)
print(dump(privs))
privs.vote = true
minetest.set_player_privs(name, privs)
特権は常に Key-Value テーブルとして指定され、キーは特権名、値はブール値です。
{
fly = true,
interact = true,
shout = true
}
Adding Privileges to basic_privs
basic_privs
特権を持つプレーヤーは、限られた特権のセットを付与および取り消すことができます。モデレーターにこの権限を付与して、「インタラクト」や「シャウト」を付与および取り消すことができるのが一般的ですが、「ギブ」や「サーバー」など、悪用される可能性の高い自分や他のプレーヤーに権限を付与することはできません。
basic_privs
に特権を追加し、モデレーターが他のプレーヤーに付与および取り消すことができる特権を調整するには、 basic_privs
設定を変更する必要があります。
デフォルトでは、 basic_privs
の値は次のとおりです。
basic_privs = interact, shout
vote
を追加するには、これを次のように更新します。
basic_privs = interact, shout, vote
これにより、 basic_privs
を持つプレイヤーは、vote
特権を付与および取り消すことができます。
12 - Chat and Commands
Introduction
Mod は、メッセージの送信、メッセージの傍受、チャットコマンドの登録など、プレーヤーのチャットと対話できます。
- Sending Messages to All Players
- Sending Messages to Specific Players
- Chat Commands
- Complex Subcommands
- Intercepting Messages
Sending Messages to All Players
ゲーム内のすべてのプレーヤーにメッセージを送信するには、 chat_send_all 関数を呼び出します。
minetest.chat_send_all("This is a chat message to all players")
これがゲーム内でどのように表示されるかの例を次に示します。
<player1> Look at this entrance
This is a chat message to all players
<player2> What about it?
メッセージは、ゲーム内のプレーヤーのチャットと区別するために別の行に表示されます。
Sending Messages to Specific Players
特定のプレーヤーにメッセージを送信するには、chat_send_player 関数を呼び出します。
minetest.chat_send_player("player1", "This is a chat message for player1")
このメッセージは、すべてのプレーヤーへのメッセージと同じ方法で表示されますが、指定されたプレーヤー(この場合は player1 )にのみ表示されます。
Chat Commands
/foo
などのチャットコマンドを登録するには、register_chatcommand
を使用します。
minetest.register_chatcommand("foo", {
privs = {
interact = true,
},
func = function(name, param)
return true, "You said " .. param .. "!"
end,
})
上記のスニペットでは、 interact
は必須のprivilegeとしてリストされています。つまり、 interact
特権を持つプレーヤーだけがコマンドを実行できます。
チャットコマンドは最大 2 つの値を返すことができます。 1 つは成功を示すブール値で、もう 1 つはユーザーに送信するメッセージです。
⚠ オフラインプレイヤーはコマンドを実行できます |
---|
Mod はオフラインプレーヤーに代わってコマンドを実行できるため、プレーヤーオブジェクトの代わりにプレーヤー名が渡されます。たとえば、IRCブリッジを使用すると、プレーヤーはゲームに参加せずにコマンドを実行できます。 |
したがって、プレーヤーがオンラインであると想定しないようにしてください。確認するにはこれをチェックします |
minetest.get_player_by_name |
プレーヤーを返します。 |
Complex Subcommands
多くの場合、次のような複雑なチャットコマンドを作成する必要があります。
/msg <to> <message>
/team join <teamname>
/team leave <teamname>
/team list
これは通常、 Lua パターンを使用して行われます。パターンは、ルールを使用してテキストからコンテンツを抽出する方法です。
local to, msg = string.match(param, "^([%a%d_-]+) (*+)$")
上記のコードは /msg <to> <message>
を実装しています。左から右に見ていきましょう。
-
^
は、文字列の先頭に一致することを意味します。 -
()
は一致するグループです-ここにあるものと一致するものはすべて string.match から返されます。 -
[]
は、このリスト内の文字を受け入れることを意味します。 -
%a
は任意の文字を受け入れることを意味し、%d
は任意の数字を受け入れることを意味します。 -
[%a%d_-]
は、任意の文字または数字、あるいは_
または-
を受け入れることを意味します。 -
+
は、 1 回以上前のものと一致することを意味します。 -
*
は、このコンテキストの任意の文字に一致することを意味します。 -
$
は、文字列の末尾に一致することを意味します。
簡単に言えば、パターンは名前(文字/数字/-/のみの単語)、スペース、メッセージ( 1 つ以上の任意の文字)に一致します。名前とメッセージは括弧で囲まれているため、返されます。
これが、ほとんどの Mod が複雑なチャットコマンドを実装する方法です。 Lua パターンのより良いガイドは、おそらくlua-users.orgチュートリアルまたはPILドキュメントでしょう。
Chat Command Builderと呼ばれるパターンなしで複雑なチャットコマンドを作成するために使用できる、この本の著者によって書かれたライブラリもあります。
Intercepting Messages
メッセージを傍受するには、 register_on_chat_message を使用します。
minetest.register_on_chat_message(function(name, message)
print(name .. " said " .. message)
return false
end)
false を返すことにより、チャットメッセージをデフォルトのハンドラーで送信できるようになります。 nil
は暗黙的に返され、 false のように扱われるため、実際には return false
の行を削除しても、同じように機能します。
⚠ 特権とチャットコマンド |
---|
プレーヤーがこのコールバックをトリガーするために、シャウト特権は必要ありません。これは、チャットコマンドが Lua に実装されており、 / で始まるチャットメッセージであるためです。 |
チャットコマンドである可能性があること、またはユーザーが「シャウト」を持っていない可能性があることを考慮に入れる必要があります。
minetest.register_on_chat_message(function(name, message)
if message:sub(1, 1) == "/" then
print(name .. " ran chat command")
elseif minetest.check_player_privs(name, { shout = true }) then
print(name .. " said " .. message)
else
print(name .. " tried to say " .. message ..
" but doesn't have shout")
end
return false
end)
13 - Chat Command Builder
Introduction
この章では、 ChatCmdBuilder を使用して、 /msg <name> <message>
、 /team join <teamname>
/team Leave <teamname>
などの複雑なチャットコマンドを作成する方法を説明します。
ChatCmdBuilder はこの本の著者によって作成されたライブラリであり、ほとんどの modder はチャットとコマンドで概説されている方法を使用する傾向があることに注意してください。
Why ChatCmdBuilder?
従来、mod は Lua パターンを使用してこれらの複雑なコマンドを実装していました。
local name = string.match(param, "^join ([%a%d_-]+)")
しかし、私は Lua パターンを書くのが面倒で読めないことに気づきました。このため、私はあなたのためにこれを行うためのライブラリを作成しました。
ChatCmdBuilder.new("sethp", function(cmd)
cmd:sub(":target :hp:int", function(name, target, hp)
local player = minetest.get_player_by_name(target)
if player then
player:set_hp(hp)
return true, "Killed " .. target
else
return false, "Unable to find " .. target
end
end)
end, {
description = "Set hp of player",
privs = {
kick = true
-- ^ probably better to register a custom priv
}
})
ChatCmdBuilder.new(name、setup_func、def)
は、 name
という新しいチャットコマンドを作成します。次に、渡された関数( setup_func
)を呼び出し、サブコマンドを作成します。各 cmd:sub(route、func)
はサブコマンドです。
サブコマンドは、入力パラメーターに対する特定の応答です。プレーヤーがチャットコマンドを実行すると、入力に一致する最初のサブコマンドが実行され、他のサブコマンドは実行されません。一致するサブコマンドがない場合、ユーザーには無効な構文が通知されます。たとえば、上記のコードスニペットでは、プレーヤーが /sethp username 12
の形式で何かを入力すると、 cmd:sub に渡された関数が呼び出されます。 /sethp 12 bleh
と入力すると、間違った入力メッセージが表示されます。
:name :hp:int
はルートです。 /teleport に渡されるパラメータのフォーマットを記述します。
Routes
ルートは、ターミナルと変数で構成されています。ターミナルは常にそこになければなりません。たとえば、 /team join :username :teamname
のjoin
です。スペースも端子としてカウントされます。
変数は、ユーザーの入力内容に応じて値を変更できます。たとえば、 :username
と :teamname
です。
変数は :name:type
として定義されます。 name
はヘルプドキュメントで使用されています。 type
は入力を照合するために使用されます。タイプが指定されていない場合、タイプは word
です。
有効なタイプは次のとおりです。
-
word
- default. Any string without spaces. -
int
- Any integer/whole number, no decimals. -
number
- Any number, including ints and decimals. -
pos
- 1,2,3 or 1.1,2,3.4567 or (1,2,3) or 1.2, 2 ,3.2 -
text
- Any string. There can only ever be one text variable, no variables or terminals can come afterwards.
:name :hp:int
には、次の 2 つの変数があります。
-
name
- タイプが指定されていないため、word
のタイプ。スペースを含まない任意の文字列を受け入れます。 -
hp
-int
のタイプ
Subcommand functions
最初の引数は発信者の名前です。次に、変数が順番に関数に渡されます。
cmd:sub(":target :hp:int", function(name, target, hp)
-- subcommand function
end)
Installing ChatCmdBuilder
ソースコードはGithubで探してダウンロードできます。
インストールには 2 つの方法があります。
- ChatCmdBuilder を mod としてインストールし、それに依存します。
- mod のchatcmdbuilder.lua として ChatCmdBuilder に init.lua ファイルを含め、それを dofile します。
Admin complex command
これを可能にするチャットコマンドを作成する例を次に示します。
-
/admin kill <username>>
- ユーザーを強制終了します -
/admin move <username> to <pos>
- テレポートユーザー -
/admin log <username>
- レポートログを表示 -
/admin log <username> <message>
- レポートログへのログ
local admin_log
local function load()
admin_log = {}
end
local function save()
-- todo
end
load()
ChatCmdBuilder.new("admin", function(cmd)
cmd:sub("kill :name", function(name, target)
local player = minetest.get_player_by_name(target)
if player then
player:set_hp(0)
return true, "Killed " .. target
else
return false, "Unable to find " .. target
end
end)
cmd:sub("move :name to :pos:pos", function(name, target, pos)
local player = minetest.get_player_by_name(target)
if player then
player:setpos(pos)
return true, "Moved " .. target .. " to " ..
minetest.pos_to_string(pos)
else
return false, "Unable to find " .. target
end
end)
cmd:sub("log :username", function(name, target)
local log = admin_log[target]
if log then
return true, table.concat(log, "\n")
else
return false, "No entries for " .. target
end
end)
cmd:sub("log :username :message", function(name, target, message)
local log = admin_log[target] or {}
table.insert(log, message)
admin_log[target] = log
save()
return true, "Logged"
end)
end, {
description = "Admin tools",
privs = {
kick = true,
ban = true
}
})
14 - Player Physics
Introduction
プレーヤーの物理は、物理オーバーライドを使用して変更できます。物理オーバーライドは、歩行速度、ジャンプ速度、および重力定数を設定できます。物理オーバーライドは、プレーヤーごとに設定され、乗数です。たとえば、重力の値が 2 の場合、重力は 2 倍強くなります。
Basic Example
呼び出し元を low G にする反重力コマンドを追加する方法の例を次に示します。
minetest.register_chatcommand("antigravity", {
func = function(name, param)
local player = minetest.get_player_by_name(name)
player:set_physics_override({
gravity = 0.1, -- set gravity to 10% of its original value
-- (0.1 * 9.81)
})
end,
})
Available Overrides
player:set_physics_override()
にはオーバーライドのテーブルが与えられます。
lua_api.txtによると、次のようになります。
- speed 速度: デフォルトの歩行速度値への乗数(デフォルト:1)
- jump ジャンプ: デフォルトのジャンプ値への乗数(デフォルト:1)
- gravity 重力: デフォルトの重力値への乗数(デフォルト:1)
- sneak: プレーヤーがこっそりできるかどうか(デフォルト:true)
Old Movement Behaviour
0.4.16 リリースより前のプレイヤーの動きには、スニークグリッチが含まれていました。これにより、特定のノードの配置から作成された「エレベーター」をスニーク(シフトを押す)やスペースを押して上昇するなど、さまざまな動きのグリッチが可能になります。この動作は意図したものではありませんが、多くのサーバーで使用されているため、オーバーライドで保持されています。
古い動きの動作を完全に復元するには、 2 つのオーバーライドが必要です。
- new_move: プレーヤーが新しい動きを使用するかどうか(デフォルト:true)
- sneak_glitch: プレーヤーが「スニークエレベーター」を使用できるかどうか(デフォルト:false)
Mod Incompatibility
プレイヤーの同じ物理値をオーバーライドする mod は、互いに互換性がない傾向があることに注意してください。オーバーライドを設定すると、以前に設定されたオーバーライドが上書きされます。これは、複数のオーバーライドがプレーヤーの速度を設定した場合、最後に実行されたものだけが有効になることを意味します。
Your Turn
- Sonic ソニック: プレーヤーがゲームに参加するときに、速度乗数を高い値(少なくとも6)に設定します。
- Super bounce スーパーバウンス: プレーヤーが 20 メートルジャンプできるようにジャンプ値を増やします( 1 メートルは 1 ノードです)。
- Space スペース: プレイヤーが高くなるにつれて重力を減少させます。
15 - GUIs (Formspecs)
Introduction
Screenshot of furnace formspec, labelled.
この章では、 formspec を作成してユーザーに表示する方法を学習します。 formspec は、フォームの仕様コードです。 Minetest では、フォームはプレーヤーインベントリなどのウィンドウであり、ラベル、ボタン、フィールドなどのさまざまな要素を含めることができます。
プレーヤーに情報を提供するだけでよい場合など、ユーザー入力を取得する必要がない場合は、formes ではなくヘッドアップディスプレイ(HUD)の要素の使用を検討する必要があることに注意してください。予期しないウィンドウはゲームプレイを混乱させる傾向があるためです。
- Real or Legacy Coordinates
- Anatomy of a Formspec
- Guessing Game
- Formspec Sources
Real or Legacy Coordinates
Minetest の古いバージョンでは、formspecs に一貫性がありませんでした。さまざまな要素が配置される方法は、予期しない方法で変化しました。要素の配置を予測して配置するのは困難でした。 Minetest 5.1.0 には、一貫した座標系を導入することでこれを修正することを目的とした実座標と呼ばれる機能が含まれています。実座標の使用を強くお勧めします。そのため、この章ではそれらを排他的に使用します。
Anatomy of a Formspec
Elements
Formspec は、通常とは異なる形式のドメイン固有言語です。これは、次の形式のいくつかの要素で構成されています。
type[param1;param2]
要素タイプが宣言され、パラメータは角括弧で囲まれています。次のように、複数の要素を結合したり、複数の行に配置したりできます。
foo[param1]bar[param1]
bo[param1]
要素は、テキストボックスやボタンなどのアイテムであるか、サイズや背景などのメタデータにすることができます。考えられるすべての要素のリストについては、lua_api.txtを参照してください。
Header
formspec のヘッダーには、最初に表示する必要のある情報が含まれています。これには、 formspec のサイズ、位置、アンカー、およびゲーム全体のテーマを適用する必要があるかどうかが含まれます。
ヘッダーの要素は特定の順序で定義する必要があります。そうしないと、エラーが発生します。この順序は上記の段落で示され、いつものように、lua_api.txtに記載されています。
サイズは formspec スロットにあります。測定単位は約 64 ピクセルですが、画面密度とクライアントのスケーリング設定によって異なります。サイズが 2,2
の formspec は次のとおりです。
formspec_version[3]
size[2,2]
formspec 言語バージョンを明示的に定義した方法に注目してください。これがないと、代わりにレガシーシステムが代わりに使用されます。これにより、一貫した要素の配置やその他の新しい機能を使用できなくなります。
位置要素とアンカー要素は、 formspec を画面に配置するために使用されます。位置は、 formspec が画面上のどこにあるかを設定し、デフォルトで中央( 0.5,0.5
)になります。アンカーは、 formspec 上の位置を設定し formspec を画面の端に揃えることができます。 formspec は、次のように画面の左側に配置できます。
formspec_version[3]
size[2,2]
real_coordinates[true]
position[0,0.5]
anchor[0,0.5]
これにより、アンカーが formspec ボックスの左中央の端に設定され、次にそのアンカーの位置が画面の左側に設定されます。
Guessing Game
学ぶための最良の方法は何かを作ることですので、推測ゲームを作りましょう。原理は単純です。 mod が数字を決定し、プレーヤーが数字を推測します。次に、 mod は、推測が実際の数値よりも高いか低いかを示します。
まず、formspec コードを作成する関数を作成しましょう。他の場所での再利用が容易になるため、これを行うことをお勧めします。
guessing = {}
function guessing.get_formspec(name)
-- TODO: display whether the last guess was higher or lower
local text = "I'm thinking of a number... Make a guess!"
local formspec = {
"formspec_version[3]",
"size[6,3.476]",
"label[0.375,0.5;", minetest.formspec_escape(text), "]",
"field[0.375,1.25;5.25,0.8;number;Number;]",
"button[1.5,2.3;3,0.8;guess;Guess]"
}
-- table.concat is faster than string concatenation - `..`
return table.concat(formspec, "")
end
上記のコードでは、フィールド、ラベル、およびボタンを配置します。フィールドはテキスト入力を許可し、ボタンはフォームを送信するために使用されます。パディングと間隔を追加するために要素が慎重に配置されていることに気付くでしょう。これについては後で説明します。
次に、プレーヤーが formspec を表示できるようにします。これを行う主な方法は、 show_formspec
を使用することです。
function guessing.show_to(name)
minetest.show_formspec(name, "guessing:game", guessing.get_formspec(name))
end
minetest.register_chatcommand("game", {
func = function(name)
guessing.show_to(name)
end,
})
show_formspec
関数は、プレーヤー名、 formspec 名、および formspec 自体を受け入れます。 formspec 名は、有効な itemname 、つまり modname:itemname
の形式である必要があります。
Padding and Spacing
パディングは、 formspec のエッジとそのコンテンツの間、または関連のない要素の間のギャップであり、赤で示されています。間隔は、関連する要素間のギャップであり、青で示されています。
「 0.375 」のパディングと「 0.25 」の間隔を持つことはかなり標準的です。
Receiving Formspec Submissions
show_formspec
が呼び出されると、 formspec がクライアントに送信されて表示されます。 formspecs を使用するには、クライアントからサーバーに情報を返す必要があります。このためのメソッドは formspec フィールド送信と呼ばれ、 show_formspec
の場合、その送信はグローバルコールバックを使用して受信されます。
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "guessing:game" then
return
end
if fields.guess then
local pname = player:get_player_name()
minetest.chat_send_all(pname .. " guessed " .. fields.number)
end
end)
minetest.register_on_player_receive_fields
で指定された関数は、ユーザーがフォームを送信するたびに呼び出されます。ほとんどのコールバックは、関数に指定されたフォーム名を確認し、それが正しいフォームでない場合は終了する必要があります。ただし、一部のコールバックは、複数のフォームまたはすべてのフォームで機能する必要がある場合があります。
関数の fields
パラメータは、ユーザーが送信した値のテーブルであり、文字列でインデックスが付けられています。名前付き要素は、送信の原因となったイベントに関連する場合にのみ、独自の名前でフィールドに表示されます。たとえば、ボタン要素は、その特定のボタンが押された場合にのみフィールドに表示されます。
⚠ 悪意のあるクライアントはいつでも何でも送信できます |
---|
formspec の送信を信頼してはいけません。悪意のあるクライアントは、 formspec を表示したことがなくても、いつでも好きなものを送信できます。これは、特権をチェックし、それらがアクションの実行を許可されていることを確認する必要があることを意味します。 |
したがって、 formspec がクライアントに送信され、クライアントが情報を送り返します。次のステップは、何らかの方法でターゲット値を生成して記憶し、推測に基づいて formspec を更新することです。これを行う方法は、「コンテキスト」と呼ばれる概念を使用することです。
Contexts
多くの場合、 minetest.show_formspec で、クライアントに送信したくない情報をコールバックに提供する必要があります。これには、チャットコマンドが呼び出された対象や、ダイアログの内容が含まれる場合があります。この場合、覚えておく必要のある目標値。
コンテキストは、情報を格納するためのプレーヤーごとのテーブルであり、すべてのオンラインプレーヤーのコンテキストは、file-local 変数に格納されます。
local _contexts = {}
local function get_context(name)
local context = _contexts[name] or {}
_contexts[name] = context
return context
end
minetest.register_on_leaveplayer(function(player)
_contexts[player:get_player_name()] = nil
end)
次に、formspec を表示する前に、 show コードを変更してコンテキストを更新する必要があります。
function guessing.show_to(name)
local context = get_context(name)
context.target = context.target or math.random(1, 10)
local fs = guessing.get_formspec(name, context)
minetest.show_formspec(name, "guessing:game", fs)
end
また、コンテキストを使用するように formspec 生成コードを変更する必要があります。
function guessing.get_formspec(name, context)
local text
if not context.guess then
text = "I'm thinking of a number... Make a guess!"
elseif context.guess == context.target then
text = "Hurray, you got it!"
elseif context.guess > context.target then
text = "Too high!"
else
text = "Too low!"
end
get_formspec
はコンテキストを読み取るだけで、まったく更新しないことをお勧めします。これにより、関数が単純になり、テストも簡単になります。
そして最後に、ハンドラーを更新して、推測でコンテキストを更新する必要があります。
if fields.guess then
local name = player:get_player_name()
local context = get_context(name)
context.guess = tonumber(fields.number)
guessing.show_to(name)
end
Formspec Sources
formspec をクライアントに配信する方法は3つあります。
-
show_formspec: 上記で使用した方法で、フィールドは
register_on_player_receive_fields
によって受信されます。 -
ノードメタフォームスペック: ノードのメタデータにフォームスペックが含まれており、クライアントはプレーヤーが右クリックしたときそれをすぐに表示します。フィールドは、
on_receive_fields
と呼ばれるノード定義のメソッドによって受信されます。 -
Player Inventory Formspecs: formspec はある時点でクライアントに送信され、プレーヤーが
i
を押すと表示されます。フィールドはregister_on_player_receive_fields
によって受信されます。
Node Meta Formspecs
minetest.show_formspec
は formspec を表示する唯一の方法ではありません。 ノードのメタデータに formspecs を追加することもできます。たとえば、これはチェストで使用され、開始時間を短縮できます。サーバーがプレーヤーにチェストのフォームスペックを送信するのを待つ必要はありません。
minetest.register_node("mymod:rightclick", {
description = "Rightclick me!",
tiles = {"mymod_rightclick.png"},
groups = {cracky = 1},
after_place_node = function(pos, placer)
-- This function is run when the chest node is placed.
-- The following code sets the formspec for chest.
-- Meta is a way of storing data onto a node.
local meta = minetest.get_meta(pos)
meta:set_string("formspec",
"formspec_version[3]" ..
"size[5,5]" ..
"label[1,1;This is shown on right click]" ..
"field[1,2;2,1;x;x;]")
end,
on_receive_fields = function(pos, formname, fields, player)
if fields.quit then
return
end
print(fields.x)
end
})
このように設定された Formspecs は、同じコールバックをトリガーしません。 meta formspecs のフォーム入力を受け取るには、ノードを登録するときに on_receive_fields
エントリを含める必要があります。
このスタイルのコールバックは、フィールドで Enter キーを押すとトリガーされます。これは、 minetest.show_formspec
では不可能です。ただし、この種のフォームは、ノードを右クリックすることによってのみ表示できます。プログラムでトリガーすることはできません。
Player Inventory Formspecs
プレイヤーインベントリフォームスペックは、プレイヤーがiを押したときに表示されるものです。グローバルコールバックは、この formspec からイベントを受信するために使用され、フォーム名は ""`です。
複数の mod でプレーヤーのインベントリをカスタマイズできるようにするさまざまな mod がいくつかあります。公式に推奨される mod は SimpleFast Inventory(sfinv) で、 MinetestGame に含まれています。
Your Turn
- 推測ゲームを拡張して、各プレーヤーの最高スコアを追跡します。最高スコアは、推測の数です。
- ユーザーが formspec を開いてメッセージを残すことができる「受信ボックス」と呼ばれるノードを作成します。このノードは、配置者の名前を「所有者」としてメタに保存し、「 show_formspec 」を使用してさまざまな formspec をさまざまなプレーヤーに表示する必要があります。
16 - HUD
Introduction
ヘッドアップディスプレイ( HUD )要素を使用すると、テキスト、画像、およびその他のグラフィック要素を表示できます。
HUD はユーザー入力を受け入れません。そのためには、formspecを使用する必要があります。
- Positioning
- Text Elements
- Image Elements
- Changing an Element
- Storing IDs
- Other Elements
Positioning
Position and Offset
画面にはさまざまな物理的サイズと解像度があり、HUDはすべての画面タイプで適切に機能する必要があります。
これに対する Minetest の解決策は、パーセンテージ位置とオフセットの両方を使用して要素の位置を指定することです。パーセンテージの位置は画面サイズを基準にしているため、要素を画面の中央に配置するには、画面の半分のパーセンテージの位置を指定する必要があります。 (50%, 50%) 、および (0, 0) のオフセット。
次に、オフセットを使用して、パーセント位置を基準にして要素を移動します。
Alignment
位置合わせは、位置とオフセットの結果が要素上にある場所です。たとえば、 {x = -1.0, y = 0.0}
は、位置とオフセットの結果を要素の境界の左側に配置します。これは、テキスト要素を左、中央、または右に揃える場合に特に便利です。
上の図は、3つのウィンドウ(青)を示しています。各ウィンドウには、 1 つの HUD 要素(黄色)があり、毎回異なる配置になっています。矢印は、位置とオフセットの計算結果です。
Scoreboard
この章では、次のようにスコアパネルを配置および更新する方法を学習します。
上のスクリーンショットでは、すべての要素の位置が同じパーセンテージ (100%, 50%) ですが、オフセットが異なります。これにより、すべてをウィンドウの右側に固定できますが、サイズを変更することはできません。
Text Elements
プレーヤーオブジェクトのコピーを取得したら、 HUD 要素を作成できます。
local player = minetest.get_player_by_name("username")
local idx = player:hud_add({
hud_elem_type = "text",
position = {x = 0.5, y = 0.5},
offset = {x = 0, y = 0},
text = "Hello world!",
alignment = {x = 0, y = 0}, -- center aligned
scale = {x = 100, y = 100}, -- covered later
})
hud_add
関数は要素 ID を返します - これは後で HUD 要素を変更または削除するために使用できます。
Parameters
要素のタイプは、定義テーブルの hud_elem_type
プロパティを使用して指定されます。他のプロパティの意味は、このタイプによって異なります。
scale
はテキストの最大境界です。これらの境界外のテキストはトリミングされます。例: {x=100, y=100}
number
はテキストの色であり、16進形式です。例:0xFF0000
Our Example
先に進んで、すべてのテキストをスコアパネルに配置しましょう。
-- Get the dig and place count from storage, or default to 0
local meta = player:get_meta()
local digs_text = "Digs: " .. meta:get_int("score:digs")
local places_text = "Places: " .. meta:get_int("score:places")
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
offset = {x = -120, y = -25},
text = "Stats",
alignment = 0,
scale = { x = 100, y = 30},
number = 0xFFFFFF,
})
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
offset = {x = -180, y = 0},
text = digs_text,
alignment = -1,
scale = { x = 50, y = 10},
number = 0xFFFFFF,
})
player:hud_add({
hud_elem_type = "text",
position = {x = 1, y = 0.5},
offset = {x = -70, y = 0},
text = places_text,
alignment = -1,
scale = { x = 50, y = 10},
number = 0xFFFFFF,
})
これにより、次のようになります。
Image Elements
画像要素は、テキスト要素と非常によく似た方法で作成されます。
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
offset = {x = -220, y = 0},
text = "score_background.png",
scale = { x = 1, y = 1},
alignment = { x = 1, y = 0 },
})
これで、次のようになります。
Parameters
text
フィールドは画像名を提供するために使用されます。
座標が正の場合、それはスケールファクターであり、 1 は元の画像サイズ、2 は 2 倍のサイズというようになります。ただし、座標が負の場合は、画面サイズのパーセンテージです。たとえば、 x = -100
は幅の 100% です。
Scale
スケールの例として、スコアパネルのプログレスバーを作成しましょう。
local percent = tonumber(meta:get("score:score") or 0.2)
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
offset = {x = -215, y = 23},
text = "score_bar_empty.png",
scale = { x = 1, y = 1},
alignment = { x = 1, y = 0 },
})
player:hud_add({
hud_elem_type = "image",
position = {x = 1, y = 0.5},
offset = {x = -215, y = 23},
text = "score_bar_full.png",
scale = { x = percent, y = 1},
alignment = { x = 1, y = 0 },
})
これで、最初の投稿のような HUD ができました。ただし、問題が 1 つありますが、統計が変更されても更新されません。
Changing an Element
hud_add
メソッドによって返された ID を使用して、 ID を更新したり、後で削除したりできます。
local idx = player:hud_add({
hud_elem_type = "text",
text = "Hello world!",
-- parameters removed for brevity
})
player:hud_change(idx, "text", "New Text")
player:hud_remove(idx)
hud_change
メソッドは、要素 ID、変更するプロパティ、および新しい値を受け取ります。上記の呼び出しにより、 text
プロパティが「 HelloWorld 」から「 Newtext 」に変更されます。
これは、 hud_add
の直後に hud_change
を実行することは、機能的には以下と同等であり、かなり非効率的な方法であることを意味します。
local idx = player:hud_add({
hud_elem_type = "text",
text = "New Text",
})
Storing IDs
score = {}
local saved_huds = {}
function score.update_hud(player)
local player_name = player:get_player_name()
-- Get the dig and place count from storage, or default to 0
local meta = player:get_meta()
local digs_text = "Digs: " .. meta:get_int("score:digs")
local places_text = "Places: " .. meta:get_int("score:places")
local percent = tonumber(meta:get("score:score") or 0.2)
local ids = saved_huds[player_name]
if ids then
player:hud_change(ids["places"], "text", places_text)
player:hud_change(ids["digs"], "text", digs_text)
player:hud_change(ids["bar_foreground"],
"scale", { x = percent, y = 1 })
else
ids = {}
saved_huds[player_name] = ids
-- create HUD elements and set ids into `ids`
end
end
minetest.register_on_joinplayer(score.update_hud)
minetest.register_on_leaveplayer(function(player)
saved_huds[player:get_player_name()] = nil
end)
Other Elements
HUD 要素の完全なリストについては、lua_api.txtをお読みください。
17 - SFINV: Inventory Formspec
Introduction
Simple Fast Inventory(SFINV)は、 Minetest Game にある mod で、プレーヤーのインベントリ formspec を作成するために使用されます。 SFINV には、表示されているページを追加または管理できる API が付属しています。
SFINV はデフォルトでページをタブとして表示しますが、 mod またはゲームが代わりに他の形式でページを表示することを決定する可能性があるため、ページはページと呼ばれます。たとえば、複数のページを 1 つの formspec に表示できます。
- Registering a Page
- Receiving events
- Conditionally showing to players
- on_enter and on_leave callbacks
- Adding to an existing page
Registering a Page
SFINV は、ページを作成するための適切な名前の sfinv.register_page
関数を提供します。ページの名前とその定義を使用して関数を呼び出すだけです。
sfinv.register_page("mymod:hello", {
title = "Hello!",
get = function(self, player, context)
return sfinv.make_formspec(player, context,
"label[0.1,0.1;Hello world!]", true)
end
})
make_formspec
関数は、 formspec を SFINV の formspec コードで囲みます。現在「 true 」に設定されている 4 番目のパラメータは、プレーヤーのインベントリを表示するかどうかを決定します。
物事をもっとエキサイティングにしましょう。これは、プレーヤー管理タブの formspec 生成部分のコードです。このタブでは、管理者がリストでプレーヤーを選択してボタンをクリックすることで、プレーヤーをキックまたは禁止することができます。
sfinv.register_page("myadmin:myadmin", {
title = "Tab",
get = function(self, player, context)
local players = {}
context.myadmin_players = players
-- Using an array to build a formspec is considerably faster
local formspec = {
"textlist[0.1,0.1;7.8,3;playerlist;"
}
-- Add all players to the text list, and to the players list
local is_first = true
for _ , player in pairs(minetest.get_connected_players()) do
local player_name = player:get_player_name()
players[#players + 1] = player_name
if not is_first then
formspec[#formspec + 1] = ","
end
formspec[#formspec + 1] =
minetest.formspec_escape(player_name)
is_first = false
end
formspec[#formspec + 1] = "]"
-- Add buttons
formspec[#formspec + 1] = "button[0.1,3.3;2,1;kick;Kick]"
formspec[#formspec + 1] = "button[2.1,3.3;2,1;ban;Kick + Ban]"
-- Wrap the formspec in sfinv's layout
-- (ie: adds the tabs and background)
return sfinv.make_formspec(player, context,
table.concat(formspec, ""), false)
end,
})
上記のコードについては何も新しいことはありません。すべての概念は、上記および前の章で説明されています。
Receiving events
sfinv 定義に on_player_receive_fields
関数を追加することで、 formspec イベントを受け取ることができます。
on_player_receive_fields = function(self, player, context, fields)
-- TODO: implement this
end,
on_player_receive_fields
は minetest.register_on_player_receive_fields
と同じように機能しますが、 formname
の代わりに context
が指定されている点が異なります。 SFINV は、ナビゲーションタブイベントなど、 SFINV 自体に関連するイベントを消費するため、このコールバックではそれらを受信しないことに注意してください。
それでは、 adminmod に on_player_receive_fields
を実装しましょう。
on_player_receive_fields = function(self, player, context, fields)
-- text list event, check event type and set index if selection changed
if fields.playerlist then
local event = minetest.explode_textlist_event(fields.playerlist)
if event.type == "CHG" then
context.myadmin_selected_idx = event.index
end
-- Kick button was pressed
elseif fields.kick then
local player_name =
context.myadmin_players[context.myadmin_selected_idx]
if player_name then
minetest.chat_send_player(player:get_player_name(),
"Kicked " .. player_name)
minetest.kick_player(player_name)
end
-- Ban button was pressed
elseif fields.ban then
local player_name =
context.myadmin_players[context.myadmin_selected_idx]
if player_name then
minetest.chat_send_player(player:get_player_name(),
"Banned " .. player_name)
minetest.ban_player(player_name)
minetest.kick_player(player_name, "Banned")
end
end
end,
ただし、これにはかなり大きな問題があります。誰でもプレイヤーを蹴ったり禁止したりできます!キックまたはバンの特権を持つプレイヤーにのみこれを表示する方法が必要です。幸いなことに、 SFINV を使用するとこれを実行できます。
Conditionally showing to players
ページがいつ表示されるかを制御したい場合は、ページの定義に is_in_nav
関数を追加できます。
is_in_nav = function(self, player, context)
local privs = minetest.get_player_privs(player:get_player_name())
return privs.kick or privs.ban
end,
1つの priv のみをチェックする必要がある場合、または「 and 」を実行する場合は、 get_player_privs
の代わりに minetest.check_player_privs()
を使用する必要があります。
is_in_nav
は、プレーヤーのインベントリフォームスペックが生成されたときにのみ呼び出されることに注意してください。これは、プレーヤーがゲームに参加したとき、タブを切り替えたとき、または mod が SFINV の再生成を要求したときに発生します。
つまり、 is_in_nav
の結果を変更する可能性のあるイベントについて、 SFINV がインベントリフォームスペックを再生成するように手動で要求する必要があります。私たちの場合、キックまたは禁止がプレーヤーに付与または取り消されるたびに、それを行う必要があります。
local function on_grant_revoke(grantee, granter, priv)
if priv ~= "kick" and priv ~= "ban" then
return
end
local player = minetest.get_player_by_name(grantee)
if not player then
return
end
local context = sfinv.get_or_create_context(player)
if context.page ~= "myadmin:myadmin" then
return
end
sfinv.set_player_inventory_formspec(player, context)
end
minetest.register_on_priv_grant(on_grant_revoke)
minetest.register_on_priv_revoke(on_grant_revoke)
on_enter and on_leave callbacks
プレーヤーは、タブが選択されたときにタブに入り、別のタブが選択されようとしているときにタブから離れます。カスタムテーマを使用すると、複数のページを選択できる可能性があります。
これらのイベントは、プレーヤーによってトリガーされない場合があることに注意してください。その時点では、プレーヤーは formspec を開いていない可能性があります。たとえば、プレーヤーがインベントリを開く前でもゲームに参加すると、ホームページに対して on_enter が呼び出されます。
プレーヤーを混乱させる可能性があるため、ページの変更をキャンセルすることはできません。
on_enter = function(self, player, context)
end,
on_leave = function(self, player, context)
end,
Adding to an existing page
既存のページにコンテンツを追加するには、ページをオーバーライドして、返された formspec を変更する必要があります。
local old_func = sfinv.registered_pages["sfinv:crafting"].get
sfinv.override_page("sfinv:crafting", {
get = function(self, player, context, ...)
local ret = old_func(self, player, context, ...)
if type(ret) == "table" then
ret.formspec = ret.formspec .. "label[0,0;Hello]"
else
-- Backwards compatibility
ret = ret .. "label[0,0;Hello]"
end
return ret
end
})
18 - Biomes and Decorations
Introduction
興味深く多様なゲーム内環境を作成することを目指す場合、バイオームと装飾を登録する機能は不可欠です。この章では、バイオームを登録する方法、バイオームの分布を制御する方法、およびバイオームに装飾を配置する方法について説明します。
- What are Biomes?
- Biome Placement
- Registering a Biome
- What are Decorations?
- Registering a Simple Decoration
- Registering a Schematic Decoration
- Mapgen Aliases
What are Biomes?
Minetest バイオームは、特定のゲーム内環境です。バイオームを登録するときに、マップ生成中にバイオームに表示されるノードのタイプを判別できます。バイオーム間で異なる可能性のある最も一般的なタイプのノードには、次のものがあります。
-
トップノード: これは、サーフェス上で最も一般的に見られるノードです。よく知られている例は、 MinetestGame の「 Dirt with Grass 」です。
-
フィラーノード: これは、最上位ノードのすぐ下のレイヤーです。草のあるバイオームでは、それはしばしば汚れになります。
-
ストーンノード: これは、地下で最もよく見られるノードです。
-
水ノード: これは通常液体であり、水域が予想される場所に表示されるノードになります。
-
Top node: This is the node most commonly found on the surface. A well-known example would be “Dirt with Grass” from Minetest Game.
-
Filler node: This is the layer immediately beneath the top node. In biomes with grass, it will often be dirt.
-
Stone node: This is the node you most commonly see underground.
-
Water node: This is usually a liquid and will be the node that appears where you would expect bodies of water.
他のタイプのノードもバイオーム間で異なる可能性があり、同じゲーム内で非常に異なる環境を作成する機会を提供します。
Biome Placement
Heat and Humidity
バイオームを登録するだけでは十分ではありません。また、ゲーム内のどこで発生するかを決定する必要があります。これは、各バイオームに熱と湿度の値を割り当てることによって行われます。
これらの値について慎重に検討する必要があります。それらは、どのバイオームが互いに隣接できるかを決定します。決定が不十分だと、氷河と国境を接する暑い砂漠となることを意味するものや、避けたいと思われる他のありそうもない組み合わせが生じる可能性があります。
ゲームでは、マップの任意のポイントでの熱と湿度の値は通常 0 から 100 の間です。値は徐々に変化し、マップ内を移動するにつれて増加または減少します。任意の時点でのバイオームは、登録されたバイオームのどれがマップ上のその位置にあるものに最も近い熱と湿度の値を持っているかによって決定されます。
熱と湿度の変化は緩やかであるため、バイオームの環境に関する合理的な期待に基づいて、バイオームに熱と湿度の値を割り当てることをお勧めします。例えば:
- 砂漠は高温多湿である可能性があります。
- 雪に覆われた森は、熱が低く、湿度が中程度の場合があります。
- 沼地バイオームは一般的に湿度が高いでしょう。 * 実際には、これは、多様な範囲のバイオームがある限り、互いに隣接するバイオームが論理的な進行を形成することに気付く可能性が高いことを意味します。
Visualising Boundaries using Voronoi Diagrams
Voronoi diagram, showing the closest point.By Balu Ertl, CC BY-SA 4.0.
使用しているバイオーム間の関係を視覚化できれば、バイオームの熱と湿度の値を微調整するのは簡単です。これは、独自のバイオームのフルセットを作成する場合に最も重要ですが、既存のセットにバイオームを追加する場合にも役立ちます。
どのバイオームが境界を共有するかを視覚化する最も簡単な方法は、ボロノイ図を作成することです。これを使用して、任意の位置が2次元図のどの点に最も近いかを示すことができます。
ボロノイ図は、互いに隣接する必要のあるバイオームが存在しない場所と、互いに隣接するべきではないバイオームが存在する場所を明らかにすることができます。また、一般的なバイオームがゲーム内でどのように機能するかについての一般的な洞察を与えることもできます。図の外縁にある小さなバイオームやバイオームよりも、大きくて中央のバイオームの方が一般的です。
これは、熱と湿度の値に基づいて各バイオームのポイントをマークすることによって行われます。ここで、x軸は熱で、y軸は湿度です。次に、ダイアグラムはエリアに分割され、特定のエリア内のすべての位置が、ダイアグラム上の他のポイントよりもそのエリア内のポイントに近くなります。
各領域はバイオームを表します。 2つのエリアが境界を共有している場合、ゲーム内でそれらが表すバイオームを隣り合わせに配置できます。他の領域と共有される長さと比較した、2つの領域間で共有される境界の長さは、2つのバイオームが互いに隣接して見つかる可能性が高い頻度を示します。
Creating a Voronoi Diagram using Geogebra
手で描くだけでなく、Geogebraなどのプログラムを使ってボロノイ図を作成することもできます。
-
ツールバーのポイントツール(アイコンは「 A 」の付いたポイント)を選択し、チャートをクリックしてポイントを作成します。ポイントをドラッグしたり、左側のサイドバーで明示的に位置を設定したりできます。また、物事を明確にするために、各ポイントにラベルを付ける必要があります。
-
次に、左側のサイドバーの入力ボックスに次の関数を入力して、ボロノイを作成します。
Voronoi({ A, B, C, D, E })
各ポイントが中括弧の内側にあり、コンマで区切られている場合。あなたは今すべきです。
-
やったぁ!これで、ドラッグ可能なすべてのポイントを含むボロノイ図が作成されます。
Registering a Biome
次のコードは、grasslandsbiome という名前の単純なバイオームを登録します。
minetest.register_biome({
name = "grasslands",
node_top = "default:dirt_with_grass",
depth_top = 1,
node_filler = "default:dirt",
depth_filler = 3,
y_max = 1000,
y_min = -3,
heat_point = 50,
humidity_point = 50,
})
このバイオームには、表面に草のノードがある1層の土と、その下に 3 層の土のノードがあります。ストーンノードを指定していないため、 mapgen_stone
の mapgen エイリアス登録で定義されたノードがダートの下に存在します。
バイオームを登録する際には多くのオプションがあり、これらはいつものようにMinetest Lua APIリファレンスに記載されています。
作成するすべてのバイオームに対してすべてのオプションを定義する必要はありませんが、特定のオプションまたは適切な mapgen エイリアスのいずれかを定義しないと、マップ生成エラーが発生する場合があります。
What are Decorations?
デコレーションは、 mapgen のマップに配置できるノードまたはスケマチックのいずれかです。一般的な例としては、花、低木、樹木などがあります。他のより創造的な用途には、洞窟につららや石筍をぶら下げたり、地下の結晶を形成したり、小さな建物を配置したりすることもあります。
装飾は、特定のバイオーム、高さ、またはそれらを配置できるノードに制限できます。それらは、特定の植物、樹木、またはその他の特徴を確実に持つことによって、バイオームの環境を開発するためによく使用されます。
Registering a Simple Decoration
単純な装飾は、マップ生成中にマップ上に単一ノードの装飾を配置するために使用されます。デコレーションとして配置するノード、配置できる場所の詳細、および発生頻度を指定する必要があります。
例:
minetest.register_decoration({
deco_type = "simple",
place_on = {"base:dirt_with_grass"},
sidelen = 16,
fill_ratio = 0.1,
biomes = {"grassy_plains"},
y_max = 200,
y_min = 1,
decoration = "plants:grass",
})
この例では、 plants:grass
という名前のノードは、base:dirt_with_grass
ノードの上にある grassy_plains という名前のバイオームに、高さ y = 1
とy = 200
の間に配置されます。
fill_ratio 値は、装飾が表示される頻度を決定します。値を 1 まで大きくすると、多数の装飾が配置されます。代わりに、ノイズパラメータを使用して配置を決定することができます。
Registering a Schematic Decoration
スケマチックの装飾は単純な装飾と非常に似ていますが、単一のノードの配置ではなく、スケマチックの配置が含まれます。例えば:
Schematic decorations are very similar to simple decoration, but involve the placement of a schematic instead of the placement of a single node. For example:
minetest.register_decoration({
deco_type = "schematic",
place_on = {"base:desert_sand"},
sidelen = 16,
fill_ratio = 0.0001,
biomes = {"desert"},
y_max = 200, y_min = 1,
schematic = minetest.get_modpath("plants") .. "/schematics/cactus.mts",
flags = "place_center_x, place_center_z",
rotation = "random",
})
この例では、cactus.mts スケマチックが砂漠のバイオームに配置されています。スケマチックへのパスを指定する必要があります。この場合、このパスは mod 内の専用のスケマチックディレクトリに保存されます。
この例では、スケマチックの配置を中央に配置するフラグも設定し、回転はランダムに設定されています。スケマチックを装飾として配置するときのランダムな回転は、非対称のスケマチックを使用するときに、より多くのバリエーションを導入するのに役立ちます。
Mapgen Aliases
既存のゲームにはすでに適切な mapgen エイリアスが含まれているはずなので、独自のゲームを作成する場合は、独自の mapgen エイリアスの登録を検討するだけで済みます。
Mapgen エイリアスは、コア mapgen に情報を提供し、次の形式で登録できます。
minetest.register_alias("mapgen_stone", "base:smoke_stone")
少なくとも、登録する必要があります。
- mapgen_stone
- mapgen_water_source
- mapgen_river_water_source
すべてのバイオームに対して洞窟液体ノード( cave liquid nodes )を定義していない場合は、以下も登録する必要があります。
- mapgen_lava_source
19 - Lua Voxel Manipulators
Introduction
Basic Map Operationの章で概説されている関数は、便利で使いやすいですが、広い領域では非効率的です。 set_node
または get_node
を呼び出すたびに、mod はエンジンと通信する必要があります。これにより、エンジンと mod の間で一定の個別のコピー操作が発生し、速度が低下し、ゲームのパフォーマンスが急速に低下します。 Lua ボクセルマニピュレーター( LVM )を使用することはより良い代替手段です。
Concepts
LVM を使用すると、マップの広い領域を mod のメモリにロードできます。その後、エンジンとの対話やコールバックを実行せずに、このデータの読み取りと書き込みを行うことができます。つまり、これらの操作は非常に高速です。完了したら、その領域をエンジンに書き戻し、照明の計算を実行できます。
Reading into the LVM
LVM にロードできるのは立方体領域のみであるため、変更する必要のある最小位置と最大位置を計算する必要があります。次に、LVM を作成して読み込むことができます。例えば:
local vm = minetest.get_voxel_manip()
local emin, emax = vm:read_from_map(pos1, pos2)
パフォーマンス上の理由から、LVM は指示された正確な領域を読み取ることはほとんどありません。代わりに、より広い領域を読み取る可能性があります。より大きな領域は emin
と emax
で与えられ、これらは emerged min pos と emerged max pos を表しています。 LVM は、メモリからのロード、ディスクからのロード、マップジェネレーターの呼び出しなど、LVM に含まれる領域をロードします。
⚠ LVMとMapgen |
---|
グリッチを引き起こす可能性があるため、 mapgen で minetest.get_voxel_manip() を使用しないでください。代わりに minetest.get_mapgen_object("voxelmanip") を使用してください。 |
Reading Nodes
特定の位置にあるノードのタイプを読み取るには、 get_data()
を使用する必要があります。これは、各エントリが特定のノードのタイプを表すフラット配列を返します。
local data = vm:get_data()
メソッド get_light_data()
と get_param2_data()
を使用して、 param2 とライティングデータを取得できます。
上記のメソッドで指定されたフラット配列のどこにノードがあるかを調べるには、 emin
と emax
を使用する必要があります。計算を処理する VoxelArea
というヘルパークラスがあります。
local a = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
}
-- Get node's index
local idx = a:index(x, y, z)
-- Read node
print(data[idx])
これを実行すると、 data [vi]
が整数であることがわかります。これは、パフォーマンス上の理由から、エンジンが文字列を使用してノードを保存しないためです。代わりに、エンジンはコンテンツ ID と呼ばれる整数を使用します。 get_content_id()
を使用して、特定のタイプのノードのコンテンツ ID を確認できます。例えば:
local c_stone = minetest.get_content_id("default:stone")
次に、ノードが石であるかどうかを確認できます。
local idx = a:index(x, y, z)
if data[idx] == c_stone then
print("is stone!")
end
ノードタイプの ID は変更されないため、ロード時にノードタイプのコンテンツ ID を見つけて保存することをお勧めします。パフォーマンス上の理由から、 ID は必ずローカル変数に格納してください。
LVM データ配列内のノードは逆座標の順序で格納されるため、常に z, y, x
の順序で反復する必要があります。例えば:
for z = min.z, max.z do
for y = min.y, max.y do
for x = min.x, max.x do
-- vi, voxel index, is a common variable name here
local vi = a:index(x, y, z)
if data[vi] == c_stone then
print("is stone!")
end
end
end
end
この理由は、コンピュータアーキテクチャのトピックに触れています。 RAM からの読み取りはかなりコストがかかるため、CPU には複数レベルのキャッシュがあります。プロセスが要求するデータがキャッシュにある場合、プロセスはそれを非常に迅速に取得できます。データがキャッシュにない場合、キャッシュミスが発生し、 RAM から必要なデータをフェッチします。要求されたデータを取り巻くデータもフェッチされ、キャッシュ内のデータが置き換えられます。これは、プロセスがその場所の近くのデータを再度要求する可能性が非常に高いためです。つまり、最適化の適切なルールは、データを次々に読み取る方法で反復し、キャッシュスラッシングを回避することです。
Writing Nodes
まず、データ配列に新しいコンテンツIDを設定する必要があります。
for z = min.z, max.z do
for y = min.y, max.y do
for x = min.x, max.x do
local vi = a:index(x, y, z)
if data[vi] == c_stone then
data[vi] = c_air
end
end
end
end
LVM でノードの設定が完了したら、データ配列をエンジンにアップロードする必要があります。
vm:set_data(data)
vm:write_to_map(true)
ライティングと param2 データを設定するには、適切な名前の set_light_data()
メソッドと set_param2_data()
メソッドを使用します。
write_to_map()
はブール値を取ります。これは、照明を計算する場合に当てはまります。 false を渡した場合は、後で minetest.fix_light
を使用して照明を再計算する必要があります。
Example
-- Get content IDs during load time, and store into a local
local c_dirt = minetest.get_content_id("default:dirt")
local c_grass = minetest.get_content_id("default:dirt_with_grass")
local function grass_to_dirt(pos1, pos2)
-- Read data into LVM
local vm = minetest.get_voxel_manip()
local emin, emax = vm:read_from_map(pos1, pos2)
local a = VoxelArea:new{
MinEdge = emin,
MaxEdge = emax
}
local data = vm:get_data()
-- Modify data
for z = pos1.z, pos2.z do
for y = pos1.y, pos2.y do
for x = pos1.x, pos2.x do
local vi = a:index(x, y, z)
if data[vi] == c_grass then
data[vi] = c_dirt
end
end
end
end
-- Write data
vm:set_data(data)
vm:write_to_map(true)
end
Your Turn
-
replace_in_area(from, to, pos1, pos2)
を作成します。これにより、指定された領域でfrom
のすべてのインスタンスがto
に置き換えられます。ここで、from
とto
はノード名です。 - すべての胸節を90°回転させる関数を作成します。
- LVM を使用して、苔むした丸石を近くの石や丸石のノードに広げる関数を作成します。あなたの実装は苔むした丸石を毎回 1 ノード分の距離以上に広げる原因になりますか?もしそうなら、どうすればこれを止めることができますか?
20 - Creating Games
Introduction
Minetest の力は、独自のボクセルグラフィックス、ボクセルアルゴリズム、または派手なネットワークコードを作成しなくても、ゲームを簡単に開発できることです。
- What is a Game?
- Game Directory
- Inter-game Compatibility
- Your Turn
What is a Game?
ゲームは、連携してまとまりのあるゲームを作成する mod のコレクションです。優れたゲームには、一貫した基本的なテーマと方向性があります。たとえば、サバイバル要素が難しい古典的なクラフターマイナーの場合もあれば、スチームパンクな自動化の美学を備えたスペースシミュレーションゲームの場合もあります。
ゲームデザインは複雑なトピックであり、実際には専門分野全体です。簡単に触れるだけでも、本の範囲を超えています。
Game design is a complex topic and is actually a whole field of expertise. It’s beyond the scope of the book to more than briefly touch on it.
Game Directory
ゲームの構造とロケーションは、mod を使用した後はかなり馴染みがあるように見えます。ゲームは、 minetest/games/foo_game
などのゲームのロケーションにあります。
foo_game
├── game.conf
├── menu
│ ├── header.png
│ ├── background.png
│ └── icon.png
├── minetest.conf
├── mods
│ └── ... mods
├── README.txt
└── settingtypes.txt
必要なのは mods フォルダーだけですが、 game.conf
と menu/icon.png
をお勧めします。
Inter-game Compatibility
API Compatibility
mod と別のゲームへの移植がより簡単になるため、 Minetest Game との API の互換性をできるだけ便利に保つようにすることをお勧めします。
別のゲームとの互換性を維持するための最良の方法は、同じ名前の mod との API の互換性を維持することです。つまり、 mod が別の mod と同じ名前を使用している場合、サードパーティであっても、互換性のある API が必要です。たとえば、ゲームに「 doors 」という mod が含まれている場合、 Minetest Game の「 doors 」と同じ API が必要です。
mod の API 互換性は、次の合計です。
- Lua API テーブル - 同じ名前を共有するグローバルテーブル内のすべての文書化/アドバタイズされた関数。たとえば、
mobs.register_mob
です。 - 登録されたノード/アイテム - アイテムの存在。
小さな破損は、実際には内部でのみ使用されるランダムなユーティリティ関数がないなど、それほど問題ありませんが、コア機能に関連する大きな破損はとてもよくないです。
Minetest Game の default のような嫌なメガ God-mod との API 互換性を維持することは困難です。その場合、ゲームに default という名前の mod を含めるべきではありません。
API の互換性は、他のサードパーティの mod やゲームにも適用されるため、新しい mod には一意の mod 名が付いていることを確認してください。 mod 名が使用されているかどうかを確認するには、content.minetest.netで mod 名を検索してください。
Groups and Aliases
グループとエイリアスはどちらも、ゲーム間の互換性を維持するのに役立つツールです。これにより、ゲームごとにアイテム名を変えることができます。石や木のような一般的なノードには、材料を示すグループが必要です。デフォルトノードから直接置換するエイリアスを提供することもお勧めします。
Your Turn
- プレイヤーが特別なブロックを掘ることでポイントを獲得する簡単なゲームを作成します。
21 - Common Mistakes
Introduction
この章では、よくある間違いとその回避方法について詳しく説明します。
- Never Store ObjectRefs (ie: players or entities)
- Don’t Trust Formspec Submissions
- Set ItemStacks After Changing Them
Never Store ObjectRefs (ie: players or entities)
ObjectRef が表すオブジェクトが削除された場合(たとえば、プレーヤーがオフラインになったり、エンティティがアンロードされたりした場合)、そのオブジェクトのメソッドを呼び出すとクラッシュします。
たとえば、こうしないでください。
minetest.register_on_joinplayer(function(player)
local function func()
local pos = player:get_pos() -- BAD!
-- `player` is stored then accessed later.
-- If the player leaves in that second, the server *will* crash.
end
minetest.after(1, func)
foobar[player:get_player_name()] = player
-- RISKY
-- It's not recommended to do this.
-- Use minetest.get_connected_players() and
-- minetest.get_player_by_name() instead.
end)
代わりにこうしてください:
minetest.register_on_joinplayer(function(player)
local function func(name)
-- Attempt to get the ref again
local player = minetest.get_player_by_name(name)
-- Check that the player is still online
if player then
-- Yay! This is fine
local pos = player:get_pos()
end
end
-- Pass the name into the function
minetest.after(1, func, player:get_player_name())
end)
Don’t Trust Formspec Submissions
悪意のあるクライアントは、好きなときに好きなコンテンツでフォームスペックを送信できます。
たとえば、次のコードには、プレーヤーが自分自身にモデレーター特権を与えることができる脆弱性があります。
local function show_formspec(name)
if not minetest.check_player_privs(name, { privs = true }) then
return false
end
minetest.show_formspec(name, "modman:modman", [[
size[3,2]
field[0,0;3,1;target;Name;]
button_exit[0,1;3,1;sub;Promote]
]])
return true
})
minetest.register_on_player_receive_fields(function(player,
formname, fields)
-- BAD! Missing privilege check here!
local privs = minetest.get_player_privs(fields.target)
privs.kick = true
privs.ban = true
minetest.set_player_privs(fields.target, privs)
return true
end)
これを解決するために特権チェックを追加します。
minetest.register_on_player_receive_fields(function(player,
formname, fields)
if not minetest.check_player_privs(name, { privs = true }) then
return false
end
-- code
end)
Set ItemStacks After Changing Them
「 InvRef 」のように、「 ItemStackRef 」ではなく、API では単に「 ItemStack 」と呼ばれていることに気づきましたか?これは、 ItemStack
が参照ではなくコピーであるためです。スタックは、インベントリ内のスタックではなく、データのコピーで機能します。つまり、スタックを変更しても、インベントリ内のそのスタックは実際には変更されません。
たとえば、これを行わないでください。
local inv = player:get_inventory()
local stack = inv:get_stack("main", 1)
stack:get_meta():set_string("description", "Partially eaten")
-- BAD! Modification will be lost
代わりにこれを行ってください:
local inv = player:get_inventory()
local stack = inv:get_stack("main", 1)
stack:get_meta():set_string("description", "Partially eaten")
inv:set_stack("main", 1, stack)
-- Correct! Item stack is set
コールバックの動作は少し複雑です。与えられた ItemStack
を変更すると、呼び出し元とその後のコールバックでも変更されます。ただし、コールバックの呼び出し元が設定した場合にのみ、エンジンに保存されます。
minetest.register_on_item_eat(function(hp_change, replace_with_item,
itemstack, user, pointed_thing)
itemstack:get_meta():set_string("description", "Partially eaten")
-- Almost correct! Data will be lost if another
-- callback cancels the behaviour
end)
コールバックがこれをキャンセルしない場合、スタックが設定され、説明が更新されますが、コールバックがこれをキャンセルすると、更新が失われる可能性があります。
代わりにこれを行うことをお勧めします。
minetest.register_on_item_eat(function(hp_change, replace_with_item,
itemstack, user, pointed_thing)
itemstack:get_meta():set_string("description", "Partially eaten")
user:get_inventory():set_stack("main", user:get_wield_index(),
itemstack)
-- Correct, description will always be set!
end)
コールバックがキャンセルされるか、コールバックランナーがスタックを設定しない場合でも、更新は設定されます。コールバックまたはコールバックランナーがスタックを設定する場合、 set_stack の使用は重要ではありません。
22 - Automatic Error Checking
Introduction
この章では、LuaCheck と呼ばれるツールを使用して、間違いがないか mod を自動的にスキャンする方法を学習します。このツールをエディターと組み合わせて使用すると、間違いを警告できます。
- Installing LuaCheck
- Running LuaCheck
- Configuring LuaCheck
- Using with editor
- Checking Commits with Travis
Installing LuaCheck
Windows
Githubリリースページから luacheck.exe をダウンロードするだけです。
Linux
まず、LuaRocks をインストールする必要があります。
sudo apt install luarocks
その後、LuaCheck をグローバルにインストールできます。
sudo luarocks install luacheck
次のコマンドでインストールされていることを確認します。
luacheck -v
Running LuaCheck
LuaCheck を初めて実行すると、多くの誤ったエラーが発生する可能性があります。これは、まだ構成する必要があるためです。
Windows では、プロジェクトのルートフォルダーにある powershell または bash を開き、 path\to\luacheck.exe .
を実行します。
Linux では、プロジェクトのルートフォルダで luacheck.
を実行します。
Configuring LuaCheck
プロジェクトのルートに .luacheckrc というファイルを作成します。これは、ゲーム、 modpack 、または mod のルートである可能性があります。
その中に次の内容を入れてください:
unused_args = false
allow_defined_top = true
globals = {
"minetest",
}
read_globals = {
string = {fields = {"split"}},
table = {fields = {"copy", "getn"}},
-- Builtin
"vector", "ItemStack",
"dump", "DIR_DELIM", "VoxelArea", "Settings",
-- MTG
"default", "sfinv", "creative",
}
次に、 LuaCheck を実行して動作することをテストする必要があります。今回はエラーが大幅に少なくなるはずです。発生した最初のエラーから始めて、コードを変更して問題を削除するか、コードが正しい場合は構成を変更します。以下のリストを参照してください。
Troubleshooting
- ** 未定義の変数foobar ** へのアクセス -
foobar
がグローバルであることが意図されている場合は、それをread_globals
に追加します。それ以外の場合は、不足しているlocal
を mod に追加します。 - ** 非標準のグローバル変数 foobar ** の設定 -
foobar
がグローバルであることを意図している場合は、それをglobals
に追加します。存在する場合は、read_globals
から削除します。それ以外の場合は、不足しているlocal
を mod に追加します。 - ** 読み取り専用グローバル変数foobar ** の変更 -
foobar
をread_globals
からglobals
に移動するか、 foobar への書き込みを停止します。
Using with editor
コマンドを実行せずにエラーを表示するために、選択したエディターのプラグインを見つけてインストールすることを強くお勧めします。ほとんどのエディターは、プラグインを利用できる可能性があります。
-
Atom -
linter-luacheck
。 -
VSCode - Ctrl + P、次に貼り付けます:
ext install dwenegar.vscode-luacheck
- Sublime - package-control を使用してインストール:SublimeLinter、SublimeLinter-luacheck 。
Checking Commits with Travis
プロジェクトが公開されており、 Github 上にある場合は、 TravisCI を使用できます。これは無料のサービスで、コミット時にジョブを実行してチェックします。これは、プッシュするすべてのコミットが LuaCheck に対してチェックされ、 LuaCheck が間違いを検出したかどうかに応じて、それらの横に緑色のチェックマークまたは赤い十字が表示されることを意味します。これは、プロジェクトがプルリクエストを受信した場合に特に役立ちます。コードをダウンロードしなくても、 LuaCheck の出力を確認できます。
まず、travis-ci.orgにアクセスし、 Github アカウントでサインインする必要があります。次に、 Travis プロファイルでプロジェクトのリポジトリを見つけ、スイッチを切り替えて Travis を有効にします。
次に、次の内容の .travis.yml というファイルを作成します。
language: generic
sudo: false
addons:
apt:
packages:
- luarocks
before_install:
- luarocks install --local luacheck
script:
- $HOME/.luarocks/bin/luacheck .
notifications:
email: false
プロジェクトが mod や modpack ではなくゲームの場合は、 script:
の後の行を次のように変更します。
- $HOME/.luarocks/bin/luacheck mods/
次に、コミットして Github にプッシュします。 Github でプロジェクトのページに移動し、[コミット]をクリックします。行ったコミットの横にオレンジ色のディスクが表示されます。しばらくすると、 LuaCheck の結果に応じて、緑色のチェックマークまたは赤い十字のいずれかに変わるはずです。いずれの場合も、アイコンをクリックして、ビルドログと LuaCheck の出力を確認できます。
23 - Security
Introduction
mod によってサーバーの所有者がデータや制御を失うことがないようにするためには、セキュリティが非常に重要です。
- Core Concepts
- Formspecs
- (Insecure) Environments
Core Concepts
セキュリティの最も重要な概念は、ユーザーを決して信頼しない ことです。ユーザーが送信するものはすべて悪意のあるものとして扱われる必要があります。つまり、入力した情報が有効であること、ユーザーが正しい権限を持っていること、その他の方法でそのアクションを実行できること(つまり、レンジ内または所有者)を常に確認する必要があります。
悪意のあるアクションは必ずしもデータの変更や破壊ではありませんが、パスワードハッシュやプライベートメッセージなどの機密データにアクセスする可能性があります。サーバーが電子メールや年齢などの情報を保存している場合、これは特に悪いことです。これは検証目的で行われる場合があります。
Formspecs
Never Trust Submissions
すべてのユーザーは、いつでも任意の値でほぼすべてのフォームスペックを送信できます。
mod で見つかった実際のコードは次のとおりです。
minetest.register_on_player_receive_fields(function(player,
formname, fields)
for key, field in pairs(fields) do
local x,y,z = string.match(key,
"goto_([%d-]+)_([%d-]+)_([%d-]+)")
if x and y and z then
player:set_pos({ x=tonumber(x), y=tonumber(y),
z=tonumber(z) })
return true
end
end
end
問題を見つけることができますか?悪意のあるユーザーは、自分の位置の値を含む formspec を送信して、好きな場所にテレポートできるようにする可能性があります。これは、クライアントの変更を使用して自動化することもでき、特権を必要とせずに「/teleport」コマンドを本質的に複製できます。
この種の問題の解決策は、 Formspecs の章で前述したように、Contextを使用することです。
Time of Check isn’t Time of Use
エンジンで禁止されている場合を除き、すべてのユーザーはいつでも任意の値で任意の formspec を送信できます。
- ユーザーが離れすぎている場合、ノード formspec の送信はブロックされます。
- 5.0 以降、名前付き formspec は、まだ表示されていない場合はブロックされます。
これは、ユーザーが最初に formspec を表示するための条件と、対応するアクションを満たしていることをハンドラーで確認する必要があることを意味します。
handle formspec ではなく showformspec でアクセス許可をチェックすることによって引き起こされる脆弱性は、Time Of Check is not Time Of Use (TOCTOU) と呼ばれます。
(Insecure) Environments
Minetest を使用すると、 mod はサンドボックス化されていない環境を要求でき、 Lua API 全体にアクセスできます。
次の脆弱性を見つけることができますか?
local ie = minetest.request_insecure_environment()
ie.os.execute(("path/to/prog %d"):format(3))
string.format
は、グローバル共有テーブル string
の関数です。悪意のある mod が関数をオーバーライドし、 os.execute にデータを渡す可能性があります。
string.format = function()
return "xdg-open 'http://example.com'"
end
mod は、リモートユーザーにマシンの制御を与えるなど、Web サイトを開くよりもはるかに悪意のあるものを渡す可能性があります。
安全でない環境を使用するためのいくつかのルール:
- 常にローカルに保存し、関数に渡さないでください。
- 上記の問題を回避するために、安全でない関数に与えられた入力を信頼できることを確認してください。これは、グローバルに再定義可能な関数を回避することを意味します。
24 - Intro to Clean Architectures
Introduction
mod が適切なサイズに達すると、コードをクリーンでバグのない状態に保つことがますます難しくなります。これは、Lua のような動的に型付けされた言語を使用する場合に特に大きな問題です。これは、型が正しく使用されていることを確認する場合など、コンパイラーがコンパイラー時のヘルプをほとんど提供しないためです。
この章では、コードをクリーンに保つために必要な重要な概念と、それを実現するための一般的なデザインパターンについて説明します。この章は規範的なものではなく、可能性についてのアイデアを提供することを目的としていることに注意してください。 mod を設計する良い方法は1つではなく、良い mod の設計は非常に主観的です。
- Cohesion, Coupling, and Separation of Concerns
- Observer
- Model-View-Controller
- Conclusion
Cohesion, Coupling, and Separation of Concerns
計画がなければ、プログラミングプロジェクトは徐々にスパゲッティコードに陥る傾向があります。スパゲッティコードは構造の欠如を特徴としています-すべてのコードは明確な境界なしで一緒にスローされます。これにより、最終的にプロジェクトは完全に保守不可能になり、放棄されてしまいます。
これの反対は、相互作用する小さなプログラムまたはコードの領域のコレクションとしてプロジェクトを設計することです。
すべての大きなプログラムの中には、抜け出そうとする小さなプログラムがあります。
–C.A.R. Hoare
これは、関心の分離を達成するような方法で行う必要があります。各領域は別個のものであり、個別のニーズまたは懸念に対処する必要があります。
This should be done in such a way that you achieve Separation of Concerns - each area should be distinct and address a separate need or concern.
これらのプログラム/エリアには、次の 2 つのプロパティが必要です。
-
高凝集性 - 領域は密接に/密接に関連している必要があります。
-
低結合 - 領域間の依存関係を可能な限り低く保ち、内部実装に依存しないようにします。カップリングの量が少ないことを確認することをお勧めします。これは、特定の領域の API を変更することがより実現可能になることを意味します。
-
High Cohesion - the area should be closely/tightly related.
-
Low Coupling - keep dependencies between areas as low as possible, and avoid relying on internal implementations. It’s a very good idea to make sure you have a low amount of coupling, as this means that changing the APIs of certain areas will be more feasible.
これらは、mod 間の関係と、mod 内の領域間の関係の両方に当てはまることに注意してください。
Observer
コードのさまざまな領域を分離する簡単な方法は、 Observer パターンを使用することです。
プレイヤーが最初に珍しい動物を殺したときにアチーブメントのロックを解除する例を見てみましょう。ナイーブなアプローチは、 mob kill 関数にアチーブメントコードを入れ、 mob 名をチェックし、一致する場合は賞のロックを解除することです。ただし、これは mobs mod をアチーブメントコードに結合させるため、悪い考えです。これを続けた場合(たとえば、mob death コードに XP を追加した場合)、多くの厄介な依存関係が発生する可能性があります。
オブザーバーパターンを入力します。賞を気にする mymobsmod の代わりに、 mymobs mod は、コードの他の領域がイベントへの関心を登録し、イベントに関するデータを受信する方法を公開します。
mymobs.registered_on_death = {}
function mymobs.register_on_death(func)
table.insert(mymobs.registered_on_death, func)
end
-- in mob death code
for i=1, #mymobs.registered_on_death do
mymobs.registered_on_death[i](entity, reason)
end
次に、他のコードがその関心を登録します。
mymobs.register_on_death(function(mob, reason)
if reason.type == "punch" and reason.object and
reason.object:is_player() then
awards.notify_mob_kill(reason.object, mob.name)
end
end)
あなたは考えているかもしれません - ちょっと待って、これはひどく馴染みがあるように見えます。そして、あなたは正しい! Minetest API はオブザーバーベースであり、エンジンが何かをリッスンしていることを気にする必要がなくなります。
Model-View-Controller
次の章では、コードを自動的にテストする方法について説明します。問題の1つは、ロジック(計算、実行する必要があること)を API 呼び出し( minetest.*
、その他の mod )から可能な限り分離する方法です。
これを行う1つの方法は、次のことを考えることです。
- あなたが持っている データ。
- このデータで実行できる アクション。
- イベント (つまり、 formspec 、 punchs など)がこれらのアクションをトリガーする方法、およびこれらのアクションがエンジンで発生する方法。
土地保護 mod の例を見てみましょう。あなたが持っているデータは、エリアと関連するメタデータです。実行できるアクションは、 create
、 edit
、または delete
です。これらのアクションをトリガーするイベントは、チャットコマンドと formspec 受信フィールドです。これらは通常、かなりうまく分離できる 3 つの領域です。
テストでは、トリガーされたときのアクションがデータに対して正しいことを行うことを確認できます。イベントがアクションを呼び出すことをテストする必要はありません(これには Minetest API を使用する必要があり、コードのこの領域はとにかくできるだけ小さくする必要があります)。
Pure Lua を使用してデータ表現を作成する必要があります。このコンテキストでの「 Pure 」とは、関数が Minetest の外部で実行される可能性があることを意味します。つまり、エンジンの関数は呼び出されません。
-- Data
function land.create(name, area_name)
land.lands[area_name] = {
name = area_name,
owner = name,
-- more stuff
}
end
function land.get_by_name(area_name)
return land.lands[area_name]
end
あなたの行動も pure でなければなりませんが、他の関数を呼び出すことは上記よりも受け入れられます。
-- Controller
function land.handle_create_submit(name, area_name)
-- process stuff
-- (ie: check for overlaps, check quotas, check permissions)
land.create(name, area_name)
end
function land.handle_creation_request(name)
-- This is a bad example, as explained later
land.show_create_formspec(name)
end
イベントハンドラーは Minetest API と対話する必要があります。この領域を簡単にテストすることはできないため、計算の数を最小限に抑える必要があります。
-- View
function land.show_create_formspec(name)
-- Note how there's no complex calculations here!
return [[
size[4,3]
label[1,0;This is an example]
field[0,1;3,1;area_name;]
button_exit[0,2;1,1;exit;Exit]
]]
end
minetest.register_chatcommand("/land", {
privs = { land = true },
func = function(name)
land.handle_creation_request(name)
end,
})
minetest.register_on_player_receive_fields(function(player,
formname, fields)
land.handle_create_submit(player:get_player_name(),
fields.area_name)
end)
上記は Model-View-Controller パターンです。モデルは、最小限の機能を備えたデータのコレクションです。ビューは、イベントをリッスンしてコントローラーに渡す関数のコレクションであり、コントローラーから呼び出しを受信して、 Minetest API で何かを実行します。コントローラーは、決定とほとんどの計算が行われる場所です。
コントローラーは MinetestAPI についての知識を持っていないはずです - Minetest 呼び出しまたはそれらに類似したビュー関数がないことに注意してください。 view.hud_add(player、def)
のような関数は持ってはいけません。代わりに、ビューは、 view.add_hud(info)
のように、コントローラーがビューに実行するように指示できるいくつかのアクションを定義します。ここで、 info は、 Minetest API にまったく関係のない値またはテーブルです。
エリアの内部または外部を変更する場合に変更する必要のある量を減らすために、上記のように、各エリアが直接隣接するエリアとのみ通信することが重要です。たとえば、 formspec を変更するには、ビューを編集するだけで済みます。ビュー API を変更するには、ビューとコントローラーを変更するだけで、モデルはまったく変更できません。
実際には、この設計は複雑さが増し、ほとんどの種類の mod に多くの利点がないため、ほとんど使用されません。代わりに、一般的に、それほど形式的で厳密ではない種類のデザイン( API-View のバリアント)が表示されます。
API-View
理想的な世界では、通常のビューに戻る前に、上記の 3 つの領域が完全に分離され、すべてのイベントがコントローラーに送られます。しかし、これは現実の世界ではありません。良い妥協案は、 mod を 2 つの部分に減らすことです。
-
API - これは上記のモデルとコントローラーでした。ここでは
minetest.
を使用しないでください。 - ビュー - これも上記のビューでした。これをイベントの種類ごとに別々のファイルに構造化することをお勧めします。
rubenwardy のcraftingmodは、おおまかにこの設計に従います。 api.lua
は、データストレージとコントローラースタイルの計算を処理するほとんどすべての pure な Lua 関数です。 gui.lua
は formspecs と formspec 送信のビューであり、async_crafter.lua
はノード formspec とノードタイマーのビューとコントローラーです。
このように mod を分離すると、次の章に示すように、Minetest APIを使用しないため、API 部分を非常に簡単にテストできます。そして craftingmod で見られます。
Conclusion
優れたコード設計は主観的なものであり、作成するプロジェクトに大きく依存します。原則として、凝集力を高く、結合度を低く保つようにしてください。別の言い方をすれば、関連するコードをまとめ、関連しないコードを分離し、依存関係を単純に保ちます。
ゲームプログラミングパターンの本を読むことを強くお勧めします。
オンラインで読むから無料で入手でき、ゲームに関連する一般的なプログラミングパターンについて詳しく説明しています。
26 - Releasing a Mod
Introduction
mod をリリースまたは公開すると、他の人がそれを利用できるようになります。 mod がリリースされると、シングルプレイヤーゲームや、パブリックサーバーを含むサーバーで使用される可能性があります。
- ライセンスの選択
- パッケージング
- アップロード
- フォーラムトピック
License Choices
mod のライセンスを指定する必要があります。これは、他の人にあなたの作品の使用を許可する方法を伝えるため、重要です。 mod にライセンスがない場合、パブリックサーバーで mod を変更、配布、または使用することが許可されているかどうかはわかりません。
あなたのコードとあなたのアートは、彼らが使用するライセンスとは異なるものを必要とします。たとえば、クリエイティブコモンズライセンスはソースコードと一緒に使用するべきではありませんが、画像、テキスト、メッシュなどの芸術作品に適した選択肢となる可能性があります。
すべてのライセンスが許可されます。ただし、デリバティブを許可しない mod は、公式の Minetest フォーラムから禁止されています。 ( mod をフォーラムで許可するには、他の開発者が mod を変更して、変更されたバージョンをリリースできる必要があります。)
定義は国によって異なるため、パブリックドメインは有効なライセンスではないことに注意してください。
LGPL and CC-BY-SA
これは Minetest コミュニティで一般的なライセンスの組み合わせであり、 Minetest と Minetest Game が使用するものです。コードは LGPL2.1
でライセンスされ、アートは CC-BY-SA
でライセンスされます。この意味は:
- 誰でも、変更されたバージョンまたは変更されていないバージョンを変更、再配布、および販売できます。
- 誰かがあなたの mod を変更する場合、彼らは彼らのバージョンに同じライセンスを与える必要があります。
- 著作権表示を保持する必要があります。
CC0
これらのライセンスにより、誰でも mod を使ってやりたいことができるようになります。つまり、帰属を変更、再配布、販売、または除外することができます。これらのライセンスは、コードとアートの両方に使用できます。
WTFPL は強く推奨されておらず、このライセンスを持っている場合、人々はあなたの mod を使用しないことを選択する可能性があることに注意することが重要です。
MIT
これは mod コードの一般的なライセンスです。 mod のユーザーに課せられる唯一の制限は、 mod または mod の実質的な部分のコピーに同じ著作権表示とライセンスを含める必要があることです。
Packaging
リリースする前に mod に含めることをお勧めするファイルがいくつかあります。
README.txt
README ファイルには次のように記載する必要があります。
- mod の機能。
- ライセンスとは何ですか。
- どのような依存関係がありますか。
- mod のインストール方法。
- mod の現在のバージョン。
- オプションで、問題を報告したり、ヘルプを入手したりする場所。
description.txt
これはあなたの mod が何をするかを説明するはずです。曖昧にならずに簡潔にしてください。スペースに限りがあるコンテンツインストーラーに表示されるため、短くする必要があります。
良い手本:
Adds soup, cakes, bakes and juices.
これは避けてください:
(BAD) The food mod for Minetest.
screenshot.png
スクリーンショットは 3:2(高さ 2 ピクセルごとに幅 3 ピクセル)で、最小サイズは 300px x 200px である必要があります。
スクリーンショットは mod ストアに表示されます。
Uploading
潜在的なユーザーがあなたの mod をダウンロードできるように、あなたはそれを公的にアクセス可能な場所にアップロードする必要があります。これを行うにはいくつかの方法がありますが、これらの要件、およびフォーラムのモデレーターによって追加される可能性のあるその他の要件を満たしている限り、自分に最適なアプローチを使用する必要があります。
- 安定 - ホスティングウェブサイトが警告なしにシャットダウンする可能性はほとんどありません。
- 直接リンク - フォーラムのリンクをクリックして、別のページを表示せずにファイルをダウンロードできるはずです。
- ウイルスフリー - 悪意のあるコンテンツを含む mod はフォーラムから削除されます。
Version Control Systems
次のようなバージョン管理システムを使用することをお勧めします。
- 他の開発者が簡単に変更を送信できるようにします。
- ダウンロードする前にコードをプレビューできるようにします。
- ユーザーがバグレポートを送信できるようにします。
Minetest の改造者の大多数は、コードをホストする Web サイトとして GitHub を使用していますが、別の方法も可能です。
GitHub の使用は、最初は難しい場合があります。これについてサポートが必要な場合、 GitHub の使用方法については、以下を参照してください。
- Pro Gitブック - オンラインで無料で読むことができます。
- GitHub for Windowsアプリ - Windows のグラフィカルインターフェイスを使用してコードをアップロードします。
Forum Attachments
バージョン管理システムを使用する代わりに、フォーラムの添付ファイルを使用して mod を共有できます。これは、 mod のフォーラムトピック(以下で説明)を作成するときに実行できます。
mod のファイルを1つのファイルに圧縮する必要があります。これを行う方法は、オペレーティングシステムによって異なります。これはほとんどの場合、すべてのファイルを選択した後、右クリックメニューを使用して行われます。
フォーラムのトピックを作成するときは、「 Create a Topic トピックの作成」ページ(以下を参照)で、下部にある「 Upload Attachment 添付ファイルのアップロード」タブに移動します。 「 Browse 参照」をクリックして、 zip ファイルを選択します。コメントフィールドに mod のバージョンを入力することをお勧めします。
Forum Topic
これで、フォーラムトピックを作成できます。 “WIP Mods”(作業中)フォーラムで作成する必要があります。
mod が進行中の作業であると見なされなくなったら、移動をリクエストに「 Mod Releases 」できます。
フォーラムのトピックには、 README と同様のコンテンツが含まれている必要がありますが、より宣伝的で、 mod をダウンロードするためのリンクも含まれている必要があります。可能であれば、実際の mod のスクリーンショットを含めることをお勧めします。
Minetest フォーラムでは、フォーマットに bbcode を使用しています。 superspecial という名前の mod の例を次に示します。
Adds magic, rainbows and other special things.
See download attached.
[b]Version:[/b] 1.1
[b]License:[/b] LGPL 2.1 or later
Dependencies: default mod (found in minetest_game)
Report bugs or request help on the forum topic.
[h]Installation[/h]
Unzip the archive, rename the folder to superspecial and
place it in minetest/mods/
( GNU/Linux: If you use a system-wide installation place
it in ~/.minetest/mods/. )
( If you only want this to be used in a single world, place
the folder in worldmods/ in your world directory. )
For further information or help see:
[url]https://wiki.minetest.net/Installing_Mods[/url]
上記の例を mod トピック用に変更する場合は、「 superspecial 」を mod の名前に変更することを忘れないでください。
Subject
トピックの主題は、次のいずれかの形式である必要があります。
- [Mod] Mod Title [modname]
- [Mod] Mod Title [バージョン番号] [modname]
例えば:
- [Mod] More Blox [0.1] [moreblox]
27 - Read More
List of Resources
この本を読んだら、以下を見てください。
Minetest Modding
- Minetest の Lua API リファレンス - HTMLバージョン | テキストバージョン
- Developer Wikiを調べてください。
- existing modsを見てください。
Lua Programming
3D Modelling
Lua Modding API
ダウンロード例
© 2014-20 | Helpful? Consider donating to support my work.