Game Design; My Damage Types

In my designed game I have 9 separate, distinct damage types that interact with everything. I’ve categorized this in such a fashion that they each have 2 separate “categories” they fall into, shown by the chart here:
8472371--1126013--upload_2022-9-28_7-23-38.png
The categories themselves are bolded for distinction. Equipment, skills, passives, and buffs will all be able to add/modify each damage type’s relative stats directly, but also can instead affect a category and subsequently affect all damage types relative to that category. I’m not sure how to better implement this system however.

Currently the system utilizes a rather large “StatType” enum that I also use for many other character stats. This is not ideal, but in terms of using the editor I personally haven’t come up with a good workaround or solution. For me to make the data easy to work with I’d like to be able to click a drop-down menu and select any of these stat types for an equipment or skill, and the only way I’ve managed to make that work is with the singular, massive enum. Each damage type and category as a result of my game design needs its own 4 separate fields to indicate Damage, Penetration, Protection, and Resistance. 4 *(9 + 6) = 60 separate stat types… which bloats the drop-down and hurts my eyes.

In an effort to drum up some new inspiration for a solution, has anyone here had a similar issue and had come up with a different approach to it? I know a lot of scripting advice is to “use a ScriptableObject in place of an enum” but I just can’t figure out how to approach this issue with that method in mind.

You could write a custom property drawer that accesses a Master-ScriptableObject which holds a list of all sub-ScritableObjects. But it’s a lot of work and not required.

An enum is perfectly fine, the only thing I don’t understand is why you need 4 entries per type? You can use two enums instead, one for the “elemental” type and the other defining if it’s Damage, Penetration, Protection or Resistance.
Each stat is then just a struct or class containing the two types and the corresponding value.
When you want to access a stat, you just use the two keys (enums).

This should be quite easy in the editor as well as in your code.

Basically instead of checking myEnum == MyEnum.Value, you can put all the logic you need inside the damage type SO’s.

So for example, if one type is weak to other types, that’s super easy with SO’s:

public class DamageType : ScriptableObject
{
    [SerializeField]
    private List<DamageType> weaknesses = new List<DamageType>();
   
    public bool IsWeakTo(DamageType damageType)
    {
        return weaknesses.Contains(damageType);
    }
}

And down the line, if you want to add more damage types, you can just add more SO’s.

1 Like

That makes more sense when people mention “use a ScriptableObject”. That sounds like it could at least help with managing adding/removing values from the enum during my coding efforts, at least.

This is an exceptional idea. This still requires me to separate my stats one way or another from each other (for instance, having a field for damage/defense data and having one for all the other core stats like health/mana) but it seems somewhat manageable.

You can also write a class hierarchy and access stats this way - but it’s more complicated to iterate over all stats or find one by it’s name.
Something like:

[System.Serializable]
public class AllStats
{
    [System.Serializable]
    public class Stat
    {
        public float value;
    }
    [System.Serializable]
    public class DamageStat
    {
        public Stat damage = new();
        public Stat penetration = new();
        public Stat protection = new();
        public Stat resistance = new();
    }

    public Stat health = new();
    public Stat mana = new();

    public DamageStat blunt = new();
    public DamageStat slash = new();
    // ...
}

And then access it like so:

void OnDamage(float currentDamage)
{
    currentDamage -= myAllStatsReference.blunt.protection.value;
    myAllStatsReference.health.value -= currentDamage;
}

Edit:
If you are a programmer it’s usually a waste of time to create a system that doesn’t require programming to add / remove anything to it.
ScriptableObjects lead to a lot of dragging and dropping, even with a field that let’s you select them like an enum, on the code side, you need to create this field, assign it in the editor, then use it in your code. Most of the time, this isn’t required. Most of the time, you want to apply special logic to specific effects, increase blunt-damage based on two stats for example.

Setup in the Editor can cost you (much) more time than just writing a line of code.

While that would absolutely work… it also wouldn’t. I have all my stats contained within a Dictionary to make accessing them simpler/easier. This lets me get any stat from the class with a simple myCharacter.Stats[StatTypeHere].Value reference. It also lets me modify those stats just as easily. If an equipment has several StatModifier values (which is contained within a list) I can iterate over them just as easily:

foreach(StatMod mod in modifiers){
    charData.attributes[mod.stat].AddModifier( new StatModifier(mod.value, mod.type, this));
}

Changing it from that format means I can no longer have a single “StatType” reference that I can add to any equipment/skills.

So a better title might be “I don’t want to change any of this but how could I have done it better?”

Everybody in here is assuming you want the Best Way™ that plays well with Unity. Above is a whole series of these great ideas but it sounds like perhaps that is not what you are asking?

Go with ScriptableObject but without logic inside.
Just use SO as damage data that can be added by people working in editor.

In your case SO can be empty, and used like an enum.

