Hi all, I’ve got a general question around Scriptable object(?) structuring. In the game I’m making, the player can hold onto to these items(artifacts), and they each have a special effect, (think artifacts in Slay the spire), and they trigger at different time slots (e.g. Before an attack, After an attack, after taking damage).
public abstract class Artifact : ScriptableObject
{
public EffectFreq artifactFreq;
public abstract void ApplyEffect(Character character);
}
So then I extended from this abstract scriptable object script to create a specific artifact
[CreateAssetMenu(fileName = "BonusAttack", menuName = "Artifacts/BonusAttack")]
public class BonusAttack : Artifact
{
public int everyNthAttack;
public override void ApplyEffect(Character character)
{
if (character.attackCount % everyNthAttack == 0)
{
character.BonusAttack();
}
}
}
So that I can drag and drop the artifact into the List() that the character script
//within the character script
public List<Artifact> artifacts;
my question is with this approach, each new artifact will be its own ScriptableObject script, and an extra ScriptableObject. Does this sound correct? Or is it redundant and there’s a better way of doing it?
I also tried the following and debating between the two, or any other methods:
public enum artifactType
{
everyNthAttack
}
...
public void ApplyEffect(Character character)
{
switch (artifactType)
{
case artifactType.everyNthAttack:
if (character.attackCount % everyNthAttack == 0)
{
character.BonusAttack();
}
break;
default:
break;
}
}
Definitely don’t go the enum route. It will not scale well.
Your first approach is fine and is the general ‘wide but shallow inheritance hierarchy’ that is common in these types of situations. Only downside is that you do end up with lots of scriptable object types, and a very full Right-Click → Create menu.
The alternative is to use [SerializeReference], which lets you serialise plain C# objects with polymorphism. Upside is that you can have one scriptable object class that encapsulates a plain C# object. Downside is that it would need custom inspector support.
The alternative would have been having each material as a scriptable object and when you configure your scene you drop script objects on colliders to define their material property instead of just picking the material from a enum list.
But in your case I think scriptable objects makes much more sense. Its how we defined projectiles in our game, which by the way reuses the material enum, but it could have been solved by pointing out the scriptabler object I guess
This post has nothing to do with materials or sound effects though? It’s to do with composing item effects.
And I would go with scriptable objects to define each type of sound effect 100% of the time. Enums really don’t have a place in that kind of system either. They should be data driven; enums are not data driven.
Thats why I also talked about our projectiles which are more like attack types.
Enums do make sense, but here a referenced scriptable object makes more sense.
Referencing scriptable objects can be a workflow pain if you need to reference them alot like materials in a scene. But I guess yuo can make a editor tool to make them look more like enums so you can pick them from a drop down.
The object picker is already a form of drop down with a built in search feature, even more so if you use the advanced picker. If you’re drag and dropping… you’re doing it wrong, quite frankly.
I would generally only use enums as a form of parameter for defining slightly variable behaviour in a method. And only if that variability is small. Otherwise, a proper object should be used. That’s why C# is an object oriented language, after all. Enums are an anti-pattern for that reason.
Hey there, thanks for replying. Honestly, I’ve got quite a bit of learning to do, and a bit of the SerializeReference is flying over my head. What do you mean by data driven or not?
Right now I separated the handling of perks into a script by instead, so all I’ll do is have a list of Perks (Scriptable Object) with an enum of PerkType to distinguish what perk effect to trigger.
[SerializeField] internal List<Perk> NormalPerks = new List<Perk>();
...
switch (perk.PerkType)
{
case PERKTYPE.BlockChance:
...
}
Scalability is the main concern. The more effects, the bigger the method gets, and the more unmanageable it becomes. Maintaining your code gets harder. You are also lumping every single variable into the one object as well, leading to tons of redundant data most of the time, not to mention an object with way to much responsibility.
To use a proper object oriented/data driven example from my code above, if you wanted to add a block chance upgrade, you simply implement a new type inheriting from IArtifactEffect:
[System.Serializable]
public sealed class ArtifactEffectBlockChange : IArtifactEffect
{
[SerializeField]
private float _blockChanceIncrease = 1f;
public void ApplyEffect(Character character)
{
// add block change
}
}
Then you just make a new artifact scriptable object, select this type from the SubclassSelector dropdown, and enter in your block chance. Then use the scriptable object where needed.
This way, the functionality and responsibility is isolated into its respective object, as is its data. You don’t have a bunch of useless fields bogging down every single perk/artifact.
When you want a new effect, you rinse and repeat. And namely, you don’t need to touch the code of any other effects as well.
That make sense to me, but that also means that for each perk/artifact I would have one Scriptable Object script + one Scriptable Object. That kinda feels wrong? Since the way I understood scriptable object is that I can create and use the same data structure but with different data easily.
Enums are not anti pattern. Haha, thats pushing it. For example super helpful when writing domain specific languages DSL (ie Internal DSL). Example from one of my projects.
private MatchOutcomeWithReason GetJobOutcome(MatchJob job)
{
if (job.LooseMatch) return MatchOutcome.Refund.Because(RefundReason.OcrMissing);
if (job.Payments.Any() && job.Invoice.InvoiceState == InvoiceState.Paid) return MatchOutcome.Refund.Because(RefundReason.AlreadyPaid);
if (!job.CanPay) return MatchOutcome.Refund.Because(RefundReason.WrongOrder);
if (job.Invoice.InvoiceState == InvoiceState.Paid) return MatchOutcome.Noop;
if (job.Diff != 0) return MatchOutcome.Deviation;
return MatchOutcome.Paid;
}
Also Im not sure I agree the SO picker is that great when doing bulk work
No you’d only have one scriptable object class but multiple regular C# object classes. Notice that the scriptable object in my first example is sealed. It can’t be inherited from.
Ahh I see what you mean now. I’ll take a dig around and find out what [SerializeReference, SubclassSelector] means, and I think I would be able to fully understand what you’re proposing. Thanks
SerializeReference is like SerializeField but a different form of serialisation. Don’t need to dig, it’s all covered in the documentation: Unity - Scripting API: SerializeReference
SubclassSelector is just from my reusable property drawer example I linked to in my first post. It’s all there in the post.
SubclassSelector is not a great name in this context since a class implementing an interface isnt a subclass. But it let you pick types implementing the interface
I mean, my property drawer example also lets you pick subtypes of concrete types. In this instance I just used a base interface as an example, but it could have easily been an abstract class.
I just used the same name as an extension I made for Odin inspector: Community Made Tools
Spent the last two hours rewriting the code, seems like it’s working great! Thankyou! Now everything has their own place, and each perk has their own class that manages the effect.
Better than both would be using a plain C# object.
Once you start using SerializeReference to a meaningful degree you realise that it trumps scriptable objects a lot of the time, and enums nearly 100% of the time.