tModLoader Modding Tutorial: World Generation

Did this thread help you?

  • Yes it did!

    Votes: 54 93.1%
  • No, it didn't.

    Votes: 4 6.9%

  • Total voters
    58
  • Poll closed .
Hey, thanks for the reply! I used your advice and it built and reloaded without error, but when I killed the NPC during testing tModLoader crashed, so I altered the code a bit, but the same thing happened. Here's my code:

using Luminesque;
using System.IO;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
...
You have several pairs of extraneous curly braces (aka { } these), but the bigger problem is positioning.
You have the base X position of int X = WorldGen.genRand.Next(1, Main.maxTilesX - 300); but then in the following couple lines you define a new X position of X + Main.rand.Next(-240, 240). This will have a very high chance of spawning outside of the world, especially since this will repeat roughly 4 * 750, or 3000, times.

Make sure your tiles can never spawn outside of the world as they are right now, i.e. 0 < x < Main.maxTilesX and 0 < y < Main.maxTilesY.
 

erikobryant

Terrarian
Ok, so i applied your advice and the mod didnt crash and the biome generated, but the generation was crazy weird, it placed dirt walls literally everywhere and only placed tiled where tiles already existed, replacing even the trees with towers of blocks, any suggestions? (an image of the world gen and my new and somewhat improved code below)
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.World.Generation;
using Microsoft.Xna.Framework;
using Terraria.GameContent.Generation;
using System.Linq;
using System;


namespace TutorialMOD.NPCs
{
public class NpcDrops : GlobalNPC
{
public override void NPCLoot(NPC npc)
{


if (npc.type == NPCID.EyeofCthulhu) //this is where you choose what vanilla npc you want , for a modded npc add this instead if (npc.type == mod.NPCType("ModdedNpcName"))
{
for (int i = 0; i < Main.maxTilesX / 900; i++) //900 is how many biomes. the bigger is the number = less biomes
{
int X = WorldGen.genRand.Next(1, Main.maxTilesX - 300);
int Y = WorldGen.genRand.Next((int)WorldGen.rockLayer - 100, Main.maxTilesY - 200);
int TileType = mod.TileType("LumTile"); //this is the tile u want to use for the biome , if u want to use a vanilla tile then its int TileType = 56; 56 is obsidian block

WorldGen.TileRunner(X, Y, 350, WorldGen.genRand.Next(100, 200), TileType, false, 0f, 0f, true, true); //350 is how big is the biome 100, 200 this changes how random it looks.

}

};
}
}
}
 

Attachments

  • Screenshot (2).png
    Screenshot (2).png
    592.1 KB · Views: 99
Ok, so i applied your advice and the mod didnt crash and the biome generated, but the generation was crazy weird, it placed dirt walls literally everywhere and only placed tiled where tiles already existed, replacing even the trees with towers of blocks, any suggestions? (an image of the world gen and my new and somewhat improved code below)
using Terraria.ID;
using Terraria.ModLoader;
...
You have addTile to false in your TileRunner parameters. Otherwise, it's working exactly as you're telling the programming to...apart from the walls. No idea why the walls are there.
 

-=Soul=-

Official Terrarian
To the lesson with furniture placement: can you explain how to place objects in a self-generated chest?
Hello everyone!

I am GabeHasWon, a fairly experienced programmer, one who happens to be very experienced in World Generation. As such, I feel that redoing this ancient tutorial that covers the basics of worldgen would be a useful thing to do. So, here I am.

Before I start on worldgen itself, I implore you to have the following basics down and good already:
* A good grasp of C# and the basic Terraria framework
* A good grasp of math, mostly algebra, trigonometry and geometry
* An incredible amount of patience - no matter how good you are, the majority of worldgen is changing minor things constantly, and you will have to make a ton of worlds in order to test your code
and finally,
* an understanding of logic, as you will need a very high amount of conditions, comparisons and adjustments to make your programming and generation work as intended

Preface
There's a lot of minutiae involved in world generation. They will constantly hang over you even as you do the most basic of structures, and are pivotal to understanding why your code does or does not work.
The most basic of these concepts are the coordinate system Terraria uses, where the position (0, 0) is the very top-left of the world. You can then use the public variables Main.maxTilesX and Main.maxTilesY in order to grab the width and height of the world, respectively.
When doing this, however, note that the playable world stops before the player can actually reach coordinate 0 on either axis - I've never tested, but the world stops scrolling maybe 40 or 50 tiles before it truly stops. Keep this in mind when you want to add stuff to the edges of the world, such as Ocean or Space content.
Refer to this for a much better visual representation of the heights in-world:
View attachment 306777

Another thing that is very important to note is generation order - in Terraria, the game uses a list of tasks called GenPasses that tell the world how to be generated in order. This is incredibly important to keep in mind - if you generate your, for example, ores too early, they could easily be overtaken by new generation that follows it. For example, I made a biome a while ago, and after a bit of testing, found that the dungeon or Lihahzrd Temple could override it, making it basically nonexistant. So, I had to move it forward in the gen list in order to not make my biome disappear.
You'll also find it easier to debug if you understand where your generation is placed in the list.
For further reference on vanilla generation steps' order, follow this list here.

Finally, something you will always have to keep in mind is that tiles are stored in a 2D array. The ramifications of that are as follows:
1. If you go outside of the bounds of the array (i.e. try to access the tile at (-1, 0)), it will crash or fail to generate;
2. If you are to use, for example, player position, you will need to divide player position by 16 in order to access the tile at the same position - for example, if I wanted to get the tile directly above the player, I would use the following code:
Framing.GetTileSafely((int)(player.position.X / 16f), (int)(player.position.Y / 16f) - 1)
The same division operation would be conducted with projectiles, NPCs, dusts and gores.
3. These tile positions are also always an integer, and never a decimal of any sort. If you try to access the tile at 1.2f, 2, you will get an error.
4. Finally, every tile is always loaded in the world, but is only active when a tile is actually placed in-world - so, if you access an air tile, you might get a non-air type (i.e. Sand), but it will be empty.

Basics
So, with all of that aside, we can begin on the most simple methods and tools you can use to create or check or do whatever:
  • WorldGen.PlaceTile(int i, int j, int type, bool mute = false, bool forced = false, int plr = -1, int style = 0)
This, as you may guess, places a single tile at i, j, of type type, plays a sound if mute is false.
As for forced - I'd like to think that it forces the tile to be placed, but I don't often use this.
As for plr - I have no idea what this is. At all. Probably not important?
Finally, style is tough to explain - it is used to show alternate versions of a single tile. I'll explain this a bit more with PlaceObject.
  • WorldGen.KillTile(int i, int j, bool fail= false, bool effectOnly = false, bool noItem = false)
Also simple - kills the tile at i, j.
If fail is true, the tile is NOT removed. This is used for when you mine a pickaxe but it needs multiple hits, for example.
If effectOnly is true, the tile ONLY spawns dust, but does not break. Usually in tandem with fail. For example, mining a tile with too low of pickaxe power.
And noItem is simple, does not drop an item when the tile is killed.
  • Framing.GetTileSafely(int i, int j)
To be completely transparent, all I know is that this is a better way of grabbing any given tile. If you are trying to grab a tile through Main.tile[x, y], I'd recommend using this instead.
This is very useful for checking a given tile's type, if it's active, or certain qualities about it.
  • WorldGen.TileRunner(int i, int j, int strength, int steps, int type, bool addTile = false, float speedX = 0, float speedY = 0, bool noYChange = false, bool overRide = true)
Alright, this is a big more complex. TileRunner is a method that, in the only way I can explain, creates a 'diamond' with noisy edges if so chosen.
Here's a more in depth explanation:
i and j are the position of the TileRunner. Again, this is in tile position. so you'd have to divide by 16 if you want to use a player or npc position to gen something.
strength is how large the resulting chunk is - the "radius" in a way.
I think a better way to explain this is visual references:
Three different examples of a TileRunner with a strength of 2:
View attachment 306278
Three different examples of a TileRunner with a strength of 4:
View attachment 306279
Three different examples of a TileRunner with a strength of 8:
View attachment 306280
Two different examples of a TileRunner with a strength of 16:
View attachment 306281
And finally, two different examples of a TileRunner with a strength of 32:
View attachment 306282
For reference, every TileRunner there was exactly the following line:
C#:
WorldGen.TileRunner((int)(Main.MouseWorld.X / 16f), (int)(Main.MouseWorld.Y / 16f), STRENGTH, 5, TileID.Dirt, true, 0, 0, false, true);
Where STRENGTH = the strength value showcased.

step is a bit more confusing to me - as far as I'm aware, it's the amount of repeats, of loops, you do on this TileRunner call. This you to place stuff in a line if need be using speedX and speedY, explained after addTile.
addTile tells the TileRunner if you want to add tiles or not. If you set this to true, it will place new tiles. If not, it will only replace old tiles, as long as overRide is set to true.
speedX and speedY refer to the displacement of the new chunk made after a loop. For example, if I have a TileRunner of any size, in any position, with 3 steps and a speedX of 5 and a speedY of 0, it will place 3 chunks, each 5 blocks apart, starting from the original position (i, j) and going right. Alternatively, if I set speedX to a negative, it'll start at (i, j) and go left. Same principle applies to speedY; just that negative speedY goes up, and positive speedY goes down, respective to (i, j). This might be a bit of a wordy explanation, but it should make sense if you mess around with it a tad.
noYChange is...odd. I've not tested it ever, but I'm to guess it just makes the TileRunner ignore speedY. I dunno. Just keep this to false I guess.
and finally, overRide. This, when true, replaces existing tiles with the tile of your type if possible. For example, if I'm placing my ore, BananaOre, I would want to set overRide to true so it'd spawn in stone and mud and dirt and stuff. Set to false when you only want to add tiles.

TileRunner is incredibly helpful for smaller scale chunks of a tile, such as ore deposits (ore being one of the most common uses for TileRunner), sand, dirt and stone chunks, and similar things. However, I'd recommend against using this for large scale biomes and structures, as it's really hard to use when you want to fill an area completely without being inefficient. Similarly, it's also very hard to control exactly how you want.
  • WorldGen.digTunnel(int i, int j, float xDir, float yDir, int Steps, int Size, bool Wet = false)
Now, imagine TileRunner, but it removes tiles. This is that. It works functionally the same, just kills tiles.
The only difference is what Wet does; that places water in the tunnel it digs. Simple enough.

Positioning
With all those methods in mind, the biggest problem I see for people doing generation is positions. Where do I put my structure to have it in <x> biome? Where's the sky? Where's the surface? These are all valid questions, which thankfully, are quite easy to solve - especially when you've been doing gen as long as I have.

You have the five major values -
  1. 0, the very top of the world;
  2. Main.worldSurface, the surface of the world and the point at which going below is considered "underground";
  3. Main.rockLayer, the deeper caves of the world and the point at which going below is considered "caverns";
  4. Main.maxTilesY - 200, or the Underworld layer, at which point going below means you're in the Underworld;
  5. and finally Main.maxTilesY, the bottom of the world.
Using any combination of these allows you to make sure that your given structure or biome or whatever it may be could spawn exclusively within a specific area, such as only in the Underworld, or just under the surface, for example. Use these wisely.

As for X coordinates, it's a bit more ambiguous, with only four major values (that I've used) -
  1. 0, the very left of the world;
  2. Main.maxTilesX / 2, the centre of the world;
  3. Main.maxTilesX, the very right of the world;
  4. and finally, Main.dungeonX. This value can be to the left of or to the right of the centre of the world, which allows you to know the position of the snow and jungle biomes. If Main.dungeonX is LESS THAN Main.maxTilesX / 2, the snow biome and the dungeon are to the left of spawn. Otherwise, they are to the right of spawn. Note that dungeonX may not be set if used before the Dungeon generation step.
This also allows you to generally guess where places and biomes will be.
Of note is also the variable WorldGen.UndergroundDesertLocation, a Rectangle. This stores the top-left corner of the Underground Desert through WorldGen.UndergroundDesertLocation.Location, and the size through WorldGen.UndergroundDesertLocation.Size(). This is useful in situations like Calamity's Sunken Sea, or Starlight River's Vitric Desert biomes.

Gen Passes, Cont.
Now for passes themselves.
GenPasses are, again, the way that Terraria handles world generation. It runs GenPass by GenPass in order, which builds the world piece by piece. These passes are as large as the whole dungeon, or as small as adding sand in early worldgen. It just matters on how you want to partition it.

