さて... また最近tModLoaderの熱が上がり、Mod公開までしてしまった。
最近少しModのネタが減ってきたので、いったんここ(Qiita)で少々難解な WorldGen
について備忘録程度に書いていこうと思う。
余談だが、私は何の資格も持ち合わせていない趣味プログラマだ。
専門的なことや、記事を書くための国語力はないことに注意していただきたい。
この記事は、https://github.com/tModLoader/tModLoader/wiki/World-Generation を基に書いている。
Terrariaのテクスチャなどを載せることができないため、分かりづらかったり、説明が不十分だと思ったらこれを見るといい...ただし英語。
参考記事
もし英語でもいいのであれば 一部1.3用のコードや解説もあるものの
- https://github.com/tModLoader/tModLoader/wiki/World-Generation
- https://forums.terraria.org/index.php?threads/modding-tutorial-world-generation.47601/
- https://forums.terraria.org/index.php?threads/world-gen-tutorial.50026/
を見るといい。ここの記事より役に立つ...かもしれない。
前提知識
まず、生成コードの説明の前に、Terrariaのタイル(=ブロック)や壁(ウォール)がどのように機能しているのか、どのような変数があるのか、など知っておく必要がある。
もし、この記事を読んでいる君が、鉱石作れた!よし、鉄とか鉛とかみたいに世界に散らばらせてみたいな~ 程度であれば、ここを飛ばして、実例を見るといい。
そこにあるコードを持っていくといい...
私も最初は おまじない として書いていたからね。
タイルの位置や世界の限界
世界の各タイルのデータを格納している変数があり、それはMain.tile[,]
である。
Main.tile[,]
は二次元配列であり、Main.tile[x, y]
のように使う。値はTile
構造体である。Tile
構造体に関しては後述する。
Terrariaの世界では、左上端が0, 0
となっている。各ワールドの大きさがあるが、右下端(X座標、Y座標共に最大になる位置)はMain.maxTilesX
とMain.maxTilesY
で取得できる。
Y座標のみ数学(のグラフ)と逆転しているので注意。 (Y座標の値が大きいほど下を表す)
世界の全ブロックを調べたかったら、次のようなコードを書く。
//using Terraria;
for (var i = 0; i < Main.maxTilesX; i++)
{
for (var j = 0; j < Main.maxTilesY; j++)
{
Tile tile = Main.tile[i, j]; // Terrariaでは x, y ではなく i, j がよく使われがち
// if (tile.HasTile && tile.TileType == ...
}
}
Main.tile[,]
のインデックスは、0
~Main.maxTilesX
と0
~Main.maxTilesY
までであり、負の値や、Main.maxTiles
を越える値は配列の範囲外になるので注意。
範囲外の対策として、if
を使ってももちろんいいが、Terrariaにはデフォルトで便利なものがある。
それはFraming.Framing.GetTileSafely(int i, int j)
。これは範囲外を指定した場合、default(Tile)
を返し、エラーを防ぐメソッドである。エラーが出たからなどと、むやみやたらに使うものではないが、有用なので活用するべし。
一応、WorldGen.InWorld
という物もあるが、ここでは割愛。
Tile
構造体
Terrariaの世界のタイルや壁のデータは全て、Tile
構造体で表される。
ここでは、使うことが多いメンバを紹介する。
-
tile.HasTile
(bool
) - タイルが設置されているか(=空気ではないこと)を表す。 -
tile.TileType
(ushort
) - タイルの種類を表す。タイルの種類を表す値はTileID
にある。 -
tile.WallType
(ushort
) - 壁の種類を表す。壁の種類を表す値はWallID
にある。 -
tile.LiquidAmount
(byte
) - 液体の量を表す。0
が空、255
で満タン。 -
tile.LiquidType
(int
) - 液体の種類を表す。液体の種類を表す値はLiquidID
にある。 -
tile.TileFrameX
/tile.TileFrameY
(short
) - タイルのフレームを表す。
TileID
を使ったことのある人なら分かると思うが、
TileID
には、ItemID
やWallID
のようなTileID.None
がない(=タイルがないことを表す値がない)。
そのため、タイルがあるかどうかを調べるときは、Tile.HasTile
を使用する。
//using Terraria;
//using Terraria.ModLoader;
Tile tile = Main.tile[i, j];
if (tile.HasTile && tile.TileType == TileID.Stone) // タイルが設置されている かつ タイルが石ブロックである
{
tile.TileType = (ushort)ModContent.TileType<MyMod.Tiles.MyTile>(); //石ブロックを自作のタイルに置き換えるってだけ
// タイルの置き換えを行ったことがある人なら、ここで
// WorldGen.SquareTileFrame(i, j);
// とか
// NetMessage.SendTileSquare(-1, i, j);
// を書くかもしれないが、**ワールドの生成では**不要。
}
ほとんどの場合、Tile.HasTile
の確認は必須である。
壁の確認の場合は、壁が設置していない場合、tile.WallType
が0
(WallID.None
)になるので、この確認方法はタイルのみのものである。
タイルの種類
ModTile
でタイルを作った人ならわかるかもしれない。
タイルは2種類あり、Framed
とFrameImportant
(これらの名称は tModLoader wiki より)である。
Framed
は簡単に言えば、ブロック。土ブロックとか石ブロックとかの地形のメインになるタイルである。
対し、FrameImportant
はワークベンチやチェスト、延べ棒(インゴット)のような、作業台になったり何かしらの機能を持っていたりなどの特殊なタイルである。
大体の見分け方は簡単で、1x1(通常のブロック)より大きいものは全てFrameImportant
である。
厳密には、1x1のものもあり、延べ棒(インゴット)や論理ゲート(ロジックゲート)などもFrameImportant
である。
Framed
タイルは、隣接しているタイルに合わせてテクスチャが変化するタイルである。
対し、FrameImportant
タイルは(特段設定されていない限り)テクスチャが固定されている。
Framed
タイルの方が難しそうに聞こえるかもしれないが、実際はFrameImportant
タイルの方がめんどくさい。
Frmaed
タイルは、テクスチャの描き方が共通であるため、タイルを置き換えても問題なく、
また、1x1ブロックのサイズであるため、コードで設置することも簡単である。
どのフレームを使うかなど面倒な処理は、ワールドの読み込み時などでTerraria側で自動的に行ってくれる。(tile.TileFrameX
やtile.TileFrameY
を手動で設定する必要が無い)
対し、FrameImportant
タイルの設置はtile.TileFrameX
やtile.TileFrameY
を使用して、適切なフレームを選ぶなど少々書かなければいけないことが増える。
まとめると、Frmaed
タイルの設置やFrmaed
タイル同士のや置き換えは簡単にできるが、FrameImportant
タイルはそう簡単には置き換えができない、ということである。
タイルフレーム
ModTile
でタイルを作った人ならわかるかもしれない。(2回目)
さっきから「フレーム」という言葉を出してどういうことだと思った人もいるだろう。ここではタイルのフレームに関して説明する。
(正直、国語力のない私には説明しがたい...少しおおざっぱになる。)
Framed
タイルであろうと、FrameImportant
タイルであろうと、フレームという概念がある。
フレームは、タイルのテクスチャの、どこを表示するか、を表すようなものである。
タイルのテクスチャは 16px ごとに区切られている。
(https://github.com/tModLoader/tModLoader/wiki/World-Generation にある画像を参考にすると分かりやすいかも...)
どの 16px x 16px を使用するかをtile.TileFrameX
とtile.TileFrameY
で指定する
tile.TileFrameX = 18
とtile.TileFrameY == 0
で、区切られたタイルの左から2番目、上から1番目にある 16px x 16px を表す。
(16
ではなく18
なのは、区切りのために2px使用しているから)
delegate
(C#の用語)
delegate
に関してはいくつもの記事があるので、特にここで解説する必要は無いのだが、基礎セクションで出てくるC#
の用語(概念?)なので少しだけ。
delegate
は、大雑把に言うと、メソッドを変数として格納する型である。
メソッドが変数になりました ってだけ。
public delegate void Hoge(int value1, int value2);
private static void HogeHoge(int a, int b){}
static void Main()
{
Hoge hoge = HogeHoge;
}
とか
tasks.Insert(index, new PassLegacy("Test", delegate (GenerationProgress progress, GameConfiguration config)
{
}));
みたいな感じで使う。
忍耐力
はて、忍耐力?
前提「知識」ではないが、ワールドの生成には労力がかかる。
後述するが、デバッグの方法はワールドを作っては確認作っては確認...を繰り返すものである。
そもそもMod製作を初めた時点で逃れることはできない。
特に、ただのソフトウェア製作でなく、Mod製作だからデバッグには時間がかかる。
とても大変だ...。
...ただ、チュートリアルを読んでいるということは、
Terrariaのコードを読んで、ワールドの生成に関わるコードを理解し、どのHookを使えばいいのか判断する
という手順は省けているだろう。
これからその書き方は紹介する。あとは健闘を祈る。
悪いが前提知識のセクションはここまでだ。あくまで補助的なもので、前提知識にある具体的なことは別の記事などを探してほしい。
基礎
さて、前提知識でぐったりしているかもしれないが、ここからが本番だ。
最悪、前提知識は、Main.tile[,]
やMain.maxTilesX
/Main.maxTilesY
、Tile.TileType
くらい知っていれば問題は無い。
ワールド生成の仕組み
Terrariaのワールドはいくつかのパス
というもを順々に実行してできている。
このパス
は、文字列と、引数有り返り値なしのdelegate
型で構成されている。
文字列には、delegate
が何の生成を担うのかを表し、
delegate
に実際の生成コードが書かれる。
(tModLoader wikiではパス
はステップ
でできていて...みたいなことが書かれているが、正直分かりづらいし、個人的に世界生成のコードを書いてて意識して書くような感じでもないので割愛)
んで、このパス
の正体は、PassLegacy
というクラスである。
PassLegacy
のコンストラクタはこの通り。
new PassLegacy(string name, WorldGenLegacyMethod method)
このname
が上記の「文字列」であり、method
に生成コードをぶち込む。
delegate
と言っておきながら、WorldGenLegacyMethod
という型が出てきているが、WorldGenLegacyMethod
はdelegate
型である。
いくつかバニラのパス
を紹介しよう。
例えば、Terrain
(おおまかな地形生成)、Jungle
(ジャングルの(おおまかな)生成)、Floating Islands
(空島の生成)、Shinies
(鉱石を埋める)、Pyramids
(ピラミッドの生成)、Life Crystals
(ライフクリスタルの設置)、Spawn Point
(スポーン地点の確定)、Flowers
(花の設置)などなど。
これらのパス
は、世界を生成する時に、リストに格納される。
Modは、そのリストに、Modで作成したパス
を挿入(Insert
)して、ワールド生成を追加、変更する。
(もちろん必要に応じて、パス
を追加(Add
)や削除(RemoveAt
)を行うこともあるが、基礎セクションでは行わない。Terrariaのワールドの生成まるごと変えるような特殊なModのみが行うからだ。)
そして、重要なのはModで作成したパス
を、リストのどこに入れるか、である
適切な位置に入れなければ、せっかく生成したものがバニラの生成で壊されたり、その逆が起こったり、過剰なタイルまで変換してしまうなどの問題を引き起こす可能性がる。
パス
の作り方とModSystem
ここから、具体的にタイルの操作に関わるコードを書く。
つまり、Main.tile[,]
などのお出ましだ。
さて、パス
を格納するリストに、Modで作成したパス
を挿入すると言った。
まずはパス
を作る必要がある。
まぁ...その前に、一旦
自分が何をしたいのか(自作の鉱石を散らばらせたいのか、構造物を作りたいのか、など)を決めておく必要がある。
ここからは実例を挙げていくからな。
それでは、初めていこう。
まず、パス
を作ることはもちろんだが、バニラのパス
を格納したリストにアクセスできなきゃぁ意味がない。
そのリストをいじるには、ModSystem
にあるModifyWorldGenTasks
というメソッドをオーバーライドして利用する。
とりあえず、ModSystem
を継承したクラスが必要である。
//using System.Collections.Generic;
//using Terraria.ModLoader;
//using Terraria.WorldBuilding;
public class TutorialWorldGen : ModSystem
{
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
{
}
}
ModifyWorldGenTasks
の1番目のパラメータtasks
が、パス
を格納したリストだ。
このtasks
にパス
を挿入すればいい。
ではパス
の作り方だ。
作り方は、前述の通りPassLegacy
のコンストラクタを用いる。
(リストの要素はGenPass
であるが、PassLegacy
で問題ない。PassLegacy
はGenPass
を継承した型である。)
PassLegacy
に必要なのは、文字列とWorldGenLegacyMethod
(delegate
型)。
WorldGenLegacyMethod
は、GenerationProgress
型とGameConfiguration
型の引数を持ち、返り値はない。
delegate
を使って書いても、メソッドを用意して書いてもどちらでもよいが、
ここでは、メソッドを用意する方法を紹介する。
実例: 鉱石の生成(鉱石を散らばらせる)
さてお待ちかね、実際のコードだ。
一番簡単で、一番チュートリアル向きの 鉱石の生成 を行う。
実例
では早速コードだ。
//using System.Collections.Generic;
//using Terraria;
//using Terraria.GameContent.Generation;
//using Terraria.IO;
//using Terraria.ModLoader;
//using Terraria.WorldBuilding;
public class TutorialWorldGen : ModSystem
{
private void GenerateOres(GenerationProgress progress, GameConfiguration config)
{
progress.Message = "Places More Ores";
for (var k = 0; k < (int)((Main.maxTilesX * Main.maxTilesY) * 6E-05); k++)
{
WorldGen.TileRunner(WorldGen.genRand.Next(0, Main.maxTilesX), WorldGen.genRand.Next((int)WorldGen.worldSurfaceLow, Main.maxTilesY - 200), WorldGen.genRand.Next(3, 6), WorldGen.genRand.Next(2, 6), ModContent.TileType<Tiles.YourOre>());
}
}
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
{
int index = tasks.FindIndex(pass => pass.Name.Equals("Shinies"));
if (index != -1)
{
index++;
tasks.Insert(index, new PassLegacy("TutorialMod: Shinies", GenerateOres));
}
}
}
はぁ?
...と言いたくなるかもしれない。特に、鉱石の生成のコードだけを見に来た人は。
鉱石を散らばらせてみたい!ってお試し感覚でここに来ている人は、考えるな感じろ でいいと思う、
ただ、
まず、ModContent.TileType<Tiles.YourOre>()
の部分にタイルの種類を入れること、
次にprogress.Message
は、鉱石の生成中に表示される文字を代入すること、
そして、"TutorialMod: Shinies"
のTutorialMod
を自分の(内部)Mod名(名前空間の方)にすること、
最後に、鉱石を生成するだけならの方法で書けば問題ない。
ということを言っておこう。
WorldGenLegacyMethod
/ WorldGen.TileRunner
それでは具体的な解説だ。
とりあえず、delegate
型なのでメソッドを用意した。この意味がわからないのであれば、まずdelegate
の解説を探して多少理解した方が良い...が、まぁ知らなくてもこの書き方をするんだな。と覚えていただければ。
用意したメソッドは、ここではGenerateOre
と名付けている。
メソッド内にあるのが、鉱石を生成するコードである。
まずGenerateOre
メソッドをよくよく見ていこう。
//using Terraria;
//using Terraria.IO;
//using Terraria.ModLoader;
//using Terraria.WorldBuilding;
private void GenerateOres(GenerationProgress progress, GameConfiguration config)
{
progress.Message = "Places More Ores";
for (var k = 0; k < (int)((Main.maxTilesX * Main.maxTilesY) * 6E-05); k++)
{
WorldGen.TileRunner(WorldGen.genRand.Next(0, Main.maxTilesX), WorldGen.genRand.Next((int)WorldGen.worldSurfaceLow, Main.maxTilesY - 200), WorldGen.genRand.Next(3, 6), WorldGen.genRand.Next(2, 6), ModContent.TileType<Tiles.YourOre>());
}
}
メソッド名は何でも構わないが、パラメータはGenerationProgress
型とGameConfiguration
型の順でなければならない。
GenerationProgress
型は、生成中のメッセージや、プログレスバーの制御を行う。
プログレスバーの制御は、少々面倒なため、また私が研究中のため、ここでは解説をしない。
生成中のメッセージは簡単に書き換えることができ、generationProgress.Message
に文字列を代入するだけである。
つまり、progress.Message = "Places More Ores";
は、「この生成を行っている最中は "Places More Ores" とメッセージを表示せよ。」という意味である。
次に、鉱石の生成を担うコードだ。
for
文があるが、ひとまずWorldGen.TileRunner
について解説しよう。
WorldGen.TileRunner(int i, int j, double strength, int steps, int type, bool addTile = false, float speedX = 0f, float speedY = 0f, bool noYChange = false, bool overRide = true, int ignoreTileType = -1)
WorldGen.TileRunner
は鉱石などブロック(Framed
タイル)をランダムな形で配置するためのメソッドである。
以下の画像は、鉄鉱石をWorldGen.TileRunner
で配置した様子だ。
(散らばり具合はWorldGen.TileRunner
とは関係ない。私が指定した位置に置いていっただけである)
各引数を見ていこう。
-
int i, int j
- これはタイルを配置する位置を代入する。 -
double strength
- タイルの塊の大きさを調整する。( https://forums.terraria.org/index.php?threads/modding-tutorial-world-generation.47601/ にstrength
を大きくして実験した結果の画像がある。1.3のチュートリアルだが、TileRunner
はおそらく1.4でも変わっていないと思う) -
int steps
- 具体的には分からない。引数名的に、生成試行回数かと思われる。 -
int type
- 言わずもかな、生成するタイルの種類を代入する。 -
bool addTile
- 鉱石は土や石などに埋める、つまり土や石などを鉱石に変換している。これがfalse
であると、ベースとなる上書きされるブロックが無いと生成できない。true
にするとブロックの有無を無視する。false
(デフォルト)を推奨。 -
float speedX, float speedY, bool noYChange
- これらは未だによく分からない引数。デフォルトのまま使用することを推奨する。 -
bool overRide
- これは既存のタイルを変換するかどうか。addTile
がtrue
である時に、これをfalse
にすると、既存のタイルを壊さずにタイルを追加できる。addTile
もfalse
でこれもfalse
だと何も起こらなくなると思われる。true
(デフォルト)を推奨。 -
int ignoreTileType
- これも名前の通りである。上書きしたくないタイルの種類を指定するのだが、まず使うことはない。-1
で指定しない。-1
(デフォルト)を推奨。
それでは、実際に代入しているものを見てみよう。
WorldGen.TileRunner(
WorldGen.genRand.Next(0, Main.maxTilesX), // i
WorldGen.genRand.Next((int)WorldGen.worldSurfaceLow, Main.maxTilesY), // j
WorldGen.genRand.Next(3, 6), // strength
WorldGen.genRand.Next(2, 6), // steps
ModContent.TileType<Tiles.YourOre>() // type
);
i
とj
(設置位置)に関しては次の生成位置
で解説する。
strength
は3
~5
までのランダムな値を代入している。
steps
は2
~5
までのランダムな値を代入している。
type
は、タイルの種類を指定する。バニラのタイルを生成したいのであれば、TileID
を使えばよい。
乱数の値はバニラの鉱石生成を基にしているので、これらの乱数の範囲をいじることはあまりお勧めはしない。(が、塊を大きくしたいのであれば、範囲を多少大きくしても問題はない。)
以降のbool addTile, float speedX, float speedY, bool noYChange, bool overRide, int ignoreTileType
は全てデフォルト値を使用している。
そうそう、ワールドの生成では、乱数を生成する場合はMain.rand
ではなく、WorldGen.genRand
を使用する。
鉱石の塊の配置はWorldGen.TileRunner
を使用すれば簡単にできる。
他にもWorldGen.OreRunner
というものもあるが、これは、バニラではハードモードの鉱石出現に使われる。
そして、バニラのパス
のリストの挿入する位置が後の方である場合は、WorldGen.TileRunner
ではなくWorldGen.OreRunner
を使用する
...みたいな感じだが、今回はバニラのShinies
(バニラの鉱石の生成)の直後に、この鉱石生成パスをぶち込んでいるのでWorldGen.OreRunner
は必要ない。
生成位置
ワールドの生成において、
バニラの生成パスのどこに挿入すれば適切なのか、この形で生成するにはどのようなコードが適切なのか、
よりも難しい問題は、どこに生成するべきなのか である。
鉱石の生成は書き方が決まっているため、考える必要は無いが、
構造物を作りたい!とか、大きなバイオームを作りたい!とかであればよくその位置を考えるべきである。
さて、最後に残したfor
文の解説とWorldGen.TileRunner
の引数int i, int j
について解説しよう。
for (var k = 0; k < (int)((Main.maxTilesX * Main.maxTilesY) * 6E-05); k++)
{
WorldGen.TileRunner(WorldGen.genRand.Next(0, Main.maxTilesX), WorldGen.genRand.Next((int)WorldGen.worldSurfaceLow, Main.maxTilesY - 200), //以下略
}
まずfor
文についてだ。繰り返し変数のk
は使われない。つまり、このfor
はただの繰り返す回数を表すのみである。
繰り返す回数は、(int)((Main.maxTilesX * Main.maxTilesY) * 6E-05)
回。これもバニラのコードを基にしているため、あまり考えるようなものではない。
強いて言うのであれば、Main.maxTilesX * Main.maxTilesY
はワールドの全ブロック数を表し、その数に6E-05
(0.00006
)を掛けている。これはワールドのサイズが変わっても生成個数の割合が変わらないようにしているのだろう。
次に、WorldGen.genRand.Next(0, Main.maxTilesX)
だ。タイルの塊を生成するX座標を表している。0
からMain.maxTilesX
までなので、X座標は完全にランダムである。
最後に、WorldGen.genRand.Next((int)WorldGen.worldSurfaceLow, Main.maxTilesY - 200)
。これはY座標なのだが...見慣れない奴もいるな。
まず、WorldGen.worldSurfaceLow
。具体的にどのくらいか正直よく分からない。私ならMain.worldSurface
を使うからな。
...いずれにせよ、worldSurface
というのは、ワールドの地上と地下の境界である。
高度計でLevel Surface
と表示されるY座標がMain.worldSurface
であり、WorldGen.worldSurfaceLow
はそれより少し上を表すようだ。
(実際にやってみたのだが、Low
と付くもののMain.worldSurface
より上(より小さい値)に鉱石が生成された。)
そして、Main.maxTilesY - 200
だが、これは地下と地底世界(地獄)の境界である。ワールドのサイズに関係なく、最大タイル数-200以下が地底世界(Underworld
)となっている。
つまり、Y座標は、地上含む、地下(地底世界より上)のランダムな位置を指定している。
応用では、他にもワールドの境界となる位置を示す変数を紹介する。
とりあえず、次のModifyWorldGenTasks
を見ていこう。
ModifyWorldGenTasks
/ パス
の挿入
まだこれで終わりではない。作成したパス
をバニラのリストに挿入せねば。
ModifyWorldGenTasks
を見ていこう。
//using System.Collections.Generic;
//using Terraria.GameContent.Generation;
//using Terraria.WorldBuilding;
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
{
int index = tasks.FindIndex(pass => pass.Name.Equals("Shinies"));
if (index != -1)
{
index++;
tasks.Insert(index, new PassLegacy("TutorialMod: Shinies", GenerateOres));
}
}
list.FindIndex
やlist.Insert
を理解しているのであれば、さっきのコードよりもはるかにスッと頭に入るのではないのだろうか。
今回(この実例で)の目的は、(バニラの鉱石のように)鉱石を埋めることである。
鉱石を埋める程度なのであれば、バニラの鉱石生成の直後にModの鉱石の生成を埋める生成を行うのが理にかなっている。
バニラの鉱石生成のパス
はShinies
という名前(文字列)がついている。
だからまず、tasks.FindIndex(pass => pass.Name.Equals("Shinies"))
でShinies
がtasks
の何番目に入っているかを探す。
次にif (index != -1)
で、Shinies
がtasks
に入っているかどうか確認をする。FindIndex
で存在しなかった場合は-1
を返す仕様になっている。
これは、他のModによってShiniesパス
が消された時の対策で、Modの競合が起きないようにするものでもある。必須だ。
最後に、index
の値に1
加算(index++;
)して、tasks
に挿入(tasks.Insert()
)する。
1
加算する理由は、tasks.Insert()
は指定したインデックスの直前に挿入しようとするから。
今回はShiniesパス
の直後なので、1
加算する必要がある。
デバッグ方法
さて、ついに鉱石の生成コードを書き、パス
にして、バニラのパス
のリストに挿入した。
Modをコンパイルして、実際にワールドを生成してみよう。
実際に鉱石が生成されたかどうかを、いちいち掘って確認するのは面倒なので、HERO's Mod
を使用するといい。(さすがにこの記事を見に来た人は既に持っていると思うが。)
ワールドを照らしたり、カメラを動かしたり、アイテムを無条件無制限で取得できるからな。
失敗したり、生成されていなかったら、スペルミスやC#の構文があっているか確認しよう。
一応、他に大型Modを入れていないかも確認しよう。
...そう。
ワールド生成のデバッグ方法は、新規ワールド作成と確認の繰り返しである。
何かミスをしていれば、コードを直し、コンパイルし、ワールドを作成し、ワールドを探索する必要がある。
とても時間がかかる大変な作業だ。
しかも基礎では大したことはできない。
実際、World Generationのチュートリアル自体、tModLoader wikiでは、Easy(初級), Intermediate(中級), Advanced(高度), Expert(専門的)の中の、 Advanced(高度) に指定されている。
そのくらい複雑で面倒なものである。
...まぁ、ゆっくりやっていけばいいさ。
とりあえず次に進む前に、お茶でも飲んだらどうだい?
応用
基礎でカバー出来るのは鉱石の生成くらいだろう。
ここからは応用だ。
とりあえず、よく使う、ワールド生成で使える変数やメソッドなどを紹介していこう。
タイルの設置方法 (WorldGen
)
WorldGen.PlaceTile()
一番単純で基本のメソッド。タイルを置くだけ。
Frmaed
タイルはもちろん、ほとんどのFrameImportant
タイルにも使用できる。
FrameImportant
タイルを指定した場合も、1x1ではなくしっかり置いてくれるので、以降にあるPlace◯x◯
を使用しなくても問題ない。(むしろPlace◯x◯
を使ってゲームがクラッシュする原因になったことがある)
WorldGen.PlaceTile(int i, int j, int Type, bool mute = false, bool forced = false, int plr = -1, int style = 0)
-
int i, int j
- 設置するタイルのワールド座標。0
~Main.maxTilesX
、0
~Main.maxTilesY
の範囲まで。 -
int Type
- 設置するタイルの種類。TileID
やModContent.TileType<T>()
の使用を推奨。 -
bool mute
- おそらく設置音を鳴らすかどうか[要検証]かと思われる。私は毎回true
を指定している。 -
bool forced
- これをtrue
にすると、設置する位置にタイルがあろうとも強制的に上書きする。ただし、坂(スロープ)などのデータは残るため、設置前にしっかりタイルを消すことをお勧めする。 -
int plr
- 不明[要検証]。-1
(デフォルト)にしておくことを推奨する。 -
int style
-FrameImportant
タイルにおいて、Alternate
タイルを指定する時に使う。
(FrameImportant
タイルは、チェストのように1つでいくつもの種類を持つものがある。チェストであれば、0
(1番目)は普通のチェスト、1
(2番目)は金のチェストを表す。)
WorldGen.PlaceWall()
こちらも1番単純で基本のメソッド。壁を置くだけ。
WorldGen.PlaceWall(int i, int j, int type, bool mute = false)
-
int i, int j
- 設置する壁のワールド座標。0
~Main.maxTilesX
、0
~Main.maxTilesY
の範囲まで。 -
int Type
- 設置する壁の種類。WallID
やModContent.WallType<T>()
の使用を推奨。壁を削除(=破壊)する場合は、これを0
にするより、WorldGen.KillWall()
の使用を推奨する。 -
bool mute
- おそらく設置音を鳴らすかどうか[要検証]かと思われる。
WorldGen.Place◯x◯()
◯には数字が入り、WorldGen.Place1x1()
やWorldGen.Place2x2()
、WorldGen.Place3x3Wall()
といったものまである。
前述の通り、WorldGen.PlaceTile()
を使用しても問題ない。
WorldGen.Place◯x◯(int i, int j, int type, int style = 0)
WorldGen.Place◯x◯(int i, int j, ushort type, int style)
WorldGen.Place◯x◯(int i, int j, ushort type, int style = 0)
// など (ほとんどが上記のうちいずれかの形をとる)
-
int i, int j
- 設置するタイルのワールド座標。 -
ushort/int Type
- 設置するタイルの種類。TileID
やModContent.TileType<T>()
の使用を推奨。 -
int style
-FrameImportant
タイルにおいて、Alternate
タイルを指定する時に使う。
WorldGen.Place◯x◯() 一覧
WorldGen.Place1x1(int x, int y, int type, int style = 0)
WorldGen.Place1x2(int x, int y, ushort type, int style)
WorldGen.Place1x2Top(int x, int y, ushort type, int style)
WorldGen.Place1xX(int x, int y, ushort type, int style = 0)
WorldGen.Place2x1(int x, int y, ushort type, int style = 0)
WorldGen.Place2x2(int x, int y, ushort type, int style)
WorldGen.Place2x2Horizontal(int x, int y, ushort type, int style = 0)
WorldGen.Place2x2Style(int x, int y, ushort type, int style = 0)
WorldGen.Place2x3Wall(int x, int y, ushort type, int style)
WorldGen.Place2xX(int x, int y, ushort type, int style = 0)
WorldGen.Place3x1(int x, int y, ushort type, int style = 0)
WorldGen.Place3x2(int x, int y, ushort type, int style = 0)
WorldGen.Place3x2Wall(int x, int y, ushort type, int style)
WorldGen.Place3x3(int x, int y, ushort type, int style = 0)
WorldGen.Place3x3Wall(int x, int y, ushort type, int style)
WorldGen.Place3x4(int x, int y, ushort type, int style)
WorldGen.Place4x2(int x, int y, ushort type, int direction = -1, int style = 0)
WorldGen.Place4x3Wall(int x, int y, ushort type, int style)
-
WorldGen.Place5x4(int x, int y, ushort type, int style)
- 私が使って、ゲームクラッシュの原因になった。使用非推奨。 WorldGen.Place6x3(int x, int y, ushort type, int direction = -1, int style = 0)
WorldGen.Place6x4Wall(int x, int y, ushort type, int style)
WorldGen.PlaceChest()
前提知識
チェストの仕組み
Terrariaでは、チェストはChest
型というものでデータを管理している。
世界に置いて機能するチェストは8000
までであり、それらは全てMain.chest[]
に格納されている。
chest.item
からチェストの中身を確認することができる。
ワールド座標からチェストのインデックスを取得する方法
タイルの座標からチェストの中身を知りたい時は、
通常、Chest.FindChest(int X, int Y)
を使用する。
このX座標とY座標は、チェストの左上の座標を指定する必要がある。
チェストを置くことに特化したメソッド。
チェストを設置する場合は、WorldGen.PlaceTile()
やWorldGen.Place2x2()
ではなく、これを使用することを推奨する。
int WorldGen.PlaceChest(int x, int y, ushort type = 21, bool notNearOtherChests = false, int style = 0)
-
int i, int j
- 設置するタイルのワールド座標。 -
ushort Type
- 設置するタイルの種類。TileID
やModContent.TileType<T>()
の使用を推奨。いずれもチェストを指定すること。デフォルトの21
は、バニラのチェストを表す値である。 -
bool notNearOtherChests
- 不明。近くにチェストがあったら生成しない的な...?[要検証] -
int style
-FrameImportant
タイルにおいて、Alternate
タイルを指定する時に使う。ここでは、チェストの種類を表す。(ex:0
(1番目)は普通のチェスト、1
(2番目)は金のチェストを表す。)
そしてこのメソッドは、int
を返す。
これは、Main.chest[]
のインデックスである。
つまり、このメソッドは、チェストを配置し、Main.chest[]
にそのチェストのデータを用意するまでしてくれる、とても便利なメソッドである。
チェストを置いた後にそのチェスト内のアイテムを設定することもできる。
タイルの置き換え (Tile
)
タイルの置き換えはとても簡単だ。
tile.TileType
の値を変えればいいだけだ。
tile.TileType = (ushort)ModContent.TileType<Tiles.YourModTile>();
ただし、tile.HasTile
がfalse
である時は設置されないので、tile.HasTile
がtrue
であることを確認する必要がある。
また、FrmaeImportant
タイルでこれを行う場合、正しいフレームを指定する必要がある。
これらより、そもそも置き換えよりも、タイルの削除 → タイルの設置 をお勧めする。
タイルの削除 (WorldGen
)
WorldGen.KillTile()
タイルを削除(破壊)するためのメソッド。FrameImportant
タイルにも使える。
WorldGen.KillTile(int i, int j, bool fail = false, bool effectOnly = false, bool noItem = false)
-
int i, int j
- 破壊するタイルのワールド座標。0
~Main.maxTilesX
、0
~Main.maxTilesY
の範囲まで。 -
bool fail
- これがtrue
の時、タイルは破壊されない。役に立たなく聞こえるが、このメソッドはピッケルを使用した時にも使用され、ピッケルパワーが足りない場合にtrue
を指定しているそうだ。しかし、ワールドの生成には関係がないので、false
を指定しよう。 -
bool effectOnly
- 不明。fail
と共に使用して音を鳴らすか鳴らさないかに関わるらしい...?[要検証] -
bool noItem
- これをtrue
にすると、タイル破壊後にそのタイルのアイテムを落とさなくなる。私はtrue
に指定している。
WorldGen.KillWall()
壁を削除(破壊)するためのメソッド。
WorldGen.KillWall(int i, int j, bool fail = false)
-
int i, int j
- 破壊する壁のワールド座標。 -
bool fail
- これがtrue
の時、壁は破壊されない。ワールドの生成ではfalse
を指定しよう。
ワールドの各境界を表す変数など (Main
)
これらは全てワールド座標(タイル基準の座標)になっている。
Main.maxTilesX
& Main.maxTilesY
前提知識に書いたので必要ないが、これもワールド生成に役に立つ変数の1つだ。
ワールドの右下端の座標を表すとも言えるし、水平/垂直方向の全ブロック数とも言えるだろう。
Main.worldSurface * 0.35
[要検証]
これより上は高度計でSpace
と表示される。
地上と宇宙の境界のY座標である。
正確に試したことはないため、要検証ではあるものの、空に何か起きたい時は、この位置を基準にするとよい。
Main.worldSurface
高度計でLevel Surface
と表示される、地上と地下の境界のY座標である。
これより小さい値は、地上。これより大きい値は地下。
Main.rockLayer
Terrariaの地下には、Dirt Layer
とRock Layer
という2つの層に分けられている。
高度計で、Dirt Layer
はUnderground
と表示され、Rock Layer
はCaverns
と表示される。
Main.rockLayer
はその境界のY座標である。
Main.maxTilesY - 200
地下と地底世界(Underworld)の境界は変数が存在しないが、どのワールドでも共通して、境界はMain.maxTilesY - 200
の位置である。
Main.spawnTileX
/ Main.spawnTileY
プレイヤーのスポーン位置である。
これを使用する場合はSpawn Point
パス
の後でないと正しく取得できない。
WorldGen
にも変数はあるのだが、だいたい上記の変数名にLow
とかHigh
とかつくとかつかないもの見たいな感じで、かつ私が良く分かってないので割愛。
バニラのパス
一覧
tModLoader wiki にあるものは 1.3用なので、ここで、1.4用の全てのパス
を書こうと思う。
tasks.FindIndex
でインデックスを調べる際に使うとよい。
(各パスがどのようなことを行うのかは割愛。文字列でなんとなく分かるからね。)
パス一覧
Reset
Terrain
Dunes
Ocean Sand
Sand Patches
Tunnels
Mount Caves
Dirt Wall Backgrounds
Rocks In Dirt
Dirt In Rocks
Clay
Small Holes
Dirt Layer Caves
Rock Layer Caves
Surface Caves
Wavy Caves
Generate Ice Biome
Grass
Jungle
Mud Caves To Grass
Full Desert
Floating Islands
Mushroom Patches
Marble
Granite
Dirt To Mud
Silt
Shinies
Webs
Underworld
Corruption
Lakes
Dungeon
Slush
Mountain Caves
Beaches
Gems
Gravitating Sand
Create Ocean Caves
Clean Up Dirt
Pyramids
Dirt Rock Wall Runner
Living Trees
Wood Tree Walls
Altars
Wet Jungle
Jungle Temple
Hives
Jungle Chests
Settle Liquids
Remove Water From Sand
Oasis
Shell Piles
Smooth World
Waterfalls
Ice
Wall Variety
Life Crystals
Statues
Buried Chests
Surface Chests
Jungle Chests Placement
Water Chests
Spider Caves
Gem Caves
Moss
Temple
Cave Walls
Jungle Trees
Floating Island Houses
Quick Cleanup
Pots
Hellforge
Spreading Grass
Surface Ore and Stone
Place Fallen Log
Traps
Piles
Spawn Point
Grass Wall
Guide
Sunflowers
Planting Trees
Herbs
Dye Plants
Webs And Honey
Weeds
Glowing Mushrooms and Jungle Plants
Jungle Plants
Vines
Flowers
Mushrooms
Gems In Ice Biome
Random Gems
Moss Grass
Muds Walls In Jungle
Larva
Settle Liquids Again
Cactus, Palm Trees, & Coral
Tile Cleanup
Lihzahrd Altars
Micro Biomes
Water Plants
Stalac
Remove Broken Traps
Final Cleanup
実例: チェスト配置(スポーン地点の近くにZenithが入ったチェストを設置する)
TODO
応用(超高難易度)
Modの特殊シードを追加する
...できなくはないんだ。実は。
私の作ったModには特殊シードがある。
TODO