Search code examples
c#unity-game-enginepropertiesactionfunc

Is it possible to use a Func<T> generic as a property that can be dynamically populated with a method?


I am working on a card game of sorts and I've been trying to figure out how to handle unique card effects on the Card class. I've included the class below. I'm trying to add an "effect" property to the class that can be populated in the constructor or later via a setter if needed. The problem I have is I don't know if this method will have a return type or the same return type. The simple way would seem to be to say that effect methods in the game cannot return values and simply define the property as an Action but it's definitely nice to be able to return something in certain situations (mostly testing situations where you might want to confirm the proper Card or List<Card> is returned or even the proper player or deck object).

I'm extremely new to c# and am probably missing something or misunderstanding generics. I've added type object because that gets me out the door with the getter and setter but I don't know if object will inherently allow me to return a class the isn't actually extended from object.

using System;
[System.Serializable]

public class Card
{
  public int Id { get; set; }
  public string Title { get; set; }
  public int Cost { get; set; }
  public int Power { get; set; }
  public string Description { get; set; }
  public Func<object> Effect { get; set; }
  
  public Card()
  {

  }

  public Card(int id, string title, int cost, int power, string description, Func<object> effect)
  {
    Id = id;
    Title = title;
    Cost = cost;
    Power = power;
    Description = description;
    Effect = effect;
  }
}

I initially tried just defining it as:

private Func<> effect;

public Func<> Effect
{
  get { return effect; }
  set { effect = value; }
}

I expected that without defining a type it would allow me to return a non-null type but I suspect I'm misunderstanding the usage of generics here.


Solution

  • Proper use of a generic would be e.g.

    private Func<T> effect;
    public Func<T> Effect => effect;
    

    This would require though that Card itself is generic as well (Card<T>) with the same type restrictions.

    The only exception is having a method which allows you to pass in the generic type once you call it

    public Func<T> GetEffect<T>()
    {
        return ???;
    }
    

    This very much depends on your other code though and how exactly you are intending to consume this property. If you go for generics you will also need to explicitly provide a type when you consume it.

    So I just claim now this is most probably not what you wanted to do anyway.


    I think instead you might want to rather have something like e.g.

    public interface ICardEffect
    {
        public void RunEffect(/*whatever you need like e.g. GameManager etc*/);
    }
    

    and then rather

    public ICardEffect Effect { get; private set; }
    

    this you can pass in via the constructor and consume properly without needing to know/return the exact implementing type.

    This will still make little sense in a [Serializable] class which you configure via the Inspector which will not use any special constructor but rather the default parameter-less one.


    Alternative Approach

    I think in your Unity specific case you would be better of using ScriptableObject assets. A ScriptableObject is mainly intended to be a "data container" but in the end it is a c# class so it can also implement own behavior. Something I use to call "scriptable behavior".

    [CreateAssetMenu]
    public class Card : ScriptableObject
    {
        //[field: SerializeField] public int Id { get; private set;} // don't really need this as the asset reference is unique already 
        //[field: SerializeField] public string Title { get; private set;} // don't really need this as the asset itself already has a name 
        [field: SerializeField] public int Cost { get; private set;}
        [field: SerializeField] public int Power { get; private set;}
        [field: SerializeField] public string Description { get; private set;}
    
        [field: SerializeField] public CardEffect Effect { get; private set;}
    }
    

    You create instances of this via the Assets -> right click -> Create -> Card

    And then populate it via the Inspector. Later you can use this just as you would e.g. Textures and other assets and drag it into your game managers list for instance.

    And then for the CardEffect you do the same and have e.g.

    public abstract class CardEffect : ScriptableObject
    {
        public abstract void RunEffect(/*whatever you need like e.g. GameManager etc*/);
    }
    

    and then create specific effects like e.g. (I don't know your use case and types of course)

    [CreateAssetMenu]
    public class DrawEffect : CardEffect
    {
        [SerializeField][Min(1)] int amount = 1;
    
        public override void RunEffect(GameManager game)
        {
            game.DrawCard(amount);
        }
    }
    

    you can create as many subtypes of effects as you wish and reference them in your Card instances. You can even make the Card.Effect rather a list and chain combine multiple effects together.