issue with updating player level by loading scene - c#

I'm having trouble saving and loading a scene. I have this code:
public void LoadData(Data data)
{
this.sceneName = data.sceneName;
SceneManager.LoadScene(sceneName);
this.level = data.level;
}
public void SaveData(ref Data data)
{
data.sceneName = SceneManager.GetActiveScene().name;
data.level = this.level;
}
and when I have a line in my code that contains "SceneManager.LoadScene(sceneName);" is the player's level, which he writes at the beginning of the code as follows `public int level = 1;
public int health = 100; I'm signing up incorrectly. The Player level object changes, but when I want to get it using player.level; it shows me level "1" even though it is equal to 2
All I know is that the problem occurs by loading the scene. When I remove the line
SceneManager.LoadScene(sceneName);
level updates normally

When using SceneManager.LoadScene, the scene loads in the next frame, that is it does not load immediately. This semi-asynchronous behavior can cause frame stuttering and can be confusing because load does not complete immediately. So your level variable gets overwritten when the scene loads and resets back to 1.
One solution could be to store the new level value in a temporary variable before loading the scene, and then assign it to the level variable after the scene has finished loading. You can do this using the SceneManager.LoadSceneAsync function, which allows you to load the scene asynchronously and use a callback function to execute code after the scene has finished loading.
public void LoadData(Data data)
{
this.sceneName = data.sceneName;
int newLevel = data.level;
// Load the scene asynchronously and use a callback function to execute code after the scene has finished loading
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
asyncLoad.completed += (operation) =>
{
this.level = newLevel;
};
}
public void SaveData(ref Data data)
{
data.sceneName = SceneManager.GetActiveScene().name;
data.level = this.level;
}
This way, the level variable will not be overwritten when you load the scene, and it will retain its updated value.
Check out the SceneManager.LoadScene documentation for further details.
Alternatively you can set your level variable as static and it will keep its value between scenes.

Related

Unity coroutine stopping for no reason

I'm making my first 2D topdown shooter game. I'm working on the loading part of the game. I have this functioning loading screen function (a method of my GameManager class) I made. I tried swapping between the main title scene and the first level a few times to test it. After loading the first level, then the title screen, when I try loading the first level back again the load screen is stuck.
The whole thing is working on a coroutine, and after some debugging I figured out the problem was that this coroutine stopped executing in a certain part of the code for some reason.
public static bool isLoadingScene { get; private set; } = false;
public void LoadScene(int sceneIndex)
{
// If another scene is already loading, abort
if (isLoadingScene)
{
Debug.LogWarning($"Attempting to load scene {sceneIndex} while another scene is already loading, aborting");
return;
}
isLoadingScene = true;
var sceneLoader = FindObjectOfType<SceneLoader>().gameObject;
if (sceneLoader != null)
{
// Make the loading screen appear
var animator = sceneLoader.GetComponent<Animator>();
animator.SetTrigger("appear");
// Make sure the SceneLoader object is maintained until the next scene
// in order to not make the animation stop
DontDestroyOnLoad(sceneLoader);
}
else // If a SceneLoader is not found in the current scene, throw an error
{
Debug.LogError($"SceneLoader could not be found in {SceneManager.GetActiveScene().name}");
}
// Unload active scene and load new one
StartCoroutine(LoadScene_(sceneIndex, sceneLoader));
}
IEnumerator LoadScene_(int sceneIndex, GameObject sceneLoader)
{
float t = Time.time;
// Start loading the new scene, but don't activate it yet
AsyncOperation load = SceneManager.LoadSceneAsync(sceneIndex, LoadSceneMode.Additive);
load.allowSceneActivation = false;
// Wait until the loading is finished, but also give the loading screen enough
// time to appear
while (load.progress < 0.9f || Time.time <= (t + LoadingScreen_appearTime))
{
yield return null;
}
// Activate loaded scene
load.allowSceneActivation = true;
// Unload old scene
AsyncOperation unload = SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());
// Wait until it's finished unloading
while (!unload.isDone) yield return null;
// --- THE COROUTINE STOPS HERE DURING THE 3RD LOADING --- //
// Make the loading screen disappear
sceneLoader.GetComponent<Animator>().SetTrigger("disappear");
// Wait until the disappearing animation is over, then destroy the SceneLoader object
// which I set to DontDestroyOnLoad before
yield return new WaitForSeconds(LoadingScreen_disappearTime);
Destroy(sceneLoader);
isLoadingScene = false;
}
Only during the 3rd loading, right after the while (!unload.isDone) yield return null; line the coroutine just stops executing (I know it because I inserted a Debug.Log inside the while loop and it got called, but I inserted one right after and it did NOT get called).
Any suggestions?
I figured out the problem by myself. The GameManager itself was programmed as a singleton, and since there was a GameManager in both scenes that was messing things up. Had to move the GameManager to a separate scene loaded additively.

