Command Pattern : how to create Commands at runtime and use them as a ScriptableObject ?

Hi,

I seem to keep doing the same code design error.

Lets says I have my base Command class :


public interface ICommand
{
    void Execute();
}

So I can create specific command like this :


[System.Serializable]
public class FireBallCommand : ICommand
{
    public int damage;

    public FireBallCommand(int damage) {
         this.damage = damage;
    }

    public void Execute()
    {
        Debug.Log("Cast fire ball" + damage);
    }
}

So now I can create at runtime my command like this :


ICommand c = new FireBallCommand(5);
c.Execute();

But I also want be able to store them as a Scriptable Object, because I would like to create predefine Command that I can put inside Cards. But now the problem is that the only way I can do it is like this :


[CreateAssetMenu]
public class ScriptableFireBallCommand : ScriptableObject
{
    public FireBallCommand fireBallCommand;
}

So now I need to have a duplicate script for every one of my Command and remember to create it, which seems like my design is flawed. What can I do about this? Do I really need to only do Scriptable Command, or only do Runtime Command?

Thank you!

You could in theory write an implicit conversion for your scriptable objects: User-defined explicit and implicit conversion operators - provide conversions to different types - C# reference | Microsoft Learn

This will work best with a base class to, like so:

public abstract class CommandObjectbase : ScriptableObject
{
    public abstract ICommand GetCommand();

    public static implicit operator ICommand(CommandObjectbase cmdBase) => cmdBase.GetCommand();
}

Then you derive from this and implement what you need in GetCommand(), which allows you to freely use the SOā€™s to assign to an ICommand field.

1 Like

Hi! Thank you for the answer @spiney199 . I never knew about implicit conversion. But Iā€™m just getting an error for :

public static implicit operator ICommand(CommandObjectbase cmdBase) => cmdBase.GetCommand();

I get the error User defined-conversion to interface
Rider suggest to me to change it for :

public static ICommand ToICommand(CommandObjectbase cmdBase) => cmdBase.GetCommand();

But I am still confused, if FireBallCommand inherit from CommandObjectBase, I canā€™t create my Command like this :

ICommand c = new FireBallCommand(5);
c.Execute();

Because that mean I am creating a Scriptable Object at Runtime and I would need to use CreateInstance and Iā€™m note sure I want this.

Thanks!

I wrote it in notepad so I admit I donā€™t know if it flies syntax-wise or not. Though is it an error or just a warning?

No you donā€™t want to be creating SOā€™s like this at run time. Youā€™d need a reference to one in the inspector, and use that to get a copy of a command. The implicit operator is just a way to shorthand this.

1 Like

You oughta just make a component that handles this. It would be how to use Unity correctly and more flexibly than what I perceive of your inheritance schema. The Inspector is your friend and the most powerful tool you have for rapid iteration and balancing. You cannot inspect an interface and I personally despise them for slowing me down and not being able to grab a persistent reference to them.

@spiney199 It gives an error. Iā€™m not sure what would I do after getting a copy because I would still need to change values, like in this example the int attack of the fireball. Thanks!

You technically can with SerializeReference and have been able to for many years, but it does need custom inspector work to make it possible. Thereā€™s plenty of addons to make it easy to use. I use it with great regularity.

It can also be used to serialise polymorphic classes.

Fair about the error, but the copy should have the values already set. The point of SOā€™s is to make instances of them with the values set up.

For example:

[CreateAssetMenu(menuName = "Command Objects/Spells/Fireball")]
public class FireballCommandObject : CommandObjectbase
{
    [SerializeField]
    private int damage;

    public virtual ICommand GetCommand()
    {
        return new FireBallCommand(damage); //returns a new command constructed with specified damage
    }
}
1 Like

@spiney199 Oh okay thanks a lot I understand better! But then I still needs to create a script FireBallCommand and a script FireBallCommandObject to have a FireBall, but itā€™s a better way to do it then making my FireBallCommand Serialized and then putting it inside a FireBallScriptableObject like I tried in my initial post? Thanks!

Well you need some manner in which to author this data donā€™t you?. If you intend to have fireballs of different damage, then scriptable objects generally are the most straight forward Unity native way to do this.

THAT SAID, the command pattern is only really necessary if you plan to have some undo/redo functionality. If you donā€™t intend to, then thereā€™s no point.

@spiney199 Yes I want to do a undo/redo functionality in a card, just like if you could replay your entire game in slay the splire because every thing was a Command. The thing is that sometime I want my Command to be inside a card so Scriptable Object are perfect, but sometime, for example moving a unit by 1, 3, or 15 etc., I just need it create the command in runtime. But I of course canā€™t create multiple scriptable object instance ā€œMove By 1ā€, ā€œMove by 3ā€, ā€œMove by 15ā€, etc. So iā€™m never sure if I want to create my architecture based on a Scriptable Command or and Interface ICommand, but when I try to do both everything seems to fail.

Edit

Also, that mean that now I would need to have a duplicate int damage inside my FireBallCommandObject and also inside my FireBallCommand which seems a bit scary for me, because I need it for my FireBallCommand constructor. Sorry If Iā€™m probably not understand everything, I am still learning.

I wouldnā€™t be scared, youā€™re just passing a value from one place to another, which is pretty bog standard as far as coding goes.

Nor would I get to overly concerned with what youā€™re doing. I feel like youā€™re hitting a bit of analysis paralysis here. Just go what what your head takes you and should you hit a bit of a wall, thereā€™s a chance to revise and learn.

In any case, command systems generally have a central point at which all commands are run through to execute. You could easily just have two method overloads, one that takes an ICommand, one that takes say, a CommandObjectBase:

