Using Scriptable objects

Hello everyone !
I am making a game right now and i need advice on the best way to do one of the step.
I have 3 types of scriptable objects (demon Card, Good Card, Bad Card) (same script). The demon one is shown below.

The next step for me would be to be able to instantiate in a scene the background + the type sprite+ the action (text) depending on the game event. If the player arrives on a demon node this would generate a scene using the demon card scriptable object (1 background and 1 text chosen randomly). For the random part i get it, i just have to fetch the index of the arrays. For the generation part, i just created a scene and i will attach a script to the canvas and link each component of the scriptable objects to my different game objects of the tree. It is in this script that i will link randomly with the scriptable object content.


My question is, how will i be able to choose which scriptable object to use in other part of my game, other scripts ? Like if i happen to be on a demon node in an other scene, how do i call the scene created by the specific demon scriptable object. Do i have to create 3 canvas ? Should i create multiple function ?
Thanks,

i am starting to doubt about the [serializefield] in the scriptable object because it looks like i can t access the value after like an array

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);
}