Search Unity

Question Some coding questions.

Discussion in 'Scripting' started by ElMagnifico, Apr 14, 2021.

  1. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71
    Coding questions:


    1. I want to have a verbal description of a state of a variable/bar. Like a healthbar. So I want it to say "near death" when low, "Badly Wounded", "injured", etc..
    Now, what are best practices to go about it? A spaghetti of if-then-else is a no-no.
    The only way I can think off is to have an array/dictionary/enumerator for a few basic values, lets say from 1-10. Then on health change/update I re-calculate the health on a 1-10 scale, rounded up, and simply display the associated string. It should work, but is there a better way?


    2. When I'm generating my NPCs (min and max heights are based on race) I randomize heights. However, the height distribution is uniform. Simply Random(minRaceHeigh, MaxRaceHeigh) doesn't feel right. What's the best way to get a normal distribution (or any curved distribution for that matter)?



    3. I'm also having issues with equipment. Right now, each character is created a tough a class (Humanoid), and that class also holds their equipped items.
    For example:
    public Weapon hand1;
    public Armor headSlot;
    public Armor chestSlot;
    They are set to null on creation.

    Weapon and Armor are custom classes that hold item data (3d model prefab, stats, etc..) The problem arises when I want to check if a character has anything equipped.
    if (selectedCharacter.hand1 == null) deosn't seem to work well (selectedCharacter is Humanoid). The only workaround I can think off is to make an "empty" item instead. Suggestions?



    4. Dialogue/choice menus. I managed to make a pretty rudimentary one trough Scriptable Objects that allows 4 choices, but I don't want to hard-code every individual choice.
    It works on the principle of message blocks. Each message block contains a list of other message blocks that it can branch into. If a messgae block contains an empty list, it is the end of a conversation. I even made 3 different types of message boxes (no image, wide image, tall image), but what I lack is executing specific things on selection. Such as give 100 gold. Take 3 wood. Generate a new NPC, etc...

    What I am thinking is to make a bunch of "Result" methods that each do one thing (like public void ChangeGold(int value)), then create a new method ProcessResults() that calls those methods based on a parameter (int or enum). 1 would be ChangeGold, 2 would be ChangeWood, 3 would be DamageRandomPartyMember, etc..

    Then, each message block would have a list of int,int - first the Result ID(which method to call), second the value to pass. The ProcessResults would go trough all the Results and process them with a switch for resultID. Should work in theory, but seems unwieldy..
    Could I use a <Enum,int> list... or Dictionary?

    I'd like input into how to improve upon it.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using TMPro;
    6.  
    7. public class MessageController : MonoBehaviour
    8. {
    9.  
    10.     public TMP_Text uitext0;
    11.     public TMP_Text uitext1;
    12.     public TMP_Text uitext2;
    13.  
    14.     public Image uisprite1;
    15.     public Image uisprite2;
    16.  
    17.     public TMP_Text activeText;
    18.     public Image activeSprite;
    19.  
    20.     public Canvas messsagebox;
    21.  
    22.     internal SO_MessageElement messageElement;
    23.  
    24.     internal bool tutorialOn = true;
    25.     internal int tutorialStage = 0;
    26.  
    27.     [SerializeField]
    28.     internal List<Button> buttons = new List<Button>();
    29.  
    30.     public MainMenuController mmc;
    31.  
    32.  
    33.  
    34.  
    35.     public void StartMessage(SO_MessageElement convo)
    36.     {
    37.  
    38.         messsagebox.gameObject.SetActive(true);
    39.  
    40.         messageElement = convo;
    41.  
    42.         InitializeElement();
    43.  
    44.     }
    45.  
    46.  
    47.     public void NextElement(int id)
    48.     {
    49.  
    50.         if (messageElement.tutorialStage == 1)
    51.         {
    52.             tutorialStage = 1;
    53.             mmc.ClickedHome();
    54.             tutorialOn = false;
    55.         }
    56.  
    57.         if (messageElement.tutorialStage == 2)
    58.         {
    59.             tutorialStage = 2;
    60.             mmc.ClickedHome();
    61.             tutorialOn = true;
    62.         }
    63.  
    64.         if (messageElement.branchingConvo[id] == null || messageElement.branchingConvo.Count == 0)
    65.         {
    66.             //end of convo, exit message box
    67.             messsagebox.transform.GetChild(0).gameObject.SetActive(false);
    68.             messsagebox.transform.GetChild(1).gameObject.SetActive(false);
    69.             messsagebox.transform.GetChild(2).gameObject.SetActive(false);
    70.  
    71.             messsagebox.gameObject.SetActive(false);
    72.         }
    73.         else
    74.         {
    75.             messageElement = messageElement.branchingConvo[id];
    76.             InitializeElement();
    77.         }
    78.     }
    79.  
    80.  
    81.  
    82.     public void InitializeElement()
    83.     {
    84.         //turn off extra buttons
    85.         buttons[0].GetComponentInChildren<Text>().text = "Next";
    86.         buttons[1].gameObject.SetActive(false);
    87.         buttons[2].gameObject.SetActive(false);
    88.         buttons[3].gameObject.SetActive(false);
    89.  
    90.  
    91.         //activate right message box type, dependion on image
    92.         //0 - no image
    93.         //1 - wide image
    94.         //2 - tall image
    95.         switch (messageElement.type)
    96.         {
    97.             case 1:
    98.                 messsagebox.transform.GetChild(0).gameObject.SetActive(false);
    99.                 messsagebox.transform.GetChild(1).gameObject.SetActive(true);
    100.                 messsagebox.transform.GetChild(2).gameObject.SetActive(false);
    101.  
    102.                 activeSprite = uisprite1;
    103.                 activeText = uitext1;
    104.                 break;
    105.  
    106.             case 2:
    107.                 messsagebox.transform.GetChild(0).gameObject.SetActive(false);
    108.                 messsagebox.transform.GetChild(1).gameObject.SetActive(false);
    109.                 messsagebox.transform.GetChild(2).gameObject.SetActive(true);
    110.  
    111.                 activeSprite = uisprite2;
    112.                 activeText = uitext2;
    113.                 break;
    114.  
    115.             default:
    116.                 messsagebox.transform.GetChild(0).gameObject.SetActive(true);
    117.                 messsagebox.transform.GetChild(1).gameObject.SetActive(false);
    118.                 messsagebox.transform.GetChild(2).gameObject.SetActive(false);
    119.  
    120.                 activeText = uitext0;
    121.                 break;
    122.         }
    123.  
    124.  
    125.         activeText.text = messageElement.textblock;
    126.  
    127.         if (messageElement.picture != null)
    128.         {
    129.             activeSprite.sprite = messageElement.picture;
    130.         }
    131.  
    132.  
    133.         //number of options and their names
    134.         for (int i = 0; i < messageElement.buttonTexts.Count; i++)
    135.         {
    136.             // set button text and make button visible
    137.             buttons[i].GetComponentInChildren<Text>().text = messageElement.buttonTexts[i];
    138.             buttons[i].gameObject.SetActive(true);
    139.         }
    140.  
    141.     }
    142.  
    143.  
    144. }
    145.  

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5.  
    6.  
    7. [CreateAssetMenu(fileName = "New Conversation Element", menuName = "TTM/ConvoElement")]
    8. public class SO_MessageElement : ScriptableObject
    9. {
    10.  
    11.     [SerializeField]
    12.     [TextArea(10,20)]
    13.     internal string textblock;
    14.  
    15.     [SerializeField]
    16.     internal Sprite picture;
    17.  
    18.     [SerializeField]
    19.     internal int type;  //controls type of message window
    20.  
    21.     [SerializeField]
    22.     internal List<string> buttonTexts = new List<string>();
    23.  
    24.     [SerializeField]
    25.     internal List<SO_MessageElement> branchingConvo = new List<SO_MessageElement>();
    26.  
    27.     [SerializeField]
    28.     internal bool tutorial = false;
    29.  
    30.     [SerializeField]
    31.     internal int tutorialStage;
    32.  
    33. }
    34.  
    messages.jpg
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    I would make a HealthDescriptionRange class with min/max float values and a name; you can set up an array of these in the inspector and loop through the array until you find a match.

    You could go down a rabbit hole of finding normal distribution formulae (and feel free to if that appeals to you!) but this is probably easiest done by putting Random.value through an AnimationCurve:
    Code (csharp):
    1. public AnimationCurve normalCurve = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(1f, 1f));
    2. . . .
    3. float distributedRandom = normalCurve.Evaluate(Random.value);
    4. float height = Mathf.Lerp(minRaceHeigh, MaxRaceHeigh, distributedRandom);
    Now you can go into the inspector and edit this distribution curve visually to get exactly the distribution that feels right. (You could, if you prefer, "bake" the heights into the Y axis of the AnimationCurve data and not need your min and max height - the important part is that X axis's range goes from 0 to 1 since that's the range Random.value gets you.)
     
  3. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    1. Rather than assuming 10 equal divisions, I'd suggest a list of (number, string) pairs that gives the threshold for when each string applies. For example, (0.87, "healthy") could mean to use the description "healthy" if (current health / max health) is at least 0.87, but less than the next-higher threshold (if there is one).

    This allows you to have as many (or as few) labels as you want, and doesn't require them to be equally-spaced.

    If you want to be really efficient, you can look for the appropriate label via binary search, although realistically I doubt you will have enough labels for a linear search to be a problem.

    But I also see nothing wrong with hard-coding a series of if/else statements for this, unless you have in mind some specific reason for doing otherwise. The parameters need to be represented somehow. A bunch of ifs is probably the fastest and simplest way for you to get something working. There are possible reasons why you might want to do it other ways, but you should be able to say what specific advantage you expect to get from doing it a harder way.


    2. Normal distributions result from adding together (or averaging) lots of independent random variables.

    For game purposes, you usually don't want a true normal distribution, which has very long tails. (i.e. there's an extremely small but non-zero chance of getting a very extreme result)

    Adding together about 3 random numbers will give you a distribution that is noticeably curved but still has non-trivial probability mass on the min and max values. So try generating 3 random numbers between minHeight and maxHeight and averaging them.


    3. What do you mean by "doesn't seem to work well?" Unclear what problem you're trying to solve.


    I'm not going to answer #4 because that's too much work for me this early in the morning. But in general terms there's lots of different ways to implement content systems, and the best way will depend on what capabilities you actually need.
     
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    I assume that these are serializable classes, which explains why they're never null - Unity always creates "blank" ones of these anytime the MonoBehaviour that owns them is serialized (anytime it's loaded, saved, or looked at in the inspector, or any number of other options). You also have the problem where you can't "save" these objects and reuse them on other characters; you have to re-enter the data on each character that uses a given piece of Armor.

    What you want is for your equipment to be UnityEngine.Object-derived, which basically means either: 1) make it a MonoBehaviour on a prefab, or 2) make is a ScriptableObject. ScriptableObject is probably what you want in this case. Be advised that any state information specific to an instance of the equipment (e.g. a weapon's ammo level) will need to be stored outside of the ScriptableObject. There are tons of tutorials online for using ScriptableObject for exactly this use case so hopefully having the keyword to search there is what you need.

    For this, check out xNode (and tutorials for it).
     
  5. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71

    Now I'm confused.
    Weapons and armor are indeed seriazable, as I want to save them. They are meant to be enchantable/changable (so in my inventory, weapons aren't stackable - it's not a list of <item, ammount>), and it is my understanding that Scriptable Objects are best used for static things - templates. Also, since Sriptable Objects do not exist within the scene but within the project asset folder itself, their use for items seemed unfeasable.

    My inventory is 4 lists (weapons, armor, misc, consumables) and assigning a weapon to a character and putting it back into the inventory works.

    And a monobehavior on a prefab - that's how my characters used to work...but having 30 models loaded(but inactive) didn't strike me as optimized, and you cannot save a object in a scene, to I had to change that.

    I have to re-think everything I've done....
     
  6. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Less than you think you do, I think. You'll need to separate the weapons (game obejcts, models, etc) from their data and knowing more details of your needs, a serialized custom class is a good way to do that. You just need to either:

    a) make Unity not fill it with empty objects. If you don't need it to be editable in the inspector, this is easy enough, just make it private. Otherwise you'd need to write a custom inspector that can make it null or non-null on demand. Or you could grab a tool like Odin Inspector which I think supports this?

    b) make your code able to distinguish a valid object from an empty one (probably a simple "isValid" boolean) and check for that in addition to checking for null

    c) Use some trickery to take advantage of Unity's serialization to your advantage. I only thought of this when I was most of the way through typing this comment but this is probably the best approach tbh. Store your actual data in an array (even in this only ever has one item); if the array's length is 0, treat that as null. You could make a property to do this simply:
    Code (csharp):
    1. public Weapon hand1 {
    2.     get {
    3.         if (hand1Data.Length == 0) return null;
    4.         else return hand1Data[0];
    5.     }
    6.     set {
    7.         if (value == null) hand1Data = new Weapon[0];
    8.         else hand1Data = new Weapon[]{value};
    9.     }
    10. }
    11. [SerializeField] private Weapon[] hand1Data;
     
  7. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71
    They are separate

    Ya know, I just thought of using a List, and checking for list count this morning...

    How my inventory and items work atm:


    I have a game object in the main scene that holds several key monobehaviors.
    Lists of Weapons/Armor/Misc that holds all weapon/armor/misc templates (hence, why it's seriazable, so I can enter items trough the inspector)
    There's also the active inventory lists - they start empty and items get added to the list.

    Adding multiple items to my inventory, equipping them on a character and putting them back into the inventory works...with the issue of weirdness with "empty/no" items.

    Since every single attribute of a weapon can be changed, I have to store full items. Every item has a list of enchantments. The code goes trough all enchantments and applies them to the weapon immediately (if they target the weapon. A sharpness enchanment would immadiately increase weapon damage, etc..)
    Enchantment that target the character (like STR boost) are only applied/unapplied on equipping/unequipping.

    However, in the context of saving the game I have to be able to save the inventory - and of course, all the generated characters.

    I can get around the Serialize issue by using Scriptable Object to create weapon templates (I think). It's far easier to generate a random weapon by simple picking a random from a list + picking random enchantment, than it is to create an empty template weapon and then fill in all the data from a scriptable object.


    Weapon wpn = weaponslist[Radnom.Range(0,weaponslist.Count)];
    AddEnchantment(wpn,1);
    inventoryWeapons.Add(wpn);

    or something along those lines


    Thank you to everyone in this thread!
     
  8. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71
    Ok, re-wrote my entire code...it mostly works..mostly.
    3D models are instantiated properly, data is read correctly..EVERYTHING except the characters inventory.


    Getting Objects Refference Not set To instance on this bit here:

    Code (CSharp):
    1.     public void RefreshPaperdoll()
    2.     {
    3.         //BODY ARMOR
    4.         if (mdm.girldata.girlList[mdm.girlpanel.selectedGirl].armorSlot.Count == 0) <--- THIS BIT RIGHT HERE
    5.         {
    6.             //if empty, use the empty sprite/icon
    7.             mdm.girlpanel.UpdateInvImage(mdm.girlpanel.inv_torso, emptyInventory);
    8.         }
    9.         else
    10.         {
    11.             //replace image with icon from the item. Call replace image method
    12.             mdm.girlpanel.UpdateInvImage(mdm.girlpanel.inv_torso, mdm.girldata.girlList[mdm.girlpanel.selectedGirl].armorSlot[0].icon);
    13.  
    14.             //enchantment
    15.             if (mdm.girldata.girlList[mdm.girlpanel.selectedGirl].armorSlot[0].enchantmentsN.Count > 0)
    16.             {
    17.                 Sprite enchantmentIcon = mdm.girldata.girlList[mdm.girlpanel.selectedGirl].armorSlot[0].enchantmentsN[0].icon;  //get icon from enchantment
    18.                 mdm.girlpanel.UpdateInvEnchantmentImage(mdm.girlpanel.inv_torso, enchantmentIcon); //set icon
    19.             }
    20.             else //NOT enchanted
    21.             {
    22.                 mdm.girlpanel.UpdateInvEnchantmentImage(mdm.girlpanel.inv_torso, emptyInventory);
    23.             }
    24.         }
    25.  
    mdm.girlpanel.selectedGirl returns an integer, which corresponds to the ID/position of the generated character in the mdm.girldata.girlList. Since every character is a Humanoid class and thus contains a List<Armor> armorSlot I cannot figure out what is wrong anymore.


    As long as I skip refereshing the paperdoll, things work.
     
  9. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    That is a very long "bit", what line is the error actually on?
     
  11. ElMagnifico

    ElMagnifico

    Joined:
    Aug 27, 2018
    Posts:
    71
    I marked it, how did you miss it?

    if (mdm.girldata.girlList[mdm.girlpanel.selectedGirl].armorSlot.Count == 0) <--- THIS BIT RIGHT HERE