Adding Subtypes Of A Class [Optimizing Blocks]

Started by
5 comments, last by SpikeViper 8 years ago

I have come far in my quest to optimize. After adding chunk pools, rewriting generation, and shrinking the data in my blocks, I've hit a roadblock. For my blocks to be changed (they are instances of a class), they need to be destroyed and a new instance of another block class takes its place. I want to know the best way to add subtypes to my block class, so instead of [new Block()] I can use [Block.BlockType =], fixing my garbage collection issues. I want my blocks to keep their functionality, including the ability to have their own methods in the future.

Current Block class:


using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[System.Serializable]

public class Block
{
    public int temperature;
    public bool light;
    public float LightRange;
    public float LightIntensity;
    public int density;
    public string BlockName;
    public byte LR;
    public byte LG;
    public byte LB;
    public byte LA;

    public Block()
    {
        this.light = false;
        this.BlockName = "Undefined";
    }

    public enum Direction { north, east, south, west, up, down };

    public struct Tile { public int x; public int y;}

    const float tileSize = 0.0625f;


    public virtual MeshData Blockdata
     (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {

            meshData.useRenderDataForCol = true;

            if (!planetchunk.GetBlock(x, y + 1, z).IsSolid(Direction.down))
            {
                meshData = FaceDataUp(planetchunk, x, y, z, meshData);
            }

            if (!planetchunk.GetBlock(x, y - 1, z).IsSolid(Direction.up))
            {
                meshData = FaceDataDown(planetchunk, x, y, z, meshData);
            }

            if (!planetchunk.GetBlock(x, y, z + 1).IsSolid(Direction.south))
            {
                meshData = FaceDataNorth(planetchunk, x, y, z, meshData);
            }

            if (!planetchunk.GetBlock(x, y, z - 1).IsSolid(Direction.north))
            {
                meshData = FaceDataSouth(planetchunk, x, y, z, meshData);
            }

            if (!planetchunk.GetBlock(x + 1, y, z).IsSolid(Direction.west))
            {
                meshData = FaceDataEast(planetchunk, x, y, z, meshData);
            }

            if (!planetchunk.GetBlock(x - 1, y, z).IsSolid(Direction.east))
            {
                meshData = FaceDataWest(planetchunk, x, y, z, meshData);
            }



            if (light == true)
            {

                if (planetchunk.GetBlock(x - 1, y, z).IsSolid(Direction.east) == false || planetchunk.GetBlock(x + 1, y, z).IsSolid(Direction.west) == false || planetchunk.GetBlock(x, y, z - 1).IsSolid(Direction.north) == false ||
                    planetchunk.GetBlock(x, y, z + 1).IsSolid(Direction.south) == false || planetchunk.GetBlock(x, y - 1, z).IsSolid(Direction.up) == false || planetchunk.GetBlock(x, y + 1, z).IsSolid(Direction.down) == false)
                {
                    meshData.AddLight(x, y, z, LR, LG, LB, LA, LightRange, LightIntensity);
                }

            }

            return meshData;


    }

    protected virtual MeshData FaceDataUp
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z - 0.5f));

        meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.up));
        return meshData;
    }

    protected virtual MeshData FaceDataDown
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z + 0.5f));

        meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.down));
        return meshData;
    }

    protected virtual MeshData FaceDataNorth
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z + 0.5f));

        meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.north));
        return meshData;
    }

    protected virtual MeshData FaceDataEast
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z + 0.5f));

        meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.east));
        return meshData;
    }

    protected virtual MeshData FaceDataSouth
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x + 0.5f, y - 0.5f, z - 0.5f));

        meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.south));
        return meshData;
    }

    protected virtual MeshData FaceDataWest
        (PlanetChunk planetchunk, int x, int y, int z, MeshData meshData)
    {
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z + 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z - 0.5f));
        meshData.AddVertex(new Vector3(x - 0.5f, y - 0.5f, z - 0.5f));
            

            meshData.AddQuadTriangles();
        meshData.uv.AddRange(FaceUVs(Direction.west));
        return meshData;
    }

    public virtual Tile TexturePosition(Direction direction)
    {
        Tile tile = new Tile();
        tile.x = 0;
        tile.y = 0;

        return tile;
    }

    public virtual Vector2[] FaceUVs(Direction direction)
    {
        Vector2[] UVs = new Vector2[4];
        Tile tilePos = TexturePosition(direction);

        UVs[0] = new Vector2(tileSize * tilePos.x + tileSize,
            tileSize * tilePos.y);
        UVs[1] = new Vector2(tileSize * tilePos.x + tileSize,
            tileSize * tilePos.y + tileSize);
        UVs[2] = new Vector2(tileSize * tilePos.x,
            tileSize * tilePos.y + tileSize);
        UVs[3] = new Vector2(tileSize * tilePos.x,
            tileSize * tilePos.y);

        return UVs;
    }

    public virtual bool IsSolid(Direction direction)
    {
        switch (direction)
        {
            case Direction.north:
                return true;
            case Direction.east:
                return true;
            case Direction.south:
                return true;
            case Direction.west:
                return true;
            case Direction.up:
                return true;
            case Direction.down:
                return true;
        }

        return false;
    }















}

