tAPI [Tutorial] Falling Blocks

blushiemagic

Retinazer
tModLoader
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.
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
}
Note: You do not use an aiStyle for the projectile, because too much hardcoding is involved.
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
}
Now let's continue with the easiest .cs file:
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);
    }
}}
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.
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 :confused:
 
Last edited:
Back
Top Bottom