0
\$\begingroup\$

I am working on a character selection menu in Unity.

If you have enough coins, you can buy your desired character. Once player buys one, that character must not be locked on game restart, the number of coins must not be changed again, and the lock image (a GameObject I defined under name "wood" and acts as a lock) must disappear.

Everything works fine when I buy one character, but when I buy another one, the previous one returns to locked mode, but its image (wood) has already disappeared (I mean, I should have enough coins to re-buy an already bought character).

I guess there must be something wrong with this code isPlanesold = PlayerPrefs.GetInt("isPlanesold"); because it changes as a character is bought.

Here is my script:

int isPlanesold;
public static int chnum; //character number that must be referenced on another scene
public Button WhitePlane;
public Button YellowPlane;
public GameObject wood1;
public GameObject wood2;


void Start () {

        PlayerPrefs.SetString ("BluePlane", "on");
        if (PlayerPrefs.GetString ("WhitePlane") == "on") {
            wood1.SetActive (false);
        }
        if (PlayerPrefs.GetString ("YellowPlane") == "on") {
            wood2.SetActive (false);
        }

                UIManager2.coin_score = PlayerPrefs.GetInt ("Score");
        isPlanesold = PlayerPrefs.GetInt ("isPlanesold");

}

            public void BuyWhitePlane () {

        if (isPlanesold == 1) {
            PlayerPrefs.SetInt ("isPlanesold", 1);
            chnum = 2;
            PlayerPrefs.Save ();
            SceneManager.LoadScene ("Menu2");
        } else if (UIManager2.coin_score >= 1) {
            PlayerPrefs.SetInt ("isPlanesold", 1);
            chnum = 2;
            PlayerPrefs.SetInt ("Score", UIManager2.coin_score--);
            PlayerPrefs.SetInt ("Score", UIManager2.coin_score);
            PlayerPrefs.Save ();
            PlayerPrefs.SetString ("WhitePlane", "on");
            SceneManager.LoadScene ("Menu2");

            }
        else 
        {
            Debug.Log ("You Don't Have The Score");

        }


    }

    public void BuyYellowPlane () {

        if (isPlanesold == 2) {
            PlayerPrefs.SetInt ("isPlanesold", 2);
            chnum = 3;
            PlayerPrefs.Save ();
            SceneManager.LoadScene ("Menu2");
        } else if (UIManager2.coin_score >= 2) {
            PlayerPrefs.SetInt ("isPlanesold", 2);
            chnum = 3;
            PlayerPrefs.SetInt ("Score", UIManager2.coin_score -= 2);
            PlayerPrefs.SetInt ("Score", UIManager2.coin_score);
            PlayerPrefs.Save ();
            PlayerPrefs.SetString ("YellowPlane", "on");
            SceneManager.LoadScene ("Menu2");

        }
        else 
        {
            Debug.Log ("You Don't Have The Score");

        }


    }

PlayerPrefs.SetString ("BluePlane", "on"); refers to a character that's unlocked by default.

\$\endgroup\$
  • \$\begingroup\$ It looks like you're trying to use "isPlanesold" like a bitfield, that can store simultaneously the sold/unsold states of both plane 1 and plane 2. But you're using it as in integer representing "most recent plane sold" - setting it to 2 means it no longer has the value of 1, and vice versa. \$\endgroup\$ – DMGregory Apr 21 at 16:19
  • \$\begingroup\$ What's your recommendation? \$\endgroup\$ – amin007 Apr 21 at 16:58
0
\$\begingroup\$

I'd factor the idea of a purchasable item into its own class, so you don't have to copy and paste code for each purchasable. Instead you can add this component to each button representing an item you want to buy:

public class Purchasable : MonoBehaviour {

    public enum PurchasableID : int {
        BluePlane,
        WhitePlane,
        YellowPlane
    }

    [System.Serializable]
    public class PurchaseEvent : UnityEvent<PurchasableID> {}

    public PurchasableID id;

    // 0 will stand for "Unlocked by default"
    public int coinCost = 0;

    public GameObject disabledImage;    

    public PurchaseEvent OnPurchase;
    public PurchaseEvent OnUse;

    public bool IsUnlocked() {
        return coinCost == 0 || PlayerPrefs.GetInt("Unlocked" + id.ToString(), 0) > 0;
    }

    void Start() {
        // If we've been unlocked already, hide our disabled image on start-up.
        if(disabledImage != null && IsUnlocked())
            disabledImage.SetActive(false);        
    }    

    // Wire up your purchase buttons to call this method when clicked.
    public void BuyOrUse() {
        if(IsUnlocked())
        {   // Already bought this one, so let's just use it!
            OnUse.Invoke(id);
        }
        else if (Wallet.instance.TryPay(coinCost))
        {   // We can afford it, so buy it and mark it unlocked.
            PlayerPrefs.SetInt("Unlocked" + id.ToString(), 1);
            PlayerPrefs.Save();
            disabledImage.SetActive(false);
            OnPurchase.Invoke(id);
        }
        else
        {   // Nope, can't afford it. Maybe play a failure sound here.
            Debug.Log ("You Don't Have The Score");
        }
    }
}

Here I also factored out a Wallet type to manage your coin score, ensuring it's saved properly when earning/spending. I set it up as a singleton for ease of referencing, while also letting you peek at your current coin balance in the Inspector, or change the value there for debugging.

public class Wallet : MonoBehaviour {

    public static Wallet instance;

    [SerializeField]
    private int _coinBalance = 0;
    public int coinBalance { get { return _coinBalance; } }

    void Awake() {
        if(instance != null)
        {
            Debug.LogWarning("You've spawned two Wallets in the same scene!");
            Destroy(this);
            return;
        }

        instance = this;
        _coinBalance = PlayerPrefs.GetInt("coinScore", 0);
    }

    public bool TryPay(int cost) {
        // If we can't afford it, report back "No."
        if(cost > _coinBalance)
            return false;

        // If we can afford it, update our balance, and reply back "Yes."
        _coinBalance -= cost;

        // Technically this is redundant, since whatever we bought will likely call
        // its own save. If you trust in that, you can delete this line. ;)
        PlayerPrefs.SetInt("coinScore", _coinBalance);

        return true;
    }

    // Increment our coin score and save the progress.
    public void Earn(int earnings) {
        _coinBalance += earnings;
        PlayerPrefs.SetInt("coinScore", _coinBalance);
    }
}

Now your UI Manager can have a function like this:

public void SelectCharacter(Purchasable.PurchasableID character) {
    characterNumber = (int)character;
    SceneManager.LoadScene ("Menu2");
}

And you can wire up your purchasables' OnBuy / OnUse methods to call this.

The reason I set these up as separate events is that someday you might have purchasable objects that aren't characters, but upgrades, or skins, or other things that still need the core purchase logic, but need to trigger a different action when the player uses them. This gives you that flexibility, without writing new code for that case when it arises.

Example of Inspector setup

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Just note that it's reasonably easy for players to modify data in PlayerPrefs, so if you want any degree of security on these unlocks, you should look into other persistence strategies. \$\endgroup\$ – DMGregory Apr 21 at 23:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.