Unity PlayerPrefs is not updating my 'high score'

I'm making a 2D game where you're in the middle of the screen and you move round an endless green (screen) world and white cubes spawn randomly around you, and I have finished the game mechanics and a main menu and game over screens. The one thing I'm trying to add now is a high score. I did a bit of research and found PlayerPrefs is probably the way to do it. I have a seperate scene for my main menu and my gameplay level (which includes the game over screen). I have no error messages. I have created a HSSetter (High Score Setter) script on the high score text in the main menu screen:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HSSetter : MonoBehaviour
{
public Text highScoreText;
// Start is called before the first frame update
void Start()
{
highScoreText.text = "High Score: " + PlayerPrefs.GetInt("HighScore").ToString();
}
// Update is called once per frame
void Update()
{
highScoreText.text = "High Score: " + PlayerPrefs.GetInt("HighScore").ToString();
}
}
and in my score script which is in my actual game level, here's the bit where I try to create the high score:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class score : MonoBehaviour
{
public int scoreCount = 0;
public int highScoreIFA;
void Start()
{
highScoreIFA = PlayerPrefs.GetInt("HighScore");
}
void Update()
{
if (scoreCount >= highScoreIFA)
{
PlayerPrefs.SetInt("HighScore", scoreCount);
}
}
public void AddToScore()
{
if (isHit == true) // i know this if loop works
{
scoreCount += 1; // and this, I use it to change the score text in-game.
isHit = false;
}
}
}
In AddToScore(), I increment scoreCount.
Through some debugging, I have found that everything in the HSSetter script works - when I change the highScoreText.text, the text on screen changes, which led me to believe the issue might be with the change of scenes? Thanks!
Multiple things you should do here
The first you already updated in your question afterwards: You had the condition wrong and always updated only
if(highScoreIFA > scoreCount)
which would almost always be the case.
Now you have changed it to
if(scoreCount >= highScoreIFA)
which still is not good since if the score is equal there is no reason to update it, yet.
I would rather use
if(scoreCount > highScoreIFA)
so only really update it when needed.
Secondly in both scripts do not use Update at all! That is extremely inefficient.
I would rather use event driven approach and only change and set stuff in the one single moment it actually happens.
You should only one single class (e.g. the score) be responsible and allowed to read and write the PlayerPrefs for this. I know lot of people tent to use the PlayerPrefs for quick and dirty cross access to variables. But it is exactly this: Quick but very dirty and error prone.
If you change the keyname in the future you'll have to do it in multiple scripts.
Instead rather let only the score do it but then let other scripts reference it and retrieve the values directly from that script instead
And finally you should use
PlayerPrefs.Save();
to create checkpoints. It is automatically done in OnApplicationQuit, bit in case your app is force closed or crashes the User would lose progress ;)
Might look like
public class score : MonoBehaviour
{
public int scoreCount = 0;
// Use an event so every other script that is interested
// can just register callbacks to this
public event Action<int> onHighScoreChanged;
// Use a property that simply always invoked the event whenever
// the value of the backing field is changed
public int highScoreIFA
{
get => _highScoreIFA;
set
{
_highScoreIFA = value;
onHighScoreChanged?.Invoke(value);
}
}
// backing field for the public property
private int _highScoreIFA;
private void Start()
{
highScoreIFA = PlayerPrefs.GetInt("HighScore");
}
public void AddToScore()
{
if (isHit == true) // i know this if loop works
{
scoreCount += 1; // and this, I use it to change the score text in-game.
isHit = false;
// Only update the Highscore if it is greater
// not greater or equal
if (scoreCount > highScoreIFA)
{
PlayerPrefs.SetInt("HighScore", scoreCount);
// Save is called automatically in OnApplicationQuit
// On certain checkpoints you should call it anyway to avoid data loss
// in case the app is force closed or crashes for some reason
PlayerPrefs.Save();
}
}
}
}
Then your other script only listens to the event and updates its display accordingly. It is even questionable if both scripts should not rather simply be one ;)
public class HSSetter : MonoBehaviour
{
public Text highScoreText;
// Reference your Score script here
[SerializeField] private score _score;
private void Awake ()
{
// Find it on runtime as fallback
if(!_score) _score = FindObjectOfType<score>();
// Register a callback to be invoked everytime there is a new Highscore
// Including the loaded one from Start
_score.onHighScoreChanged += OnHighScoreChanged;
}
private void OnDestroy()
{
_score.onHighScoreChanged += OnHighScoreChanged;
}
private void OnHighScoreChanged(int newHighScore)
{
highScoreText.text = $"High Score: {newHighScore}";
}
}

