Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Question Creating Weapon Stats

Discussion in 'Scripting' started by BamBuStik, Oct 1, 2023.

  1. BamBuStik

    BamBuStik

    Joined:
    May 24, 2022
    Posts:
    1
    Greetings, I don't usually ask for help when coding because usually I can fix the problem after a few all nighters. But as a beginner to Unity I have a problem creating the weapon stats for a game I'm making and request assistance. I just can't find what the problem is and there isn't anyone I can ask for help. This is a long one so bear with me please. And if you have any questions I will happily respond at the earliest convenience. That said..

    As you can see from the title, I'm trying to create a weapon stats for my game. To be more specific, I'm making an indie game that is mostly similar to games like Fire Emblem or Xcom. But unlike the stated titles, the units on the player's team are all unique with their own respected weapons. While, of course, the enemy are usually mobs that are cloned and have their respective "mob" classes. You could think of it as an rpg with a turn based mechanic.

    So initially, when creating stats for a character, because it is an rpg, I needed to make it so that characters would have their respective "databases" storing stuff like max health, their name, level etc, so I went the route of using scriptable objects to create the stats of the characters and attaching them to their respective character models. At the same time, I know that it isn't wise to create stats that constantly need to change as values in a scriptable object. So I made the Stats.cs script that will be a script component attached to every character. The script is meant to open a public slot in the inspector where I would attach the corresponding data of the respected character. And Stats.cs would copy all the data in the SO into changeable, "current" values. For instance, the data would have the MaxHealth of a character so I would copy that in with Stats.cs and call it currentHealth. During a match in the game, all values that are changing regarding stats would refer to Stats.cs instead of the corresponding data SO of the character.
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. [CreateAssetMenu(fileName = "BaseData", menuName = "Character Data/Base Data")]
    5. public class BaseData : ScriptableObject
    6. {
    7.     [Header("Base Info")]
    8.     public int unitID = 0000;
    9.     public string characterName = "Character";
    10.     public int baseHealth = 100;
    11.     public int baseActionPoints = 2;
    12.     public int Level = 1;
    13.     [Header("Aggro Info")]
    14.     public int baseAggro = 0;
    15.     public int revealAggro = 20;
    16.     public int aggroDecrease = 5;
    17.  
    18.     [Header("Class Info")]
    19.     public string characterClass = "Class";
    20.     public int baseArmor = 20;
    21.     public int resistence = 5;
    22.  
    23.     [Header("Accuracy Info")]
    24.     public int baseAccuracy = 70;
    25.     public int rangeAccuracyDeduction = 10;
    26.  
    27.     [Header("Movement Info")]
    28.     public int movementRange = 5;
    29.     public int baseSpeed = 70;
    30.     [Header("Weapons")]
    31.     public List<WeaponData> weapons = new List<WeaponData>();
    32.  
    33.     [Header("Passives")]
    34.     public List<PassiveData> passives = new List<PassiveData>();
    35.  
    36.     // You can add more shared attributes here...
    37. }
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(fileName = "LinaData", menuName = "Character Data/Lina Data")]
    4. public class LinaData : BaseData
    5. {
    6.     [Header("Lina-specific Info")]
    7.     public WeaponData solaris;
    8.     public WeaponData pistol;
    9.  
    10.     public PassiveData glimmeringHero; // Passive ability
    11. }
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. public class Stats : MonoBehaviour
    5. {
    6.     public BaseData baseData; // Assign the corresponding BaseData scriptable object in the Inspector
    7.     public StatsUI statsUI;
    8.     private static Stats instance;
    9.  
    10.     // These fields store the current stats
    11.     private int currentLevel;
    12.     private int currentHealth;
    13.     private int currentArmor;
    14.     private int currentResistence;
    15.     private int actionPoints;
    16.     private int currentAggro;
    17.     private int currentAccuracy;
    18.     private int currentMovementRange;
    19.     private int currentActionPoints;
    20.     private int currentSpeed;
    21.     private string characterName;
    22.     private string characterClass;
    23.  
    24.     // You can add more fields for other current stats...
    25.  
    26.     private void Awake()
    27.     {
    28.         instance = this;
    29.         // Initialize the current stats with base values
    30.         currentLevel = baseData.Level;
    31.         currentHealth = baseData.baseHealth;
    32.         currentArmor = baseData.baseArmor;
    33.         currentResistence = baseData.resistence;
    34.         actionPoints = baseData.baseActionPoints;
    35.         currentAggro = baseData.baseAggro;
    36.         currentAccuracy = baseData.baseAccuracy;
    37.         currentMovementRange = baseData.movementRange;
    38.         currentActionPoints = baseData.baseActionPoints;
    39.         currentSpeed = baseData.baseSpeed;
    40.         characterName = baseData.characterName;
    41.         characterClass = baseData.characterClass;
    42.  
    43.         // Add DontDestroyOnLoad functionality to persist the Stats object
    44.         DontDestroyOnLoad(gameObject);
    45.     }
    46.  
    47.     // Properties to access the private variables
    48.     public int CurrentLevel => currentLevel;
    49.     public int CurrentHealth => currentHealth;
    50.     public int CurrentArmor => currentArmor;
    51.     public int CurrentResistence => currentResistence;
    52.     public int ActionPoints => actionPoints;
    53.     public int CurrentAggro => currentAggro;
    54.     public int CurrentAccuracy => currentAccuracy;
    55.     public int CurrentMovementRange => currentMovementRange;
    56.     public int CurrentActionPoints => currentActionPoints;
    57.     public int CurrentSpeed => currentSpeed;
    58.     public string CharacterName => characterName;
    59.     public string CharacterClass => characterClass;
    60.  
    61.     public static Stats GetInstance()
    62.     {
    63.         return instance;
    64.     }
    65. }
    Another example, movement range? You can refer stats.currentMovementRange, armor? stats.Armor. Like this, I thought this concept worked very well and I thought any data would have no problem when designed like this. I also managed to create a UI displaying the stats of a character as well.
    Code (CSharp):
    1. using UnityEngine;
    2. using TMPro;
    3. using DG.Tweening;
    4. using UnityEngine.UI;
    5.  
    6. public class StatsUI : MonoBehaviour
    7. {
    8.     [SerializeField] private bool isEnemyUI;
    9.     [Header("Base Info")]
    10.     public TextMeshProUGUI characterNameText;
    11.     public TextMeshProUGUI characterClassText;
    12.     //Ph Atk, Hit
    13.     public TextMeshProUGUI levelText;//Lvl
    14.     public TextMeshProUGUI healthText;//HP
    15.     public TextMeshProUGUI armorText;//Def
    16.     //Resistence is the resistence the unit has against various attacks.
    17.     //Resistence provide damage reduction on attacks.
    18.     //Modifiers can mean both buffs or debuffs. This depends on class and environment effectiveness.
    19.     //Resistence = baseResistence + Defence/10 + modifiers.
    20.     public TextMeshProUGUI resistenceText;//Res
    21.     //Aggrovation grows or decreases depending on the actions the unit does.
    22.     //Aggro is the likeliness the enemy will chase this unit on the map or focus their attack on the battle scene.
    23.     //Having 0 aggro provides the unit to be concealed, which can allow ambushes or tactical movement for the team.
    24.     public TextMeshProUGUI aggroText;
    25.     //Accuracy is the likeliness the attack proceeded by the player hits the enemy.
    26.     //Each weapon for each character has a base accuracy that is changed by modifiers or upgrades.
    27.     public TextMeshProUGUI accuracyText;
    28.     //Movement range is the max range this unit can move
    29.     public TextMeshProUGUI movementRangeText;
    30.     //ActionPoints = int(1 + speed/15).
    31.     public TextMeshProUGUI actionPointsText;
    32.     //Speed is the overall reflexes of the unit. Speed effects various matters.
    33.     //Avoid = int(BaseAvoid + speed/12).
    34.     //ActionPoints = int(1 + speed/15).
    35.     //Counter = BaseCounter + Speed/10. (Max is 1)
    36.     //Counter is the likeliness the character can counter an incoming attack and fight back.
    37.     //Movement = int(BaseMovement + Speed/10).
    38.     public TextMeshProUGUI speedText;
    39.     [Header("Weapon Info")]
    40.     public TextMeshProUGUI currentWeaponText;
    41.     public GameObject statsPanel;
    42.     public GameObject modelVisualization;
    43.  
    44.     private static StatsUI instance;
    45.     private Unit selectedUnit; // Add this field to store the currently selected unit
    46.  
    47.     private void Awake()
    48.     {
    49.         instance = this;
    50.     }
    51.  
    52.  
    53.     public void SetCurrentWeaponText(string weaponName)
    54.     {
    55.         currentWeaponText.text = "Current Weapon: " + weaponName;
    56.     }
    57.  
    58.     // Show the stats panel and update it with the selected unit's stats
    59.     public void ShowPanel(Unit unit)
    60.     {
    61.         //if(unit.IsEnemy())
    62.         selectedUnit = unit;
    63.         statsPanel.SetActive(true);
    64.         modelVisualization.SetActive(true);
    65.         // Set the initial alpha of the modelVisualization to 0
    66.         Color initialColor = modelVisualization.GetComponent<RawImage>().color;
    67.         initialColor.a = 0f;
    68.         modelVisualization.GetComponent<RawImage>().color = initialColor;
    69.  
    70.         Debug.Log(isEnemyUI + " != " + unit.IsEnemy());
    71.         // Animate the panel to move from the left outside the screen to the left side of the screen
    72.         statsPanel.transform.DOLocalMoveX(-500f, 0.3f).SetEase(Ease.OutCubic);
    73.         modelVisualization.transform.DOLocalMoveX(0f, 0.3f).SetEase(Ease.OutCubic);
    74.  
    75.         // Fade in the modelVisualization
    76.         modelVisualization.GetComponent<RawImage>().DOFade(1f, 0.3f).SetEase(Ease.InOutCubic);
    77.  
    78.         UpdateStatsForUnit(selectedUnit);
    79.     }
    80.  
    81.     // Hide the stats panel
    82.     public void HidePanel(Unit previousUnit)
    83.     {
    84.         selectedUnit = null; // Clear the selected unit reference
    85.  
    86.         // Animate the panel to move to the left outside the screen
    87.         statsPanel.transform.DOLocalMoveX(-statsPanel.GetComponent<RectTransform>().rect.width, 0.3f)
    88.             .SetEase(Ease.InCubic)
    89.             .OnComplete(() =>
    90.             {
    91.                 statsPanel.SetActive(false);
    92.             });
    93.  
    94.         // Fade out the modelVisualization
    95.         modelVisualization.GetComponent<RawImage>().DOFade(0f, 0.3f).SetEase(Ease.InOutCubic)
    96.             .OnComplete(() =>
    97.             {
    98.                 modelVisualization.SetActive(false);
    99.             });
    100.     }
    101.  
    102.     public static StatsUI GetInstance()
    103.     {
    104.         return instance;
    105.     }
    106.  
    107. private void ResetUIElements()
    108. {
    109.     characterNameText.text = "";
    110.     characterClassText.text = "";
    111.     levelText.text = "";
    112.     healthText.text = "";
    113.     armorText.text = "";
    114.     resistenceText.text = "";
    115.     aggroText.text = "";
    116.     accuracyText.text = "";
    117.     movementRangeText.text = "";
    118.     actionPointsText.text = "";
    119.     speedText.text = "";
    120. }
    121.  
    122.     private void UpdateStatsForUnit(Unit selectedUnit)
    123.     {
    124.         if (selectedUnit != null)
    125.         {
    126.             Stats stats = selectedUnit.GetComponent<Stats>();
    127.  
    128.             if (stats != null)
    129.             {
    130.                 // Update UI elements with current stats
    131.                 characterNameText.text = stats.CharacterName;
    132.                 characterClassText.text = stats.CharacterClass;
    133.                 levelText.text = "LVL " + stats.CurrentLevel;
    134.                 healthText.text = "HP " + stats.CurrentHealth;
    135.                 armorText.text = "DEF " + stats.CurrentArmor;
    136.                 resistenceText.text = "RES " + stats.CurrentResistence;
    137.                 aggroText.text = "AGGRO " + stats.CurrentAggro;
    138.                 accuracyText.text = "ACR " + stats.CurrentAccuracy;
    139.                 movementRangeText.text = "MVR " + stats.CurrentMovementRange;
    140.                 actionPointsText.text = "AP" + stats.CurrentActionPoints;
    141.                 speedText.text = "SPD " + stats.CurrentSpeed;
    142.             }
    143.         }
    144.         else
    145.         {
    146.             // Reset UI elements if no unit is selected
    147.             ResetUIElements();
    148.         }
    149.     }
    150. }
    The problem started when I decided that I should now make a separate database for the weapon stats of each units. Each unit has their own unique weapons that don't overlap with other characters.
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. public class WeaponStats : MonoBehaviour
    5. {
    6.     // List to store weapon data for each character
    7.     public List<WeaponData> weaponDataList = new List<WeaponData>();
    8.     // Define a delegate for updating weapon data
    9.     public delegate void WeaponDataUpdatedEventHandler(WeaponData weaponData);
    10.     // Define an event based on the delegate
    11.     public event WeaponDataUpdatedEventHandler WeaponDataUpdated;
    12.  
    13.     private string currentWeaponName = "Nothing";
    14.     // ... other weapon-related attributes
    15.     // Make these properties public with private setters
    16.     public int CurrentDamage { get; private set; } = 0;
    17.     public int CurrentAccuracy { get; private set; } = 0;
    18.     public int CurrentWeaponID { get; private set; } = 0;
    19.     private int currentWeaponRange = 0;
    20.     private int currentHealAmount = 0;
    21.     private int currentDefenseRegeneration = 0;
    22.     private int currentDuration = 0;
    23.     private int currentCooldown = 0;
    24.     private int currentAggroGain = 0;
    25.     private int currentAggroDecrease = 0;
    26.  
    27.     // Dictionary to store weapon data with IDs as keys
    28.     private Dictionary<int, WeaponData> weaponDataDictionary = new Dictionary<int, WeaponData>();
    29.  
    30.     // Initialize weapon-related attributes to their default values
    31.     private void Awake()
    32.     {
    33.         // Populate the dictionary with weapon data
    34.         foreach (WeaponData weaponData in weaponDataList)
    35.         {
    36.             weaponDataDictionary[weaponData.weaponID] = weaponData;
    37.         }
    38.     }
    39.  
    40.     public string GetCurrentWeaponName()
    41.     {
    42.         Debug.Log("CurrentWeaponID: " + CurrentWeaponID);
    43.  
    44.         if (weaponDataDictionary.TryGetValue(CurrentWeaponID, out WeaponData weaponData))
    45.         {
    46.             Debug.Log("Found weapon data for ID " + CurrentWeaponID);
    47.             return weaponData.weaponName;
    48.         }
    49.  
    50.         Debug.LogWarning("Weapon data not found for ID " + CurrentWeaponID);
    51.         return "Nothing";
    52.     }
    53.  
    54.     public int GetCurrentDamage()
    55.     {
    56.         return CurrentDamage;
    57.     }
    58.  
    59.     public int GetCurrentAccuracy()
    60.     {
    61.         return CurrentAccuracy;
    62.     }
    63.  
    64.     // Method to get the current weapon's ID
    65.     public int GetCurrentWeaponID()
    66.     {
    67.         return CurrentWeaponID;
    68.     }
    69.  
    70.     // Method to retrieve the current weapon data based on a weapon ID
    71.     public WeaponData GetCurrentWeapon(int weaponID)
    72.     {
    73.         if (weaponDataDictionary.TryGetValue(weaponID, out WeaponData weaponData))
    74.         {
    75.             RecordCurrentWeapon(weaponData);
    76.             //Debug.Log(GetCurrentWeaponName());
    77.             // Return the weapon data found in the dictionary
    78.             return weaponData;
    79.         }
    80.         else
    81.         {
    82.             Debug.LogWarning("Weapon data with ID " + weaponID + " not found in WeaponStats dictionary.");
    83.             ResetCurrentWeapon();
    84.             Debug.Log(GetCurrentWeaponID());
    85.             return null;
    86.         }
    87.     }
    88.  
    89.     public void RecordCurrentWeapon(WeaponData weaponData)
    90.     {
    91.         // Set the current values based on the selected weapon data
    92.         currentWeaponName = weaponData.weaponName;
    93.         currentWeaponRange = weaponData.weaponRange;
    94.         CurrentWeaponID = weaponData.weaponID;
    95.         Debug.Log(CurrentWeaponID + " record");
    96.         CurrentDamage = weaponData.damage;
    97.         currentHealAmount = weaponData.healAmount;
    98.         currentDefenseRegeneration = weaponData.defenseRegeneration;
    99.         CurrentAccuracy = weaponData.accuracy;
    100.         currentDuration = weaponData.duration;
    101.         currentCooldown = weaponData.cooldown;
    102.         currentAggroGain = weaponData.aggroGain;
    103.         currentAggroDecrease = weaponData.aggroDecrease;
    104.  
    105.         // Invoke the event to notify listeners (like WeaponStatsUI)
    106.         WeaponDataUpdated?.Invoke(weaponData);
    107.     }
    108.  
    109.     private void ResetCurrentWeapon()
    110.     {
    111.         currentWeaponName = "Nothing";
    112.         currentWeaponRange = 0;
    113.         CurrentWeaponID = 0;
    114.         CurrentDamage = 0;
    115.         currentHealAmount = 0;
    116.         currentDefenseRegeneration = 0;
    117.         CurrentAccuracy = 0;
    118.         currentDuration = 0;
    119.         currentCooldown = 0;
    120.         currentAggroGain = 0;
    121.         currentAggroDecrease = 0;
    122.     }
    123. }
    and this is where the problem lies. WeaponStats is a script component attached to each unit just like Stats.cs. It has it's own list of WeaponData stored which each unit will refer to when their corresponding weapon was selected.
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [CreateAssetMenu(fileName = "WeaponData", menuName = "Character Data/Weapon Data")]
    4. public class WeaponData : ScriptableObject
    5. {
    6.     public string weaponName;
    7.     // ... other weapon-related attributes
    8.     public int weaponRange;
    9.     public int weaponID;
    10.     public int damage;
    11.     public int healAmount;
    12.     public int defenseRegeneration;
    13.     public int accuracy;
    14.     public int duration;
    15.     public int cooldown;
    16.     public int aggroGain;
    17.     public int aggroDecrease;
    18. }
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class SolarisAction : BaseAction
    7. {
    8.     // Specify the weapon ID you want to retrieve (e.g., 101 for Solaris)
    9.     private int weaponID = 100;
    10.     private void Start()
    11.     {
    12.         // In SolarisAction or any other action script
    13.         WeaponStats weaponStats = GetComponent<WeaponStats>();
    14.         WeaponData currentWeaponData = weaponStats.GetCurrentWeapon(weaponID);
    15.     }
    16.  
    17.     private enum State
    18.     {
    19.         Aiming,
    20.         Shooting,
    21.         Cooloff,
    22.     }
    23.  
    24.     private State state;
    25.     private float stateTimer;
    26.     private Unit targetUnit;
    27.     private bool canShootBullet;
    28.  
    29.     private void Update()
    30.     {
    31.         if (!isActive)
    32.         {
    33.             return;
    34.         }
    35.  
    36.         stateTimer -= Time.deltaTime;
    37.  
    38.         switch (state)
    39.         {
    40.             case State.Aiming:
    41.                 Vector3 aimDir = (targetUnit.GetWorldPosition() - unit.GetWorldPosition()).normalized;
    42.                
    43.                 float rotateSpeed = 10f;
    44.                 transform.forward = Vector3.Lerp(transform.forward, aimDir, Time.deltaTime * rotateSpeed);
    45.                 break;
    46.             case State.Shooting:
    47.                 if (canShootBullet)
    48.                 {
    49.                     Shoot();
    50.                     canShootBullet = false;
    51.                 }
    52.                 break;
    53.             case State.Cooloff:
    54.                 break;
    55.         }
    56.  
    57.         if (stateTimer <= 0f)
    58.         {
    59.             NextState();
    60.         }
    61.     }
    62.  
    63.     private void NextState()
    64.     {
    65.         switch (state)
    66.         {
    67.             case State.Aiming:
    68.                 state = State.Shooting;
    69.                 float shootingStateTime = 0.1f;
    70.                 stateTimer = shootingStateTime;
    71.                 break;
    72.             case State.Shooting:
    73.                 state = State.Cooloff;
    74.                 float coolOffStateTime = 0.5f;
    75.                 stateTimer = coolOffStateTime;
    76.                 break;
    77.             case State.Cooloff:
    78.                 ActionComplete();
    79.                 break;
    80.         }
    81.     }
    82.  
    83.     private void Shoot()
    84.     {
    85.         targetUnit.Damage();
    86.     }
    87.  
    88.     public override string GetActionName()
    89.     {
    90.         return "Solaris";
    91.     }
    92.  
    93.     public override int GetID()
    94.     {
    95.         return weaponID;
    96.     }
    97.  
    98.     public override List<GridPosition> GetValidActionGridPositionList()
    99.     {
    100.         List<GridPosition> validGridPositionList = new List<GridPosition>();
    101.  
    102.         GridPosition unitGridPosition = unit.GetGridPosition();
    103.         // Access the current weapon from the WeaponStats component
    104.         WeaponStats weaponStats = GetComponent<WeaponStats>();
    105.         // Specify the weapon ID you want to retrieve (e.g., 101 for Solaris)
    106.         int weaponID = 100;
    107.         WeaponData currentWeaponData = weaponStats.GetCurrentWeapon(weaponID);
    108.  
    109.         if (weaponStats != null)
    110.         {
    111.             // Get maxShootDistance directly from solarisWeapon
    112.             int maxShootDistance = currentWeaponData.weaponRange;
    113.             for (int x = -maxShootDistance; x <= maxShootDistance; x++)
    114.             {
    115.                 for (int z = -maxShootDistance; z <= maxShootDistance; z++)
    116.                 {
    117.                     GridPosition offsetGridPosition = new GridPosition(x, z);
    118.                     GridPosition testGridPosition = unitGridPosition + offsetGridPosition;
    119.  
    120.                     if (!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
    121.                     {
    122.                         continue;
    123.                     }
    124.  
    125.                     int testDistance = Mathf.Abs(x) + Mathf.Abs(z);
    126.                     if (testDistance > maxShootDistance)
    127.                     {
    128.                         continue;
    129.                     }
    130.  
    131.                     if (!LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
    132.                     {
    133.                         // Grid Position is empty, no Unit
    134.                         continue;
    135.                     }
    136.  
    137.                     Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(testGridPosition);
    138.  
    139.                     if (targetUnit.IsEnemy() == unit.IsEnemy())
    140.                     {
    141.                         // Both Units on the same 'team'
    142.                         continue;
    143.                     }
    144.  
    145.                     validGridPositionList.Add(testGridPosition);
    146.                 }
    147.             }
    148.         }
    149.         else
    150.         {
    151.             Debug.LogError("WeaponStats component not found.");
    152.         }
    153.         return validGridPositionList;
    154.     }
    155.  
    156.     public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    157.     {
    158.         ActionStart(onActionComplete);
    159.  
    160.         targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);
    161.  
    162.         state = State.Aiming;
    163.         float aimingStateTime = 1f;
    164.         stateTimer = aimingStateTime;
    165.  
    166.         canShootBullet = true;
    167.     }
    168.  
    169. }
    For example here is a weapon called Solaris. The weapon's data ID is 100, and I also made a SO that I named Solaris and had the ID to be 100. So when this Action script is called, weaponStats.GetCurrentWeapon will find the corresponding weaponID in the weaponStats and if it exists, in WeaponStats.cs, RecordCurreentWeapon() will take the data and make the stats of that weapon the "current" stats because it is the currently selected weapon. Now here's the heart of the problem. Firstly, this logic seems to check out and work perfectly fine. But the GetCurrentWeaponName() or and GetCurrent functions other than GetCurrentWeapon() seem to only get the default value of 0 or "nothing". I made these GetCurrent() functions so I can refer the stats to a new UI called WeaponStatsUI(). Which will display the stats of the current weapon.
    Code (CSharp):
    1. using UnityEngine;
    2. using TMPro;
    3.  
    4. public class WeaponStatsUI : MonoBehaviour
    5. {
    6.     public TextMeshProUGUI currentWeaponText;
    7.     public TextMeshProUGUI damage;
    8.     public TextMeshProUGUI accuracy;
    9.     public TextMeshProUGUI weaponIDText; // Add a new TextMeshProUGUI field
    10.  
    11.     // Reference to the WeaponStats component
    12.     private WeaponStats weaponStats;
    13.  
    14.     private string lastWeaponName;
    15.     private int lastDamage;
    16.     private int lastAccuracy;
    17.  
    18.     private void Start()
    19.     {
    20.         // Find the WeaponStats component in the scene
    21.         weaponStats = FindObjectOfType<WeaponStats>();
    22.  
    23.         if (weaponStats != null)
    24.         {
    25.             Debug.Log("WeaponStats retrieved");
    26.             // Initialize last values
    27.             lastWeaponName = weaponStats.GetCurrentWeaponName();
    28.             lastDamage = weaponStats.GetCurrentDamage();
    29.             lastAccuracy = weaponStats.GetCurrentAccuracy();
    30.  
    31.             // Subscribe to the WeaponDataUpdated event
    32.             weaponStats.WeaponDataUpdated += OnWeaponDataUpdated;
    33.  
    34.             // Update the UI with the current weapon's information
    35.             UpdateUI();
    36.         }
    37.         else
    38.         {
    39.             Debug.LogError("WeaponStats component not found in the scene.");
    40.         }
    41.     }
    42.  
    43.     // Define a method to handle the event
    44.     private void OnWeaponDataUpdated(WeaponData weaponData)
    45.     {
    46.         // Update the UI with the new weapon data
    47.         currentWeaponText.text = "Weapon: " + weaponData.weaponName;
    48.         damage.text = "Damage: " + weaponData.damage.ToString();
    49.         accuracy.text = "Accuracy: " + weaponData.accuracy.ToString();
    50.         weaponIDText.text = "Weapon ID: " + weaponStats.GetCurrentWeaponID().ToString(); // Update the weapon ID text
    51.     }
    52.  
    53.     private void Update()
    54.     {
    55.         // Find the WeaponStats component in the scene
    56.         weaponStats = FindObjectOfType<WeaponStats>();
    57.         UpdateUI();
    58.     }
    59.  
    60.     private void UpdateUI()
    61.     {
    62.         if (weaponStats != null)
    63.         {
    64.             // Display the current weapon's information from the WeaponStats script using the new methods
    65.             currentWeaponText.text = "Weapon: " + weaponStats.GetCurrentWeaponName();
    66.             damage.text = "Damage: " + weaponStats.GetCurrentDamage().ToString();
    67.             accuracy.text = "Accuracy: " + weaponStats.GetCurrentAccuracy().ToString();
    68.             weaponIDText.text = "Weapon ID: " + weaponStats.GetCurrentWeaponID().ToString(); // Update the weapon ID text
    69.         }
    70.         else
    71.         {
    72.             Debug.LogError("WeaponStats component not found in UpdateUI.");
    73.         }
    74.     }
    75. }
    As you can see, I'm using FindObjectOfType to see if the selected unit has the WeaponStats component. Unfortunately, because I can't fix the problem with WeaponStat's GetCurrentWeaponName() or any GetCurrent functions, I can't really confirm if this script can find the correct weapon stats and take the correct weapon to display on the UI. And on the UI the result always display 0 or "Nothing", the default values from WeaponStats. But I believe the real problem just lies in WeaponStats.cs and I just can't think of the correct logic.

    So to make this long story short, I need help in getting the updated values for the GetCurrent functions in WeaponStats.cs. RecordCurreentWeapon() is where the values get updated and I of course tried a debug log inside RecordCurreentWeapon() and public WeaponData GetCurrentWeapon(int weaponID) for the currentweapon values. They all update correctly but never the otherGetCurrent functions.

    And to be honest, I'm starting to question on if using SO was a mistake and I'm doing this all wrong. Even when posting this question I doubt if I even know what I am asking about but I suppose the coding logic says it all.
    Anyway thank you for reading my long post and I will happily take any feedback or answer any questions.
     
  2. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,669
    If you have repeating collection of fields, that means you want to t urn them into a class or a struct. Meaning if t here's "weaponRange, weaponDamage, weapon..." and then "currentWeaponRange, currentWeaponDamage....". This should probably be the same type.

    Normally in games like t his you shouldn't copy weapon data anywhere, because weapon data does not change. Normally there's "base stats" which is stuff like weapon characteristics, character stats and so on. And then there's "derived stats" which are calculated based on base stats on demand, use character modifiers and perk into account.

    The thing you're doing with weaponIds - that's a reference. It should be probably a reference type and instead of weaponId you should probably refer to the object itself, unless you're interacting with a database. Reference to scriptable object would effectively act as an ID. Id would be needed only during serialization.

    Regarding getting current values. instead of GetCurrent... use properties. Make a property "currentWeaponStats", make it calculate all stats on demand within getter (or lazy-evaluate them), use it everywhere, and hide base weapon stats as protected fields. This way other classes will not be able to accidentally access internals, and will be forced to use proper weapons. So, basically...

    Code (csharp):
    1.  
    2. class WeaponDataBlock{
    3.     int range;
    4.     int damage;
    5. }
    6.  
    7. class WeaponConfig: ScriptableObject{
    8.      WeaponDataBlock dataBlock;
    9. }
    10.  
    11. class WeaponController: MonoBehaviour{
    12.      [SerializeField] protected WeaponConfig currentWeaponConfig;
    13.      protected WeaponDataBlock calculateDerivedStats(WeaponConfig config){
    14.      }
    15.      public WeaponDataBlock currentWeaponStats {
    16.          get => calculateDerivedStats(curretWeaponConfig);
    17.      }
    18. }
    19.  
    That's the rough idea.
     
  3. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,761
    Please don't use the General Discussion for support questions, in this case use the Scripting forum. I'll move your thread.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,571
    In general, DO NOT use Find-like or GetComponent/AddComponent-like methods unless there truly is no other way, eg, dynamic runtime discovery of arbitrary objects. These mechanisms are for extremely-advanced use ONLY.

    If something is built into your scene or prefab, make a script and drag the reference(s) in. That will let you experience the highest rate of The Unity Way(tm) success of accessing things in your game.



    Otherwise, sounds like you just have a bug, and that can only mean...

    Time to start debugging! Here is how you can begin your exciting new debugging adventures:

    You must find a way to get the information you need in order to reason about what the problem is.

    Once you understand what the problem is, you may begin to reason about a solution to the problem.

    What is often happening in these cases is one of the following:

    - the code you think is executing is not actually executing at all
    - the code is executing far EARLIER or LATER than you think
    - the code is executing far LESS OFTEN than you think
    - the code is executing far MORE OFTEN than you think
    - the code is executing on another GameObject than you think it is
    - you're getting an error or warning and you haven't noticed it in the console window

    To help gain more insight into your problem, I recommend liberally sprinkling
    Debug.Log()
    statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run? what order does it run in?
    - what are the names of the GameObjects or Components involved?
    - what are the values of the variables involved? Are they initialized? Are the values reasonable?
    - are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

    Knowing this information will help you reason about the behavior you are seeing.

    You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as
    Debug.Log("Problem!",this);


    If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

    You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

    You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

    You could also just display various important quantities in UI Text elements to watch them change as you play the game.

    Visit Google for how to see console output from builds. If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer for iOS: https://forum.unity.com/threads/how-to-capturing-device-logs-on-ios.529920/ or this answer for Android: https://forum.unity.com/threads/how-to-capturing-device-logs-on-android.528680/

    If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

    Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

    If your problem is with OnCollision-type functions, print the name of what is passed in!

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494

    "When in doubt, print it out!(tm)" - Kurt Dekker (and many others)

    Note: the
    print()
    function is an alias for Debug.Log() provided by the MonoBehaviour class.




    This might be interesting to you as well:

    ScriptableObject usage in RPGs:

    https://forum.unity.com/threads/scr...tiple-units-in-your-team.925409/#post-6055289

    https://forum.unity.com/threads/cre...ssigned-in-the-inspector.946240/#post-6174205

    Usage as a shared common data container:

     
  5. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    538
    Just to answer the question about your bug very specifically: The WeaponStats.GetCurrent... methods with no parameters don't work until WeaponStats.GetCurrentWeapon(int weaponID) is called because that's the only method that calls RecordCurrentWeapon(WeaponData), which sets WeaponStats.CurrentWeaponID. If you call any of the other WeaponStats.GetCurrent... methods before you call WeaponStats.GetCurrentWeapon(int weaponID) or RecordCurrentWeapon(WeaponData), then they will not work because no CurrentWeaponID has been set yet.

    Now that the specifics of that particular bug have been addressed I just want to comment more broadly on your usage of ScriptableObject and your doubts about using them at all. It is not a mistake to use ScriptableObjects for something like weapon and character stats. However, I think you need to more clearly define in your mind when and where you are using your ScriptableObjects, and why. Then you can confidently apply them where they are best suited. A ScriptableObject is most useful for saving default data with your game that doesn't change after you release it.

    You can tweak those values happily at runtime while designing and testing. They are perfect for that because the values you set during playmode will not be reset when you stop playing. However, you don't need a ScriptableObject when coding an object that changes state during the game. ScriptableObjects are good at storing something like an enemy's starting health, but are not at all necessary for tracking the current health of a particular enemy while the game is running. Either a typical MonoBehaviour or a plain old C# object can handle that. So, you can have typical objects that reference the ScriptableObject assets to determine their starting data, but then handle everything that happens at runtime from there. Ideally those classes would be written so that the logic can handle as many different types of ScriptableObject data as possible. It's not always possible. You might have to have a class for MeleeWeapon and one for RangedWeapon because the logic is just too different, but if you could have just those two classes handle all of the melee and ranged weapons in the game simply by applying different data to the same logic in code, that's great. You've got to separate the logic sometimes, but keeping your class hierarchy as flat as possible, not repeating yourself in the logic of your code, and having the data of the ScriptableObjects drive the uniqueness of the behavior is a good goal to strive for.

    If the instantiated state data that varies between obect instances at runtime does need to be saved, like if the player saves the game mid battle, then you need to handle that your own way with save files and config files that are created after release for that user. SerializedObjects are not for making a save system. You can use the default data of a SerializedObject so that you only save data that is no longer at default, which will result in smaller save files, but they're not really for saving persistent runtime data.

    I don't think you're way off track with your use of ScriptableObjects, but you seem a bit unsure of how you're using them. They are just blocks of common data that you ship with your project. Ideally those blocks of common data run through some common methods that combine for unique results. Also, I would try to reference them directly if at all possible, instead of creating your own ID system.

    They can also have unserialized state data, and you can instantiate them into individual instances that run separately at runtime. I've done that myself. However, that's not the reason ScriptableObjects exist. Any other class or MonoBehaviour script can handle the runtime state of all of your other game object instances, no ScriptableObject necessary. ScriptableObjects provide an excellent way of saving default values that ship with your game. Isolating their usage to that one benefit and leaving the runtime stuff to other classes helps to strictly define that separation of concerns.