-2

For some reason I need to start the scene with the menu open, then close it, then grab the coin and go to the shop menu in order for the shop ui to update my moneyAmount. If i start the scene with the shop menu closed and pick up the coin then go to my shop menu it doesnt update. And when i buy my helmet it says reference not set to an object even though all im doing is getting my player health component and adding 50 to it so why do i need to reference any kind of object? here are the scripts with my GameControl script first.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameControl : MonoBehaviour
{
public Text moneyText;
public static int moneyAmount;
int isHelmetSold;
int isBeltSold;
int isPantsSold;
int isShirtSold;
int isBootsSold;


// Start is called before the first frame update
void Start()
{
    moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
    isHelmetSold = PlayerPrefs.GetInt("IsHelmetSold");
    isBeltSold = PlayerPrefs.GetInt("IsBeltSold");
    isPantsSold = PlayerPrefs.GetInt("IsPantsSold");
    isShirtSold = PlayerPrefs.GetInt("IsShirtSold");
    isBootsSold = PlayerPrefs.GetInt("IsBootsSold");
}

// Update is called once per frame
void Update()
{
    moneyText.text = moneyAmount.ToString();
}

Here is my PageOneShop script which makes my item buyable when amount is reached (for the time being ive only finished my helmet not the rest so if you can ignore all of the public texts and buttons as i know i havent added them yet)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class PageOneShop : MonoBehaviour
{

public static int moneyAmount;
int isHelmetSold;
int isBeltSold;
int isPantsSold;
int isShirtSold;
int isBootsSold;

public Text moneyText;

public Text helmetPrice;
public Text beltPrice;
public Text pantsPrice;
public Text shirtPrice;
public Text bootsPrice;

public Button buyHelmetButton;
public Button buyBeltButton;
public Button buyPantsButton;
public Button buyShirtButton;
public Button buyBootsButton;

void Start()
{
    moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
    isHelmetSold = PlayerPrefs.GetInt("IsHelmetSold");
    isBeltSold = PlayerPrefs.GetInt("IsBeltSold");
    isPantsSold = PlayerPrefs.GetInt("IsPantsSold");
    isShirtSold = PlayerPrefs.GetInt("IsShirtSold");
    isBootsSold = PlayerPrefs.GetInt("IsBootsSold");
}

// Update is called once per frame
void FixedUpdate()
{
    moneyText.text = moneyAmount.ToString();

    isHelmetSold = PlayerPrefs.GetInt("IsHelmetSold");

    if (moneyAmount >= 150 && isHelmetSold == 0)
        buyHelmetButton.interactable = true;
    else
        buyHelmetButton.interactable = false;

    isBeltSold = PlayerPrefs.GetInt("IsBeltSold");

    if (moneyAmount >= 120 && isBeltSold == 0)
        buyBeltButton.interactable = true;
    else
        buyBeltButton.interactable = false;

    isPantsSold = PlayerPrefs.GetInt("IsPantsSold");

    if (moneyAmount >= 100 && isPantsSold == 0)
        buyPantsButton.interactable = true;
    else
        buyPantsButton.interactable = false;

    isShirtSold = PlayerPrefs.GetInt("IsShirtSold");

    if (moneyAmount >= 100 && isShirtSold == 0)
        buyShirtButton.interactable = true;
    else
        buyShirtButton.interactable = false;

    isBootsSold = PlayerPrefs.GetInt("IsBootsSold");

    if (moneyAmount >= 80 && isBootsSold == 0)
        buyBootsButton.interactable = true;
    else
        buyBootsButton.interactable = false;
}

public void buyHelmet()
{
    moneyAmount -= 150;
    GetComponent<PlayerHealth>().maxHealth += 50;
    PlayerPrefs.SetInt("IsHelmetSold", 1);
    helmetPrice.text = "Sold!";
    buyHelmetButton.gameObject.SetActive(false);
}

public void buyBelt()
{
    moneyAmount -= 120;
    GetComponent<PlayerHealth>().maxHealth += 50;
    PlayerPrefs.SetInt("IsBeltSold", 1);
    helmetPrice.text = "Sold!";
    buyBeltButton.gameObject.SetActive(false);
}

public void buyShirt()
{
    moneyAmount -= 100;
    GetComponent<PlayerHealth>().maxHealth += 50;
    PlayerPrefs.SetInt("IsShirtSold", 1);
    helmetPrice.text = "Sold!";
    buyShirtButton.gameObject.SetActive(false);
}

public void buyPants()
{
    moneyAmount -= 100;
    GetComponent<PlayerHealth>().maxHealth += 50;
    PlayerPrefs.SetInt("IsPantsSold", 1);
    helmetPrice.text = "Sold!";
    buyPantsButton.gameObject.SetActive(false);
}

public void buyBoots()
{
    moneyAmount -= 80;
    GetComponent<PlayerHealth>().maxHealth += 50;
    PlayerPrefs.SetInt("IsBootsSold", 1);
    helmetPrice.text = "Sold!";
    buyBootsButton.gameObject.SetActive(false);
}

And here is my coin script on my coins that i pick up.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Coin : MonoBehaviour
{
void OnTriggerEnter2D (Collider2D col)
{
    PageOneShop.moneyAmount += 200;
    GameControl.moneyAmount += 200;
    Destroy(gameObject);
}
  • Have you looked in the editor and checked the value of the text? – BugFinder Dec 01 '21 at 00:37
  • I have not. Would that point me in the right direction? I can pretty much assume its the value not being added correctly I’m just not sure if seeing it in the editor is gonna point me in the right direction as to which part of my script is wrong, that’ll just tell me the value is being added incorrectly right? or is there more of a reason to do this? Im a beginner so hopefully none of this comes off rude I just wanna make sure I understand everything when I do it so i can continue to use this info – FighterOne12344 Dec 01 '21 at 00:45
  • 1
    Because i think it is its not fitting in the box – BugFinder Dec 01 '21 at 08:49
  • 1
    in general you should stop poll checking the value and assign it to the text **every frame** bu rather make your code event driven and only update the text in the moment the value is actually changed. And then yes, what @BugFinder means is: Your text rect is probably too small to fit the entire number into it and your overflow set to `Clip` so the rest of the text is cut off – derHugo Dec 01 '21 at 08:54
  • So this was partly the issue and I’ve updated the post to correspond to my new question which is, the ui in my shop that shows the money amount isnt updating and I did fix the text rect in the shop menu so that isnt the issue there and the code is the exact same so im not exactly sure what could be going wrong at this point – FighterOne12344 Dec 01 '21 at 16:45
  • 1
    Your most recent edits actually obscure your issue. You've no longer got a minimum reproducible example, since you are mixing new issues in and you're moving farther from the "minimum". – NSJacob1 Dec 02 '21 at 01:39
  • Questions about debugging that have an error should include script name, line number, and other info included in the error. See [ask] for more info. – Ruzihm Dec 02 '21 at 20:26
  • Does this answer your question? [What is a NullReferenceException, and how do I fix it?](https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it) – Ruzihm Dec 02 '21 at 20:26

1 Answers1

1

It's important to figure out the pieces involved. Starting from the minimum amount of code, I think this is a good start:

"GameControl"

using UnityEngine;
using UnityEngine.UI;

public class GameControl : MonoBehaviour
{
    public Text moneyText;
    public static int moneyAmount;

    // Start is called before the first frame update
    // Start is only called while this is active and enabled
    void Start()
    {
        moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
    }

    void Update()
    {
        moneyText.text = moneyAmount.ToString();
    }
}

"PageOneShop"

using UnityEngine;
using UnityEngine.UI;

public class PageOneShop : MonoBehaviour
{
    public static int moneyAmount;

    public Text moneyAmountText;

    public Text helmetPrice;

    // Start is called before the first frame update
    // Start is only called while this is active and enabled
    void Start()
    {
        moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
    }

    // Update is called once per frame
    void Update()
    {
        moneyAmountText.text = moneyAmount.ToString();
    }

    public void buyHelmet()
    {
        moneyAmount -= 150;
        helmetPrice.text = "Sold!";
    }
}

"Coin"

using UnityEngine;

public class Coin : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D col)
    {
        PageOneShop.moneyAmount += 200;
        GameControl.moneyAmount += 200;
        Destroy(gameObject);
    }
}

Everything in these files is used, and we can replicate our issue. Before this point, my previous answer was correct and you weren't updating the moneyAmount instance inside PageOneShop. Now there is a new issue and it's more subtle, because it's strictly speaking not in your code. Start is a Unity called method, and is called "before the first frame update". Start is also only called if the component is enabled and the object it's attached to is active. If the object it's attached to is de-activated (like I'm guessing your menu is when you load your scene) Start will be called the first time you activate the object (open the menu). Since nowhere are you updating the "MoneyAmount" in PlayerPrefs, that value is still 0. If you pick up the coin, then open the menu for the first time, the value would be 200 but instead is re-set to 0 when this runs:

    void Start()
    {
        moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
    }