GenPasses themselves end up being one of the most simple things to use, in all honesty.
Under the ModifyWorldGenTasks method, shown here,
C#:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
you simply add your method or even code directly into a new task, as shown here,
C#:
tasks.Add(new PassLegacy("My Custom Generation", MyGenCode));
and all you need to do is make a method called MyGenCode with a GenerationProgress parameter, such as this,
C#:
public void MyGenCode(GenerationProgress p)

However, in most cases, you do not want to Add() your gen pass to the tasks list.
Usually, you want to insert your pass somewhere specifically, as described in the "Generation Order" subsection of the Preface. For example,
Code:
            int shiniesIndex = tasks.FindIndex(genpass => genpass.Name.Equals("Shinies"));
            if (shiniesIndex != -1)
                tasks.Insert(shiniesIndex + 1, new PassLegacy("MyGenPass", MyGenPass));
This would insert your generation right after the Shinies step, which is a step most often used to place ores.
Again, you can find the names of all vanilla passes here.

You can organize your worldgen into as many (or as little) genpasses as you want, but, I do recommend trying to keep them in different steps as you see fit. Especially for different biomes which could place in different places with different overlaps, sometimes you want a biome or structure to be placed in a different place compared to another biome you may add. Just make sure you understand where and when your biome is placed at all times, and you should be good.

Notices & Afterword
Thanks for reading through this tutorial, unless you really wanted to read the afterword for no reason.
I'll be updating this tutorial as inspiration strikes me, or at user request (if possible), so feel free to respond to this thread with whatever you may want to know.


Now, remember:
  • I will not program your biome or structure for you.
  • I will not usually go out of my way to give you very specific information on how to generate or implement something - I'd like to keep this tutorial relatively general, as WorldGen is an art that I cannot really explain without reducing it's utility to the general public. However, that does not mean I will not sometimes find something interesting enough to tackle regardless.
  • I will not help with visual or texture-based errors; this is to be a programming-based tutorial and I'd feel that trying to add spriting to it would be difficult to balance.
  • I will add FAQ if needed.
  • I will always further remind you that this takes patience - for my mod, I've generated well over five hundred worlds and I'm guessing I have a couple hundred more to go.
  • I am just one lad, one man, and I can only do so much.
  • Finally, if something doesn't work, try to work it out a bit before coming here for help. Some problems are easily solved after a bit of thought, even if it seems difficult for a while. I, for one, know I've been stuck on many a problem despite them being as simple as a single number that's too high or the lack of a method call.
Note from 2021 Gabe: This tutorial is very outdated, and uses only the most basic of my knowledge. It also hasn't been rewritten in 5 years, so don't expect anything great here. I just wanted to keep it on this post in case people find that parts of this are missing from the main post while I redo it.

2016 Gabe:
Hello people!
It seems that there has been an explosion of World Generation in mods, and, well, I thought I'd throw whatever I can in.

Note: This tutorial is a bit complicated. You should know how to at least make a basic NPC before you read this. Or be crazy and read it anyways.

So, here will be a basic tutorial on how to make simple lines/passageways, blobs of block, or pools/mini biomes, which will come later.
Ok, let's start off now :D
First off, it'd be useful to have something to base off, so ExampleMod has a decent example (duh) with it's Well. Use that and tinker for a bit, change the water to dirt, as to get a feel for it.
Basic Blob Making
Then, once you're done turning a well into a blob, we can begin.
Go into your ModWorld.cs file add a method called ModifyWorldGenTasks()
The full method is here:
Code:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)

Obviously, put {} around it. Now, let's start with a blob because I'm lazy.
Code:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
{
    int genIndex = tasks.FindIndex(genpass => genpass.Name.Equals("Micro Biomes"));
    tasks.Insert(genIndex + 1, new PassLegacy("Dirt Blob", delegate (GenerationProgress progress)
    {
         WorldGen.TileRunner(Main.spawnTileX, Main.spawnTileY - 46, 6, Main.rand.Next(1, 3), TileID.Dirt, true, 0f, 0f, true, true);
    }));
}

