I have made 2 codes to manage my interstitial ads by making the ads show every 5 mins when the player losses but the problem is that I tried to reset them when the player passes the 5 mins and press the button when he losses but it didn't work so how to reset the timer when the player presses the button?
this is the 1st code :
public int LastShownIntTime = 300;
void Start()
{
#if UNITY_ANDROID
Advertisement.Initialize(androidID);
#endif
}
public void Update()
{
LastShownIntTime = PlayerPrefs.GetInt("LastShownIntTime");
}
public void showInterstitial()
{
if (LastShownIntTime <=0)
{
showInterstitialwith5mint();
}
}
public void showInterstitialwith5mint()
{
Advertisement.Show("video");
PlayerPrefs.SetInt("LastShownIntTime", 300);
}
and the 2nd one :
public float LastShownIntTimefloat;
public int LastShownIntTime = 300;
void Start()
{
LastShownIntTime = PlayerPrefs.GetInt("LastShownIntTime");
LastShownIntTimefloat = LastShownIntTime;
}
public void Update()
{
LastShownIntTimefloat -= Time.deltaTime;
LastShownIntTime = (int)LastShownIntTimefloat;
PlayerPrefs.SetInt("LastShownIntTime", LastShownIntTime);
}
}
The main issue here:
You would have to reset the LastShownIntTimefloat in your script2!
Otherwise you simply continue overwriting it with new values reducing the value more and write it back to PlayerPrefs
→ the next time your script1 polls the value it is not reset but already overwritten by script2!
In general: You should not use PlayerPrefs in order to make two components communicate!
In your case here I wouldn't even separate the logic and bother with implementing the communication between them but rather merge them into one single component.
Then it is not necessary to read and write PlayerPrefs every frame but rather only on certain checkpoints like
Read once in Start
Write once in OnApplicationQuit
Write once in OnDestroy (This is for the case you e.g. switch Scene but don't quit the app)
Write once ever time your user loses (showInterstitial is called)
Write once when resetting the value after showing the advertisement
I would also simply directly use a float and GetFloat and SetFloat instead of converting it from and to an int.
public class MergedClass : MonoBehaviour
{
// Rather sue a FLOAT for time!
public float LastShownTime = 300;
void Start()
{
#if UNITY_ANDROID
Advertisement.Initialize(androidID);
#endif
// use 300 as default value if no PlayerPrefs found
LastShownTime = PlayerPrefs.GetFloat("LastShownTime", 300f);
}
public void Update()
{
if(LastShownTime > 0f) LastShownTime -= Time.deltaTime;
}
public void showInterstitial()
{
PlayerPrefs.SetFloat("LastShownTime", LastShownTime);
PlayerPrefs.Save();
if (LastShownTime <= 0f)
{
showInterstitialwith5mint();
}
}
public void showInterstitialwith5mint()
{
#if UNITY_ANDROID
Advertisement.Show("video");
#else
LastShownTime = 300f;
PlayerPrefs.SetFloat("LastShownTime", LastShownTime);
PlayerPrefs.Save();
}
private void OnApplicationQuit()
{
PlayerPrefs.SetFloat("LastShownTime", LastShownTime);
PlayerPrefs.Save();
}
private void OnDestroy()
{
PlayerPrefs.SetFloat("LastShownTime", LastShownTime);
PlayerPrefs.Save();
}
}
Related
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);
}
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?
I'm making a new game in Unity and I'm stuck in my script. I'm really noob in C# scripting. I'm already looking for all information but no luck. The game is very simple, it is 2D game where need just click on bubbles and they are rotating when are clicked. I will describe what I exactly need to create. I need a script when all objects are clicked then scene automatically changes to the next level + I need it to have a timeline, for example, for each level have 30 seconds to click all bubbles, when the time is over the game is over and a window pops up with a message "Game Over" and you can press the Reply and Exit buttons. I really hope that someone helps me. Thanks!
P.S. This is my script now for my gameObjects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class pop : MonoBehaviour
{
public AudioClip sound;
AudioSource audio;
// Start is called before the first frame update
void Start()
{
audio = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
}
bool wasClicked = false;
private void OnMouseDown()
{
if (!wasClicked)
{
wasClicked = true;
transform.Rotate(0, 0, 180);
audio.PlayOneShot(sound);
}
}
}
You should separate these.
I would have a central manager for the scene change like e.g.
public class Manager : MonoBehaviour
{
[SerializeField] private float timer = 30;
private void Awake ()
{
// Register to an event we will add which is fired everytime
// a pop was clicked
pop.onClicked += PopClicked;
}
private void OnDestroy ()
{
// Don't forget to remove the callback as soon as not needed anymore
pop.onClicked -= PopClicked;
}
private void PopClicked ()
{
// Check if there is no Unclicked pop remaining
if(pop.Unclicked.Count == 0)
{
// if so go to the next scene
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
private void Update ()
{
// Every Frame reduce the timer by the time passed since the last frame
timer -= Time.deltaTime;
// Maybe also update a timer display text here
if(timer <= 0)
{
// if it reaches 0 -> GameOver scene
SceneManager.LoadScene("GameOver");
}
}
}
And then modify your Pop class accordingly with some additional things:
public class pop : MonoBehaviour
{
// Stores all Unclicked instances
// As this is static it is "shared" between all instances or better said
// it is part of the type itself
private static HashSet<pop> _unclicked = new HashSet<pop>();
// Public readonly access
public static HashSet<pop> Unclicked => new HashSet<pop>(_unclicked);
// Event that is invoked everytime a pop is clicked
public static event Action onClicked;
public AudioClip sound;
[SerializeField] AudioSource audio;
void Awake()
{
if(!audio) audio = GetComponent<AudioSource>();
// Add yourself to the collection of Unclicked instances
_uncliked.Add(this);
}
private void OnDestroy ()
{
// Don't forget to also remove in case this is destroyed
// e.g. due to the scene change to GameOver
if(_unclicked.Contains(this)) _unclicked.Remove(this);
}
private void OnMouseDown()
{
// Is this still Unclicked?
if (!_unclicked.Contains(this)) return;
transform.Rotate(0, 0, 180);
audio.PlayOneShot(sound);
// Remove yourself from the Unclicked instances
_unclicked.Remove(this);
// Invoke the event
onClicked?.Invoke();
}
}
I have 1 script to PlayerMovement and one for powerUp I the power-up code I reference player movement to change the speed and change the bool named timer to true and I write that in log and when I touch the paper the speed doesn't change and the timer don't turn to true but in the log, its say that is yes
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
private float TargetPos;
public float Speed;
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.position = new Vector2(TargetPos, transform.position.y);
}
public void right()
{
TargetPos = transform.position.x + Speed * Time.deltaTime;
}
public void left()
{
TargetPos = transform.position.x - Speed * Time.deltaTime;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Powrups : MonoBehaviour
{
public PlayerMovement pm;
public float PowerUpActiveTime;
public float StartPowerUpActiveTime;
public float peperSpeed;
float NormalSpeed;
bool timer;
bool timerover;
private void OnTriggerEnter2D(Collider2D col)
{
if (col.name == "peper")
{
pm.Speed = peperSpeed;
timer = true;
Debug.Log("timerOn");
Debug.Log(pm.Speed);
Debug.Log(timer);
}
}
private void Update()
{
while(timer)
{
GameObject Pause = GameObject.Find("Pause");
PauseScript pausescript = Pause.GetComponent<PauseScript>();
if (!pausescript.pause)
{
PowerUpActiveTime -= Time.deltaTime;
if(PowerUpActiveTime <= 0 )
{
timerover = true;
}
if (timerover)
{
timer = false;
}
}
}
if (timerover)
{
PowerUpActiveTime = StartPowerUpActiveTime;
pm.Speed = NormalSpeed;
}
}
private void Start()
{
PowerUpActiveTime = StartPowerUpActiveTime;
timerover = false;
NormalSpeed = pm.Speed;
}
}
Your mistake is that while loop.
You are lucky that until now you probably have tested this always while not being in pause mode ;)
This while would completely freeze your app and the entire Unity Editor!
In general be extremely careful with while loops and nested conditions like here, where the exit condition might never be fulfilled!
What happens currently is that you are not in pause mode so this while loop gets activated and runs until timer is set to false .. completely within one single frame. That is the reason why to you it seems that the value is never true.
What you rather want anyway is that code block be executed once per frame.
And in particular in a frame based application like Unity also have some performance impacts in mind.
You shouldn't use Find and GetComponent repeatedly within Update but store and re-use the results.
So your code should rather be
// If possible already drag this in via the Inspector
[SerializeField] private PauseScript _pauseScript;
private void Start()
{
PowerUpActiveTime = StartPowerUpActiveTime;
timerover = false;
NormalSpeed = pm.Speed;
// Get this ONCE as fallback on runtime
if(!_pauseScript)
{
_pauseScript = GameObject.Find("Pause"). GetComponent<PauseScript>();
// Or simply use
//_pauseScript = FindObjectOfType<_pauseScript>();
}
}
private void Update()
{
if(timer)
{
if (!_pauseScript.pause)
{
PowerUpActiveTime -= Time.deltaTime;
if(PowerUpActiveTime <= 0 )
{
timer = false:
PowerUpActiveTime = StartPowerUpActiveTime;
pm.Speed = NormalSpeed;
}
}
}
}
Besides all that, you should rather not let an external power-up control your player values. I would rather go the other way round and have your player object have a component which checks into which power-up items you run and react to it accordingly.
So your power-up itself would actually only be a trigger without any clue if or how exactly the player will be influenced by it.
This is what I tried but the text is never show on the text mesh :
SecurityKeypadKeys script :
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
public class SecurityKeypadKeys : MonoBehaviour
{
// I would rather go this way round
// Configure the number for each key via the Inspector
// they will then set their own names for debugging
// I wouldn't the other way round rely on that the names are correct
[SerializeField] private int _number;
// Here you can attach callback handlers that will be executed
// For each time this key is pressed
public event Action<int> onKeyPressed;
private void Awake()
{
name = $"Key {_number}";
}
private void OnMouseDown()
{
// This class is only responsible for invoking its event
// It doesn't know and has no authority over how many digits are allowed in the display etc
// it doesn't even know a display exists or that the values get stored by someone
onKeyPressed?.Invoke(_number);
}
}
The SecurityKeypadSystem script :
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SecurityKeypadSystem : MonoBehaviour
{
[Header("References")]
// rather let this class control the display text
[SerializeField] private TextMesh _text;
[Header("Settings")]
// also rather let this class control the length of a code
[SerializeField] private int _codeLength = 8;
[Header("Debugging")]
[SerializeField] private GameObject[] _keyPadNumbers;
[SerializeField] private List<int> _code = new List<int>();
// This will be invoked once the code length has reached the target length
public event Action<int> OnCodeComplete;
// Start is called before the first frame update
private void Start()
{
_keyPadNumbers = GameObject.FindGameObjectsWithTag("Keypad");
// register a callback to each key that handles the numbers
foreach (var keyPadNumber in _keyPadNumbers)
{
// It is save to remove an event even if it hasn't been added yet
// this makes sure it is only added exactly once
// only adding this here for the case you later have to move this again to Update for some reason ;)
var securityKeypadKeys = keyPadNumber.GetComponent<SecurityKeypadKeys>();
securityKeypadKeys.onKeyPressed -= HandleKeyPressed;
securityKeypadKeys.onKeyPressed += HandleKeyPressed;
}
}
private void OnDestroy()
{
// just for completeness you should always remove callbacks as soon as they are not needed anymore
// in order to avoid any exceptions
foreach (var keyPadNumber in _keyPadNumbers)
{
var securityKeypadKeys = keyPadNumber.GetComponent<SecurityKeypadKeys>();
securityKeypadKeys.onKeyPressed -= HandleKeyPressed;
}
}
// this is called when a keypad key was pressed
private void HandleKeyPressed(int value)
{
// add the value to the list
_code.Add(value);
_text.text += value.ToString();
// Check if the code has reached the target length
// if not do nothing
if (_code.Count < _codeLength) return;
// if it reached the length combine all numbers into one int
var exponent = _code.Count;
float finalCode = 0;
foreach (var digit in _code)
{
finalCode =digit * Mathf.Pow(10, exponent);
exponent--;
}
// invoke the callback event
OnCodeComplete?.Invoke((int)finalCode);
// and reset the code
ResetCode();
}
// Maybe you later want an option to clear the code field from the outside as well
public void ResetCode()
{
_code.Clear();
_text.text = "";
}
// also clear the input if this gets disabled
private void OnDisable()
{
ResetCode();
}
}
Last the UnlockCrate script :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class UnlockCrate : MonoBehaviour
{
[Header("Settings")]
[SerializeField] private int _targetCode;
public GameObject securityKeyPad;
public GameObject player;
public Animation anim;
public IKControl ikControl;
public InteractableItem interactableItem;
public CinemachineVirtualCamera virtualCam;
public CinemachineFreeLook freeLookCam;
public CamerasControl camerasContorl;
private bool playAnimOnce = false;
[SerializeField] private SecurityKeypadSystem securityKeypadSystem;
private void Start()
{
// as before attach a handler that is triggered once the system completes one code
securityKeypadSystem.OnCodeComplete -= HandleInputCode;
securityKeypadSystem.OnCodeComplete += HandleInputCode;
}
private void OnDestroy()
{
securityKeypadSystem.OnCodeComplete -= HandleInputCode;
}
private void Update()
{
var distance = Vector3.Distance(player.transform.position, transform.position);
if (distance <= interactableItem.distance)
{
if (!playAnimOnce)
{
if (ikControl.handFinishedMove == true)
{
securityKeyPad.SetActive(true);
virtualCam.enabled = true;
freeLookCam.enabled = false;
Cursor.visible = true;
camerasContorl.enabled = false;
/*if(securityKeyPad.GetComponent<SecurityKeypadKeys>().UnlockedCode() == true)
{
securityKeyPad.SetActive(false);
virtualCam.enabled = false;
freeLookCam.enabled = true;
Cursor.visible = false;
anim.Play("Crate_Open");
}*/
//anim.Play("Crate_Open");
playAnimOnce = true;
}
}
}
else if (playAnimOnce)
{
securityKeyPad.SetActive(false);
virtualCam.enabled = false;
freeLookCam.enabled = true;
Cursor.visible = false;
camerasContorl.enabled = true;
anim.Play("Crate_Close");
playAnimOnce = false;
}
}
private void HandleInputCode(int inputCode)
{
if (inputCode == _targetCode)
{
Debug.Log("Code correct!", this);
anim.Play("Crate_Open");
// TODO whatelse has to be done
}
else
{
Debug.Log($"WRONG CODE: Expected {_targetCode} but was {inputCode}!", this);
// TODO some wrong code animation?
}
}
}
This screenshot is showing the Security Keypad System GameObject with the script attached to it :
The game start when the Security Keypad (1) is enabled false not active :
Security Keypad System
If I enable true just to show how it looks like the Security Keypad (1) gameobject then each Key cube is like a button with the OnMouseDown event and each cube have attached the script SecurityKeypadKeys :
Security Keypad (1) enabled true just to show how it looks like in the game
The Security Keypad GameObject is the small keypad system on the left in the scene view and when the player is getting closer to it then the Security Keypad (1) the bigger one is getting active.
On the bottom the gameobject Security Keypad Text is the Text Mesh that should show the numbers pressed.
On the GameObject Crate_0_0 is attached the script UnlockCrate and as target code for testing I added 12345678
The problem now is that it's never getting to the HandleKeyPressed method in the SecurityKeypadSystem script. I used a break point and it's never get there.
I also checked with a break point on the SecurityKeypadKeys script and it's getting the pressed number name but it's never get to the HandleKeyPressed method.
As mentioned there are some uncleared things but most importantly: I would make the entire system more event driven and not poll-check stuff in Update every frame.
I would also separate the responsibilities for each component as clear as possible. E.g. like
SecurityKeypadKeys is only responsible for handling it's own key press. It doesn't know/care what happens with this keypress. It doesn't even know who will handle it or that something like a code or display text exist.
SecurityKeypadSystem this is responsible for handling key presses, store the so far input code, control the display, combine the digits and fire an event once a code is complete. It has no further knowledge about what will happen with this code. It doesn't even know who will handle it
UnlockCrate is in your setup the most "powerful" class. It enables/disables the entire SecurityKeypadSystem, handles the code once it is complete, validates it and finally is responsible for opening the box .. or not. It doesn't have to know that something like a display text exists - it only cares about the final code.
So it could look somewhat like e.g.
public class SecurityKeypadKeys : MonoBehaviour
{
// I would rather go this way round
// Configure the number for each key via the Inspector
// they will then set their own names for debugging
// I wouldn't the other way round rely on that the names are correct
[SerializeField] private int _number;
// Here you can attach callback handlers that will be executed
// For each time this key is pressed
public event Action<int> onKeyPressed;
private void Awake()
{
name = $"Key {_number}";
}
private void OnMouseDown()
{
// This class is only responsible for invoking its event
// It doesn't know and has no authority over how many digits are allowed in the display etc
// it doesn't even know a display exists or that the values get stored by someone
onKeyPressed?.Invoke(_number);
}
}
Then one step up in the System you register a callback handler to these keys so every time a key is pressed you update your text field and store the values:
public class SecurityKeypadSystem : MonoBehaviour
{
[Header("References")]
// rather let this class control the display text
[SerializeField] private TextMesh _text;
[Header("Settings")]
// also rather let this class control the length of a code
[SerializeField] private int _codeLength = 8;
[Header("Debugging")]
[SerializeField] private GameObject[] _keyPadNumbers;
[SerializeField] private List<int> _code = new List<int>();
// This will be invoked once the code length has reached the target length
public event Action<int> OnCodeComplete;
// Start is called before the first frame update
private void Start()
{
_keyPadNumbers = GameObject.FindObjectsOfType<SecurityKeypadKeys>();
// register a callback to each key that handles the numbers
foreach(var keyPadNumber in _keyPadNumbers)
{
// It is save to remove an event even if it hasn't been added yet
// this makes sure it is only added exactly once
// only adding this here for the case you later have to move this again to Update for some reason ;)
keyPadNumber.onKeyPressed -= HandleKeyPressed;
keyPadNumber.onKeyPressed += HandleKeyPressed;
}
}
private void OnDestroy()
{
// just for completeness you should always remove callbacks as soon as they are not needed anymore
// in order to avoid any exceptions
foreach(var keyPadNumber in _keyPadNumbers)
{
keyPadNumber.onKeyPressed -= HandleKeyPressed;
}
}
// this is called when a keypad key was pressed
private void HandleKeyPressed(int value)
{
// add the value to the list
_code.Add(value);
_text.text += value.ToString();
// Check if the code has reached the target length
// if not do nothing
if(_code.Count < _codeLength) return;
// if it reached the length combine all numbers into one int
var exponent = code.Count;
var finalCode = 0;
foreach(var digit in _code)
{
finalCode = digit * Mathf.Pow(10, exponent);
exponent--;
}
// invoke the callback event
OnCodeComplete?.Invoke(finalCode);
// and reset the code
ResetCode();
}
// Maybe you later want an option to clear the code field from the outside as well
public void ResetCode()
{
_code.Clear();
_text.text = "";
}
// also clear the input if this gets disabled
private void OnDisable()
{
ResetCode();
}
}
So finally again from the last class you would add another callback handler which validates the User input and triggers your unlocking
public class UnlockCrate : MonoBehaviour
{
[Header("Settings")]
[SerilaizeField] private int _targetCode;
public GameObject player;
public Animation anim;
public IKControl ikControl;
public InteractableItem interactableItem;
public CinemachineVirtualCamera virtualCam;
public CinemachineFreeLook freeLookCam;
public CamerasControl camerasContorl;
private bool playAnimOnce = false;
[SerializeField] private SecurityKeypadSystem securityKeypadSystem;
private void Start()
{
// as before attach a handler that is triggered once the system completes one code
securityKeypadSystem.OnCodeComplete -= HandleInputCode;
securityKeypadSystem.OnCodeComplete += HandleInputCode;
}
private void OnDestroy()
{
securityKeypadSystem.OnCodeComplete -= HandleInputCode;
}
private void Update()
{
var distance = Vector3.Distance(player.transform.position, transform.position);
if (distance <= interactableItem.distance)
{
if (!playAnimOnce)
{
if(ikControl.handFinishedMove == true)
{
securityKeyPad.SetActive(true);
virtualCam.enabled = true;
freeLookCam.enabled = false;
Cursor.visible = true;
camerasContorl.enabled = false;
/*if(securityKeyPad.GetComponent<SecurityKeypadKeys>().UnlockedCode() == true)
{
securityKeyPad.SetActive(false);
virtualCam.enabled = false;
freeLookCam.enabled = true;
Cursor.visible = false;
anim.Play("Crate_Open");
}*/
//anim.Play("Crate_Open");
playAnimOnce = true;
}
}
}
else if (playAnimOnce)
{
securityKeyPad.SetActive(false);
virtualCam.enabled = false;
freeLookCam.enabled = true;
Cursor.visible = false;
camerasContorl.enabled = true;
anim.Play("Crate_Close");
playAnimOnce = false;
}
}
private void HandleInputCode(int inputCode)
{
if(inputCode == _targetCode)
{
Debug.Log("Code correct!", this);
anim.Play("Crate_Open");
// TODO whatelse has to be done
}
else
{
Debug.Log($"WRONG CODE: Expected {_targetCode} but was {inputCode}!", this);
// TODO some wrong code animation?
}
}
}
Of course I don't know your entire project so this is just a general idea - I hope I made it clear for you to understand the concept :)
After closing the game and reopening it, I want to load the last scene, so the player can continue from the level he reached. I've tried using PlayerPrefs as you see in the code bellow, but the game crash on startup.
GameManager Script:
bool GameHasEnded = false;
public float RestartDelay = 2f;
public float NextLevelDelay = 5f;
int level_index;
private void Start()
{
level_index = PlayerPrefs.GetInt("Last_Level");
SceneManager.LoadScene(level_index);
}
public void CompleteLevel()
{
Invoke("NextLevel", NextLevelDelay);
level_index = level_index++;
PlayerPrefs.SetInt("Last_Level", level_index);
}
public void EndGame()
{
if (GameHasEnded == false)
{
GameHasEnded = true;
Invoke("Restart", RestartDelay);
}
}
void NextLevel()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex +1);
}
void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().path);
}
}
All scenes are linked with GameManager, they all have the same code that load the next scene:
FindObjectOfType<GameManager>().CompleteLevel();
You need to specify a defaultValue for GetInt so that the first time it starts up, it can pick the appropriate scene to start at.
Also, you need to track whether or not you have already loaded at scene start, and only do this load if it hasn't yet happened. GameManager seems like it should be made into a singleton but since it's not, you can just make this loaded flag static, so that is common among all GameManager instances.
Altogether, this might look like this:
public readonly int defaultLastLevel = 1; // Set as appropriate
private static bool loaded = false;
void Start()
{
if (!loaded) {
loaded = true;
level_index = PlayerPrefs.GetInt("Last_Level", defaultLastLevel);
SceneManager.LoadScene(level_index);
}
}
Also, your CompleteLevel has some very strange code where you post-increment and assign in the same line:
level_index = level_index++;
This is extremely hard to read and actually doesn't do what you want it to do. You can do level_index = SceneManager.GetActiveScene().buildIndex + 1; instead.
Also, you need to Save changes you make to PlayerPrefs with PlayerPrefs.Save();
Altogether, this makes your CompleteLevel look like this:
public void CompleteLevel()
{
Invoke("NextLevel", NextLevelDelay);
level_index = SceneManager.GetActiveScene().buildIndex + 1; // use this instead
PlayerPrefs.SetInt("Last_Level", level_index);
PlayerPrefs.Save();
}