Tutorial Unity: Decorations

Full Doc: https://drive.google.com/file/d/1RlGjlgRHcbUDO7lka3b2whpFhyfFTqUI/view?usp=sharing

 

Decorators are used to create anything besides terrain. This can be  objects such as trees, fruit, creatures, etc. These are currently not Unity gameobjects, but will eventually include those. 

For convenience, I’ll refer to the C# classes as scripts.

Creating a Decorator



In the WorldDecorations scripts folder, you should already see some C# scripts, such as StandardTreeDecorator



Add a new C# script to this folder, and name it something like MyCoolNewDecorator. You can change the name later, but please keep the text Decorator on the last part of the name.



The new script should resemble this:

public class MyCoolNewDecoration

{

}



Your C# class will need to implement IDecoration. Go ahead and add this to the class definition:

public class MyCoolNewDecoration : IDecoration

{

}

Add a constructor which accepts WorldData as a parameter and initializes m_WorldData with it:

public class MyCoolNewDecoration : IDecoration

{

    private readonly WorldData m_WorldData;

 

    public MyCoolNewDecoration(WorldData worldData)

    {

        m_WorldData = worldData;

    }    

}

When this decorator is actually created to draw itself in the world, it uses WorldData to add blocks.



Add the Decorate method:

public class MyCoolNewDecoration : IDecoration

{

    private readonly WorldData m_WorldData;

 

    public MyCoolNewDecoration(WorldData worldData)

    {

        m_WorldData = worldData;

    }

 

    public bool Decorate(int blockX, int blockY, int blockZ, Random random)

    {

        

    }

}

The Decorate method is called automatically.  The WorldDecorator processes each chunk, looking at each topsoil block. It asks your Decorate method if this block is a good place to place your new decoration. It calls the Decorate method, giving it the block world coordinates and a random number between 1 and 100. 



The purpose of the Decorator method is to decide if the block at x,y, and z is a good place to place your decoration. The random number is so that you can quickly decide if it’s a good “time” to place the decoration. For example, if you want many rocks placed, you could check to see if the number is less than 50. If not, no rock would be placed. If you only wanted a few rocks to be placed, you would check against a lower number…like between 1 and 5. 

Notice that the Decorate method returns a bool value. You should return true if you decided to place a decoration here, and false if you did not. The reason for this is, if you do place a decoration here (and returned true), no other decorations would be asked if they wanted to be placed here also. It would be odd if you placed a rock here, returned false, and the next decoration placed a tree there!

Here is a sample Decorate method from the PineTreeDecorator:

public bool Decorate(int blockX, int blockY, int blockZ, Random random)

    {

        if (IsAValidLocationforDecoration(blockX, blockY, blockZ, random))

        {

            CreateDecorationAt(blockX, blockY, blockZ, random);

            return true;

        }

 

        return false;

    }

This calls the ISAValidLocationForDecoration method. If you look at the code for this method, it basically gives it a 1% chance to place this tree here (the random value is 100). If it isn’t, it returns false.

It then checks to see if the tree is at least 10 blocks below the maximum height here:

        // Trees don't like to grow too high

        if (blockZ >= m_WorldData.DepthInBlocks - 20)

        {

            return false;

        }

It then performs one last check: Is there enough empty space at this place to even place a tree? It would be odd to place a tree RIGHT NEXT to a wall..you would end up with the tree “growing” into the wall, which would seem odd.

        // Trees like to have a minimum amount of space to grow in.

        return SpaceAboveIsEmpty(blockX, blockY, blockZ, 8, 2, 2);

These helper functions like SpaceAboveIsEmpty will eventually be moved into a common base class, so you won’t have to put them into each decoration. The numbers given are the depth above the target block, and the height and width. The example above checks to see if there is an empty space 8 blocks high, 2 blocks wide and 2 blocks long at the block we are checking for a decoration.

So, the IsAValidLocationForDecoration checks to see if we will even place a decoration here. Depending on the size of your decoration, adjust the values accordingly.

Back to the Decorate method, if we determine this IS a valid place to put the decoration, the CreateDecorationAt method is called to actually draw it.

    private void CreateDecorationAt(int blockX, int blockY, int blockZ, Random random)

    {

        //PINE TREES

        int trunkLength = random.RandomRange(10, 20);

        // Trunk

        for (int z = blockZ + 1; z <= blockZ + trunkLength; z++)

        {

            CreateTrunkAt(blockX, blockY, z);

        }

        //leaves

        for (int z = blockZ + 3; z <= blockZ + trunkLength; z += 2)

        {

            CreateDiskAt(blockX, blockY, z, (blockZ + trunkLength - z) / 3);

        }

        CreateLeavesAt(blockX, blockY, blockZ + trunkLength);

        //PINE TREES

        ///

    }

The pine tree is a trunk of length 10 to 20 blocks high. Along the length, a disk of pine leaves are created at various intervals. The CreateTrunkAt, CreateDiskAt and CreateLeavesAt methods were created to place blocks, take a look. 

The simplest block you might place would be a stone block. This could be done by doing this:

    private bool IsAValidLocationforDecoration(int blockX, int blockY, int blockZ, Random random)

    {

        if (random.RandomRange(1, 100) < 90)

        {

            return false;

        }

 

        return m_WorldData.GetBlock(blockX, blockY, blockZ).Type == BlockType.Air;

    }

This gives us a 10% chance to place a stone here, and returns true if it is an empty spot.

 

Here is the finished decorator that places a stone block:

public class MyCoolNewDecoration : IDecoration

{

    private readonly WorldData m_WorldData;

 

    public MyCoolNewDecoration(WorldData worldData)

    {

        m_WorldData = worldData;

    }

 

    public bool Decorate(int blockX, int blockY, int blockZ, Random random)

    {

        if (IsAValidLocationforDecoration(blockX, blockY, blockZ, random))

        {

            CreateDecorationAt(blockX, blockY, blockZ, random);

            return true;

        }

 

        return false;

    }

 

    private void CreateDecorationAt(int blockX, int blockY, int blockZ, Random random)

    {

        m_WorldData.SetBlockType(blockX, blockY, blockZ, BlockType.Stone);

    }

 

    private bool IsAValidLocationforDecoration(int blockX, int blockY, int blockZ, Random random)

    {

        if (random.RandomRange(1, 100) < 90)

        {

            return false;

        }

 

        return m_WorldData.GetBlock(blockX, blockY, blockZ).Type == BlockType.Air;

    }

}

When you create this script and save it, MinePackage will automatically use this for terrains. You don’t have to do anything else.

Note: Your decorator should never be bigger than a chunk, otherwise drawing could go outside the world boundaries and cause an exception. So if your chunk blocks are 32x32x128, don’t make the decorator wider or longer than 32 blocks. Again, it all depends on the block array size defined in your WorldData script. See ChunkBlockWidth, ChunkHeight, and ChunkBlockDepth.

Future improvements:

I’m going to add a Decorator class, that the decorators will derive from. It will have built in functions like CreateDisk, CreateColumn (for tree trunks, etc), CreateSphere, and others. This will allow people to create decorators without having to copy/paste these into their own decorators.

Decorators will eventually be able to notify the game that Unity GameObjects should be created at these locations. Any Unity gameobject could be added this way, from particles (fire/smoke), monsters, buildings, powerups, and so forth.