Here's an explanation of what's in the TileRunner method.
Code:
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)
So, I and J is X and Y, respectively. Strength decides how big it is (I can't understand the code enough to give a presise measurement.) Type is the type of tile, in TileID, int, or mod.tiletype. Add Tile checks if you're adding tiles or replacing them. SpeedX and Y sets the direction it goes is (also don't understand the code). noYChange is just that. It doesn't change Y. overRide means it can override other tiles.

That should place a blob of dirt above the player. You can replace the TileID with any other ID or mod.TileType("") if you wish.
That is the simplest of the things you can possibly do.

I'll keep the above code for future inserts, so I don't have to re-write it over. And over. And over again :D
So when I say something of this sort: Add code into your existing method, it means add the code into the Dirt Blob thing :D
Making a Line
Now for something a bit less...blobby.

I personally did this in a method as to make it easier to randomize, but you may do whatever you want.
So, there is a method in WorldGen called PlaceTile. It's a more precise way of using TileRunner.
Here is the method.
Code:
WorldGen.PlaceTile(int X, int Y, int TileType);
Please note that it places in tile positions, so if you want to place it in game at, say, npc.position.X, do npc.position.X / 16.

So, we use a for loop to make a straight line, like this:
Code:
for (int i = 0; i < 10; i++)
{
    WorldGen.PlaceTile(Main.spawnTileX + i, Main.spawnTileY - 30, TileID.Iron);
}

If you add that, it should make a 10 block long iron ore thing. I might've gotten the TileID wrong, but you get the point.
View attachment 131880
Now let's get to the really fun stuff.
Overriding a Biome
Are you tired of having Jungles in your world? Don't Jungle Bats bother you when you're setting up your mud hut? Well, I got the fix for you! Code. yay?
Anyways, yes, you can remove biomes and put a bunch of whatever there.
Code:
int jungleGen = tasks.FindIndex(genpass => genpass.Name.Equals("Jungle"));
tasks[jungleGen] = new PassLegacy("More Dirt Blobs", delegate (GenerationProgress progress)
{
    WorldGen.TileRunner(Main.spawnTileX, Main.spawnTileY + 12, 6, Main.rand.Next(1, 3), TileID.Dirt, true, 0f, 0f, true, true);
});
So now you should have no jungle (if I remembered the name correctly) and an extra blob of dirt, but below you. Do not put this is the same code block (These: { }) as the previous dirt blob, that will give an error.
Tested: it works with any proper gen step.

How to Place Multi blocks


As shown above, people want to know how to place something such as a Table or Chair.
So, you have to use the WorldGen.PlaceTile method.
Code:
//PlaceObject(int x, int y, int type, bool mute = false, int style = 0, int alternate = 0, int random = -1, int direction = -1)[code] That's an explanation of the args.[/SPOILER]
So, this will place a modded chair in the X and Y:
[code] WorldGen.PlaceObject(Main.spawnTileX + 10, Main.spawnTileY, mod.TileType("ExampleChair"), false, 0, 0, -1, 1);
This, without testing, should place an Example Chair 10 blocks away from the player.
Placing a Chest
I've barely explored this concept; you could make a "starter chest" near spawn with some good lewt. Or, just a Meowmere. Either work.
So, you can't place chests with PlaceObject (for some reason) so you need PlaceChest. Code shown below.
Code:
WorldGen.PlaceChest(int X, int Y, int type, bool noNearOtherChests = false, int style = 0);
So, basic explanation of what those do: X and Y is duh. Type is what chest type. notNearOtherChests makes it so you can't place a chest there if it's near another. Style is what frame.X is should be at, used for placing locked chests.
So, just do
Code:
WorldGen.PlaceChest(Main.spawnTileX - 10, Main.spawnTileY, 21, false, 2);
for a chest to appear next to the player. Not sure what chest though, the chest tilesheet is the longest thing ever. (Needs testing)
Making a Meteor-like biome

Easy enough ;)
Code:
        public void OreComet()
        {
            int x = Main.rand.Next(0, Main.maxTilesX);
            int y = Main.worldSurface - 200;
            int[] tileIDs = { 6, 7, 8, 9 , 166, 167, 168, 169};
            if (Main.tile[x, y].type <= -1)
            {
                y++;
            }
            else
            {
                WorldGen.TileRunner(x, y, 2, 4, tileIDs[Main.rand.Next(tileIDs.Length)], false, 0f, 0f, true, true);
                return;
            }
        }
So, here is a method that makes a 'comet' out of any pre-hardmode ore, except Demonite, Hellstone and Crimtane. It places this anywhere above ground, with it initially spawning above the world, and doesn't place until it hits a block. This method won't do anything alone, though, you'll place it in the Kill method of a projectile, which is easiest. The projectile should call Kill OnTileCollide(). Code:
Code:
        public override void Kill()
        {
            OreComet();
        }
That should have a 0.1% chance to spawn every frame.
Have fun! :D
Floating Islands
Let's spam us some candy cane w/ red team block floating islands.

As suggested by @Jenosis ;)
REWRITTEN:
It's only clouds now, but the generation & code is much smoother.
Code:
        private void GenIsland(Point topCentre, int size, int type)
        {
            for (int i = -size / 2; i < size / 2; ++i)
            {
                int repY = (size / 2) - (Math.Abs(i));
                int offset = repY / 5;
                repY += WorldGen.genRand.Next(4);
                for (int j = -offset; j < repY; ++j)
                {
                    WorldGen.PlaceTile(topCentre.X + i, topCentre.Y + j, type);
                }
            }
        }
topCentre is the position of the, well, top and centre tile of the island. It'll be slightly under the top (around 4 blocks on a 50 tile wide island), but point stands.
size is the width. Not much more to say.
type is the TileID of the island; in the following screenshot, it's TileID.Clouds.
View attachment 208405
I am willing to create a more functional floating island with dirt, grass, and the like, but for now this should be enough. It's much more efficient too.

