Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Minetest Modding Book ( ͡° ͜ʖ ͡°) (Lua)

この本についてのこと

これは 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

間違いに気づきましたか、それともフィードバックを送りたいですか?ぜひ連絡してください。

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 の場所が順番にチェックされます。:

  1. Game mods これらは、world が実行しているゲームを形成する mods です。例: minetest/games/minetest_game/mods//usr/share/minetest/games/minetest/
  2. Global mods 、ほとんどの場合 mods がインストールされる場所。疑わしい場合は、ここに配置してください。例: minetest/mods/
  3. World mods 、特定のワールドに固有の mods を保存する場所。例: minetest/worlds/world/worldmods/

Minetest は、上記の順序で場所を確認します。以前に見つかったものと同じ名前の mod が見つかった場合、前の mod の代わりに後の mod がロードされます。これは、global mod のロケーションに同じ名前の mod を配置することで、game mod をオーバーライド(上書き)できることを意味します。

各 mod ロードパスの実際の場所は、使用しているオペレーティングシステムと、Minetest のインストール方法によって異なります。

  • Windows:
    • ポータブルビルドの場合、例:あなたが .zip ファイルから、抽出したディレクトリに移動します。 gamesmods および 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

Find the mod's 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 AD = nil
Number 整数または10進数。 local A = 4
String テキストの一部 local D = "one two three"
Boolean True or False local is_true = falselocal 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?

ノード、クラフトアイテム、ツールはすべてアイテムです。アイテムは、通常のゲームプレイでは不可能な場合でも、インベントリ内で見つけることができるものです。

ノードは、世界に配置されているか、または発見することができるアイテムです。世界のすべての位置空間は、たった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 → tofromがエイリアスで、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 を使用する場合、鉛筆ツールはツールボックスから選択できます。

Pencil in 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

Normal Drawtype

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

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'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

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

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

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 に似ています。

Firelike nodes

Firelike nodes

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?

ItemStack は、インベントリ内の単一セルの背後にあるデータです。

inventoryinventory 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 - ノード定義を参照してください。

ブロックが非アクティブの場合、関数は含まれているブロックをロードせず、代わりに nameignore のテーブルを返すことに注意してください。代わりに 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_metafind_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_tablefrom_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_punchminetest.set_node を使います)

10 - Objects, Players, and Entities

Introduction


この章では、オブジェクトを操作する方法と、オブジェクトを定義する方法を学習します。

What are Objects, Players, and Entities?

プレーヤーとエンティティはどちらもオブジェクトのタイプです。オブジェクトは、ノードグリッドとは独立して移動できるものであり、速度やスケールなどのプロパティがあります。オブジェクトはアイテムではなく、独自の個別の登録システムがあります。

プレイヤーとエンティティの間にはいくつかの違いがあります。最大のものは、プレイヤーがプレイヤーによって制御されるのに対し、エンティティは mod によって制御されることです。これは、プレイヤーの速度を mod で設定できないことを意味します。プレイヤーはクライアント側であり、エンティティはサーバー側です。もう1つの違いは、プレーヤーによってマップブロックが読み込まれるのに対し、エンティティは保存されて非アクティブになることです。

この区別は、後で説明するように、エンティティが Lua エンティティと呼ばれるテーブルを使用して制御されるという事実によって混乱しています。

Position and Velocity

get_posset_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_joinplayerset_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

特権はプレイヤーに何かをする能力を与えるべきです。クラスまたはステータスを示すための特権ではありません

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

ゲーム内のすべてのプレーヤーにメッセージを送信するには、 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 :teamnamejoin です。スペースも端子としてカウントされます。

変数は、ユーザーの入力内容に応じて値を変更できます。たとえば、 :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 つの方法があります。

  1. ChatCmdBuilder を mod としてインストールし、それに依存します。
  2. 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

Furnace InventoryScreenshot of furnace formspec, labelled.

この章では、 formspec を作成してユーザーに表示する方法を学習します。 formspec は、フォームの仕様コードです。 Minetest では、フォームはプレーヤーインベントリなどのウィンドウであり、ラベル、ボタン、フィールドなどのさまざまな要素を含めることができます。

プレーヤーに情報を提供するだけでよい場合など、ユーザー入力を取得する必要がない場合は、formes ではなくヘッドアップディスプレイ(HUD)の要素の使用を検討する必要があることに注意してください。予期しないウィンドウはゲームプレイを混乱させる傾向があるためです。

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

Guessing FormspecThe guessing game formspec.

学ぶための最良の方法は何かを作ることですので、推測ゲームを作りましょう。原理は単純です。 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

Padding and spacingThe guessing game formspec.

パディングは、 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

Position and Offset

Diagram showing a centered text element

画面にはさまざまな物理的サイズと解像度があり、HUDはすべての画面タイプで適切に機能する必要があります。