Why does Unity C# Coroutine execute upon click of an unrelated button

The script is part of a Unity/Vuforia AR app in which the user is able to place various PreFab models via the Vuforia ground detection system. These Prefabs are loaded into AssetBundles in order to manage memory overhead.
The script to accomplish this is executing prematurely.
This Coroutine script is attached to each button. Upon "onClick" the script is intended to load an AssetBundle containing a Prefab, and then instantiate the loaded Prefab object into the AR world.
The problem currently is that the script is executing upon the click on a UI panel opening button, which enables access to the actual placement UI buttons to which the script has been attached.
I arrived at this script the excellent via input from #derHugo. That thread can be found here: Can a Prefab loaded from asssetBundle be passed to Vuforia AnchorBehavior declaration
Here is the script that is attached to the placement buttons
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Vuforia;
public class anchorManagerBundles : MonoBehaviour
{
public PlaneFinderBehaviour plane;
public ContentPositioningBehaviour planeFinder;
private AnchorBehaviour model;
public string nameOfAssetBundle;
public string nameOfObjectToLoad;
private static bool alreadyLoading;
private static AssetBundle assetBundle;
void Start()
{
// only load the bundle once
if (!alreadyLoading)
{
// set the flag to make sure this is never done again
alreadyLoading = true;
StartCoroutine(LoadAsset(nameOfAssetBundle, nameOfObjectToLoad));
}
else
{
LoadObjectFromBundle(nameOfObjectToLoad);
}
}
private IEnumerator LoadAsset(string assetBundleName, string objectNameToLoad)
{
string filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "AssetBundles");
filePath = System.IO.Path.Combine(filePath, assetBundleName);
if (assetBundle == null)
{
Debug.Log("Failed to Load assetBundle!!");
yield break;
}
{
var assetBundleCreateRequest = AssetBundle.LoadFromFileAsync(filePath);
yield return assetBundleCreateRequest; assetBundle = assetBundleCreateRequest.assetBundle;
}
private IEnumerator LoadObjectFromBundle(string objectNameToLoad)
{
AssetBundleRequest assetRequest = assetBundle.LoadAssetAsync<GameObject>(objectNameToLoad);
yield return assetRequest;
GameObject loadedAsset = (GameObject)assetRequest.asset;
model = loadedAsset.GetComponent<AnchorBehaviour>();
}
public void create()
{
planeFinder.AnchorStage = model;
}
}
The desired/expected result was that upon clicking the UI button, the selected AssetBundle is loaded and the named PreFab is loaded for placement into the AR world upon tapping the screen.
I added a debug break in order to identify if the Asset Bundle is successfully loaded. The error below is then registered when the unrelated UI panel opening button is pressed, which does not have the script attached, indicating that the script is running prematurely.
Failed to Load assetBundle!!
UnityEngine.Debug:Log(Object)
<LoadAsset>d__8:MoveNext() (at Assets/Scripts/anchorManagerBundles.cs:38)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
anchorManagerBundles:Start() (at Assets/Scripts/anchorManagerBundles.cs:23)
Then i proceed to the actual placement button, of course the there is no prefab placed because of the premature execution.
There is no content to place at the anchor. Set the "Anchor Stage" field to the content you wish to place.
UnityEngine.Debug:LogError(Object)
Vuforia.ContentPositioningBehaviour:CreateAnchorAndPlaceContent(Func`2, Vector3, Quaternion)
Vuforia.ContentPositioningBehaviour:PositionContentAtPlaneAnchor(HitTestResult)
UnityEngine.Events.UnityEvent`1:Invoke(HitTestResult)
Vuforia.PlaneFinderBehaviour:PerformHitTest(Vector2)
UnityEngine.Events.UnityEvent`1:Invoke(Vector2)
Vuforia.AnchorInputListenerBehaviour:Update()
The question is why does this script execute prematurely, and thus thwarting the loading of the asset bundle and its selected PreFab
Despite the fact you have a { too much there
your check for
if (assetBundle == null)
makes no sense at this point .. it will always be null so you tell your routine "If the assetBundle was not loaded yet .. then please do not load it".
You should move the check to the end. I personally would then invert the check you had in order to skip the loading if it was loaded already.
Note: You also missed the last line
LoadObjectFromBundle(objectNameToLoad);
at the end of LoadAsset so the object is never loaded for the first instance calling LoadAsset. For the others it actually makes no sense to call LoadObjectFromBundle in Start (sorry my bad for not noticing it the first time). The coroutine is not done yet but they will try to load an object from an assetBundle which wasn't set yet.
So I would change it to
private IEnumerator LoadAsset(string assetBundleName, string objectNameToLoad)
{
// can be done in one single call
var filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "AssetBundles", assetBundleName);
// if at this point the assetBundle is already set we can skip the loading
// and directly continue to load the specific object
if (!assetBundle)
{
var assetBundleCreateRequest = AssetBundle.LoadFromFileAsync(filePath);
yield return assetBundleCreateRequest;
assetBundle = assetBundleCreateRequest.assetBundle;
if (!assetBundle)
{
Debug.LogError("Failed! assetBundle could not be loaded!", this);
}
}
else
{
Debug.Log("assetBundle is already loaded! Skipping", this);
}
// YOU ALSO MISSED THIS!!
yield return LoadObjectFromBundle(objectNameToLoad);
}
private IEnumerator LoadObjectFromBundle(string objectNameToLoad)
{
// This time we wait until the assetBundle is actually set to a valid value
// you could simply use
//yield return new WaitUntil(() => assetBundle != null);
// but just to see what happens I would let them report in certain intervals like
var timer = 0f;
while (!assetBundle)
{
timer += Time.deltaTime;
if (timer > 3)
{
timer = 0;
Debug.Log("Still waiting for assetBundle ...", this);
}
yield return null;
}
var assetRequest = assetBundle.LoadAssetAsync<GameObject>(objectNameToLoad);
yield return assetRequest;
var loadedAsset = (GameObject)assetRequest.asset;
model = loadedAsset.GetComponent<AnchorBehaviour>();
// add a final check
if(!model)
{
Debug.LogError("Failed to load object from assetBundle!", this);
}
else
{
Debug.Log("Successfully loaded model!", this);
}
}
// and now make sure you can't click before model is actually set
public void create()
{
if(!model)
{
Debug.LogWarning("model is not loaded yet ...", this);
return;
}
planeFinder.AnchorStage = model;
}

Loading/unloading levels using SceneManager.LoadScene(int, LoadSceneMode.Additive) / SceneManager.UnloadSceneAsync(int); Deletes default level

Using additive loading in unity project, load a default level (Game manager) then run a method to select and activate additive levels (Lighting/Map). The game has rounds, and at the end of each round I run a method meant to unload the additive levels (Lighting/Map), however it deletes the default level as well.
Error upon crash: Display 1 No Cameras rendering
Have referenced the unity documentation pages;
https://docs.unity3d.com/ScriptReference/SceneManagement.UnloadSceneOptions.html
https://docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.html
Section for loading in level:
private IEnumerator RoundStarting ()
{
//Calling method meant to load in additive levels. Seems to work...
LoadMapForRound();
// As soon as the round starts reset the tanks and make sure they can't move.
ResetAllTanks ();
DisableTankControl ();
// Snap the camera's zoom and position to something appropriate for the reset tanks.
m_CameraControl.SetStartPositionAndSize ();
// Increment the round number and display text showing the players what round it is.
m_RoundNumber++;
m_MessageText.text = "ROUND " + m_RoundNumber;
// Wait for the specified length of time until yielding control back to the game loop.
yield return m_StartWait;
}
//Method meant to load in additive levels.
private void LoadMapForRound()
{
int LevelIndex = Random.Range(2, 4);
SceneManager.LoadScene(1, LoadSceneMode.Additive);
SceneManager.LoadScene(LevelIndex, LoadSceneMode.Additive);
//Debug.Log("SceneLoaded");
}
Section for unloading level:
private IEnumerator RoundEnding ()
{
// Stop tanks from moving.
DisableTankControl();
// Clear the winner from the previous round.
m_RoundWinner = null;
// See if there is a winner now the round is over.
m_RoundWinner = GetRoundWinner();
// If there is a winner, increment their score.
if (m_RoundWinner != null)
m_RoundWinner.m_Wins++;
// Now the winner's score has been incremented, see if someone has one the game.
m_GameWinner = GetGameWinner();
// Get a message based on the scores and whether or not there is a game winner and display it.
string message = EndMessage();
m_MessageText.text = message;
// Wait for the specified length of time until yielding control back to the game loop.
yield return m_EndWait;
//calling ethod meant to unload levels, but ends up unloading default level as well.
DestroyRoundMap();
}
//Method meant to unload levels
private void DestroyRoundMap()
{
SceneManager.UnloadSceneAsync(LevelIndex);
SceneManager.UnloadSceneAsync(1);
}
Unload additive levels (Light/Map) at rounds end, keep default level (0), instead the default level gets unloaded
and crashes the game.
Included local variable within the LoadMapForRound for the Global Variable to be equal to.
private void LoadMapForRound()
{
int mapChosen = Random.Range(2, 4); Solution Line
LevelLoaded = mapChosen;
SceneManager.LoadScene(LevelLoaded, LoadSceneMode.Additive);
SceneManager.LoadScene(1, LoadSceneMode.Additive);
//Debug.Log("SceneLoaded");
}
Got the idea for the solution from a separate thread on the same question. Thread here:
How do I unload additive scenes?

Unity set PlayerPrefs data regularly

I am using PlayerPerfabs to store player data like which level the player has unlocked and the point has got during the game, and I want the PlayerPerfabs data updated regularly(like per 5 seconds) during playing, so I made a class called ApplicationModel to hold the global data like points player get the level info etc..
public ApplicationModel: MonoBehaviour{
public static int point {get;set;}
//try to load the PlayerPerfabs when start game.
static ApplicationModel(){
if(PlayerPerfabs.HasKey("point")){
point = PlayerPerfabs.GetInt("point");
}else{
point = 0;
}
}
//coroutine to set the data to PlayerPerfabs
IEnumerator Save()
{
while (true)
{
Debug.Log("setting playerperfab ....");
yield return new WaitForSeconds(5f);
ApplicationModel.RestorePerfab();
}
}
public static void RestorePerfab(){
PlayerPerfabs.SetInt("point", point);
}
private void Start(){
StartCoroutine(Save());
}
}
But the coroutine was never executed according to the console log (cause the log was never printed).
Maybe because I didn't attach this script to a gameobject but once I attach this to a game object, then when the scene changed or that gameobject is destroyed, the regularly saving will ended, right? So how to do this?
You need to create a GameObject and then attach this script to it if you want it to work. If you're changing scenes and want it to continue working you could call DontDestroyOnLoad on that GameObject and it won't get destroyed when you switch scenes.

Categories

Resources