Change any of the TileIDs to your liking. Have fun! :D
Placing a Tree, and growing the Tree
This one is pretty simple, really. I'll just slap in some code and done :p
Code:
WorldGen.PlaceTile(X, Y, TileID.Dirt);
do that as a base. Make it 3 wide, so the tree will go properly.
Then:
Code:
WorldGen.PlaceObject(X, Y - 1, mod.TileType("MagicSapling));
WorldGen.GrowTree(X, Y - 1);
This will grow the MagicSapling if it can. Otherwise, it'll simply place a sapling.
Notify me on this thread/my profile page if this doesn't work. Thanks!
Placing Walls
It's basically the same as placing a tile. But with walls. Woah.
Here's two:
Code:
WorldGen.PlaceTile(int i, int j, int type)
or
Code:
Main.tile[i, j].wall = WallID.MudUnsafe;
Simple enough :p
Changing Tile Slopes
Also simple.
Code:
Main.tile[i, j].slope(0);
From what I know, it goes from 0 to 12. Tell me if I'm wrong.

TileIDs can be found here: tModLoader/tModLoader

Some useful (ints) things for WorldGen are Main.maxTilesY, same thing but with X, Main.spawnTileY, and X.
Main.spawnTileX +/- 300-400 is the Snow/Desert biome. Main.maxTilesY - 200 is the top of the Underworld.

The amount of derps done here: 11 so far.

I will expand this upon user request, though I won't do your WorldGen for you.
Anyways, that'll be it for now. Leave any comments/questions/suggestions below :D
Credits
Thanks to @Graydee for being helpful with worldgen on occasion;)
Thanks to @EchoNex for the first WorldGen code he gave me.
Thanks to @jopojelly for random helps :D

I request that anyone who uses this tutorial and publishes it in any way, Mod Browser or not, gives me any sort of credit. Either it be a link to this thread, a link to me, a simple "thanks to gabe for some worldgen stuff", please do it :D
To the lesson with furniture placement: can you explain how to place objects in a self-generated chest?
P.S. Towards the generation of structures: how to define the generation of a structure in a biome, as well as in a biome, but with a different size of the world? How to create a different direction of structure generation from the player?
 
Last edited:
To the lesson with furniture placement: can you explain how to place objects in a self-generated chest?

To the lesson with furniture placement: can you explain how to place objects in a self-generated chest?
P.S. Towards the generation of structures: how to define the generation of a structure in a biome, as well as in a biome, but with a different size of the world?
Excuse the long wait; I've added a little section explaining how chests work in the main post.

As for the P.S.; that is explained a good amount in the Positioning section of the tutorial.
If you want further explanation, some biomes are very abstract and you have to detect it yourself. It depends greatly on which biome, like the Jungle - with the jungle, you'd have to scan an area for Jungle related tiles yourself in order to know if it's a jungle biome. This is independent of world size.
Similarly, biomes with defined locations, like the Underground Desert, are unaffected by world size as you can already tell where they are using those WorldGen values.
How to create a different direction of structure generation from the player?
I do not understand what you mean by this. Could you elaborate, please?
 

-=Soul=-

Official Terrarian
Excuse the long wait; I've added a little section explaining how chests work in the main post.

As for the P.S.; that is explained a good amount in the Positioning section of the tutorial.
If you want further explanation, some biomes are very abstract and you have to detect it yourself. It depends greatly on which biome, like the Jungle - with the jungle, you'd have to scan an area for Jungle related tiles yourself in order to know if it's a jungle biome. This is independent of world size.
Similarly, biomes with defined locations, like the Underground Desert, are unaffected by world size as you can already tell where they are using those WorldGen values.

I do not understand what you mean by this. Could you elaborate, please?
I meant that. How to determine the side of the structure from the character? For example, the entrance should face the character.
 

Ysak

Terrarian
Hello , i'm new in modding Terraria and i have a problem and i would like to fix that , the question is how to add music in the biome ?

If you can answer that , thank you.
 
Hello , i'm new in modding Terraria and i have a problem and i would like to fix that , the question is how to add music in the biome ?

If you can answer that , thank you.
ExampleMod goes over this; basically, in your ModWorld, use TileCountsAvailable and set a static int field to the tile count of your biome's tiles. Then, check that field in your ModPlayer. If it's over a number, let's say 50, set a bool value in that player. Then, if that bool value is true, update the music in, well, UpdateMusic.
I meant that. How to determine the side of the structure from the character? For example, the entrance should face the character.
Depends on when. In worldgen, check if the X position of the structure you're placing is > Main.spawnTileX. If it is, place the structure with the entrance to the left. Otherwise, right.
In-game, such as with an item, repeat the above but check for player.position in UseItem().
 

Ysak

Terrarian
Hey i did it but how i can create my biome in the sky i tried multiple times ! Do you can help me ?
 
i have no idea how to create an underground biome. I have tried, but it seems like the best thing i can find are tutorials that spawn blobs of tiles. Thats not a biome. Can you help me so that I can create a legit biome that spawns underground?
 

ZSolar1

Terrarian
Hello everyone!

I am GabeHasWon, a fairly experienced programmer, one who happens to be very experienced in World Generation. As such, I feel that redoing this ancient tutorial that covers the basics of worldgen would be a useful thing to do. So, here I am.

Before I start on worldgen itself, I implore you to have the following basics down and good already:
* A good grasp of C# and the basic Terraria framework
* A good grasp of math, mostly algebra, trigonometry and geometry
* An incredible amount of patience - no matter how good you are, the majority of worldgen is changing minor things constantly, and you will have to make a ton of worlds in order to test your code
and finally,
* an understanding of logic, as you will need a very high amount of conditions, comparisons and adjustments to make your programming and generation work as intended

Preface
There's a lot of minutiae involved in world generation. They will constantly hang over you even as you do the most basic of structures, and are pivotal to understanding why your code does or does not work.
The most basic of these concepts are the coordinate system Terraria uses, where the position (0, 0) is the very top-left of the world. You can then use the public variables Main.maxTilesX and Main.maxTilesY in order to grab the width and height of the world, respectively.
When doing this, however, note that the playable world stops before the player can actually reach coordinate 0 on either axis - I've never tested, but the world stops scrolling maybe 40 or 50 tiles before it truly stops. Keep this in mind when you want to add stuff to the edges of the world, such as Ocean or Space content.
Refer to this for a much better visual representation of the heights in-world:
View attachment 306777

Another thing that is very important to note is generation order - in Terraria, the game uses a list of tasks called GenPasses that tell the world how to be generated in order. This is incredibly important to keep in mind - if you generate your, for example, ores too early, they could easily be overtaken by new generation that follows it. For example, I made a biome a while ago, and after a bit of testing, found that the dungeon or Lihahzrd Temple could override it, making it basically nonexistant. So, I had to move it forward in the gen list in order to not make my biome disappear.
You'll also find it easier to debug if you understand where your generation is placed in the list.
For further reference on vanilla generation steps' order, follow this list here.

Finally, something you will always have to keep in mind is that tiles are stored in a 2D array. The ramifications of that are as follows:
1. If you go outside of the bounds of the array (i.e. try to access the tile at (-1, 0)), it will crash or fail to generate;
2. If you are to use, for example, player position, you will need to divide player position by 16 in order to access the tile at the same position - for example, if I wanted to get the tile directly above the player, I would use the following code:
Framing.GetTileSafely((int)(player.position.X / 16f), (int)(player.position.Y / 16f) - 1)
The same division operation would be conducted with projectiles, NPCs, dusts and gores.
3. These tile positions are also always an integer, and never a decimal of any sort. If you try to access the tile at 1.2f, 2, you will get an error.
4. Finally, every tile is always loaded in the world, but is only active when a tile is actually placed in-world - so, if you access an air tile, you might get a non-air type (i.e. Sand), but it will be empty.

Basics
So, with all of that aside, we can begin on the most simple methods and tools you can use to create or check or do whatever:
  • WorldGen.PlaceTile(int i, int j, int type, bool mute = false, bool forced = false, int plr = -1, int style = 0)
This, as you may guess, places a single tile at i, j, of type type, plays a sound if mute is false.
As for forced - I'd like to think that it forces the tile to be placed, but I don't often use this.
As for plr - I have no idea what this is. At all. Probably not important?
Finally, style is tough to explain - it is used to show alternate versions of a single tile. I'll explain this a bit more with PlaceObject.
  • WorldGen.KillTile(int i, int j, bool fail= false, bool effectOnly = false, bool noItem = false)
Also simple - kills the tile at i, j.
If fail is true, the tile is NOT removed. This is used for when you mine a pickaxe but it needs multiple hits, for example.
If effectOnly is true, the tile ONLY spawns dust, but does not break. Usually in tandem with fail. For example, mining a tile with too low of pickaxe power.
And noItem is simple, does not drop an item when the tile is killed.
  • Framing.GetTileSafely(int i, int j)
To be completely transparent, all I know is that this is a better way of grabbing any given tile. If you are trying to grab a tile through Main.tile[x, y], I'd recommend using this instead.
This is very useful for checking a given tile's type, if it's active, or certain qualities about it.
  • WorldGen.TileRunner(int i, int j, int strength, int steps, int type, bool addTile = false, float speedX = 0, float speedY = 0, bool noYChange = false, bool overRide = true)
Alright, this is a big more complex. TileRunner is a method that, in the only way I can explain, creates a 'diamond' with noisy edges if so chosen.
Here's a more in depth explanation:
i and j are the position of the TileRunner. Again, this is in tile position. so you'd have to divide by 16 if you want to use a player or npc position to gen something.
strength is how large the resulting chunk is - the "radius" in a way.
I think a better way to explain this is visual references:
Three different examples of a TileRunner with a strength of 2:
View attachment 306278
Three different examples of a TileRunner with a strength of 4:
View attachment 306279
Three different examples of a TileRunner with a strength of 8:
View attachment 306280
Two different examples of a TileRunner with a strength of 16:
View attachment 306281
And finally, two different examples of a TileRunner with a strength of 32:
View attachment 306282
For reference, every TileRunner there was exactly the following line:
C#:
WorldGen.TileRunner((int)(Main.MouseWorld.X / 16f), (int)(Main.MouseWorld.Y / 16f), STRENGTH, 5, TileID.Dirt, true, 0, 0, false, true);
Where STRENGTH = the strength value showcased.

step is a bit more confusing to me - as far as I'm aware, it's the amount of repeats, of loops, you do on this TileRunner call. This you to place stuff in a line if need be using speedX and speedY, explained after addTile.
addTile tells the TileRunner if you want to add tiles or not. If you set this to true, it will place new tiles. If not, it will only replace old tiles, as long as overRide is set to true.
speedX and speedY refer to the displacement of the new chunk made after a loop. For example, if I have a TileRunner of any size, in any position, with 3 steps and a speedX of 5 and a speedY of 0, it will place 3 chunks, each 5 blocks apart, starting from the original position (i, j) and going right. Alternatively, if I set speedX to a negative, it'll start at (i, j) and go left. Same principle applies to speedY; just that negative speedY goes up, and positive speedY goes down, respective to (i, j). This might be a bit of a wordy explanation, but it should make sense if you mess around with it a tad.
noYChange is...odd. I've not tested it ever, but I'm to guess it just makes the TileRunner ignore speedY. I dunno. Just keep this to false I guess.
and finally, overRide. This, when true, replaces existing tiles with the tile of your type if possible. For example, if I'm placing my ore, BananaOre, I would want to set overRide to true so it'd spawn in stone and mud and dirt and stuff. Set to false when you only want to add tiles.

TileRunner is incredibly helpful for smaller scale chunks of a tile, such as ore deposits (ore being one of the most common uses for TileRunner), sand, dirt and stone chunks, and similar things. However, I'd recommend against using this for large scale biomes and structures, as it's really hard to use when you want to fill an area completely without being inefficient. Similarly, it's also very hard to control exactly how you want.
  • WorldGen.digTunnel(int i, int j, float xDir, float yDir, int Steps, int Size, bool Wet = false)
Now, imagine TileRunner, but it removes tiles. This is that. It works functionally the same, just kills tiles.
The only difference is what Wet does; that places water in the tunnel it digs. Simple enough.

Positioning
With all those methods in mind, the biggest problem I see for people doing generation is positions. Where do I put my structure to have it in <x> biome? Where's the sky? Where's the surface? These are all valid questions, which thankfully, are quite easy to solve - especially when you've been doing gen as long as I have.

You have the five major values -
  1. 0, the very top of the world;
  2. Main.worldSurface, the surface of the world and the point at which going below is considered "underground";
  3. Main.rockLayer, the deeper caves of the world and the point at which going below is considered "caverns";
  4. Main.maxTilesY - 200, or the Underworld layer, at which point going below means you're in the Underworld;
  5. and finally Main.maxTilesY, the bottom of the world.
Using any combination of these allows you to make sure that your given structure or biome or whatever it may be could spawn exclusively within a specific area, such as only in the Underworld, or just under the surface, for example. Use these wisely.

As for X coordinates, it's a bit more ambiguous, with only four major values (that I've used) -
  1. 0, the very left of the world;
  2. Main.maxTilesX / 2, the centre of the world;
  3. Main.maxTilesX, the very right of the world;
  4. and finally, Main.dungeonX. This value can be to the left of or to the right of the centre of the world, which allows you to know the position of the snow and jungle biomes. If Main.dungeonX is LESS THAN Main.maxTilesX / 2, the snow biome and the dungeon are to the left of spawn. Otherwise, they are to the right of spawn. Note that dungeonX may not be set if used before the Dungeon generation step.