public void ExecuteCommand(ICommand command)
{
    command.Execute();
    //add command to stack
}

public void ExecuteCommand(CommandObjectBase commandObject)
{
    ICommand command = commandObject.GetCommand();
    command.Execute();
    //add command to stack
}

So regardless whether you create one on the fly, or use a scriptable object, your command system shouldnā€™t care.

1 Like

Why not make your SO itself an ICommand?

public class FireCommand : ScriptableObject, ICommand
{
    [SerializeField]
    private float damage;

    public void Execute()
    {
       // Do damage...
    }
}

@spiney199 thanks a lot for your help! I really appreciate it!

I agree, but I tend to do what I wanted to do in unity way too quickly without planning and I kept hitting walls and now I want to plan everything but sometimes I need to just do it and see where it guides me.

There is also another option that I know is working but scared to use because I donā€™t really know why it works. But with Odin Inspector I can do a SerializedScriptableObject, where I can simply just create an empty ICommand command like this :

public class SerializedCommandObjectBase : SerializedScriptableObject
{
    public ICommand command;
}

Then I can simply create a scriptable object instance with the script I want and fill up the details I want like this :

This solution avoid a duplicate scriptable object script for every Command script. Do you think this is a good solution or itā€™s dangerous because I donā€™t fully quite understand the Serialized aspect of the Scriptable object?

I also received the suggestion to do a Generic Scriptable Class :

public class ScriptableCommand<T> : ScriptableObject where T : ICommand {
    public T Command;
  
    public void Execute() {
        Command.Execute();
    }
}

And use a asset like this : GitHub - SolidAlloy/GenericUnityObjects: Generic UnityEngine.Objects to create my Scriptable Generic

Well it works because the Odin serialiser can serialise just about anything you throw at it. While you shouldnā€™t (and canā€™t) use it with prefabs, Odin serialisation is pretty damn stable on scriptable objects.

Iā€™ll note that you donā€™t need to use Odin serialisation if you do own Odin. As mentioned already, SerializeReference can serialise plain classes implementing interfaces without the added overhead of Odinā€™s serialisation.

public class SerializedCommandObjectBase : ScriptableObject
{
    [SerializeReference]
    private ICommand command;

    public ICommand GetCommand() => //get command copy
}

Just remember you want copies of the command, not the serialised command instance itself, otherwise youā€™ll run into issues having the same object referenced in multiple locations.

I wouldnā€™t do this, as it boxes you in. With this method you canā€™t have collections of these scriptable objects with different command types.

If you have Odin I would 100% go down the SerializeReference route.

1 Like

Worth pointing out this will fall into similar issue as I mentioned above. When you deal with the command pattern you want instances/copies of commands, as commands generally have their own per instance data they manage. This isnā€™t going to be the case with scriptable objects unless you Instantiate/CreateInstance them, which I think is pointless and you should just deal with plain classes.

1 Like

@spiney199 Thanks a lot, I have almost have it all figured out! The only things that is stopping me is that I canā€™t seem to be able to clone a copy of a ICommand, because it is a an interface and I canā€™t just return a new interface.

This is what it look like now :

ICommand.cs

public interface ICommand
{
    void Execute();
}

FireBallObject.cs

[CreateAssetMenu]
public class FireBallObject : SerializedCommandObjectBase { }

public class FireBallCommand : ICommand
{
    public int damage;

    public IceShardCommand(int damage)
    {
        this.damage = damage;
    }

    public void Execute()
    {
        Debug.Log("Damage Ice + " + damage);
    }
}

SerializedCommandObjectBase.cs

public abstract class SerializedCommandObjectBase : ScriptableObject
{
    [SerializeReference]
    [ShowInInspector]
    public ICommand command;
  
    public ICommand GetCommand() => command; //How can I say that I want a new copy of the command here?
}

Outline a method in your interface for the copy, which all your command can implement by using a constructor that takes in a parameter of itself.

public interface ICommand
{
    public void Execute();
 
    public ICommand GetCopy();
}
[System.Serializable] //this is required to be visible in inspector
public class FireBallCommand : ICommand
{
    public int damage;

    public FireBallCommand(int damage)
    {
        this.damage = damage;
    }
 
    private FireBallCommand(FireBallCommand command)
    {
        this.damage = command.damage;
    }
    public void Execute()
    {
        Debug.Log("Damage Ice + " + damage);
    }
 
    public ICommand GetCopy()
    {
        return new FireBallCommand(this);
    }
}

Then it becomes:

[CreateAssetMenu(menuName = "Commands/Command Object")]
public class CommandObject : ScriptableObject
{
    [SerializeReference]
    private ICommand command;
 
    public ICommand GetCommand() => command.GetCopy();
}

So on that note:

[CreateAssetMenu]
public class FireBallObject : SerializedCommandObjectBase { }

This is not needed. You donā€™t need a derived type if youā€™re not doing anything with it. You could just make instances of the ā€˜baseā€™ object (it no longer is a base object at that point, mind you) without the need to make derived types.

You also donā€™t want to use ShowInInspector and SerializeReference, just the latter. ShowInInspector doesnā€™t serialise values so it can easily lead to errors. Just make sure all your classes implementing ICommand are decorated with System.Serializable so they can be properly serialised.

1 Like

@spiney199 Thank you! I really really appreciate your help!!. I understand everything now. Iā€™m just scared that I forget to put [System.Serializable], redo the two different constructor and fill without error the GetCopy() Function for every new Command that I create. But I am probably just overthinking everything, now that Iā€™m trying to do a bigger project than usual.