Probably the quickest way to fix this is to add this line to coin:

public class Coin : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D col)
    {
        PageOneShop.moneyAmount += 200;
        GameControl.moneyAmount += 200;
        // Set the PlayerPrefs value
        PlayerPrefs.SetInt("MoneyAmount", GameControl.moneyAmount);
        Destroy(gameObject);
    }
}

At this point even if the menu opens after the coin is picked up, the correct value is retrieved from PlayerPrefs so nothing is overwritten. Potentially a new problem has been introduced, which is even in the editor during testing, this write to PlayerPrefs is occurring so money may accumulate as multiple play sessions are run. There are a few other issues as well. As @derHugo accurately points out you should stop polling for changes in update to an event based approach, and the use of two static variables which are kept in sync like this is at best wasteful, and at worst can cause some real serious headaches.

Let's start by addressing the double static variable use. You could replace all the references of one with the other, but I propose introducing a third class to handle the player's "Wallet" where they keep their money. You could write something as simple as:

public static class Wallet
{
    public static int MoneyAmount;
}

All the references to the other two static moneyAmount variables could be replaced with this Wallet.MoneyAmount.

The polling could now be replaced with events pretty quickly by adding an event to the Wallet class, and replacing the field with a property:

using System;

public static class Wallet
{
    public static event Action OnMoneyAmountChanged;

