Card Effects for a Game - "Bad" coding vs "Good" coding.

So, I’m making a “Slay the spire” type card game and I have found two ways to create card effects.

The first and this is the one that’s already coded into the game is one that seems to be “Bad coding”.

I have a single class where I code Card Effects into their separate methods. When I play a card I send this class a Message with the card’s name which then plays the method. Worked alright so far but it seemed “wrong”

So I was looking for a better way to do that. I created a base “CardEffect.cs” which I could then create scripts to Inherit that class and every effect will have its own separate script. Problem is, the Card object is a Scriptable Object and I can’t just attach the script to it, so I had to make “CardEffect.cs” into a scriptable object too so I could then create ScriptableObjects of each separate Card.

So you see, the “Bad coding” approach is simple, a single script and whenever I have a new card I go into that script and create a new method.

The one I thought would be “Good coding” has me create a script, and then a Scriptable object and THEN attach the SO to the Card SO. Seems like what I though was good coding is only blotting my project and making it cumbersome.

How would you approach this?

Thank you!

I mean… your first option isn’t wrong necessarily.

It’s certainly not “OOP”. But OOP isn’t necessarily “right”.

My only issue personally with your first option is that as you add more and more card types is going to get bigger and bigger and bigger. Just thousands of lines of code. Which I personally don’t like having to deal with.

I’d rather several individual small classes. Sure it’s more script files, but once the game is compiled all of those things get turned into a single dll. So it’s not “more bloat”.

But that’s all personal preference really, and on a forum like this you’ll get all sorts of opinions in regards to that.

With that said some options you could go with are:

1) The route you already described with the interface and different SO’s.

2) User the new/ish ‘SerializeReference’ attribute. Create a “ICardEffect” interface. Then each card effect implements that interface. Then on your scriptable object you’ll have a field/var that is that type like so:

public interface ICardEffect
{
    void PerformEffect();
}

public class Card : ScriptableObject
{
    [SerializeReference]
    public ICardEffect Effect;

    public void PerformEffect()
    {
        Effect?.PerformEffect();
    }
}

public class CardEffectTypeA : ICardEffect
{
    public void PerformEffect()
    {
        //your effect's logic here
    }
}

This route will require creating some custom editor that allows you to change which “effect” it uses. Something like this:

[CustomEditor(typeof(Card))]
public class CardInspector : Editor
{

    private static List<System.Type> _availableTypes;
    private static GUIContent[] _dropDownOptions;

    static CardInspector()
    {
        _availableTypes = GetTypes((t) => typeof(ICardEffect).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract).ToList();
        _dropDownOptions = _availableTypes.Select(t => new GUIContent(t.Name)).ToArray();
    }

    public override void OnInspectorGUI()
    {
        this.serializedObject.Update();

        EditorGUILayout.PropertyField(this.serializedObject.FindProperty("m_Script"));

        var prop = this.serializedObject.FindProperty("Effect");
      
        if((prop.isExpanded = EditorGUILayout.Foldout(prop.isExpanded, prop.displayName)))
        {
            EditorGUI.indentLevel++;
            int index = GetIndexOf(_availableTypes, (t) => GetUnityFormattedFullTypeName(t) == prop.managedReferenceFullTypename);
            EditorGUI.BeginChangeCheck();
            index = EditorGUILayout.Popup(index, _dropDownOptions);
            if (EditorGUI.EndChangeCheck())
            {
                var tp = index >= 0 ? _availableTypes[index] : null;
                var obj = tp != null ? System.Activator.CreateInstance(tp) : null;
                prop.managedReferenceValue = obj;
                this.serializedObject.ApplyModifiedProperties();
            }
            if(prop.hasChildren)
            {
                while(prop.NextVisible(true))
                {
                    EditorGUILayout.PropertyField(prop);
                }
            }

            EditorGUI.indentLevel--;
        }


        this.serializedObject.ApplyModifiedProperties();
    }


    public static IEnumerable<System.Type> GetTypes(System.Func<System.Type, bool> predicate)
    {
        foreach (var assemb in System.AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (var tp in assemb.GetTypes())
            {
                if (predicate == null || predicate(tp)) yield return tp;
            }
        }
    }

    public static int GetIndexOf(IList<System.Type> lst, System.Func<System.Type, bool> predicate)
    {
        for(int i = 0; i < lst.Count; i++)
        {
            if (predicate(lst[i])) return i;
        }
        return -1;
    }

    public static string GetUnityFormattedFullTypeName(System.Type tp)
    {
        return string.Format("{0} {1}", tp.Assembly.GetName().Name, tp.FullName);
    }

}