Example of a Block type:


using UnityEngine;
using System.Collections;

[System.Serializable]

public class BlockStone : Block
{

    public BlockStone()
        : base()
    {

        this.temperature = 0;
        this.light = false;
        this.density = 400;
        this.BlockName = "Stone";

    }

    public override Tile TexturePosition(Direction direction)
    {
        Tile tile = new Tile();

        tile.x = 3;
        tile.y = 1;

        return tile;
    }

}

Thanks in advance!

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

Advertisement

If all that is different between block types is a few field/parameter values you should not create subtypes, this will add complexity to your code that is just pointless. It is best to favor composition in place of inheritance where possible as it makes your design more fluid to change if you realise you have boxed yourself into a corner etc.

To get the same effect you simply need a static on your Block object call BuildStoneBlock() or similar that is as follows

public static Block BuildStoneBlock()

{

return new Block

{

temperature = 0,
light = false,
density = 400,
BlockName = "Stone"

};

}

or even this if blocks can change type a lot

public static void MakeBlockIntoStone(Block block)

{

block.temperature = 0;

block.light = false;
block.density = 400;
block.BlockName = "Stone";

}

You could even argue that if all the difference is are some parameters and these do not change, so density of all stone blocks is 400 and say 100 for some other block type you could extract this data to a lookup table and have a field/property called something like blockType that has a string/enum which acts as the lookup key

This would in turn give you zero memory churn as you simply change the block type and it then gains the standard attributes of that type of block.

var density = blockAttributes[block.BlockType].density;

You could cache the attributes reference in the block if you so desire to speed up lookup and avoid the dict lookup each time. This attributes could also include your Tile if for stone if the x/y are just fixed texture offsets into a tilemap and always 3/1 for stone.

If you go this route make the attribute objects immutable otherwise a mistake like changing the density of rock would effect all rock blocks :)

Also, unless you really need Blocks to have string names, I think I'd go with an enumerated type, that string is probably your biggest field in that class.

As an aside, does it matter how members are placed for alignment in a C# class? My C++ sense wants me to rearrange everything to be largest member variable first, then smallest.

If all that is different between block types is a few field/parameter values you should not create subtypes, this will add complexity to your code that is just pointless. It is best to favor composition in place of inheritance where possible as it makes your design more fluid to change if you realise you have boxed yourself into a corner etc.

To get the same effect you simply need a static on your Block object call BuildStoneBlock() or similar that is as follows

public static Block BuildStoneBlock()

{

return new Block

{

temperature = 0,
light = false,
density = 400,
BlockName = "Stone"

};

}

or even this if blocks can change type a lot

public static void MakeBlockIntoStone(Block block)

{

block.temperature = 0;

block.light = false;
block.density = 400;
block.BlockName = "Stone";

}

You could even argue that if all the difference is are some parameters and these do not change, so density of all stone blocks is 400 and say 100 for some other block type you could extract this data to a lookup table and have a field/property called something like blockType that has a string/enum which acts as the lookup key

This would in turn give you zero memory churn as you simply change the block type and it then gains the standard attributes of that type of block.

var density = blockAttributes[block.BlockType].density;

You could cache the attributes reference in the block if you so desire to speed up lookup and avoid the dict lookup each time. This attributes could also include your Tile if for stone if the x/y are just fixed texture offsets into a tilemap and always 3/1 for stone.

If you go this route make the attribute objects immutable otherwise a mistake like changing the density of rock would effect all rock blocks :)

The problem is, I want certain blocks to have different behaviors and functions. That's why I'm having trouble deciding the best way to do this.

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

