Hi there,
I’ve been struggling to debug this myself for like 8hrs and have kind of given up on it. Even asked chatGPT for multiple iterations of help, and still no luck, so I thought I’d ask here.
The strange behavior that I’m getting:
Lives are correctly persistent from level to level, both in the data and displayed in the UI. Looping around back to level 1 maintains lives too.
Score does not transfer from Scene 0 to Scene 1, and Scene 1 to Scene 2. If I loop around back to Scene 0, however, it does remember my score from that Scene 0 only. The UI updates on Scene 0 only, and doesn’t do so in later scenes.
When I die and reload a level, it correctly remembers which coins have already been collected.
I copied my code below.
I am using prefabs for both the GameSession and LevelPersist objects.
My debug printouts make it seem like the GameSession is indeed being destroyed on load for each level. Yet when a coin is captured, it manages to call a gameSession.captureCoin() on some other copy of gameSession, because I can still query gameSession.getCurrentScore() and output a restarted-score rather than the one that should be saved in the DontDestroyOnLoad version of gameSession.
Any help greatly appreciated! It’s so frustrating because I feel like I understand the principles but am getting really confusing behavior.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
public class GameSession : MonoBehaviour
{
[SerializeField] int playerLives = 3;
[SerializeField] int playerScore = 0;
[SerializeField] float deathDelay;
[SerializeField] TextMeshProUGUI livesText;
[SerializeField] TextMeshProUGUI scoreText;
int currentLevelIndex;
void Awake(){
// singleton creation below
int numGameSessions = FindObjectsOfType<GameSession>().Length; // Find ObjectS (plural) Of Type, bc could have several of these
Debug.Log("Awake for GameSession is called. Active Scene: " + SceneManager.GetActiveScene().buildIndex);
if (numGameSessions > 1){
Destroy(gameObject);
Debug.Log("GameSession was a copy, destroyed self");
}
else{
DontDestroyOnLoad(gameObject); // this will keep original copy of singleton, persisten when future levels load with a GameSession object
}
}
private void Start(){
scoreText.text = playerScore.ToString();
livesText.text = playerLives.ToString();
}
public void ProcessPlayerDeath(){
currentLevelIndex = SceneManager.GetActiveScene().buildIndex;
if (playerLives > 1){
TakeLife();
Debug.Log("Died. Reloading level. " + getPlayerStats());
Invoke("ReloadScene", deathDelay);
}
else{
TakeLife(); // for the screen text
Debug.Log("No more lives. Resetting game session");
Invoke("ResetGameSession", deathDelay);
}
}
private void TakeLife() {
playerLives --;
livesText.text = playerLives.ToString(); // do I need this, or is it constantly pulling from the variable? I don't think so
}
private void ResetGameSession()
{
FindObjectOfType<LevelPersist>().ResetLevelPersist(); // destroy that version of LevelPersist, we're moving on!
SceneManager.LoadScene(0); // start over
Destroy(gameObject); // destroy this instance of GameSession, get a whole new instance of main session
}
private void ReloadScene(){
SceneManager.LoadScene(currentLevelIndex);
}
public void captureCoin(int value){
playerScore += value; // doing it like this, because might be other ways to increase your score. Score != coins.
Debug.Log("GameSession msg: coin captured, current score: " + playerScore);
scoreText.text = playerScore.ToString();
}
public int getCurrentScore(){
return playerScore;
}
public int getCurrentLives(){
return playerLives;
}
public string getPlayerStats(){
return "Lives remaining: " + getCurrentLives().ToString() + ", score: " + getCurrentScore().ToString();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelPersist : MonoBehaviour
{
void Awake(){
Debug.Log("Awake for LevelPersist is called. Active Scene: " + SceneManager.GetActiveScene().buildIndex);
int numLevelPersist = FindObjectsOfType<LevelPersist>().Length;
if(numLevelPersist > 1){
Destroy(gameObject);
Debug.Log("Level Persist sees it is a copy, destroys itself");
}
else{
DontDestroyOnLoad(gameObject);
}
}
public void ResetLevelPersist(){
Destroy(gameObject);
Debug.Log("Deleting LevelPersist");
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelExit : MonoBehaviour
{
[SerializeField] ParticleSystem finishEffect;
bool hasFinished;
int currentLevelIndex;
GameSession gameSession;
void Start(){
hasFinished = false;
gameSession = FindObjectOfType<GameSession>();
currentLevelIndex = SceneManager.GetActiveScene().buildIndex;
}
void OnTriggerEnter2D(Collider2D other) {
if(hasFinished == true){
return;
}
if(other.tag == "Player") { // only hits if hasFinished == false, obviously
hasFinished = true; // so you can't hit the exit twice
Debug.Log("End of level. " + gameSession.getPlayerStats());
StartCoroutine(exitLevel(1f));
finishEffect.Play(); // particle system
}
}
public IEnumerator exitLevel(float time){
yield return new WaitForSecondsRealtime(time);
// it will wait before executing the following
FindObjectOfType<LevelPersist>().ResetLevelPersist(); // destroy that version of LevelPersist, we're moving on!
// yield return null; // to make sure it's getting the full destroy in
if(currentLevelIndex == SceneManager.sceneCountInBuildSettings-1){
// you finished last level, so loop back to first level
// Debug.Log("End of level. " + gameSession.getPlayerStats());
SceneManager.LoadScene(0);
}
else{
// Debug.Log("End of level. " + gameSession.getPlayerStats());
SceneManager.LoadScene(currentLevelIndex+1);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Coin : MonoBehaviour
{
GameSession gameSession;
SpriteRenderer coinRenderer;
// [SerializeField] AudioClip coinSFX;
[SerializeField] ParticleSystem coinParticle;
[SerializeField] int coinValue = 10; // default
CircleCollider2D coinCollider;
AudioSource audiosource;
bool isCaptured;
void Start(){
gameSession = FindObjectOfType<GameSession>();
coinRenderer = GetComponent<SpriteRenderer>();
coinCollider = GetComponent<CircleCollider2D>();
isCaptured = false;
audiosource = GetComponent<AudioSource>();
}
private void OnTriggerEnter2D(Collider2D other) {
if(other.tag == "Player" && !isCaptured){
gameSession.captureCoin(coinValue);
Debug.Log("Coin msg: coin captured, current score: " + gameSession.getCurrentScore().ToString());
// Debug.Log("Current coins: " + gameSession.currentCoins());
coinParticle.Play();
// AudioSource.PlayClipAtPoint(coinSFX, Camera.main.transform.position, 0.5f);
audiosource.Play();
isCaptured = true;
// gameObject.SetActive(false); // can do this instead of disabling the collider etc
coinRenderer.enabled = false;
coinCollider.enabled = false;
RemoveCoin();
}
}
void RemoveCoin(){
if(!isCaptured){return;} // not captured = nothing to do
else if(coinParticle.IsAlive()){return;} // it is captured, but still emitting, so keep Updating
else{Destroy(gameObject,1.5f);} // animation is done, destroy the object. Add extra 1.5s for the audio to finish
}
}