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.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "BaseData", menuName = "Character Data/Base Data")]
public class BaseData : ScriptableObject
{
[Header("Base Info")]
public int unitID = 0000;
public string characterName = "Character";
public int baseHealth = 100;
public int baseActionPoints = 2;
public int Level = 1;
[Header("Aggro Info")]
public int baseAggro = 0;
public int revealAggro = 20;
public int aggroDecrease = 5;
[Header("Class Info")]
public string characterClass = "Class";
public int baseArmor = 20;
public int resistence = 5;
[Header("Accuracy Info")]
public int baseAccuracy = 70;
public int rangeAccuracyDeduction = 10;
[Header("Movement Info")]
public int movementRange = 5;
public int baseSpeed = 70;
[Header("Weapons")]
public List<WeaponData> weapons = new List<WeaponData>();
[Header("Passives")]
public List<PassiveData> passives = new List<PassiveData>();
// You can add more shared attributes here...
}
using UnityEngine;
[CreateAssetMenu(fileName = "LinaData", menuName = "Character Data/Lina Data")]
public class LinaData : BaseData
{
[Header("Lina-specific Info")]
public WeaponData solaris;
public WeaponData pistol;
public PassiveData glimmeringHero; // Passive ability
}
using UnityEngine;
using System.Collections.Generic;
public class Stats : MonoBehaviour
{
public BaseData baseData; // Assign the corresponding BaseData scriptable object in the Inspector
public StatsUI statsUI;
private static Stats instance;
// These fields store the current stats
private int currentLevel;
private int currentHealth;
private int currentArmor;
private int currentResistence;
private int actionPoints;
private int currentAggro;
private int currentAccuracy;
private int currentMovementRange;
private int currentActionPoints;
private int currentSpeed;
private string characterName;
private string characterClass;
// You can add more fields for other current stats...
private void Awake()
{
instance = this;
// Initialize the current stats with base values
currentLevel = baseData.Level;
currentHealth = baseData.baseHealth;
currentArmor = baseData.baseArmor;
currentResistence = baseData.resistence;
actionPoints = baseData.baseActionPoints;
currentAggro = baseData.baseAggro;
currentAccuracy = baseData.baseAccuracy;
currentMovementRange = baseData.movementRange;
currentActionPoints = baseData.baseActionPoints;
currentSpeed = baseData.baseSpeed;
characterName = baseData.characterName;
characterClass = baseData.characterClass;
// Add DontDestroyOnLoad functionality to persist the Stats object
DontDestroyOnLoad(gameObject);
}
// Properties to access the private variables
public int CurrentLevel => currentLevel;
public int CurrentHealth => currentHealth;
public int CurrentArmor => currentArmor;
public int CurrentResistence => currentResistence;
public int ActionPoints => actionPoints;
public int CurrentAggro => currentAggro;
public int CurrentAccuracy => currentAccuracy;
public int CurrentMovementRange => currentMovementRange;
public int CurrentActionPoints => currentActionPoints;
public int CurrentSpeed => currentSpeed;
public string CharacterName => characterName;
public string CharacterClass => characterClass;
public static Stats GetInstance()
{
return instance;
}
}
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.
using UnityEngine;
using TMPro;
using DG.Tweening;
using UnityEngine.UI;
public class StatsUI : MonoBehaviour
{
[SerializeField] private bool isEnemyUI;
[Header("Base Info")]
public TextMeshProUGUI characterNameText;
public TextMeshProUGUI characterClassText;
//Ph Atk, Hit
public TextMeshProUGUI levelText;//Lvl
public TextMeshProUGUI healthText;//HP
public TextMeshProUGUI armorText;//Def
//Resistence is the resistence the unit has against various attacks.
//Resistence provide damage reduction on attacks.
//Modifiers can mean both buffs or debuffs. This depends on class and environment effectiveness.
//Resistence = baseResistence + Defence/10 + modifiers.
public TextMeshProUGUI resistenceText;//Res
//Aggrovation grows or decreases depending on the actions the unit does.
//Aggro is the likeliness the enemy will chase this unit on the map or focus their attack on the battle scene.
//Having 0 aggro provides the unit to be concealed, which can allow ambushes or tactical movement for the team.
public TextMeshProUGUI aggroText;
//Accuracy is the likeliness the attack proceeded by the player hits the enemy.
//Each weapon for each character has a base accuracy that is changed by modifiers or upgrades.
public TextMeshProUGUI accuracyText;
//Movement range is the max range this unit can move
public TextMeshProUGUI movementRangeText;
//ActionPoints = int(1 + speed/15).
public TextMeshProUGUI actionPointsText;
//Speed is the overall reflexes of the unit. Speed effects various matters.
//Avoid = int(BaseAvoid + speed/12).
//ActionPoints = int(1 + speed/15).
//Counter = BaseCounter + Speed/10. (Max is 1)
//Counter is the likeliness the character can counter an incoming attack and fight back.
//Movement = int(BaseMovement + Speed/10).
public TextMeshProUGUI speedText;
[Header("Weapon Info")]
public TextMeshProUGUI currentWeaponText;
public GameObject statsPanel;
public GameObject modelVisualization;
private static StatsUI instance;
private Unit selectedUnit; // Add this field to store the currently selected unit
private void Awake()
{
instance = this;
}
public void SetCurrentWeaponText(string weaponName)
{
currentWeaponText.text = "Current Weapon: " + weaponName;
}
// Show the stats panel and update it with the selected unit's stats
public void ShowPanel(Unit unit)
{
//if(unit.IsEnemy())
selectedUnit = unit;
statsPanel.SetActive(true);
modelVisualization.SetActive(true);
// Set the initial alpha of the modelVisualization to 0
Color initialColor = modelVisualization.GetComponent<RawImage>().color;
initialColor.a = 0f;
modelVisualization.GetComponent<RawImage>().color = initialColor;
Debug.Log(isEnemyUI + " != " + unit.IsEnemy());
// Animate the panel to move from the left outside the screen to the left side of the screen
statsPanel.transform.DOLocalMoveX(-500f, 0.3f).SetEase(Ease.OutCubic);
modelVisualization.transform.DOLocalMoveX(0f, 0.3f).SetEase(Ease.OutCubic);
// Fade in the modelVisualization
modelVisualization.GetComponent<RawImage>().DOFade(1f, 0.3f).SetEase(Ease.InOutCubic);
UpdateStatsForUnit(selectedUnit);
}
// Hide the stats panel
public void HidePanel(Unit previousUnit)
{
selectedUnit = null; // Clear the selected unit reference
// Animate the panel to move to the left outside the screen
statsPanel.transform.DOLocalMoveX(-statsPanel.GetComponent<RectTransform>().rect.width, 0.3f)
.SetEase(Ease.InCubic)
.OnComplete(() =>
{
statsPanel.SetActive(false);
});
// Fade out the modelVisualization
modelVisualization.GetComponent<RawImage>().DOFade(0f, 0.3f).SetEase(Ease.InOutCubic)
.OnComplete(() =>
{
modelVisualization.SetActive(false);
});
}
public static StatsUI GetInstance()
{
return instance;
}
private void ResetUIElements()
{
characterNameText.text = "";
characterClassText.text = "";
levelText.text = "";
healthText.text = "";
armorText.text = "";
resistenceText.text = "";
aggroText.text = "";
accuracyText.text = "";
movementRangeText.text = "";
actionPointsText.text = "";
speedText.text = "";
}
private void UpdateStatsForUnit(Unit selectedUnit)
{
if (selectedUnit != null)
{
Stats stats = selectedUnit.GetComponent<Stats>();
if (stats != null)
{
// Update UI elements with current stats
characterNameText.text = stats.CharacterName;
characterClassText.text = stats.CharacterClass;
levelText.text = "LVL " + stats.CurrentLevel;
healthText.text = "HP " + stats.CurrentHealth;
armorText.text = "DEF " + stats.CurrentArmor;
resistenceText.text = "RES " + stats.CurrentResistence;
aggroText.text = "AGGRO " + stats.CurrentAggro;
accuracyText.text = "ACR " + stats.CurrentAccuracy;
movementRangeText.text = "MVR " + stats.CurrentMovementRange;
actionPointsText.text = "AP" + stats.CurrentActionPoints;
speedText.text = "SPD " + stats.CurrentSpeed;
}
}
else
{
// Reset UI elements if no unit is selected
ResetUIElements();
}
}
}
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.
using System.Collections.Generic;
using UnityEngine;
public class WeaponStats : MonoBehaviour
{
// List to store weapon data for each character
public List<WeaponData> weaponDataList = new List<WeaponData>();
// Define a delegate for updating weapon data
public delegate void WeaponDataUpdatedEventHandler(WeaponData weaponData);
// Define an event based on the delegate
public event WeaponDataUpdatedEventHandler WeaponDataUpdated;
private string currentWeaponName = "Nothing";
// ... other weapon-related attributes
// Make these properties public with private setters
public int CurrentDamage { get; private set; } = 0;
public int CurrentAccuracy { get; private set; } = 0;
public int CurrentWeaponID { get; private set; } = 0;
private int currentWeaponRange = 0;
private int currentHealAmount = 0;
private int currentDefenseRegeneration = 0;
private int currentDuration = 0;
private int currentCooldown = 0;
private int currentAggroGain = 0;
private int currentAggroDecrease = 0;
// Dictionary to store weapon data with IDs as keys
private Dictionary<int, WeaponData> weaponDataDictionary = new Dictionary<int, WeaponData>();
// Initialize weapon-related attributes to their default values
private void Awake()
{
// Populate the dictionary with weapon data
foreach (WeaponData weaponData in weaponDataList)
{
weaponDataDictionary[weaponData.weaponID] = weaponData;
}
}
public string GetCurrentWeaponName()
{
Debug.Log("CurrentWeaponID: " + CurrentWeaponID);
if (weaponDataDictionary.TryGetValue(CurrentWeaponID, out WeaponData weaponData))
{
Debug.Log("Found weapon data for ID " + CurrentWeaponID);
return weaponData.weaponName;
}
Debug.LogWarning("Weapon data not found for ID " + CurrentWeaponID);
return "Nothing";
}
public int GetCurrentDamage()
{
return CurrentDamage;
}
public int GetCurrentAccuracy()
{
return CurrentAccuracy;
}
// Method to get the current weapon's ID
public int GetCurrentWeaponID()
{
return CurrentWeaponID;
}
// Method to retrieve the current weapon data based on a weapon ID
public WeaponData GetCurrentWeapon(int weaponID)
{
if (weaponDataDictionary.TryGetValue(weaponID, out WeaponData weaponData))
{
RecordCurrentWeapon(weaponData);
//Debug.Log(GetCurrentWeaponName());
// Return the weapon data found in the dictionary
return weaponData;
}
else
{
Debug.LogWarning("Weapon data with ID " + weaponID + " not found in WeaponStats dictionary.");
ResetCurrentWeapon();
Debug.Log(GetCurrentWeaponID());
return null;
}
}
public void RecordCurrentWeapon(WeaponData weaponData)
{
// Set the current values based on the selected weapon data
currentWeaponName = weaponData.weaponName;
currentWeaponRange = weaponData.weaponRange;
CurrentWeaponID = weaponData.weaponID;
Debug.Log(CurrentWeaponID + " record");
CurrentDamage = weaponData.damage;
currentHealAmount = weaponData.healAmount;
currentDefenseRegeneration = weaponData.defenseRegeneration;
CurrentAccuracy = weaponData.accuracy;
currentDuration = weaponData.duration;
currentCooldown = weaponData.cooldown;
currentAggroGain = weaponData.aggroGain;
currentAggroDecrease = weaponData.aggroDecrease;
// Invoke the event to notify listeners (like WeaponStatsUI)
WeaponDataUpdated?.Invoke(weaponData);
}
private void ResetCurrentWeapon()
{
currentWeaponName = "Nothing";
currentWeaponRange = 0;
CurrentWeaponID = 0;
CurrentDamage = 0;
currentHealAmount = 0;
currentDefenseRegeneration = 0;
CurrentAccuracy = 0;
currentDuration = 0;
currentCooldown = 0;
currentAggroGain = 0;
currentAggroDecrease = 0;
}
}
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.
using UnityEngine;
[CreateAssetMenu(fileName = "WeaponData", menuName = "Character Data/Weapon Data")]
public class WeaponData : ScriptableObject
{
public string weaponName;
// ... other weapon-related attributes
public int weaponRange;
public int weaponID;
public int damage;
public int healAmount;
public int defenseRegeneration;
public int accuracy;
public int duration;
public int cooldown;
public int aggroGain;
public int aggroDecrease;
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SolarisAction : BaseAction
{
// Specify the weapon ID you want to retrieve (e.g., 101 for Solaris)
private int weaponID = 100;
private void Start()
{
// In SolarisAction or any other action script
WeaponStats weaponStats = GetComponent<WeaponStats>();
WeaponData currentWeaponData = weaponStats.GetCurrentWeapon(weaponID);
}
private enum State
{
Aiming,
Shooting,
Cooloff,
}
private State state;
private float stateTimer;
private Unit targetUnit;
private bool canShootBullet;
private void Update()
{
if (!isActive)
{
return;
}
stateTimer -= Time.deltaTime;
switch (state)
{
case State.Aiming:
Vector3 aimDir = (targetUnit.GetWorldPosition() - unit.GetWorldPosition()).normalized;
float rotateSpeed = 10f;
transform.forward = Vector3.Lerp(transform.forward, aimDir, Time.deltaTime * rotateSpeed);
break;
case State.Shooting:
if (canShootBullet)
{
Shoot();
canShootBullet = false;
}
break;
case State.Cooloff:
break;
}
if (stateTimer <= 0f)
{
NextState();
}
}
private void NextState()
{
switch (state)
{
case State.Aiming:
state = State.Shooting;
float shootingStateTime = 0.1f;
stateTimer = shootingStateTime;
break;
case State.Shooting:
state = State.Cooloff;
float coolOffStateTime = 0.5f;
stateTimer = coolOffStateTime;
break;
case State.Cooloff:
ActionComplete();
break;
}
}
private void Shoot()
{
targetUnit.Damage();
}
public override string GetActionName()
{
return "Solaris";
}
public override int GetID()
{
return weaponID;
}
public override List<GridPosition> GetValidActionGridPositionList()
{
List<GridPosition> validGridPositionList = new List<GridPosition>();
GridPosition unitGridPosition = unit.GetGridPosition();
// Access the current weapon from the WeaponStats component
WeaponStats weaponStats = GetComponent<WeaponStats>();
// Specify the weapon ID you want to retrieve (e.g., 101 for Solaris)
int weaponID = 100;
WeaponData currentWeaponData = weaponStats.GetCurrentWeapon(weaponID);
if (weaponStats != null)
{
// Get maxShootDistance directly from solarisWeapon
int maxShootDistance = currentWeaponData.weaponRange;
for (int x = -maxShootDistance; x <= maxShootDistance; x++)
{
for (int z = -maxShootDistance; z <= maxShootDistance; z++)
{
GridPosition offsetGridPosition = new GridPosition(x, z);
GridPosition testGridPosition = unitGridPosition + offsetGridPosition;
if (!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
{
continue;
}
int testDistance = Mathf.Abs(x) + Mathf.Abs(z);
if (testDistance > maxShootDistance)
{
continue;
}
if (!LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
{
// Grid Position is empty, no Unit
continue;
}
Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(testGridPosition);
if (targetUnit.IsEnemy() == unit.IsEnemy())
{
// Both Units on the same 'team'
continue;
}
validGridPositionList.Add(testGridPosition);
}
}
}
else
{
Debug.LogError("WeaponStats component not found.");
}
return validGridPositionList;
}
public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
{
ActionStart(onActionComplete);
targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);
state = State.Aiming;
float aimingStateTime = 1f;
stateTimer = aimingStateTime;
canShootBullet = true;
}
}
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.
using UnityEngine;
using TMPro;
public class WeaponStatsUI : MonoBehaviour
{
public TextMeshProUGUI currentWeaponText;
public TextMeshProUGUI damage;
public TextMeshProUGUI accuracy;
public TextMeshProUGUI weaponIDText; // Add a new TextMeshProUGUI field
// Reference to the WeaponStats component
private WeaponStats weaponStats;
private string lastWeaponName;
private int lastDamage;
private int lastAccuracy;
private void Start()
{
// Find the WeaponStats component in the scene
weaponStats = FindObjectOfType<WeaponStats>();
if (weaponStats != null)
{
Debug.Log("WeaponStats retrieved");
// Initialize last values
lastWeaponName = weaponStats.GetCurrentWeaponName();
lastDamage = weaponStats.GetCurrentDamage();
lastAccuracy = weaponStats.GetCurrentAccuracy();
// Subscribe to the WeaponDataUpdated event
weaponStats.WeaponDataUpdated += OnWeaponDataUpdated;
// Update the UI with the current weapon's information
UpdateUI();
}
else
{
Debug.LogError("WeaponStats component not found in the scene.");
}
}
// Define a method to handle the event
private void OnWeaponDataUpdated(WeaponData weaponData)
{
// Update the UI with the new weapon data
currentWeaponText.text = "Weapon: " + weaponData.weaponName;
damage.text = "Damage: " + weaponData.damage.ToString();
accuracy.text = "Accuracy: " + weaponData.accuracy.ToString();
weaponIDText.text = "Weapon ID: " + weaponStats.GetCurrentWeaponID().ToString(); // Update the weapon ID text
}
private void Update()
{
// Find the WeaponStats component in the scene
weaponStats = FindObjectOfType<WeaponStats>();
UpdateUI();
}
private void UpdateUI()
{
if (weaponStats != null)
{
// Display the current weapon's information from the WeaponStats script using the new methods
currentWeaponText.text = "Weapon: " + weaponStats.GetCurrentWeaponName();
damage.text = "Damage: " + weaponStats.GetCurrentDamage().ToString();
accuracy.text = "Accuracy: " + weaponStats.GetCurrentAccuracy().ToString();
weaponIDText.text = "Weapon ID: " + weaponStats.GetCurrentWeaponID().ToString(); // Update the weapon ID text
}
else
{
Debug.LogError("WeaponStats component not found in UpdateUI.");
}
}
}
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.