The problem is, I want certain blocks to have different behaviors and functions. That's why I'm having trouble deciding the best way to do this.

I think you somewhat mix up what "data" and "functionality" is :unsure:, hence you are trying to solve problems with polymorphism which do not even require it (meaning: it is overkill). This is pretty much detrimental for multiple reasons, since it clutters/complicates your code, and too much virtual calls, especially unnecessary ones, may become a performance hit (not a big one, but still)...
If a problem does not require polymorphism do not make the function virtual. If later it turns out, that some specialization is required you can change it than, or you can come up with an entirely different design based on the new requirements (maybe even more efficient/elegant based on the new facts/requirements).
Looking at the member functions of the "Block" class, clearly non of the methods seem to be doing stuff that should be or need-to-be overridden by specific Block implementations...
The "TexturePosition" specialization example you posted in the "BlockStone" class is a pretty bad one too. The funtion does not modify the old functionality at all, it simply does the same, returning data. Indeed a different tile, but it still simply returns a tile, which screams for it's own field/property instead, and not a virtual function.

On the other hand, what ferrous proposed is actually what you need. It is usually called "Type object pattern" or something pretty similar. Here is good summary (and example implementation) about this design pattern:
http://gameprogrammingpatterns.com/type-object.html

And before you think it will limit you in giving specializations/behaviors for each block type, it will not!
Here is a simple implementation for your case:


// I could not identify which fields/properties compose the "actual state" of your blocks...
// you have to separate the "state", and "description" of the blocks, since the "description"
// belongs to the "Type" of the block, and not to each instance of the "Block" object!
// in your current implementation you have a myriad of redundant data, wasting an insane amount of memory.
// so the fields in this class are based on guesses made upon reading your code.

// this class "describes" a specific block, like: Rock, Sand, Water etc...
public class BlockType
{
    public int temperature;
    public bool light;
    public int density;
    public string blockName;
    public Tile tile;
    
    // it can hold behaviours too, specific to a given block type
    // like in case of water or magma you could implement different collision reactions etc...
    // simply you specialize this class: public class WaterBlockType : BlockType { ... }
}

public class Block
{
    public int life;
    public BlockType type;
    
    // behavior specific to block instances...
}

// creating block types:
var typeStone = new BlockType
{
    temperature = 0,
    light = false,
    density = 400,
    blockName = "Stone",
    tile = new Tile { x = 3, y = 1 }
};
var typeSand = new BlockType
{
    temperature = 0,
    light = false,
    density = 100,
    blockName = "Sand",
    tile = new Tile { x = 1, y = 0 }
}

// creating blocks:
var stone1 = new Block { life = 10, type = typeStone };
var stone2 = new Block { life = 15, type = typeStone };
var sand1 = new Block { life = 2, type = typeSand };
var sand2 = new Block { life = 1, type = typeSand };
// ...

// your block instances can have methods/behaviors like:
stone1.DamagedByPlayer(2);
stone2.ReinforcedByPlayer(1);

// once you want to implement a specific behavior, which are specific to a block type
// (like water acts this way, magma acts that way...)
// you can still implement and execute behaviors specific to the type of the blocks:
sand1.type.CreateBlockdata(chunck, 10, 20, 30, mesh);
sand2.type.HandleCollisionEventWithPlayer(player);

// here is another neat trick:
// with this system, you can easily create a library of block types, and a block "Factory":
public class BlockFactory
{
    private Dictionary<string, BlockType> types = new Dictionary<string, BlockType>();
    
    public void AddType(BlockType type) {
        this.types.Add(type.blockName, type);
    }
    
    public Block CreateType(string typeName, int life) {
        return new Block { life = life, type = this.types[typeName] };
    }
}

var factory = new BlockFactory();
factory.AddType(typeStone);
factory.AddType(typeSand);

var stone3 = factory.CreateType("Stone", 1);
var sand3 = factory.CreateType("Sand", 10);

So to sum it up in a short sentence: you have to separate data from the blocks which are not actual state relevant to block instances, but relevant to a specific block types (like water, magma, stone etc...), and you have to do the same with behaviors too!
Think of these as two separate concepts, so they most probably should be different classes/objects (an object which represents a block in the world, and an object which describes a given type of blocks, like water or stone) :wink:.

Blog | Overburdened | KREEP | Memorynth | @blindmessiah777 Magic Item Tech+30% Enhanced GameDev

