One question I've seen asked a lot is how to create blocks that fall, like sand. Recently, the time came that I had to do the same... and as I delved into the source code for answers, the job quickly became a nightmare. (So... much... hardcoding...) However, I have finally found a way to work around all the hardcoding. This won't be as much of a tutorial as it is copy/pasting code, but I will try to explain every part of the code I put up here. This tutorial will cover how to make blocks fall, how to make falling blocks reform into blocks on the ground, how to much your blocks cause suffocation, how to make your blocks fire-able by the Sandgun, and how to make everything server-compatible. Be sure to read every single comment in case there are things you don't want. Also remember to remove every comment from json files, just in case the compiler doesn't like them.
The most obvious things to start with would be the .jsons for the sand tile and sand item.
Now let's work on the part that actually makes the sand fall. This requires two things: .cs file for your sand tile, and a projectile that represents falling sand. Let's work on the .cs file first (keep in mind that you will need the projectile for the mod to work).
Now, we can finally work on the projectile code. The projectile is the actual falling sand, and it also creates the sand tile when it hits the ground.
Note: You do not use an aiStyle for the projectile, because too much hardcoding is involved.
Now, you should have a tile that is affected by gravity, and even works on servers! But surely there's more we can do, right? One thing that many falling blocks do is suffocate you. In order to accomplish this, we must use a class that overrides ModPlayer. So, here we go:
There is one last thing (which is entirely optional): making your sand block ammo for the SandGun. To do this, we'll need three more .cs files! We'll also need another .json file. Let's start with the easy one first (note the file name, and note that it isn't hostile):
Now let's continue with the easiest .cs file:
Now we will need to make the Sandgun actually recognize the custom sand. In order to do this, we will check the inventory to see what kind of ammo it is using, then do other things accordingly. The following file will apply to all items, so if statements are necessary.
Finally, to make things less buggy, we will need one more file. Don't worry if you don't understand this one; it can be a bit complicated.
And that's it! Feel free to ask any questions. Also, please point out if there's any mistakes; I made this tutorial right after I had wisdom tooth surgery
The most obvious things to start with would be the .jsons for the sand tile and sand item.
Code:
{
"displayName": "Example Sand",
"size": [1, 1],
"solid": true,
"breaksByPick": true,
"blocksLight": true,
"blocksSun": true,
"brick": true,
"mergeDirt": true,
"tileSand": true,
"sound": 1,
"soundGroup": 0,
"dust": /* put what you want here */,
"mapColor": /* put what you want here*/,
"drop": "ExampleMod:ExampleSand"
}
Code:
{
"displayName": "Example Sand Block",
"width": 12,
"height": 12,
"maxStack": 999,
"rare": 0,
"value": [0, 0, 0, 0],
"useStyle": 1,
"useTurn": true,
"useAnimation": 15,
"useTime": 10,
"autoReuse": true,
"consumable": true,
"createTile": "ExampleMod:ExampleSand",
"ammo": 42 //only include this line if you want the Sandgun to shoot it
}
Now let's work on the part that actually makes the sand fall. This requires two things: .cs file for your sand tile, and a projectile that represents falling sand. Let's work on the .cs file first (keep in mind that you will need the projectile for the mod to work).
Code:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using TAPI;
namespace ExampleMod.Tiles {
public class ExampleSand : ModTileType
{
//this code will run whenever the sand is placed or a block next to it changes
public override bool TileFrame(int x, int y)
{
if(WorldGen.noTileActions)
{
return false;
}
Tile above = Main.tile[x, y - 1];
Tile below = Main.tile[x, y + 1];
//we need to check if there is no block below, and make sure the block above isn't something like a chest
if(below != null && !below.active() && (!above.active() || !(above.type == 21 || TileDef.chest[(int)above.type] || above.type == 323)))
{
//gets rid of the sand block
Main.tile[x, y].active(false);
//singleplayer
if (Main.netMode == 0)
{
//creates the falling sand and notifies surrounding blocks of the change
int proj = Projectile.NewProjectile((float)(x * 16 + 8), (float)(y * 16 + 8), 0f, 0.41f, "ExampleMod:ExampleSandBall", 10, 0f, Main.myPlayer, 1f, 0f);
WorldGen.SquareTileFrame(x, y, true);
}
//server
else if (Main.netMode == 2)
{
//creates the falling sand
int proj = Projectile.NewProjectile((float)(x * 16 + 8), (float)(y * 16 + 8), 0f, 2.5f, "ExampleMod:ExampleSandBall", 10, 0f, Main.myPlayer, 1f, 0f);
//I have no idea what this part is for
Main.projectile[proj].velocity.Y = 0.5f;
Main.projectile[proj].position.Y += 2f;
//notifies clients of the projectile's new speed and position
Main.projectile[proj].netUpdate = true;
//notifies surrounding blocks of the change, and sends the change to clients
NetMessage.SendTileSquare(-1, x, y, 1);
WorldGen.SquareTileFrame(x, y, true);
}
//the tile no longer exists in place
return true;
}
//nothing changed
return false;
}
}}
Now, we can finally work on the projectile code. The projectile is the actual falling sand, and it also creates the sand tile when it hits the ground.
Code:
{
"displayName": "Example Sand Ball",
"width": 10,
"height": 10,
"knockback": 6,
"penetrate": -1,
"friendly": true,
"hostile": true,
"tileCollide": false //this is necessary to get around some hard-coding
}
Code:
using System;
using Microsoft.Xna.Framework;
using Terraria;
using TAPI;
namespace ExampleMod.Projectiles {
public class ExampleSandBall : ModProjectile
{
public override void AI()
{
if(Main.rand.Next(2) == 0)
{
//replace with your own dust type
int dust = Dust.NewDust(projectile.position, projectile.width, projectile.height, 17, 0f, 0f, 0, default(Color), 1f);
Main.dust[dust].velocity.X *= 0.4f;
}
if (projectile.ai[0] == 1f)
{
//this part is necessary for sandgun projectiles
if (!projectile.hostile)
{
projectile.ai[1] += 1f;
if (projectile.ai[1] >= 60f)
{
//gravity doing its work slowly
projectile.ai[1] = 60f;
projectile.velocity.Y += 0.2f;
}
}
//this is for all falling sand
else
{
//gravity doing its work
projectile.velocity.Y += 0.41f;
}
}
//I think this part is for antlions, not sure though
else if (projectile.ai[0] == 2f)
{
projectile.velocity.Y += 0.2f;
if (projectile.velocity.X < -0.04f)
{
projectile.velocity.X += 0.04f;
}
else if (projectile.velocity.X > 0.04f)
{
projectile.velocity.X -= 0.04f;
}
else
{
projectile.velocity.X = 0f;
}
}
projectile.rotation += 0.1f;
if (projectile.velocity.Y > 10f)
{
//don't fall too fast
projectile.velocity.Y = 10f;
}
}
public override void PostAI()
{
//so falling sand has some hardcoded collision; this is our workaround
Vector2 oldVelocity = projectile.velocity;
//this part is for all falling sand
if(projectile.hostile)
{
projectile.velocity = Collision.AnyCollision(projectile.position, projectile.velocity, projectile.width, projectile.height);
}
//this part is for the Sandgun
else
{
projectile.velocity = Collision.TileCollision(projectile.position, projectile.velocity, projectile.width, projectile.height, true, true, 1);
}
//detects whether there was a collision with a tile
if(projectile.velocity != oldVelocity)
{
projectile.position += projectile.velocity;
//we will turn the projectile back into a tile now
projectile.Kill();
}
}
public override void Kill()
{
if(projectile.owner == Main.myPlayer && !projectile.noDropItem)
{
int tileX = (int)(projectile.position.X + (float)(projectile.width / 2)) / 16;
int tileY = (int)(projectile.position.Y + (float)(projectile.width / 2)) / 16;
int tileType = TileDef.byName["ExampleMod:ExampleSand"];
if (Main.tile[tileX, tileY].halfBrick() && projectile.velocity.Y > 0f && System.Math.Abs(projectile.velocity.Y) > System.Math.Abs(projectile.velocity.X))
{
tileY--;
}
//make sure the projectile will create sand in an empty place
if (!Main.tile[tileX, tileY].active())
{
string conditions = TileDef.placementConditions[tileType];
TileDef.placementConditions[tileType] = "air"; //this line is only necessary for Sandguns
bool flag = WorldGen.PlaceTile(tileX, tileY, tileType, false, true, -1, 0);
TileDef.placementConditions[tileType] = conditions; //this line is only necessary for Sandguns
if (flag)
{
if (Main.tile[tileX, tileY + 1].halfBrick() || Main.tile[tileX, tileY + 1].slope() != 0)
{
WorldGen.SlopeTile(tileX, tileY + 1, 0);
if (Main.netMode == 2)
{
NetMessage.SendData(17, -1, -1, "", 14, (float)tileX, (float)(tileY + 1), 0f, 0f);
}
}
if (Main.netMode != 0)
{
//this part actually creates the sand on the ground and sends it to the server or clients
ExampleModNet.SendSandPlacement(tileType, "air"); //this line is only necessary for Sandguns
NetMessage.SendData(17, -1, -1, "", 1, (float)tileX, (float)tileY, (float)tileType, 0f);
ExampleModNet.SendSandPlacement(tileType, conditions); //this line is only necessary for Sandguns
}
}
}
}
}
}}
Now, you should have a tile that is affected by gravity, and even works on servers! But surely there's more we can do, right? One thing that many falling blocks do is suffocate you. In order to accomplish this, we must use a class that overrides ModPlayer. So, here we go:
Code:
using System;
using Microsoft.Xna.Framework;
using Terraria;
using TAPI;
namespace ExampleMod {
public class ExamplePlayer : ModPlayer
{
//the player has 5 ticks (1/12 of a second) to get out of the sand before suffocation starts, so this keeps track of that
private int customSuffocateDelay = 0;
public override void PostUpdate()
{
//determine which blocks we will check
Vector2 playerPos = player.position;
int left = (int)(player.position.X / 16f) - 1;
int right = (int)((player.position.X + (float)player.width) / 16f) + 2;
int up = (int)(player.position.Y / 16f) - 1;
int down = (int)((player.position.Y + (float)player.height) / 16f) + 2;
if (left < 0)
{
left = 0;
}
if (right > Main.maxTilesX)
{
right = Main.maxTilesX;
}
if (up < 0)
{
up = 0;
}
if (down > Main.maxTilesY)
{
down = Main.maxTilesY;
}
bool suffocating = false;
for (int i = left; i < right; i++)
{
for (int j = up; j < down; j++)
{
if (Main.tile[i, j] != null && Main.tile[i, j].slope() == 0 && !Main.tile[i, j].inActive() && Main.tile[i, j].active())
{
int type = Main.tile[i, j].type;
Vector2 tilePos;
tilePos.X = (float)(i * 16);
tilePos.Y = (float)(j * 16);
int tileHeight = 16;
if (Main.tile[i, j].halfBrick())
{
tilePos.Y += 8f;
tileHeight -= 8;
}
//detects whether the player is inside a sand block
if (type == TileDef.byName["ExampleMod:ExampleSand"] && playerPos.X + (float)player.width - 2f >= tilePos.X && playerPos.X + 2f <= tilePos.X + 16f && playerPos.Y + (float)player.height - 2f >= tilePos.Y && playerPos.Y + 2f <= tilePos.Y + (float)tileHeight)
{
if(customSuffocateDelay < 5)
{
customSuffocateDelay += 1;
}
else
{
//if the player has been inside for 5 ticks (1/12 of a second), start suffocation
player.AddBuff(68, 1, true);
}
suffocating = true;
}
}
}
}
if(!suffocating)
{
//the player is not inside sand, so reset the counter to 0
customSuffocateDelay = 0;
}
}
}}
There is one last thing (which is entirely optional): making your sand block ammo for the SandGun. To do this, we'll need three more .cs files! We'll also need another .json file. Let's start with the easy one first (note the file name, and note that it isn't hostile):
Code:
{
"displayName": "Example Sand Ball",
"code": "ExampelMod.Projectiles.ExampleSandBall",
"texture": "Projectiles/ExampleSandBall",
"width": 10,
"height": 10,
"knockback": 6,
"penetrate": -1,
"friendly": true,
"maxUpdates": 1,
"tileCollide": false
}
Code:
using System;
using Terraria;
using TAPI;
namespace ExampleMod.Items {
public class ExampleSand : ModItem
{
//Basically, this makes it so that the Sandgun doesn't consume the sand.
//This is necessary so we can tell what kind of sand the Sandgun is firing.
//Because if the Sandgun doesn't recognize its ammo, it will fire normal sand.
//We will make the Sandgun consume ammo later.
public override bool ConsumeAmmo(Player player)
{
return false;
}
}}
Now we will need to make the Sandgun actually recognize the custom sand. In order to do this, we will check the inventory to see what kind of ammo it is using, then do other things accordingly. The following file will apply to all items, so if statements are necessary.
Code:
using System;
using Microsoft.Xna.Framework;
using Terraria;
using TAPI;
namespace ExampleMod.Items {
//this part in brackets makes this code apply to all items
[GlobalMod]
public class GlobalItem : ModItem
{
public override bool PreShoot(Player player, Vector2 position, Vector2 velocity, int projType, int damage, float knockback)
{
if(projType == 42) //this is the ID for normal sand fired from the Sandgun
{
//now we will check what kind of ammo the Sandgun is using
Item sand = null;
for(int k = 54; k < 58 && sand == null; k++)
{
if(player.inventory[k].ammo == 42)
{
sand = player.inventory[k];
}
}
for(int k = 0; k < 54 && sand == null; k++)
{
if(player.inventory[k].ammo == 42)
{
sand = player.inventory[k];
}
}
//this part checks to see if it is using our custom sand
if(sand.type == ItemDef.byName["ExampleMod:ExampleSand"].type)
{
//this is the part that checks whether the ammo should be consumed, since it wasn't consumed earlier
bool consumeAmmo = true;
if(player.ammoBox && Main.rand.Next(5) == 0)
{
consumeAmmo = false;
}
if(player.ammoPotion && Main.rand.Next(5) == 0)
{
consumeAmmo = false;
}
if(player.ammoCost80 && Main.rand.Next(5) == 0)
{
consumeAmmo = false;
}
if(player.ammoCost75 && Main.rand.Next(4) == 0)
{
consumeAmmo = false;
}
if(!player.inventory[player.selectedItem].ConsumeAmmo(player))
{
consumeAmmo = false;
}
//now here the ammo actually gets consumed
if(consumeAmmo)
{
sand.stack--;
if(sand.stack <= 0)
{
sand.active = false;
sand.name = "";
sand.type = 0;
}
}
//now we finally get to create the projectile!
projType = ProjDef.byName["ExampleMod:ExampleSandGunBall"].type;
damage += 10;
Vector2 direction = velocity;
direction.Normalize();
position += direction * item.width;
Projectile.NewProjectile(position, velocity, projType, damage, knockback, player.whoAmI, 1f, 0f);
return false;
}
}
//do what we would normally do if we're not firing our custom sand
return base.PreShoot(player, position, velocity, projType, damage, knockback);
}
}}
Code:
using System;
using Microsoft.Xna.Framework;
using Terraria;
using TAPI;
using ExampleMod.Projectiles;
namespace ExampleMod {
public class ExampleNet : ModNet
{
public static void SendSandPlacement(int tileID, string conditions, int ignore = -1)
{
NetMessage.SendModData(Mods.GetMod("ExampleMod"), 1, -1, ignore, tileID, conditions);
}
public override void NetReceive(BinBuffer bb, int messageID, MessageBuffer buffer)
{
if(messageID == 1)
{
int tileID = bb.ReadInt();
string conditions = bb.ReadString();
TileDef.placementConditions[tileID] = conditions;
if(Main.netMode == 2)
{
SendSandPlacement(tileID, conditions, buffer.whoAmI);
}
}
}
}}
And that's it! Feel free to ask any questions. Also, please point out if there's any mistakes; I made this tutorial right after I had wisdom tooth surgery
Last edited: