Best way to implement scriptable objects and SOLID code when working with objects of same general type, but each have their own set unique attributes?

The idea is that the player has a few block types to choose from, with each block type itself having specific sub types.


For example, the player has the option of choosing a red block or a blue block. Not only that, but the player also needs to know what specific kind of red block or blue block it is. That means that the red block can have different attributes, like it can be a block of fire, a block that causes an instant game over, a block that takes away health, etc.

In the case of the blue block, it could either be a block of ice, a block of water, a block that gives health, etc.


What I would like to know is what would be the best way to handle this using scriptable objects and keeping things abstract? For example, I was thinking about having a scriptable object that acts as a database for the blocks. The player class would ask the database to give it a block according to its color and attribute.


A method like the one below won’t work because I don’t want the player to be able ask the database for a blue block, but with an attribute that should only belong to a red block.

GetBlock(Color.Blue, Attribute.Fire);

public Block GetBlock(Color color, Attribute attribute) 
{
} 

I know an alternative would be to create methods that ask to get a specific block like in the method below.

public BlueBlock GetBlueBlock(BlueBlockAttribute attribute)
{
} 

However, the issue with this is that it’s too specific, and isn’t really SOLID code as I would have to keep adding methods to this script every time I want to introduce a new block color.

I have not used scriptable objects before, but I think this should still apply.

Do all the attributes belong to specific block colors? For example, does the GameOver attribute only get applied to the RedBlock? If so then you can simplify your public Block GetBlock(Attribute attribute) and have it contain the logic for determining which block to return based on the requested attribute. Then just add that method to a factory object.

In my game I need random quest objectives, so I use the following factory code to generate them. It gets a list of all classes derived from Objective and then creates a random when when CreateGoal(...) is called. You could modify it to return the appropriate Block based upon the params passed in. You would still need to update the logic used to create specific blocks whenever you add new ones, however, you have to do that anyway. Just like if I create a derived Objective that requires more than a float I will have to update my code, however, for now I don’t have to touch the factory as long as all derived classes only require a float input.

using System;
using System.Collections.Generic;
using System.Linq;

[System.Serializable]
public static class ObjectiveFactory
{
    #region Members
    private static Random _rand = new Random();
    private static IEnumerable<Type> _goals;
    #endregion Member

    #region Ctor
    static ObjectiveFactory()
    {
        _goals = typeof(Objective).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(Objective)) && !x.IsAbstract);
    }
    #endregion Ctor

    #region Methods
    public static Objective CreateGoal(float targetModifier)
    {
        var ans = _goals.ElementAt(_rand.Next(_goals.Count()));
        return (Objective)Activator.CreateInstance(ans, targetModifier);
    }
    #endregion Methods
}