The problem is, I want certain blocks to have different behaviors and functions. That's why I'm having trouble deciding the best way to do this.

I think you somewhat mix up what "data" and "functionality" is :unsure:, hence you are trying to solve problems with polymorphism which do not even require it (meaning: it is overkill). This is pretty much detrimental for multiple reasons, since it clutters/complicates your code, and too much virtual calls, especially unnecessary ones, may become a performance hit (not a big one, but still)...
If a problem does not require polymorphism do not make the function virtual. If later it turns out, that some specialization is required you can change it than, or you can come up with an entirely different design based on the new requirements (maybe even more efficient/elegant based on the new facts/requirements).
Looking at the member functions of the "Block" class, clearly non of the methods seem to be doing stuff that should be or need-to-be overridden by specific Block implementations...
The "TexturePosition" specialization example you posted in the "BlockStone" class is a pretty bad one too. The funtion does not modify the old functionality at all, it simply does the same, returning data. Indeed a different tile, but it still simply returns a tile, which screams for it's own field/property instead, and not a virtual function.

On the other hand, what ferrous proposed is actually what you need. It is usually called "Type object pattern" or something pretty similar. Here is good summary (and example implementation) about this design pattern:
http://gameprogrammingpatterns.com/type-object.html

And before you think it will limit you in giving specializations/behaviors for each block type, it will not!
Here is a simple implementation for your case:


// I could not identify which fields/properties compose the "actual state" of your blocks...
// you have to separate the "state", and "description" of the blocks, since the "description"
// belongs to the "Type" of the block, and not to each instance of the "Block" object!
// in your current implementation you have a myriad of redundant data, wasting an insane amount of memory.
// so the fields in this class are based on guesses made upon reading your code.

// this class "describes" a specific block, like: Rock, Sand, Water etc...
public class BlockType
{
    public int temperature;
    public bool light;
    public int density;
    public string blockName;
    public Tile tile;
    
    // it can hold behaviours too, specific to a given block type
    // like in case of water or magma you could implement different collision reactions etc...
    // simply you specialize this class: public class WaterBlockType : BlockType { ... }
}

public class Block
{
    public int life;
    public BlockType type;
    
    // behavior specific to block instances...
}

// creating block types:
var typeStone = new BlockType
{
    temperature = 0,
    light = false,
    density = 400,
    blockName = "Stone",
    tile = new Tile { x = 3, y = 1 }
};
var typeSand = new BlockType
{
    temperature = 0,
    light = false,
    density = 100,
    blockName = "Sand",
    tile = new Tile { x = 1, y = 0 }
}

// creating blocks:
var stone1 = new Block { life = 10, type = typeStone };
var stone2 = new Block { life = 15, type = typeStone };
var sand1 = new Block { life = 2, type = typeSand };
var sand2 = new Block { life = 1, type = typeSand };
// ...

// your block instances can have methods/behaviors like:
stone1.DamagedByPlayer(2);
stone2.ReinforcedByPlayer(1);

// once you want to implement a specific behavior, which are specific to a block type
// (like water acts this way, magma acts that way...)
// you can still implement and execute behaviors specific to the type of the blocks:
sand1.type.CreateBlockdata(chunck, 10, 20, 30, mesh);
sand2.type.HandleCollisionEventWithPlayer(player);

// here is another neat trick:
// with this system, you can easily create a library of block types, and a block "Factory":
public class BlockFactory
{
    private Dictionary<string, BlockType> types = new Dictionary<string, BlockType>();
    
    public void AddType(BlockType type) {
        this.types.Add(type.blockName, type);
    }
    
    public Block CreateType(string typeName, int life) {
        return new Block { life = life, type = this.types[typeName] };
    }
}

var factory = new BlockFactory();
factory.AddType(typeStone);
factory.AddType(typeSand);

var stone3 = factory.CreateType("Stone", 1);
var sand3 = factory.CreateType("Sand", 10);

So to sum it up in a short sentence: you have to separate data from the blocks which are not actual state relevant to block instances, but relevant to a specific block types (like water, magma, stone etc...), and you have to do the same with behaviors too!
Think of these as two separate concepts, so they most probably should be different classes/objects (an object which represents a block in the world, and an object which describes a given type of blocks, like water or stone) :wink:.

Thank you! That was extremely helpful, and I'm implementing it now.

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

Works beautifully.

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

This topic is closed to new replies.

Advertisement