これに対する Minetest の解決策は、パーセンテージ位置とオフセットの両方を使用して要素の位置を指定することです。パーセンテージの位置は画面サイズを基準にしているため、要素を画面の中央に配置するには、画面の半分のパーセンテージの位置を指定する必要があります。 (50%, 50%) 、および (0, 0) のオフセット。

次に、オフセットを使用して、パーセント位置を基準にして要素を移動します。

Alignment

位置合わせは、位置とオフセットの結果が要素上にある場所です。たとえば、 {x = -1.0, y = 0.0} は、位置とオフセットの結果を要素の境界の左側に配置します。これは、テキスト要素を左、中央、または右に揃える場合に特に便利です。

Diagram showing alignment

上の図は、3つのウィンドウ(青)を示しています。各ウィンドウには、 1 つの HUD 要素(黄色)があり、毎回異なる配置になっています。矢印は、位置とオフセットの計算結果です。

Scoreboard

この章では、次のようにスコアパネルを配置および更新する方法を学習します。

screenshot of the HUD we're aiming for

上のスクリーンショットでは、すべての要素の位置が同じパーセンテージ (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,
})

これにより、次のようになります。

screenshot of the HUD we're aiming for

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 },
})

これで、次のようになります。

screenshot of the HUD so far

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

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,
})

上記のコードについては何も新しいことはありません。すべての概念は、上記および前の章で説明されています。

Player Admin Page

Receiving events

sfinv 定義に on_player_receive_fields 関数を追加することで、 formspec イベントを受け取ることができます。

on_player_receive_fields = function(self, player, context, fields)
    -- TODO: implement this
end,

on_player_receive_fieldsminetest.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?

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

VernoiVoronoi 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などのプログラムを使ってボロノイ図を作成することもできます。

  1. ツールバーのポイントツール(アイコンは「 A 」の付いたポイント)を選択し、チャートをクリックしてポイントを作成します。ポイントをドラッグしたり、左側のサイドバーで明示的に位置を設定したりできます。また、物事を明確にするために、各ポイントにラベルを付ける必要があります。

  2. 次に、左側のサイドバーの入力ボックスに次の関数を入力して、ボロノイを作成します。

   Voronoi({ A, B, C, D, E })

各ポイントが中括弧の内側にあり、コンマで区切られている場合。あなたは今すべきです。

  1. やったぁ!これで、ドラッグ可能なすべてのポイントを含むボロノイ図が作成されます。

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 = 1y = 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 は指示された正確な領域を読み取ることはほとんどありません。代わりに、より広い領域を読み取る可能性があります。より大きな領域は eminemax で与えられ、これらは emerged min posemerged 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 とライティングデータを取得できます。

上記のメソッドで指定されたフラット配列のどこにノードがあるかを調べるには、 eminemax を使用する必要があります。計算を処理する 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 に置き換えられます。ここで、fromto はノード名です。
  • すべての胸節を90°回転させる関数を作成します。
  • LVM を使用して、苔むした丸石を近くの石や丸石のノードに広げる関数を作成します。あなたの実装は苔むした丸石を毎回 1 ノード分の距離以上に広げる原因になりますか?もしそうなら、どうすればこれを止めることができますか?

20 - Creating Games

Introduction

Minetest の力は、独自のボクセルグラフィックス、ボクセルアルゴリズム、または派手なネットワークコードを作成しなくても、ゲームを簡単に開発できることです。

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.confmenu/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)

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

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 ** の変更 - foobarread_globals から globalsに移動するか、 foobar への書き込みを停止します。

Using with editor

コマンドを実行せずにエラーを表示するために、選択したエディターのプラグインを見つけてインストールすることを強くお勧めします。ほとんどのエディターは、プラグインを利用できる可能性があります。

  • Atom - linter-luacheck
  • VSCode - Ctrl + P、次に貼り付けます: ext install dwenegar.vscode-luacheck
  • Sublime - package-control を使用してインストール:SublimeLinterSublimeLinter-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

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

計画がなければ、プログラミングプロジェクトは徐々にスパゲッティコードに陥る傾向があります。スパゲッティコードは構造の欠如を特徴としています-すべてのコードは明確な境界なしで一緒にスローされます。これにより、最終的にプロジェクトは完全に保守不可能になり、放棄されてしまいます。

これの反対は、相互作用する小さなプログラムまたはコードの領域のコレクションとしてプロジェクトを設計することです。

すべての大きなプログラムの中には、抜け出そうとする小さなプログラムがあります。

–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 の例を見てみましょう。あなたが持っているデータは、エリアと関連するメタデータです。実行できるアクションは、 createedit 、または 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 にまったく関係のない値またはテーブルです。

Diagram showing a centered text element

エリアの内部または外部を変更する場合に変更する必要のある量を減らすために、上記のように、各エリアが直接隣接するエリアとのみ通信することが重要です。たとえば、 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 のバージョンを入力することをお勧めします。

Upload AttachmentUpload Attachment tab.

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

Lua Programming

3D Modelling

Lua Modding API

Lua Modding APIリファレンス

ダウンロード例

© 2014-20 | Helpful? Consider donating to support my work.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away