This also allows you to generally guess where places and biomes will be.
Of note is also the variable WorldGen.UndergroundDesertLocation, a Rectangle. This stores the top-left corner of the Underground Desert through WorldGen.UndergroundDesertLocation.Location, and the size through WorldGen.UndergroundDesertLocation.Size(). This is useful in situations like Calamity's Sunken Sea, or Starlight River's Vitric Desert biomes.

Gen Passes, Cont.
Now for passes themselves.
GenPasses are, again, the way that Terraria handles world generation. It runs GenPass by GenPass in order, which builds the world piece by piece. These passes are as large as the whole dungeon, or as small as adding sand in early worldgen. It just matters on how you want to partition it.

GenPasses themselves end up being one of the most simple things to use, in all honesty.
Under the ModifyWorldGenTasks method, shown here,
C#:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
you simply add your method or even code directly into a new task, as shown here,
C#:
tasks.Add(new PassLegacy("My Custom Generation", MyGenCode));
and all you need to do is make a method called MyGenCode with a GenerationProgress parameter, such as this,
C#:
public void MyGenCode(GenerationProgress p)

However, in most cases, you do not want to Add() your gen pass to the tasks list.
Usually, you want to insert your pass somewhere specifically, as described in the "Generation Order" subsection of the Preface. For example,
Code:
            int shiniesIndex = tasks.FindIndex(genpass => genpass.Name.Equals("Shinies"));
            if (shiniesIndex != -1)
                tasks.Insert(shiniesIndex + 1, new PassLegacy("MyGenPass", MyGenPass));