3) Forego the ScriptableObject route and instead use prefabs (this is actually how we used to get SO like behaviour back in the day before SO’s existed… prefabs that had no graphical aspects to them). Then you attach the card/effect components however you want. May it be a ‘Card’ class and you inherit from it for each card type. Or a ‘Card’ class, a ICardEffect interface, and a CardEffectXXX component for each type.

This allows even more robust compositing of behaviours via the existing gameobject component options.

4) You could go completely non-traditional routes. Like using an enum instead of the type name lookup from your existing design. Or a factory pattern that built the effect object from the enum/type name so you weren’t using the switch statement every time you called. Or a factory that just returned a delegate instead.

How would I do it!?

Ehhhhhhh… that’s hard to say since I don’t k now exactly what future expansion you may do to this.

Knowing me though I’d likely go the route of Option 2 with some tweaking. I’d have a GameObject ref on the Card SO to reference that GUI prefab that is instantiated and shown on screen. I’d have a more robust inpsector than what I showed as an example. I’d create a factory as well to create cards on the fly at runtime.

But yeah… that’s likely what I’d do today.

Back in the day… before SerializeReference existed. I would have gone sort of the option 3 route. I actually did something like this in one of our games. Where I had a ScriptableObject that represented the entire thing, and then GameObject prefabs for the GUI aspect of it. But if I needed some composited behaviour like you are looking for I attached it to the GameObject prefab and pulled it off of that.

I did this for inventory items where some items had custom behaviour. And I also did this with my footstep sound effects. And a few other things.

With all that said.

If you got something to work right now. Work on getting more of your game to work.

Don’t refactor your current system until it becomes unbearable to maintain in the current state.

You know…

Don’t fix what ain’t broke.

2 Likes

Thank you for you in-Depth reply. What I meant by “bloating” is the unity project files, which are gonna have 3 files per card. (Card SO, Card Effect SO, CardEffectSO script) Plus the added cumbersome of having to attach them to each card. That’s why it felt wrong.

I don’t want to seem lazy but I might go with the last option, don’t fix what isn’t broken. Each card takes about 4 lines of code. These are five cards, for example. It doesn’t seem to be going out of hand anytime soon.

Thank you for the help and attention.

public void Swiftness(){
        _Player.ApplyStatus(Status.Swiftness,3);
    }
    public void Ignite(){
        _EManager.ApplyStatus(Status.Ignite,3,_TargetPos);
        _Player._Animator.SetTrigger("Attack");
      
    }
    public void Steroids(){
        _Player.ApplyStatus(Status.Steroids,3);
    }
    public void Net(){
        _EManager.ApplyStatus(Status.Immobile,2,_TargetPos);
        _Player._Animator.SetTrigger("Attack");
    }
    public void Painkillers(){
        _Player.ApplyStatus(Status.Painkiller,3);
    }

The only “Bad coding” is the coding that does not ship out to the customer in a timely fashion.

Everything else is “Good coding.”

If you pick the code up six months later and can’t figure it out, that might be sub-optimal coding, but if it has been out in the wild making you money all that time, you tuck in and understand it and make it Good Code again. :slight_smile:

I read this opinion a lot. But I can’t really know if my code will or won’t ship out to the customer. I don’t have any formal programming education so I barely know what is or isn’t good practice when coding. So it would be a shame to discover far down the line that I have to remake a big part of the game because of a lack of knowledge early on. Of course, you can’t avoid this problem entirely but though we can’t reach perfection, we should try to walk toward it.

Thank you for the reply.

It’s hard to say at a broad scale, but there are some things to avoid:

  • keep your code simple, as simple as humanly possible. Code not written cannot have bugs.

  • use the DRY approach (don’t repeat yourself)

  • separate your concerns (https://en.wikipedia.org/wiki/Separation_of_concerns)

  • don’t be afraid to refactor regularly to make things better

(and any one of many other simple rules of thumb others can add to this)

Ever since Mac and Windows brought event-driven UI “view” programming to the masses in the mid-1980s, there has been the endless struggle of controllers, views, windows, overlays, managers, delegates, messages, buttons, clicks, etc. Some people swear by one method, some people swear by another method.

The only constant thing is if you’re in Unity you probably want to “play nice” with Unity, which is to say recognize its fundamental Component architecture, know its lifecycle management, and know when it will call your methods.

Here is some timing diagram help:

https://docs.unity3d.com/Manual/ExecutionOrder.html

Beyond that your best bet is to get really good at interactively tearing into a running Unity game and finding out how stuff works. Do it with lots of tutorials and other code examples, and then when your own game gets old enough for you to forget (and you will), you’ll be able to dig in and figure it out.