Why have 4 * (9 + 6) when you can already uniquely define every state with 3 serial selections?
What you have shown us is a 3x3 matrix where all cells are uniquely defined as a (row, column) intersect.
As you said that’s 4 * (4*4 - 1) combinations, but you’re supposed to allow for graceful selection, otherwise you’re very unproductive in the long run.

If this was me, I would design a custom drawer where you’d pick from three enum fields in a stack and this would uniquely define the selection and actually behave like a giant enumeration state internally for the rest of the system (and internally you don’t care about it, the true state can be set as bit flags evaluated as an integer, or even a string).

Damage
Penetration
Protection
Resistance
<blank>
Brute
Finesse
True
<blank>
Physical
Energy
Ethereal

So if you picked Brute and Energy, this would stand for ‘Fire’. If you picked blank and Physical, this would stand for ‘Physical’. Two blanks are obviously ‘None’ (and likewise invalid, just as it is in your table). You could also make the actual fields smaller so they would fit into a row, if you dislike the stacked approach.

Now, whether this evaluates to 104, 21837, and 0, or “Fire”, “Physical”, “None”, that’s exactly the point of every UI, to separate the internal state logic from the human interface design.

Internally, I would either encode it into groups of 2 bits each, aabbcc (because 2 bits fully encode 2^2=4 states), or as 3-letter strings. You can then easily map such encoding to another scheme via dictionary or something else, if you so desire.

This drop-down field combo then can be used wherever you need it, and Unity will always present you with a choice, while keeping the books clean. IN FACT, you can implement this union type so thoroughly that it operates opaquely as a compound information throughout your code as well, one that serializes as a string or whatever, and displays nicely, without bloating anything, while providing you with the luxury of querying the original input.

Edit:
You can also check out my recent article/post on custom enumeration type (serialized into strings) for an in-depth explanation how to introduce a new property type to Unity editors. This helps you build proper foundations and tools for smarter design.

Thats… fair. I guess I’ve got to keep an open mind to redoing most of this. I’m daunted by how much I’d have to refactor my code to get what I have to a functional state again; I’m not a particularly good programmer so it all took me a long time to get as far as I have.

Having been doing this all from scratch on my own having naught but what the internet has to offer in it’s teachings I have very little idea as to any of the specifics behind game programming logic. One of the biggest things I learned when I was taking programming courses was to make sure my design was as modular as possible so as to allow for expansion in the future. My stat system in place allows me to quickly add/remove stat fields by modifying an enum (which may not be the best course of action, but is incredibly effective for my own individual use). I don’t know how to best ask my questions.

Refactoring is part of the landscape. If you don’t, then your code will get old and rot and be hard to work with. You may think I’m being facetious or trivial, but I guarantee if you keep writing more and more games, after two or three games over a period of time, you go back to the first game and you’ll think, “Yuck! I need to rewrite ALL of this!”

I’d actually say that’s waaaaay down on the “useful” list of things. That will come over time. Only a tiny fraction of code is actually still usable into the future, FAR less than most people think. The reasons are manifold: the original code doesn’t solve the same problem (eg, the problem has changed), there is a better way to do it which you didn’t realize, etc.

The main thing is, do lots of stuff, have lots of fun, pay attention to what makes it easy, what makes it hard, and learn as you go.

1 Like

I made a pokemon fighter before, I am a little afraid to open it as i wasnt using stream write and stream read correctly at the time.

My data management looked like this.

Character Types;;;
[DONALD J TRUMP]
[STATS]
TYPE=ECONOMIC,
CLASS=CAPITALIST,
MOVELIST=DEBATE,STOCKS,STOCK MARKET,TARIFFS,NEGOTIATE,PEACE AGREEMENT,SPEECH,RALLY,
MOVESET=DEBATE,SPEECH,NEGOTIATE,TARIFFS,
MOVECHOICES=DEBATE,NEGOTIATE,TARIFFS,SPEECH,
STATUS=UNLOCKED,

// MOVESET = MY CURRENTLY SELECTED MOVES
// MOVELIST = ALL MOVES I CAN LEARN;
/// MOVE CHOICES = CURRENT CHOICES AVAILABLE TO PLAYER PROFILE

[JOE BIDEN]
[STATS]
TYPE = DIPLOMATIC
CLASS = CAPITALIST
MOVELIST = DEBATE, NEGOTIATE, NATIONAL GUARD, PASS THE BUCK, LOCKDOWN, MANDATE, ARM THE REBELS, REFORM, BUILD BACK BETTER, CONDEMN, WITHDRAW,
MOVESET=DEBATE, NEGOTIATE, LOCKDOWN, MANDATE,
MOVECHOICES = DEBATE, NEGOTIATE, NATIONAL GUARD, PASS THE BUCK, LOCKDOWN, MANDATE, ARM THE REBELS, REFORM, BUILD BACK BETTER, CONDEMN, WITHDRAW,
STATUS = UNLOCKED,