    private static int moneyAmount;
    public static int MoneyAmount
    {
        get
        {
            return moneyAmount;
        }
        set
        {
            moneyAmount = value;
            // maybe null check, or follow a "never null" approach
            OnMoneyAmountChanged();
        }
    }
}

Instead of always checking moneyAmount you can now add some code like:

    void Start()
    {
        // Listen for changes
        Wallet.OnMoneyAmountChanged += handleMoneyAmountChanged;
    }

    private void OnDestroy()
    {
        // Remember to always remove your listeners when you're done with them
        Wallet.OnMoneyAmountChanged -= handleMoneyAmountChanged;
    }

    // set the text to the value held in the wallet whenever the amount changes
    void handleMoneyAmountChanged()
    {
        moneyText.text = Wallet.MoneyAmount.ToString();
    }

Maybe you don't want the PlayerPrefs.SetInt call in Coin any more, and instead you could move it into the Wallet now. If you put it in a [Conditional] method you could also protect yourself from constantly overwriting PlayerPrefs with every play session in the editor.

using System;
using System.Diagnostics;
using UnityEngine;

public static class Wallet
{
    public static event Action OnMoneyAmountChanged;

    private static readonly string moneyAmountKey = "MoneyAmount";

    private static int moneyAmount;
    public static int MoneyAmount
    {
        get
        {
            return moneyAmount;
        }
        set
        {
            moneyAmount = value;
            OnMoneyAmountChanged();
            // this method call is conditional on a defined constant
            saveMoneyAmount();
        }
    }

    // This part will only run if ENABLE_PLAYERSPREFS is defined.
    [Conditional("ENABLE_PLAYERPREFS")]
    private static void saveMoneyAmount()
    {
        PlayerPrefs.SetInt(moneyAmountKey, moneyAmount);
    }
}

You can type whatever you want into the scripting define symbols text box described in the "Platform custom #defines" section on this page. If you define "ENABLE_PLAYERPREFS" that method will call.

You probably now want to initialize your wallet as well, and to keep things symmetrical you could "close" it as well. This would let you cut down on a lot of reads and writes to player prefs.

    public static void Open()
    {
// this is another way to interact with platform define constants
#if ENABLE_PLAYERPREFS
        moneyAmount = PlayerPrefs.GetInt(moneyAmountKey);
#else
        moneyAmount = 0;
#endif
    }

    public static void Close()
    {
        // maybe you want to do other stuff in here
        saveMoneyAmount();
    }

If you called Open at the start of your application, and Close at the end you could just use the variables in memory the rest of the time and remove the saveMoneyAmount call from the MoneyAmount setter.

As for this second question you added later than my original answer, "And when i buy my helmet it says reference not set to an object even though all im doing is getting my player health component and adding 50 to it so why do i need to reference any kind of object?". You didn't post the line number, but I can guess that it originates on any/all the calls that look like this GetComponent<PlayerHealth>().maxHealth += 50; since those are all in methods on PageOneShop so unless you've added your PlayerHealth component to your menu object that GetComponent will be null, at which point you can refer to What is a NullReferenceException, and how do I fix it?

NSJacob1
  • 509
  • 3
  • 12
  • So I’ve even added “PageOneShop.moneyAmount += 200;” to my coin script and it still did not update. Sorry im on mobile Im not sure how to do code in here on mobile – FighterOne12344 Dec 01 '21 at 16:57
  • Does your store text update when you update the `PageOnShop.moneyAmount`? – NSJacob1 Dec 01 '21 at 16:58
  • No it did not, it stayed the same after adding that to my coin script and then running the game, picking up a coin, seeing gamecontrol update +200 but no update to the store text +200 it stayed at 0. – FighterOne12344 Dec 01 '21 at 17:36
  • ok so oddly enough if i start the scene with the shop page open, then close it, then grab the coin it actually does update the ui in the shop menu. why do i need to start the scene with the menu open for it to work? – FighterOne12344 Dec 02 '21 at 00:18
  • If your Start method hasn't run yet, and you're not updating the value in prefs, when you open the menu `PageOneShop.moneyAmount` gets set back to whatever value was held in player prefs. – NSJacob1 Dec 02 '21 at 01:29
  • 1
    I've written you a more comprehensive answer that addresses your current concerns as well as some of the other issues this approach brings light to, and some valid points raised in the comments on your question. There are plenty of other ways to build this, this is the one I chose. There is plenty more to do, but I think this covers your questions. – NSJacob1 Dec 03 '21 at 01:53