• Advertisement
Sign in to follow this  

Unity Adding Subtypes Of A Class [Optimizing Blocks]

This topic is 669 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

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!

Share this post


Link to post
Share on other sites
Advertisement

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.

Edited by ferrous

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

 

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:.

Share this post


Link to post
Share on other sites

 

 

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.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
  • Advertisement
  • Popular Tags

  • Advertisement
  • Popular Now

  • Similar Content

    • By Manuel Berger
      Hello fellow devs!
      Once again I started working on an 2D adventure game and right now I'm doing the character-movement/animation. I'm not a big math guy and I was happy about my solution, but soon I realized that it's flawed. My player has 5 walking-animations, mirrored for the left side: up, upright, right, downright, down. With the atan2 function I get the angle between player and destination. To get an index from 0 to 4, I divide PI by 5 and see how many times it goes into the player-destination angle.

      In Pseudo-Code:
      angle = atan2(destination.x - player.x, destination.y - player.y) //swapped y and x to get mirrored angle around the y axis
      index = (int) (angle / (PI / 5));
      PlayAnimation(index); //0 = up, 1 = up_right, 2 = right, 3 = down_right, 4 = down

      Besides the fact that when angle is equal to PI it produces an index of 5, this works like a charm. Or at least I thought so at first. When I tested it, I realized that the up and down animation is playing more often than the others, which is pretty logical, since they have double the angle.

      What I'm trying to achieve is something like this, but with equal angles, so that up and down has the same range as all other directions.

      I can't get my head around it. Any suggestions? Is the whole approach doomed?

      Thank you in advance for any input!
       
    • By khawk
      Watch the latest from Unity.
       
    • By GytisDev
      Hello,
      without going into any details I am looking for any articles or blogs or advice about city building and RTS games in general. I tried to search for these on my own, but would like to see your input also. I want to make a very simple version of a game like Banished or Kingdoms and Castles,  where I would be able to place like two types of buildings, make farms and cut trees for resources while controlling a single worker. I have some problem understanding how these games works in the back-end: how various data can be stored about the map and objects, how grids works, implementing work system (like a little cube (human) walks to a tree and cuts it) and so on. I am also pretty confident in my programming capabilities for such a game. Sorry if I make any mistakes, English is not my native language.
      Thank you in advance.
    • By Ovicior
      Hey,
      So I'm currently working on a rogue-like top-down game that features melee combat. Getting basic weapon stats like power, weight, and range is not a problem. I am, however, having a problem with coming up with a flexible and dynamic system to allow me to quickly create unique effects for the weapons. I want to essentially create a sort of API that is called when appropriate and gives whatever information is necessary (For example, I could opt to use methods called OnPlayerHit() or IfPlayerBleeding() to implement behavior for each weapon). The issue is, I've never actually made a system as flexible as this.
      My current idea is to make a base abstract weapon class, and then have calls to all the methods when appropriate in there (OnPlayerHit() would be called whenever the player's health is subtracted from, for example). This would involve creating a sub-class for every weapon type and overriding each method to make sure the behavior works appropriately. This does not feel very efficient or clean at all. I was thinking of using interfaces to allow for the implementation of whatever "event" is needed (such as having an interface for OnPlayerAttack(), which would force the creation of a method that is called whenever the player attacks something).
       
      Here's a couple unique weapon ideas I have:
      Explosion sword: Create explosion in attack direction.
      Cold sword: Chance to freeze enemies when they are hit.
      Electric sword: On attack, electricity chains damage to nearby enemies.
       
      I'm basically trying to create a sort of API that'll allow me to easily inherit from a base weapon class and add additional behaviors somehow. One thing to know is that I'm on Unity, and swapping the weapon object's weapon component whenever the weapon changes is not at all a good idea. I need some way to contain all this varying data in one Unity component that can contain a Weapon field to hold all this data. Any ideas?
       
      I'm currently considering having a WeaponController class that can contain a Weapon class, which calls all the methods I use to create unique effects in the weapon (Such as OnPlayerAttack()) when appropriate.
  • Advertisement