[GEORGE W BUSH]
[STATS]
TYPE = INDEPENDENT,
CLASS = CAPITALIST
MOVELIST = DEBATE, NEGOTIATE, DECLARATION OF WAR, REFORM
MOVESET=DEBATE, NEGOTIATE, REFORM, DECLARATION OF WAR,
MOVECHOICES = DEBATE, NEGOTIATE, REFORM, DECLARATION OF WAR,
STATUS = UNLOCKED,

Move Type example;
[AIR STRIKE]
MAXPP=9,
TYPE=MILITARY,
TYPEBONUS=DIPLOMATIC,
ANIM=4,
SPECIALEFFECT=None,
OVERLAY=BOMBER,BOMBER,BOMBER,BOMBER,
DAMAGE=25,
ACCURACY=50,
SPEED=20,
BOOST=0,
EFFECTOR=0,
CRITICAL=35,

[DECLARATION OF WAR]
MAXPP=15,
TYPE=ECONOMIC,
TYPEBONUS=INDEPENDENT,MILITARY
ANIM=1,
SPECIALEFFECT=WAR,
OVERLAY=TALK01,DECREE01,TALK02,DECREE02,BLANK,DECREE02,
DAMAGE=4,
ACCURACY=100,
SPEED=50,
BOOST=0,
EFFECTOR=2,
CRITICAL=0,

[LOCKDOWN]
MAXPP=15,
TYPE=INDEPENDENT,
TYPEBONUS=DIPLOMATIC,
ANIM=4,
SPECIALEFFECT=None,
OVERLAY=BLANK,LOCK01,LOCK01,LOCK01,LOCK02,LOCK02,LOCK02,LOCK03
DAMAGE=4,
ACCURACY=70,
SPEED=90,
BOOST=0,
EFFECTOR=1,
CRITICAL=10,


[RULES]
[CHARACTERS] // I Listed all the characters for register
DONALD J TRUMP
BARACK OBAMA
GEORGE W BUSH
BORIS JOHNSON
TUCKER CARLSON
JOE BIDEN
TONY BLAIR
HILLARY CLINTON
KAMALA HARRIS
MINOTAUR // lol yes i moved on
CYCLOPS // in art style
ARCHER // started to change course

[MOVES] // I listed all the moves for register.
DEBATE
NEGOTIATE
SANCTION
RALLY
CONDEMN
COLLUSION
REFORM
RESIGN
LOOPHOLE
REFERENDUM
FUND RAISER
BACK BENCH
DEPORT
COALITION
COMMITTEE
CAUCUS
TIE BREAKER
REPEAL
CENSORSHIP
QUELL
DECREE
BAILOUT
TRADE AGREEMENT
AIRSTRIKE
1 Like

@BlackSabin
Just to reiterate what Kurt said once again.
Imagine if programming was like a very simple 2D game, but the level is a maze, and almost everything you can see is just fog of war. There is almost no value in planning your moves ahead, if you don’t know much about the actual layout.

This fog of war will never change, actually, not even after you’ve shipped several games. Sure you’ll always find a way to discover that one level, and to master it, but there’s always a level after that one. The APIs will change, as will your solutions, languages, even your problems will change. Next year there will be some different technology that will render most of your solutions obsolete in some respect. Some new platform with a new language and new APIs. Some things you can make useful for a long time, but these are extremely rare and are usually very abstract, only your general mindset will actually evolve and stick around.

We do need to plan our actions though, but when the problem space is complex, it helps to bite it on the perimeter, like a caterpillar, make a little sandbox, experiment with possibilities, make mistakes, do it from scratch. Learn as much as you can about different approaches in a test environment, only then you can start planning ahead and build a proper sand castle, and make something that relies on several interconnected systems, and does something you deeply understand.

What you’re learning about, is essentially meta. What moves will reveal the level the fastest? How can you wrap up the level with the least amount of motion or time? What do you do if you hit a dead-end? How do you plan for it? That sort of thing. This process will never change or go away, and unless you can push yourself to freely experiment, you’re stuck with analysis-paralysis.

To be blunt about it, you essentially need to be a little “dumb” to push yourself into it, each time. Because the more you know and the better you are, the more lazy you get, that’s inevitable, because you know better right? But this is extremely bad for coming up with creative solutions in this domain of software development. You constantly need to be able to close your eyes and just let go. You need to love the process of coding itself and you need to be able to avoid messing it all up. In other words, don’t, set it up so that you don’t care if you mess it up. Learn something from that experience, and try something differently.

After decades of programming, I can say with certainty that you can reach a level where you can type in literally several thousands lines of code without testing, and if you truly commit yourself cognitively, you can do it in two or three days and it’ll work without any error whatsoever. It happened to me recently, but I’ve experimented a lot with the tools I had at my disposal. But you need two things to make this happen: 1) an absolute control over your problem space, and 2) massive amounts of experience with Unity and C#. And even then, this situation happens only once or twice in a project, usually in the beginning. Everything else is much less monolithic, and has to be approached iteratively.

3 Likes