Search Unity

Discussion Game Design; My Damage Types

Discussion in 'Scripting' started by BlackSabin, Sep 28, 2022.

  1. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    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:
    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.
     
  2. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    651
    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.
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,924
    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:
    Code (CSharp):
    1. public class DamageType : ScriptableObject
    2. {
    3.     [SerializeField]
    4.     private List<DamageType> weaknesses = new List<DamageType>();
    5.    
    6.     public bool IsWeakTo(DamageType damageType)
    7.     {
    8.         return weaknesses.Contains(damageType);
    9.     }
    10. }
    And down the line, if you want to add more damage types, you can just add more SO's.
     
    Shiverish likes this.
  4. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    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.
     
  5. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    651
    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:

    Code (CSharp):
    1. [System.Serializable]
    2. public class AllStats
    3. {
    4.     [System.Serializable]
    5.     public class Stat
    6.     {
    7.         public float value;
    8.     }
    9.     [System.Serializable]
    10.     public class DamageStat
    11.     {
    12.         public Stat damage = new();
    13.         public Stat penetration = new();
    14.         public Stat protection = new();
    15.         public Stat resistance = new();
    16.     }
    17.  
    18.     public Stat health = new();
    19.     public Stat mana = new();
    20.  
    21.     public DamageStat blunt = new();
    22.     public DamageStat slash = new();
    23.     // ...
    24. }
    25.  
    And then access it like so:
    Code (CSharp):
    1. void OnDamage(float currentDamage)
    2. {
    3.     currentDamage -= myAllStatsReference.blunt.protection.value;
    4.     myAllStatsReference.health.value -= currentDamage;
    5. }
    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.
     
    Last edited: Sep 28, 2022
  6. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    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:
    Code (CSharp):
    1. foreach(StatMod mod in modifiers){
    2.     charData.attributes[mod.stat].AddModifier( new StatModifier(mod.value, mod.type, this));
    3. }
    Changing it from that format means I can no longer have a single "StatType" reference that I can add to any equipment/skills.
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,730
    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(tm) 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?
     
  8. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,073
    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.
     
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    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).

    Code (csharp):
    1. Damage
    2. Penetration
    3. Protection
    4. Resistance
    Code (csharp):
    1. <blank>
    2. Brute
    3. Finesse
    4. True
    Code (csharp):
    1. <blank>
    2. Physical
    3. Energy
    4. 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.
     
    Last edited: Sep 29, 2022
  10. BlackSabin

    BlackSabin

    Joined:
    Feb 27, 2021
    Posts:
    75
    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.
     
    Last edited: Oct 3, 2022
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,730
    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.
     
    BlackSabin likes this.
  12. AnimalMan

    AnimalMan

    Joined:
    Apr 1, 2018
    Posts:
    1,164
    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.

    Code (CSharp):
    1.  
    2. Character Types;;;
    3. [DONALD J TRUMP]
    4. [STATS]
    5. TYPE=ECONOMIC,
    6. CLASS=CAPITALIST,
    7. MOVELIST=DEBATE,STOCKS,STOCK MARKET,TARIFFS,NEGOTIATE,PEACE AGREEMENT,SPEECH,RALLY,
    8. MOVESET=DEBATE,SPEECH,NEGOTIATE,TARIFFS,
    9. MOVECHOICES=DEBATE,NEGOTIATE,TARIFFS,SPEECH,
    10. STATUS=UNLOCKED,
    11.  
    12. // MOVESET = MY CURRENTLY SELECTED MOVES
    13. // MOVELIST = ALL MOVES I CAN LEARN;
    14. /// MOVE CHOICES = CURRENT CHOICES AVAILABLE TO PLAYER PROFILE
    15.  
    16. [JOE BIDEN]
    17. [STATS]
    18. TYPE = DIPLOMATIC
    19. CLASS = CAPITALIST
    20. MOVELIST = DEBATE, NEGOTIATE, NATIONAL GUARD, PASS THE BUCK, LOCKDOWN, MANDATE, ARM THE REBELS, REFORM, BUILD BACK BETTER, CONDEMN, WITHDRAW,
    21. MOVESET=DEBATE, NEGOTIATE, LOCKDOWN, MANDATE,
    22. MOVECHOICES = DEBATE, NEGOTIATE, NATIONAL GUARD, PASS THE BUCK, LOCKDOWN, MANDATE, ARM THE REBELS, REFORM, BUILD BACK BETTER, CONDEMN, WITHDRAW,
    23. STATUS = UNLOCKED,
    24.  
    25. [GEORGE W BUSH]
    26. [STATS]
    27. TYPE = INDEPENDENT,
    28. CLASS = CAPITALIST
    29. MOVELIST = DEBATE, NEGOTIATE, DECLARATION OF WAR, REFORM
    30. MOVESET=DEBATE, NEGOTIATE, REFORM, DECLARATION OF WAR,
    31. MOVECHOICES = DEBATE, NEGOTIATE, REFORM, DECLARATION OF WAR,
    32. STATUS = UNLOCKED,
    33.  
    34. Move Type example;
    35. [AIR STRIKE]
    36. MAXPP=9,
    37. TYPE=MILITARY,
    38. TYPEBONUS=DIPLOMATIC,
    39. ANIM=4,
    40. SPECIALEFFECT=None,
    41. OVERLAY=BOMBER,BOMBER,BOMBER,BOMBER,
    42. DAMAGE=25,
    43. ACCURACY=50,
    44. SPEED=20,
    45. BOOST=0,
    46. EFFECTOR=0,
    47. CRITICAL=35,
    48.  
    49. [DECLARATION OF WAR]
    50. MAXPP=15,
    51. TYPE=ECONOMIC,
    52. TYPEBONUS=INDEPENDENT,MILITARY
    53. ANIM=1,
    54. SPECIALEFFECT=WAR,
    55. OVERLAY=TALK01,DECREE01,TALK02,DECREE02,BLANK,DECREE02,
    56. DAMAGE=4,
    57. ACCURACY=100,
    58. SPEED=50,
    59. BOOST=0,
    60. EFFECTOR=2,
    61. CRITICAL=0,
    62.  
    63. [LOCKDOWN]
    64. MAXPP=15,
    65. TYPE=INDEPENDENT,
    66. TYPEBONUS=DIPLOMATIC,
    67. ANIM=4,
    68. SPECIALEFFECT=None,
    69. OVERLAY=BLANK,LOCK01,LOCK01,LOCK01,LOCK02,LOCK02,LOCK02,LOCK03
    70. DAMAGE=4,
    71. ACCURACY=70,
    72. SPEED=90,
    73. BOOST=0,
    74. EFFECTOR=1,
    75. CRITICAL=10,
    76.  
    77.  
    78. [RULES]
    79. [CHARACTERS] // I Listed all the characters for register
    80. DONALD J TRUMP
    81. BARACK OBAMA
    82. GEORGE W BUSH
    83. BORIS JOHNSON
    84. TUCKER CARLSON
    85. JOE BIDEN
    86. TONY BLAIR
    87. HILLARY CLINTON
    88. KAMALA HARRIS
    89. MINOTAUR // lol yes i moved on
    90. CYCLOPS // in art style
    91. ARCHER // started to change course
    92.  
    93. [MOVES] // I listed all the moves for register.
    94. DEBATE
    95. NEGOTIATE
    96. SANCTION
    97. RALLY
    98. CONDEMN
    99. COLLUSION
    100. REFORM
    101. RESIGN
    102. LOOPHOLE
    103. REFERENDUM
    104. FUND RAISER
    105. BACK BENCH
    106. DEPORT
    107. COALITION
    108. COMMITTEE
    109. CAUCUS
    110. TIE BREAKER
    111. REPEAL
    112. CENSORSHIP
    113. QUELL
    114. DECREE
    115. BAILOUT
    116. TRADE AGREEMENT
    117. AIRSTRIKE
    118.  
     
    BlackSabin likes this.
  13. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    @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.
     
    Kurt-Dekker, mopthrow and BlackSabin like this.