This would insert your generation right after the Shinies step, which is a step most often used to place ores.
Again, you can find the names of all vanilla passes here.

You can organize your worldgen into as many (or as little) genpasses as you want, but, I do recommend trying to keep them in different steps as you see fit. Especially for different biomes which could place in different places with different overlaps, sometimes you want a biome or structure to be placed in a different place compared to another biome you may add. Just make sure you understand where and when your biome is placed at all times, and you should be good.

Chests
(Suggested by @-=Soul=-)
Chests are naturally a very important part of designing a biome or structure and filling it with content.

Filling chests during worldgen (or even during play) is fairly simple:
C#:
WorldGen.PlaceChest(x, y, type, notNearOtherChests, style)
PlaceChest is a method used during worldgen that, well, places a chest. It also returns the index of the Chest object in Main.chest that tile is associated with if the chest is placed successfully. If it's not placed successfully, it simply returns -1.
So, we can do this:
C#:
int ChestIndex = WorldGen.PlaceChest(x, y, (ushort)type, false, style);
if (ChestIndex != -1)
to check if, when we place down a chest, it is successful.
Then, within the if statement, we can do something like this:
C#:
Main.chest[ChestIndex].item[0].SetDefaults(ItemID.Bananarang);
This would set the first item in the chest you've placed down to a single Bananarang.
If you want to increase the stack, simply do:
C#:
Main.chest[ChestIndex].item[0].stack = 20;
And boom, you have a stack of 20 Bananarangs.
Further manipulation of chests and their inventories just requires some logic and thought, so that about rounds this section up.

Notices & Afterword
Thanks for reading through this tutorial, unless you really wanted to read the afterword for no reason.
I'll be updating this tutorial as inspiration strikes me, or at user request (if possible), so feel free to respond to this thread with whatever you may want to know.


Now, remember:
  • I will not program your biome or structure for you.
  • I will not usually go out of my way to give you very specific information on how to generate or implement something - I'd like to keep this tutorial relatively general, as WorldGen is an art that I cannot really explain without reducing it's utility to the general public. However, that does not mean I will not sometimes find something interesting enough to tackle regardless.
  • I will not help with visual or texture-based errors; this is to be a programming-based tutorial and I'd feel that trying to add spriting to it would be difficult to balance.
  • I will add FAQ if needed.
  • I will always further remind you that this takes patience - for my mod, I've generated well over five hundred worlds and I'm guessing I have a couple hundred more to go.
  • I am just one lad, one man, and I can only do so much.
  • Finally, if something doesn't work, try to work it out a bit before coming here for help. Some problems are easily solved after a bit of thought, even if it seems difficult for a while. I, for one, know I've been stuck on many a problem despite them being as simple as a single number that's too high or the lack of a method call.
Note from 2021 Gabe: This tutorial is very outdated, and uses only the most basic of my knowledge. It also hasn't been rewritten in 5 years, so don't expect anything great here. I just wanted to keep it on this post in case people find that parts of this are missing from the main post while I redo it.

2016 Gabe:
Hello people!
It seems that there has been an explosion of World Generation in mods, and, well, I thought I'd throw whatever I can in.

Note: This tutorial is a bit complicated. You should know how to at least make a basic NPC before you read this. Or be crazy and read it anyways.

So, here will be a basic tutorial on how to make simple lines/passageways, blobs of block, or pools/mini biomes, which will come later.
Ok, let's start off now :D
First off, it'd be useful to have something to base off, so ExampleMod has a decent example (duh) with it's Well. Use that and tinker for a bit, change the water to dirt, as to get a feel for it.
Basic Blob Making
Then, once you're done turning a well into a blob, we can begin.
Go into your ModWorld.cs file add a method called ModifyWorldGenTasks()
The full method is here:
Code:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)

Obviously, put {} around it. Now, let's start with a blob because I'm lazy.
Code:
public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight)
{
    int genIndex = tasks.FindIndex(genpass => genpass.Name.Equals("Micro Biomes"));
    tasks.Insert(genIndex + 1, new PassLegacy("Dirt Blob", delegate (GenerationProgress progress)
    {
         WorldGen.TileRunner(Main.spawnTileX, Main.spawnTileY - 46, 6, Main.rand.Next(1, 3), TileID.Dirt, true, 0f, 0f, true, true);
    }));
}

