Forum begins after the advertisement:
[General] Adding a Shop Passive Upgrades!
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [General] Adding a Shop Passive Upgrades!
- This topic has 10 replies, 5 voices, and was last updated 4 weeks ago by Terence.
-
AuthorPosts
-
August 12, 2024 at 8:56 am #15565::
Hey Everybody I said i would show my Shop scripts in the stream the other day. its not perfect but its working so here you go
The first part of this was to build a Scriptable object i can make for each upgrade, this was an easy part. it was to show the upgrade name an icon and then to take the players stats for what we can upgrade then i added levels to the upgrade so when you buy it it can have differant costs and stats for each level
UpgradeData
using UnityEngine; [CreateAssetMenu(fileName = "UpgradeData", menuName = "Shop/UpgradeData")] public class UpgradeData : ScriptableObject { public string upgradeName; public Sprite icon; [System.Serializable] public class UpgradeLevel { public string description; public int cost; public PlayerStats.Stats statBoost; } public UpgradeLevel[] levels; }
i made a new folder called Skill Shop to hold the script.
this will let you make shop items with levels for each upgradeShopManager next i made a shop manager script for handling showing and buying the upgrades
using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; using System.Collections.Generic; public class ShopManager : MonoBehaviour { public UpgradeData[] upgrades; public OneOffItemData[] oneOffItems; // Array of one-off items public Transform shopContent; public GameObject upgradeButtonPrefab; // Use the same prefab for both upgrades and one-off items public PlayerStats playerStats; public TMP_Text goldText; public PassiveUpgrades passiveUpgrades; public GameObject shopUI; public Button removeAllButton; // Reference to the remove all button [Header("Cannot Buy")] public Color cannotBuyFlashColor = Color.red; // Color to flash when unable to purchase public float cannotBuyShakeDuration = 0.5f; // Duration of the shake effect public float cannotBuyShakeMagnitude = 10f; // Magnitude of the shake effect [Header("Can Buy")] public Color canBuyFlashColor = Color.green; // Color to flash when purchase is successful public float canBuyFlashDuration = 0.5f; // Duration of the flash effect private int playerGold = 100; // Example starting gold amount private bool isShopOpen = false; // Track if the shop is open private void Start() { InitializeShop(); UpdateGoldUI(); ToggleShop(false); // Ensure the shop starts closed removeAllButton.onClick.AddListener(RemoveAllBoughtItems); // Add listener to the remove all button } private void Update() { if (Input.GetKeyDown(KeyCode.O)) { ToggleShop(!isShopOpen); } } private void InitializeShop() { // Clear existing buttons first foreach (Transform child in shopContent) { Destroy(child.gameObject); } // Create buttons for upgrades foreach (UpgradeData upgrade in upgrades) { CreateUpgradeButton(upgrade); } // Create buttons for one-off items foreach (OneOffItemData item in oneOffItems) { CreateOneOffItemButton(item); } } private void CreateUpgradeButton(UpgradeData upgrade) { GameObject button = Instantiate(upgradeButtonPrefab, shopContent); button.transform.Find("Icon").GetComponent<Image>().sprite = upgrade.icon; button.transform.Find("Name").GetComponent<TMP_Text>().text = upgrade.upgradeName; int currentLevel = GetUpgradeLevel(upgrade.upgradeName); if (currentLevel < upgrade.levels.Length) { button.transform.Find("Cost").GetComponent<TMP_Text>().text = upgrade.levels[currentLevel].cost.ToString(); button.transform.Find("Level").GetComponent<TMP_Text>().text = $"Level {currentLevel} -> {currentLevel + 1}"; button.transform.Find("Description").GetComponent<TMP_Text>().text = upgrade.levels[currentLevel].description; Button purchaseButton = button.transform.Find("PurchaseButton").GetComponent<Button>(); purchaseButton.onClick.AddListener(() => PurchaseUpgrade(upgrade, button)); } else { button.transform.Find("Cost").GetComponent<TMP_Text>().text = ""; button.transform.Find("Level").GetComponent<TMP_Text>().text = "Maxed Level"; button.transform.Find("Description").GetComponent<TMP_Text>().text = ""; button.transform.Find("PurchaseButton").GetComponent<Button>().interactable = false; Debug.Log($"{upgrade.upgradeName} is maxed out"); } } private void CreateOneOffItemButton(OneOffItemData item) { GameObject button = Instantiate(upgradeButtonPrefab, shopContent); button.transform.Find("Icon").GetComponent<Image>().sprite = item.icon; button.transform.Find("Name").GetComponent<TMP_Text>().text = item.itemName; button.transform.Find("Cost").GetComponent<TMP_Text>().text = item.cost.ToString(); button.transform.Find("Description").GetComponent<TMP_Text>().text = item.description; button.transform.Find("Level").GetComponent<TMP_Text>().text = ""; // No level for one-off items Button purchaseButton = button.transform.Find("PurchaseButton").GetComponent<Button>(); purchaseButton.onClick.AddListener(() => PurchaseOneOffItem(item, button)); if (passiveUpgrades.HasOneOffItem(item.itemName)) { button.transform.Find("Icon").GetComponent<Image>().color = Color.grey; purchaseButton.interactable = false; } } private int GetUpgradeLevel(string upgradeName) { foreach (var upgrade in passiveUpgrades.purchasedUpgrades) { if (upgrade.upgradeName == upgradeName) { return upgrade.level + 1; // Upgrade levels start from 0, so return the next level } } return 0; // If not found, assume it’s at level 0 } private void PurchaseUpgrade(UpgradeData upgrade, GameObject button) { int currentLevel = GetUpgradeLevel(upgrade.upgradeName); Debug.Log($"Attempting to purchase {upgrade.upgradeName} at level {currentLevel}"); if (currentLevel < upgrade.levels.Length && playerGold >= upgrade.levels[currentLevel].cost) { playerGold -= upgrade.levels[currentLevel].cost; Debug.Log($"Purchased {upgrade.upgradeName} level {currentLevel} for {upgrade.levels[currentLevel].cost} gold."); ApplyUpgrade(upgrade, currentLevel); StartCoroutine(FlashButton(button, canBuyFlashColor, canBuyFlashDuration)); // Flash green on successful purchase UpdateGoldUI(); InitializeShop(); // Reinitialize the shop to refresh the buttons } else { StartCoroutine(FlashRedAndShake(button)); Debug.Log("Not enough gold or upgrade maxed out!"); } } private void PurchaseOneOffItem(OneOffItemData item, GameObject button) { Debug.Log($"Attempting to purchase one-off item {item.itemName}"); if (playerGold >= item.cost) { playerGold -= item.cost; Debug.Log($"Purchased one-off item {item.itemName} for {item.cost} gold."); StartCoroutine(FlashButton(button, canBuyFlashColor, canBuyFlashDuration)); // Flash green on successful purchase passiveUpgrades.AddOneOffItem(item); UseOneOffItem(item); UpdateGoldUI(); InitializeShop(); // Reinitialize the shop to refresh the buttons } else { StartCoroutine(FlashRedAndShake(button)); Debug.Log("Not enough gold!"); } } private void ApplyUpgrade(UpgradeData upgrade, int level) { Debug.Log($"Applying upgrade: {upgrade.upgradeName} at level {level}"); passiveUpgrades.AddUpgrade(upgrade, level); passiveUpgrades.ApplyUpgrades(playerStats); } private void UseOneOffItem(OneOffItemData item) { Debug.Log($"Using one-off item {item.itemName}"); // Implement the effect of the one-off item here // For example, if it's a Double XP item, you can double the XP gain for a certain period // Simulate the effect and removal of the one-off item StartCoroutine(RemoveOneOffItemAfterUse(item)); } private IEnumerator RemoveOneOffItemAfterUse(OneOffItemData item) { yield return new WaitForSeconds(5f); // Simulate some delay before removing the item passiveUpgrades.RemoveOneOffItem(item); Debug.Log($"One-off item {item.itemName} used and removed from inventory."); } private void UpdateGoldUI() { goldText.text = $"Gold: {playerGold}"; Debug.Log($"Updated gold: {playerGold}"); } private void ToggleShop(bool isOpen) { isShopOpen = isOpen; shopUI.SetActive(isOpen); Debug.Log($"Shop is now {(isOpen ? "open" : "closed")}"); } private void RemoveAllBoughtItems() { Debug.Log("Removing all bought items"); // Clear the shop buttons first foreach (Transform child in shopContent) { Destroy(child.gameObject); } passiveUpgrades.purchasedUpgrades.Clear(); passiveUpgrades.purchasedOneOffItems.Clear(); PlayerPrefs.DeleteAll(); // This will reset all upgrades to level 0 InitializeShop(); // Reinitialize the shop to reflect the changes passiveUpgrades.ApplyUpgrades(playerStats); // Reapply upgrades to ensure no stats are carried over Debug.Log("All bought items removed"); } private IEnumerator FlashRedAndShake(GameObject button) { Image[] images = button.GetComponentsInChildren<Image>(); Color[] originalColors = new Color[images.Length]; for (int i = 0; i < images.Length; i++) { originalColors[i] = images[i].color; images[i].color = cannotBuyFlashColor; } Vector3 originalPosition = button.transform.localPosition; float elapsedTime = 0f; while (elapsedTime < cannotBuyShakeDuration) { float xOffset = Random.Range(-cannotBuyShakeMagnitude, cannotBuyShakeMagnitude); float yOffset = Random.Range(-cannotBuyShakeMagnitude, cannotBuyShakeMagnitude); button.transform.localPosition = new Vector3(originalPosition.x + xOffset, originalPosition.y + yOffset, originalPosition.z); elapsedTime += Time.deltaTime; yield return null; } for (int i = 0; i < images.Length; i++) { images[i].color = originalColors[i]; } button.transform.localPosition = originalPosition; } private IEnumerator FlashButton(GameObject button, Color flashColor, float flashDuration) { Image[] images = button.GetComponentsInChildren<Image>(); Color[] originalColors = new Color[images.Length]; for (int i = 0; i < images.Length; i++) { originalColors[i] = images[i].color; images[i].color = flashColor; } yield return new WaitForSeconds(flashDuration); for (int i = 0; i < images.Length; i++) { images[i].color = originalColors[i]; } } }
this script i attached it to a empty game object and called in shop manager
this script holds handling the ui for the shop and pushing “o” opens the shop ui.you put each upgrade scriptable Object in the upgrades tab build the shop ui in the canvas i can go in to mmore details in what ive done if you need me to but for now ill just show you layout in an image
ive also been working on another type of item that only lasts one death but for now it just gets removed after a few seconds but its just a test for now
OneOffItemData
using UnityEngine; [CreateAssetMenu(fileName = "OneOffItemData", menuName = "Shop/OneOffItemData")] public class OneOffItemData : ScriptableObject { public string itemName; public PlayerStats.Stats statBoost; public int cost; public string description; public Sprite icon; }
next was how and where to store the passive upgrades that have been brought, this is where it can be a bit more painfull youll also need to create a save and load manager there are heaps of referances you can find online to get this stated its need to save when you buy the updrades and then loads when the game starts, but ill heres the PassiveUpgrades script that store the skills you have brought, this will be attached to the player. you should try to add things to this in the editor the game will manage the pasives skills by its self.
PassiveUpgrades
using System.Collections.Generic; using UnityEngine; public class PassiveUpgrades : MonoBehaviour { private SaveLoadManager saveLoadManager; [System.Serializable] public class Upgrade { public string upgradeName; public int level; public PlayerStats.Stats statBoost; } [System.Serializable] public class OneOffItem { public string itemName; public PlayerStats.Stats statBoost; } public List<Upgrade> purchasedUpgrades = new List<Upgrade>(); public List<OneOffItem> purchasedOneOffItems = new List<OneOffItem>(); private void Start() { saveLoadManager = FindObjectOfType<SaveLoadManager>(); LoadUpgrades(); } public void AddUpgrade(UpgradeData upgradeData, int level) { Upgrade existingUpgrade = purchasedUpgrades.Find(upgrade => upgrade.upgradeName == upgradeData.upgradeName); if (existingUpgrade != null) { existingUpgrade.level = level; existingUpgrade.statBoost = upgradeData.levels[level].statBoost; Debug.Log($"Upgraded {upgradeData.upgradeName} to level {level}"); } else { Upgrade newUpgrade = new Upgrade { upgradeName = upgradeData.upgradeName, level = level, statBoost = upgradeData.levels[level].statBoost }; purchasedUpgrades.Add(newUpgrade); Debug.Log($"Added new upgrade: {upgradeData.upgradeName} at level {level}"); } SaveUpgrades(); } public void AddOneOffItem(OneOffItemData itemData) { OneOffItem newItem = new OneOffItem { itemName = itemData.itemName, statBoost = itemData.statBoost }; purchasedOneOffItems.Add(newItem); Debug.Log($"Added new one-off item: {itemData.itemName}"); SaveUpgrades(); } public bool HasOneOffItem(string itemName) { return purchasedOneOffItems.Exists(item => item.itemName == itemName); } public void ApplyUpgrades(PlayerStats playerStats) { foreach (Upgrade upgrade in purchasedUpgrades) { playerStats.ActualStats += upgrade.statBoost; } foreach (OneOffItem item in purchasedOneOffItems) { playerStats.ActualStats += item.statBoost; } playerStats.RecalculateStats(); } public void RemoveOneOffItem(OneOffItemData itemData) { purchasedOneOffItems.RemoveAll(item => item.itemName == itemData.itemName); Debug.Log($"Removed one-off item: {itemData.itemName}"); SaveUpgrades(); } private void SaveUpgrades() { if (saveLoadManager != null) { saveLoadManager.SaveUpgrades(purchasedUpgrades, purchasedOneOffItems); } } private void LoadUpgrades() { if (saveLoadManager != null) { (purchasedUpgrades, purchasedOneOffItems) = saveLoadManager.LoadUpgrades(); } } }
here while my game is running you can see i have level 5 heath upgrade and level 2 might
and now to apply the stats to the player ive eddited the Playerstat.cs
public void RecalculateStats() { // Start with base stats actualStats = baseStats; // Apply equipped passive item boosts foreach (PlayerInventory.Slot s in inventory.passiveSlots) { Passive p = s.item as Passive; if (p != null) { actualStats += p.GetBoosts(); } } // Apply upgrades from PassiveUpgrades PassiveUpgrades passiveUpgrades = GetComponent<PassiveUpgrades>(); if (passiveUpgrades != null) { foreach (var upgrade in passiveUpgrades.purchasedUpgrades) { actualStats += upgrade.statBoost; } foreach (var oneOffItem in passiveUpgrades.purchasedOneOffItems) { actualStats += oneOffItem.statBoost; } } // Apply gem effects ApplyEquippedRuneEffects(); // Update collector radius collector.SetRadius(actualStats.magnet); // Update health if it exceeds maxHealth if (CurrentHealth > actualStats.maxHealth) { CurrentHealth = actualStats.maxHealth; } }
now you can see when i play the game my stats have been boosted
ive forgot to say above but in the ShopManager.cs there is a line “private int playerGold = 100; // Example starting gold amount” this sets the gold you are given at the start of every game you can change this to what evey you want ive have writen mine so each enemy has a gold drop and then the players luck also add a higher drop rate for gold and then the shop looks for the plays gold and that what it spends
this was just the 1st basic setup for my shop hope you guys like it
and 1 other person have upvoted this post. August 12, 2024 at 10:54 am #15568::Thank you Cam. You’re awesome.
I’ve stickied your topic and gave you a new badge on your profile. We are rolling out a new XP system on the blog, and these badges will contribute to your XP points in future. You will be able to use these XP points to get perks, as well as unlock Patron-locked content permanently for your account.
The XP system is meant to do 2 things:
- Encourage participation, so that we cultivate a more vibrant community here.
- Make the content more accessible for people from countries with smaller currencies. US$10 is a lot of money for certain countries in Asia, and a lot of our viewers there cannot afford the content.
October 1, 2024 at 10:13 am #15940October 2, 2024 at 5:56 am #15945::Yes of course! I’m happy to help if you need also.
- 1 anonymous person
October 2, 2024 at 9:42 pm #15947November 2, 2024 at 2:51 am #16226::using UnityEngine; [CreateAssetMenu(fileName = "UpgradeData", menuName = "Shop/UpgradeData")] public class UpgradeData : ScriptableObject { public string upgradeName; public Sprite icon; [System.Serializable] public class UpgradeLevel { public string description; public int cost; //public PlayerStats.Stats statBoost; public CharacterData.Stats statboosts; } public UpgradeLevel[] levels; }
public CharacterData.Stats statboosts;
Is it okay to replace it with this?
November 2, 2024 at 4:37 pm #16235::
If the change doesn’t create any errors in your code, this is totally fine.public CharacterData.Stats statboosts;
Is it okay to replace it with this?November 10, 2024 at 5:09 am #16289November 28, 2024 at 6:23 pm #16598::Amazing upgrade system, Cam. I have a few questions if you could help me. This shop opens in the main scene (Game), right? I was trying to add something similar to the character selection scene, but it always goes wrong because it can’t find the PlayerStats. Even when I use DontDestroyOnLoad, it doesn’t work. Do you know a way to make this work? Another question is if you could share your SaveAndLoad system? Thank you so much for all your hard work.
November 30, 2024 at 4:19 pm #16633::Hey mate. I have my shop upgrade it only accessible in the character section, I have customised my a lot different from the tutorial, but I will have a look on my next day off, I’ve also added more to my shop system that I should update to this
Yeah I’m happy to share my save and load system I don’t know if it’s done in the right ways but it seems to work for now
has upvoted this post. December 2, 2024 at 12:02 pm #16646::@singlebignamelol Cam has written a post for you here: https://blog.terresquall.com/community/topic/general-save-load-system/
-
AuthorPosts
- You must be logged in to reply to this topic.