You don’t need to make all your fields public; you can make all of them private, and then define your own properties and methods that other classes can use to interact with them. And most crucially: try to define as few and as simple public members as possible (while still satisfying the use case of the class)!
The more fields you are able to hide from the outside, and the simpler you can make the public-facing parts of your classes, the easier it will be to keep working in your code base in the long run (read: way fewer bugs; much easier to make changes).
Try to think of simple, intuitive abstractions, like Card and Deck. Then implement them in a way that makes intuitive sense. For example, does a single physical “card” in the real world have multiple different backgrounds? No. So perhaps your Card class also should only contain a single background. Like this:
public sealed class Card
{
public CardAction Action { get; }
public CardType Type { get; }
public Sprite Background { get; }
public Card(CardAction action, CardType type, Sprite background)
{
Action = action;
Type = type;
Background = background;
}
}
Then what could you call an object that provides random cards? In the real world a “deck” of cards would usually serve this function. So perhaps this would make for a good abstraction for the game as well:
public abstract class CardDeck : ScriptableObject
{
public abstract Card GetNextCard();
}
[CreateAssetMenu]
public sealed class RandomCardDeck : CardDeck
{
[SerializeField]
private CardActionCollection actions;
[SerializeField]
private SpriteCollection type;
[SerializeField]
private SpriteCollection backgrounds;
public override Card GetNextCard()
{
return new Card(GetRandomAction(), GetRandomBackground());
CardAction GetRandomAction() => actions[Random.Range(0, actions.Count)];
Sprite GetRandomBackground() => backgrounds[Random.Range(0, backgrounds.Count)];
}
}
Notice how RandomCardDeck defines three fields, but only exposes a single method for the outside. Doing stuff like this helps reduce the overall complexity in your code base, by pushing complexity down into the implementation details of your classes, which the users of your classes don’t need to know anything about.
All the clients of this class need to do is call GetNextCard() and that’s it.
Next up, something that “renders” a card for the camera could perhaps be called simply a “card renderer”.
public sealed class CardRenderer : MonoBehaviour
{
[SerializeField]
private TMP_Text action;
[SerializeField]
private Image background;
public Card Card { get; private set; }
public void SetCard(Card card)
{
Card = card;
action.text = card.Action;
background.sprite = card.Background;
}
}
Again, clients of this class don’t need to know that is uses a TextMeshPro component and an Image component internally; those are unimportant implementation details. Just pass it a Card, and it’ll handle rendering it somehow.
Then all we’d need after this is a component that draws a card from a deck and assigns it into a card renderer.
[RequireComponent(typeof(CardRenderer))]
public sealed class LoadCardFromDeck : MonoBehaviour
{
[SerializeField]
private CardDeck fromDeck;
[SerializeField]
private CardRenderer toRenderer;
private void Awake() => toRenderer.SetCard(fromDeck.GetNextCard());
}
Implementation details
public abstract class Collection<TItem> : ScriptableObject
{
[SerializeField]
private TItem[] items;
public TItem this[int index] => items[index];
public int Count => items.Length;
}
[CreateAssetMenu]
public sealed class CardActionCollection : Collection<CardAction> { }
[CreateAssetMenu]
public sealed class SpriteCollection : Collection<Sprite> { }
public abstract class CardAction : ScriptableObject
{
[SerializeField]
private string text;
public string Text => text;
public abstract void Apply(ICardActionTarget target);
public static implicit operator string(CardAction action) => action.text;
}
[CreateAssetMenu]
public sealed class CardType : ScriptableObject
{
[SerializeField]
private Sprite icon;
public Sprite Icon => icon;
public static implicit operator Sprite(CardType type) => type.icon;
}
public interface ICardActionTarget
{
void Attack(DamageType damageType, int amount);
void Heal(int amount);
}