Here's an explanation of what's in the TileRunner method.
Code:
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)
So, I and J is X and Y, respectively. Strength decides how big it is (I can't understand the code enough to give a presise measurement.) Type is the type of tile, in TileID, int, or mod.tiletype. Add Tile checks if you're adding tiles or replacing them. SpeedX and Y sets the direction it goes is (also don't understand the code). noYChange is just that. It doesn't change Y. overRide means it can override other tiles.

That should place a blob of dirt above the player. You can replace the TileID with any other ID or mod.TileType("") if you wish.
That is the simplest of the things you can possibly do.

I'll keep the above code for future inserts, so I don't have to re-write it over. And over. And over again :D
So when I say something of this sort: Add code into your existing method, it means add the code into the Dirt Blob thing :D
Making a Line
Now for something a bit less...blobby.

I personally did this in a method as to make it easier to randomize, but you may do whatever you want.
So, there is a method in WorldGen called PlaceTile. It's a more precise way of using TileRunner.
Here is the method.
Code:
WorldGen.PlaceTile(int X, int Y, int TileType);
Please note that it places in tile positions, so if you want to place it in game at, say, npc.position.X, do npc.position.X / 16.

So, we use a for loop to make a straight line, like this:
Code:
for (int i = 0; i < 10; i++)
{
    WorldGen.PlaceTile(Main.spawnTileX + i, Main.spawnTileY - 30, TileID.Iron);
}

If you add that, it should make a 10 block long iron ore thing. I might've gotten the TileID wrong, but you get the point.
View attachment 131880
Now let's get to the really fun stuff.
Overriding a Biome
Are you tired of having Jungles in your world? Don't Jungle Bats bother you when you're setting up your mud hut? Well, I got the fix for you! Code. yay?
Anyways, yes, you can remove biomes and put a bunch of whatever there.
Code:
int jungleGen = tasks.FindIndex(genpass => genpass.Name.Equals("Jungle"));
tasks[jungleGen] = new PassLegacy("More Dirt Blobs", delegate (GenerationProgress progress)
{
    WorldGen.TileRunner(Main.spawnTileX, Main.spawnTileY + 12, 6, Main.rand.Next(1, 3), TileID.Dirt, true, 0f, 0f, true, true);
});
So now you should have no jungle (if I remembered the name correctly) and an extra blob of dirt, but below you. Do not put this is the same code block (These: { }) as the previous dirt blob, that will give an error.
Tested: it works with any proper gen step.

How to Place Multi blocks


As shown above, people want to know how to place something such as a Table or Chair.
So, you have to use the WorldGen.PlaceTile method.
Code:
//PlaceObject(int x, int y, int type, bool mute = false, int style = 0, int alternate = 0, int random = -1, int direction = -1)[code] That's an explanation of the args.[/SPOILER]
So, this will place a modded chair in the X and Y:
[code] WorldGen.PlaceObject(Main.spawnTileX + 10, Main.spawnTileY, mod.TileType("ExampleChair"), false, 0, 0, -1, 1);
This, without testing, should place an Example Chair 10 blocks away from the player.
Placing a Chest
I've barely explored this concept; you could make a "starter chest" near spawn with some good lewt. Or, just a Meowmere. Either work.
So, you can't place chests with PlaceObject (for some reason) so you need PlaceChest. Code shown below.
Code:
WorldGen.PlaceChest(int X, int Y, int type, bool noNearOtherChests = false, int style = 0);
So, basic explanation of what those do: X and Y is duh. Type is what chest type. notNearOtherChests makes it so you can't place a chest there if it's near another. Style is what frame.X is should be at, used for placing locked chests.
So, just do
Code:
WorldGen.PlaceChest(Main.spawnTileX - 10, Main.spawnTileY, 21, false, 2);
for a chest to appear next to the player. Not sure what chest though, the chest tilesheet is the longest thing ever. (Needs testing)
Making a Meteor-like biome

Easy enough ;)
Code:
        public void OreComet()
        {
            int x = Main.rand.Next(0, Main.maxTilesX);
            int y = Main.worldSurface - 200;
            int[] tileIDs = { 6, 7, 8, 9 , 166, 167, 168, 169};
            if (Main.tile[x, y].type <= -1)
            {
                y++;
            }
            else
            {
                WorldGen.TileRunner(x, y, 2, 4, tileIDs[Main.rand.Next(tileIDs.Length)], false, 0f, 0f, true, true);
                return;
            }
        }
So, here is a method that makes a 'comet' out of any pre-hardmode ore, except Demonite, Hellstone and Crimtane. It places this anywhere above ground, with it initially spawning above the world, and doesn't place until it hits a block. This method won't do anything alone, though, you'll place it in the Kill method of a projectile, which is easiest. The projectile should call Kill OnTileCollide(). Code:
Code:
        public override void Kill()
        {
            OreComet();
        }
That should have a 0.1% chance to spawn every frame.
Have fun! :D
Floating Islands
Let's spam us some candy cane w/ red team block floating islands.

As suggested by @Jenosis ;)
REWRITTEN:
It's only clouds now, but the generation & code is much smoother.
Code:
        private void GenIsland(Point topCentre, int size, int type)
        {
            for (int i = -size / 2; i < size / 2; ++i)
            {
                int repY = (size / 2) - (Math.Abs(i));
                int offset = repY / 5;
                repY += WorldGen.genRand.Next(4);
                for (int j = -offset; j < repY; ++j)
                {
                    WorldGen.PlaceTile(topCentre.X + i, topCentre.Y + j, type);
                }
            }
        }
topCentre is the position of the, well, top and centre tile of the island. It'll be slightly under the top (around 4 blocks on a 50 tile wide island), but point stands.
size is the width. Not much more to say.
type is the TileID of the island; in the following screenshot, it's TileID.Clouds.
View attachment 208405
I am willing to create a more functional floating island with dirt, grass, and the like, but for now this should be enough. It's much more efficient too.

Change any of the TileIDs to your liking. Have fun! :D
Placing a Tree, and growing the Tree
This one is pretty simple, really. I'll just slap in some code and done :p
Code:
WorldGen.PlaceTile(X, Y, TileID.Dirt);
do that as a base. Make it 3 wide, so the tree will go properly.
Then:
Code:
WorldGen.PlaceObject(X, Y - 1, mod.TileType("MagicSapling));
WorldGen.GrowTree(X, Y - 1);
This will grow the MagicSapling if it can. Otherwise, it'll simply place a sapling.
Notify me on this thread/my profile page if this doesn't work. Thanks!
Placing Walls
It's basically the same as placing a tile. But with walls. Woah.
Here's two:
Code:
WorldGen.PlaceTile(int i, int j, int type)
or
Code:
Main.tile[i, j].wall = WallID.MudUnsafe;
Simple enough :p
Changing Tile Slopes
Also simple.
Code:
Main.tile[i, j].slope(0);
From what I know, it goes from 0 to 12. Tell me if I'm wrong.

TileIDs can be found here: tModLoader/tModLoader

Some useful (ints) things for WorldGen are Main.maxTilesY, same thing but with X, Main.spawnTileY, and X.
Main.spawnTileX +/- 300-400 is the Snow/Desert biome. Main.maxTilesY - 200 is the top of the Underworld.

The amount of derps done here: 11 so far.

I will expand this upon user request, though I won't do your WorldGen for you.
Anyways, that'll be it for now. Leave any comments/questions/suggestions below :D
Credits
Thanks to @Graydee for being helpful with worldgen on occasion;)
Thanks to @EchoNex for the first WorldGen code he gave me.
Thanks to @jopojelly for random helps :D

I request that anyone who uses this tutorial and publishes it in any way, Mod Browser or not, gives me any sort of credit. Either it be a link to this thread, a link to me, a simple "thanks to gabe for some worldgen stuff", please do it :D
When I try to move the underworld i get this error: 'Main' does not contain a definition for 'underworld' and just saying Main.maxTilesY - 200 or Main.maxTilesY doesnt work. I just get more errors. Does anyone know how i could interact with the underworld?
 
When I try to move the underworld i get this error: 'Main' does not contain a definition for 'underworld' and just saying Main.maxTilesY - 200 or Main.maxTilesY doesnt work. I just get more errors. Does anyone know how i could interact with the underworld?
"Main.underworld" does not exist. What are you trying to do? What is your code?
 
Top Bottom