Keeping Scores in Unity and pass to next scene - c#

After reading and trying many different examples, I am still stuck with this fairly simple problem on getting a score from one level to the next in Unity.
I have a C# script that handles my game logic for a level:
First, I set my level variables:
public class GameLogic : MonoBehaviour {
public GUIText countText;
public GUIText targetCity;
public GUIText gameOverText;
public GUIText endScoreText;
public GUIText timerText;
public Texture2D bgImage;
private int count;
public GameObject cityPrefab;
List<MyCity> mycities;
public float finalScore; // I Want this value to be available when my next scene loads
private float startTime;
After this, my level code executes fine, until the GameOver condition is met (time is up). Then, this code executes:
public void GameOver ()
{
gameOverText.text = "time is up!";
endScoreText.text = "You have found " + count.ToString() + " cities. Good wrok!";
Destroy (GameObject.FindWithTag("EmptyCity"));
Destroy (GameObject.FindWithTag("City"));
Destroy (GameObject.FindWithTag("TargetCity"));
finalScore = count; // SO AT THIS STAGE, MY FINAL SCORE IS SET AND READY TO BE PASSED TO THE NEXT SCENE. QUESTION IS HOW?
StartCoroutine(Wait());
}
IEnumerator Wait() {
yield return new WaitForSeconds(7);
Application.LoadLevel (3);
}
So, I end my level with an updated value in public float 'finalScore'. So far so good. Now my problem starts: Level 3 loads and all gameobjects from level 2 are destroyed. In my game, level 3 is a simple Unity scene where I congratulate the player on his performance. I want to have access to that public float finalScore from my previous scene (level2).
I know I have to use Dontdestroyonload somewhere. But I don't know how. How and where do I create a GameObject that has the public float 'finalScore' in it? How do I call that GameObject in my new level so that I can do something like this in my new level:
public GUIText ContratsOnScore;
void SetContratsText() {
CongratsOnScore = "Congratulations, you scored" + (finalScoreFloatValue from Previous level).ToString();

The DontDestroyOnLoad method is callable on an object, not on a single variable. It represents the best possible approach for a value of that kind, that needs to be "exported" from your actual scene.
However, you can also use PlayerPrefs. They are intended for informations at a "higher" level of persistency, like flags for unlocked levels, final scores for a ranking system, or attributes of a customizable character. That's why the DontDestroyOnLoad way is better suited (for your "temporary" score), but you can use this one also. Basically, PlayerPrefs are information stored in the registry of your operative system.
Before exiting from the actual scene, save the score this way:
PlayerPrefs.SetInt("Player Score", finalScore);
And, when next scene starts, initialize score's variable by retrieving that information:
finalScore = PlayerPrefs.GetInt("Player Score");
(here a similar post)

Someone else said, and I shamelessly quote, use a static variable for your data in a C# class, and there you go...
(Still I'd prefer to pass parameters to LoadLevel itself, but that's another story)

Related

Check for "Are all enemies dead ?" in Dungeaon Generation for each room

Im in my 1st year in game design / dev ,
and my project is to make a simpler "binding of isaac" type of game 3D
I managed to make Dungeon Generation (with a lot of help from YouTube lol)
And now I'm working on a system that will detect in each room if all the enemies have died so that the doors in that room will be in "open" mode [bool] .
my idea was to create a C# script in the prefab of each room.
and it goes something like this > Timer [after 5sec to check if all enemies in rooms is dead] and loop until they are.
1- now the problem is if they are all dead in room X how to keep room Y closed ? [ because its the same prefab for the Dungeon generation]
2 - and what is the best way to check if they are all dead? [the easiest way for me is just by using: game tag "enemies" ] but using colliders / tags is very costly...
using Unity/c# btw
Any kind of help would be greatly appreciated
:)
Instead of using a timer to check periodically (if the player kills the last monster 0.01s after the previous check, they have to wait 4.99s before the doors open), have the room manage a collection of monsters in it, and when a monster dies have the monster notify the room. Psuedo code for the room:
private bool DoorsOpen = true;
private List<GameObject> MonstersInMe { get; set; }
private void GenerateMonster(GameObject monsterPrefab)
{
var monster = Instantiate(monsterPrefab);
monster.transform.SetParent(this.transform);
MonstersInMe.Add(monster);
}
public void RemoveMonster(GameObject monster)
{
MonstersInMe.Remove(monster);
if (MonstersInMe.Count == 0)
{
OnClear();
}
}
private void OnClear()
{
DoorsOpen = false;
// do whatever else
}
The room shouldn't be dependent on the Prefab after its been instantiated, so you shouldn't have to worry about room X confusing room Y due to being the same prefab.
In general instead of poll checking values or existence of things to be more efficient you want to make your code as event driven as possible.
I would have a dedicated component on the monsters so you can make all your code event based and track when one is destroyed:
public class Monster : MonoBehaviour
{
// event to be invoked when this monster is destroyed
public UnityEvent<Monster> whenDestroyed;
private void OnDestroy()
{
whenDestroyed.Invoke(this);
}
}
Then in your rooms you could do something like e.g.
public class BasicRoom : MonoBehaviour
{
// the default prefab to spawn, can be different for each room instance
[SerializeField] protected Monster defaultMonsterPrefab;
private readonly HashSet<Monster> monsters = new ();
// event to be invoked when this room is cleared
// per default needs at least one monster to have existed
public UnityEvent whenCleared;
// making this virtual - maybe different rooms do different things
// also adding an optional prefab parameter to allow to overrule the default fallback prefab
public virtual void SpawnMonster(Monster monsterPrefab = null)
{
// if monsterPrefab is not passed in use the defaultMonsterPrefab as fallback instead
if(monsterPrefab == null)
{
monsterPrefab = defaultMonsterPrefab;
}
// create the monster instance from the given prefab
// optionally as a child of he room
var instance = Instantiate(monsterPrefab, transform);
// keep track of the created monsters
monsters.Add(monster);
// and react when it gets destroyed
instance.whenDestroyed.AddListener(OnMonsterDestroyed);
}
// make this virtual - maybe different rooms do different / additional things
protected virtual void OnMonsterDestroyed(Monster monster)
{
monsters.Remove(monster);
CheckAllDestroyed();
}
// again making this virtual - maybe there is a special room that has different condition
// also make this public so you could still check it from somewhere else if needed
public virtual bool CheckIsCleared()
{
if(monsters.Count == 0)
{
whenCleared?.Invoke();
return true;
}
return false;
}
}
Now in the whenAllDestroyed field you can either assign callbacks via the Unity Inspector (like e.g. for Button.onClick) or via code like
someRoom.whenCleared.AddListener(OpenDoor);
By having some methods virtual you could have different room implementations with different behavior. One could e.g. not have monsters at all but simply play some sequence or whatever and automatically call whenCleared after X seconds

Play in sequence of 4 game objects - animators (Not single animator with multiple states) in a queue

I'm new to unity. I have 4 GameObjects say Red, Green, Blue, Yellow - with index mapped 0, 1, 2 and 3 respectively. Also, I kept index sequence in the list List<int> systemTrack.
Now, I'm calling the sequence of animator with respect to list { 0, 1, 2, 3 } (this can be random 0-3).
I can perform one animator loop at a given time.
void Start() {
animator.Play("blue_animation", 0, 0);
}
How can I call in sequence based on list ? Perhaps Update() is right place to call.
So far I found this thread - But it has single Animator object with multiple state called in sequence which is not my case.
Few others discussions also available for Animation component Not for the new Animator component.
In your current class you could use a Coroutine like
List<AnimatorQueueController> animatorQueues;
private void Start()
{
StartCoroutine(RunAllControllersSequencial());
}
IEnumerator RunAllControllersSequencial()
{
foreach (var queue in animatorQueues)
{
// This runs the routine and waits until it finishes
yield return queue.RunAnimationQueueAndWait();
}
}
Now the only thing to do is define/implement how each if these controllers "knows", that its own animation queue has finished.
There are probably many possible ways to go. Straight away: None of them will be beautiful ;) Since the Animator uses a lot of string based stuff you always will end up either having to make sure all animation/trigger names are written correctly, or you will have to reference the animationClip and either hope the state is called the same or you have to expensively find the states via the according clip, you'd have to know how much time to wait for the queue to finish, etc.
This said, also this solution won't be perfect but in my opinion for your setup it would be the most convenient ;)
You could e.g. use an Animation Event and do something like
public class AnimationQueueController : MonoBehaviour
{
[SerializeField] private Animator _animator;
// Here set the first state name for each instance via the Inspector
[SerializeField] private string firstStateName = "first_state";
private void Awake ()
{
if(!_animator) _animator = GetComponent<Animator>();
}
// Simply have another routine you can yield so it is executed and at the same time waits until it is done
public IEnumerator RunAnimationQueueAndWaitUntilEnd()
{
hasReachedEnd = false;
_animator.Play(firstStateName, 0 ,0);
yield return new WaitUntil(() => hasReachedEnd);
}
// This is the method you will invoke via the Animation Event
// See https://docs.unity3d.com/Manual/script-AnimationWindowEvent.html
public void AnimationEnd()
{
hasReachedEnd = true;
}
}
This at least reduces your implementation overhead to
make sure you fire the Animation Event at the end of the last state
make sure you provide the name of the first state in the AnimatorQueueController
in your original script instead of Animator references rather store the AnimatorQueueController in your list

Unity Destroy and other code not working?

I'm trying to make the game Snake, and I'm trying to get the apple functionality working. What this script is meant to do, is whenever my Snake goes over the Apple, the apple disappears and reappears at a random location on the screen. But instead it does nothing, any idea as to why?
P.S: Camera is Size 10 and Aspect Ratio is 16:9 which is why I have some weird Random.Range values. Also I used Debug.Log in Update to make sure that the variable worked, and yes it does, whenever my snake moves its coordinates are displayed.
public class Apple_RandomSpawn : MonoBehaviour
{
private Vector2 foodPos;
private Snake_Move snakeMove;
void Start()
{
SpawnApple();
}
public void Update()
{
transform.position = new Vector2(foodPos.x, foodPos.y);
SnakeAte();
}
public void SpawnApple()
{
foodPos = new Vector2(Random.Range(-17, 17), Random.Range(-9, 9));
}
public void SnakeAte()
{
if (Mathf.Approximately(foodPos.x, snakeMove.pos.x) && Mathf.Approximately(foodPos.y, snakeMove.pos.y))
{
SpawnApple();
}
}
}
First of all, it does not directly have something to do with your problem, but DO NOT put GetComponent() or GameObject.Find() in the Update() function. These two, especially GameObject.Find() function is super heavy, so It's recommended that you should call these kinds of function inside Start() or Awake(), or at the initiallization of a class. It can directly, and heavily impact the performance of your game, so here's my suggestion:
[SerializeField]
private Snake_Move snakeMove;
And drag your gameobject (which Snake_Head component is attatched) via Inspector. You should always consider this way first, rather than using GameObject.Find() and GetComponent().
Second, Float is not recommend to compare equality directly through =, since there must be rounding error. There is a help function with regard to comparing two float value in Unity, like Mathf.Approximately(float a, float b). Directly comparing two float value via = would almost always not work as you might think.
Third, It doesn't seem to be that there are no Instantiate() function in your code, but you are try to use one apple object, and every time you consume it, just change it's position. Then why does Object.Destroy(gameObject) exists? What you're doing is, just destroying the apple when you get it first time. I think you have to remove the Destroy() function, and SpawnApple() changes the target coordinate of apple, and the position will be updated in Update() function.
And it's no need to indirectly set the target position, and update to it in Update() function. You can directly set the position of apple, like:
// assuming you followed my suggestio aboven about snakeMove.
public void Update()
{
SnakeAte();
Debug.Log(snakeMove.pos);
}
public void SpawnApple()
{
transform.position = new Vector2(Random.Range(-17, 17), Random.Range(-9, 9));
}
public void SnakeAte()
{
if (foodPos == snakeMove.pos)
{
SpawnApple();
}
}
First of all your null ref in your last comment comes from:
private Snake_Move snakeMove;
Its a private variable and its never assigned. You either need to make it public/[SerialistField] and assign it in inspect or have some sort of initialize function that give it a value.
For the hit detection Mathf.Approximately is good if you wan to check if 2 floats are exactly the same. If you'r checking 2 positions of moving objects the chance of them being exactly the same is very low and may rely on frame rate and such. Keeping your implementation you can check instead for a minimum distance between the 2 positions. You can tweak DISTANCE_THRESHOLD for a value that suits your better.
public class Apple_RandomSpawn : MonoBehaviour
{
private const float DISTANCE_THRESHOLD = 0.1f;
private Vector2 foodPos;
private Snake_Move snakeMove;
void Start()
{
SpawnApple();
}
public void Update()
{
SnakeAte();
}
public void SpawnApple()
{
foodPos = new Vector2(Random.Range(-17, 17), Random.Range(-9, 9));
transform.position = new Vector2(foodPos.x, foodPos.y);
}
public void SnakeAte()
{
if (Vector3.Distance(foodPos, snakeMove) < DISTANCE_THRESHOLD)
{
SpawnApple();
}
}
}
Now remember that since your teleporting the apple to a purely random location it might teleport right back on top of your snake :).

Incrementing integer with multiple sources of input C#

In my game there is a score. When an object collides, a set increment is added to the score. There is one scoreboard. What is happening in my game is that when an object collides it hijacks the scoreboard to show only its own person history of scores. When the next object collides, it takes over the scoreboard and shows its own personal history. I am trying to show the amalgamate score of all of the objects put together, each contributing their part to a grand total.
void OnCollisionEnter (Collision col)
{
if(col.gameObject.name == "Plane")
{
score += 50;
textMeshComponent = GameObject.Find ("Score").GetComponent<TextMesh> ();
textMeshComponent.text = score.ToString ();
}
}
There are 10 of (col.gameObject.name) and there is one "Plane" that they all interact with.
How can I make the score a conglomerate like I described? I am really at a loss for how to manipulate the (col.gameObject.name) side of that equation.
Thanks for your advice.
Make score a member of Plane, instead of this, and make it public so all the other gameObjects can get it.
You can also create score as its own gameObject, or create a gameObject that contains all the globals.
Let me suggest another approach although not recommended textMeshComponent.text = (score+(int.Parse(textMeshComponent.text))).toString();
now this will add points as a whole regardless each gameobject adding its own to the main value
Consider having an object in your scene whose sole concern is counting (and maybe displaying) the score. Lets name it ScoreController.
Whenever an event occurs for which you want to award score to the player, the responsible code just calls your ScoreController and tells them how much score to award.
Your ScoreController GameObject might have a script like this:
public class ScoreController : MonoBehaviour
{
public TextMesh scoreDisplay;
public int Score { get; private set; }
void Start()
{
UpdateView();
}
public void AwardScore(int amount)
{
Score += amount;
UpdateView();
}
private void UpdateView()
{
scoreDisplay.text = Score.ToString();
}
}
Now, other code can award score by calling
GameObject.FindObjectOfType<ScoreController>().AwardScore(123);
If you want to read up on related topics, consider reading about the Model-View-Control design pattern, and about SoC (seperation of concerns).

Need a logic for my android game

Hi I am new to Unity game engine and I am creating a 3d shooting game. In the level 1 I want to shoot 5 enemies in certain amount of time let's say 30sec. After completing level 1, I want to go to level 2 where my total enemies are 10 and want to kill it in 60sec and if fail there will be game over. I wrote some script for it, it works a little bit but it is not perfect because after starting level 2 the game become slow and after game over the level 2 restart again but not with the default value of 10 enemies, rather it starts from the no. which reaches at the time of game over. needs some idea and good logic and script for my game. here is my code.
public class Status : MonoBehaviour
{
public static int TotalZombies=5;
public static float timeLeft=25.0f;
// destry this game object.
Destroy (this.gameObject);
TotalZombies--;
}
and here is my other script where I am handling my levels and time etc.
using UnityEngine;
using System.Collections;
public class Generate : MonoBehaviour {
public GUIText Zombiesobject;
public string zombiesscore;
public GUIText countdown;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
zombiesscore = "Zombies Left: " + Status.TotalZombies.ToString ();
Zombiesobject.text = (zombiesscore);
Status.timeLeft -= Time.deltaTime;
if (Status.timeLeft <= 0.0f && Status.TotalZombies > 0)
{
countdown.text = "Game Over";
Application.LoadLevel(Application.loadedLevel);
}
else if (Status.timeLeft <= 10.0f && Status.TotalZombies > 0)
{
countdown.text = "Time left = " + (int)Status.timeLeft + " seconds" + " \n You are running out of time!";
}
else if (Status.timeLeft > 0.0f && Status.TotalZombies <= 0) {
countdown.text = "You win!";
Application.LoadLevel("level 2");
Status.TotalZombies=10;
Status.timeLeft=59.0f;
}
else
{
countdown.text = "Time left = " + (int)Status.timeLeft + " seconds";
}
}
}
Your problem is that you use static variables. static's are exactly like global variables. The only difference is that you need to access those through a Class name.
So what does that exactly mean? When you load your game, and when the class itself gets loaded the static variables are created and initialised. In your case it is TotalZombies set to 5 and timeLeft to 25f.
But those variables persist and never gets re-initialized as long as your game runs. Even if you do a Application.LoadLevel these variables and their values persists.
That means if you change those variables, and reload your level TotalZombies and timeLeft still have their last values.
Because of this i encourage to never use static variables. They easily introduce hard to spot bugs. Let's for assume a simple fix to your code.
You additionally add an initialization to your Start() Method. For example in your Status class you add.
void Start() {
TotalZombies = 5;
timeLeft = 25.0f;
}
In your case it could solve the problem completely, but you also could say this is just by accident or luck.
In Unity there don't exists an order in which Start() is called. It could for example still happen that the Start method in your Generate class is called first on loading a Scene. If you used Status.TotalZombies or Status.timeleft in Start to initialize something in Generate you still have your bug that your initializiation is wrong because it uses the variables from the previous run. The problem is Unity could sometimes first execute Status.Start() before Generate.Start() sometimes the other way around. That would lead to a bug that just sometimes occur and is extremely hard to debug.
If you knew the above you could also put your initialization in the Awake method. Because Awake methods will be called before any Start method. So this will be a better fix.
But there exists other problems. For example lets look into your Generate.Update() method. You for example directly do a Status.timeLeft -= Time.deltaTime; in your Update method. But when you for example have multiple GameObjects in your game that has the Generate Component it means timeLeft will be decreased multiple times in a single frame. If you have two Generate Components it means your time will run out twice as fast.
So even putting an initialization into Start or Awake can fix some bugs, but you still have different problems with statics
That is a reason why i encourage not to use static at all. So how do you fix also this problem? Instead of having static you should create Attributes of a class. And on top of that you should make all your attributes only set-able only from your own class. That also has an impact on other code. For example you could not reduce the timeLeft attribute anymore from Generate. That sounds like a disadvantage, but it forces to think you about how to change timeLeft correctly. In your case you do not really want that any class from everywhere can change timeLeft. It is a time that should be constantly reduced and it is just an error to reduce it multiple time. The result of that is. Your Status class should only change the timeLeft in the Update. The same goes for TotalZombies. It would be better to just have a method like IncrementTotalZombies and DecrementTotalZombies instead of doing Status.TotalZombies++ and so on. For example your Status class should now look like
public class Status : MonoBehaviour {
public int TotalZombies { get; private set; }
public float TimeLeft { get; private set; }
void Awake() {
this.TotalZombies = 5;
this.TimeLeft = 25f;
}
void Update() {
this.TimeLeft -= Time.deltaTime;
}
public void IncreaseTotalZombies() {
this.TotalZombies++;
}
public void DecreaseTotalZombies() {
if ( this.TotalZombies <= 0 ) {
throw new ApplicationException("Cannot decrease TotalZombies. Already 0. Possible Bug in your code.");
}
this.TotalZombies--;
}
}
Now the IncreaseTotalZombies or DecreaseTotalZombies sounds like overhead, but you can do a lot of extra checking here. For example check if the counter never gets smaller than zero. Because when it does, you have a bug somewhere in your code. For example Increasing your TotalZombies accidentally by two, or somewhere else decreasing it by two and so on. You also could implement a MaxTotalZombies attribute that ensures that you never get more Zombies as defined. And if that happens it will throw an Exception pointing it to your code directly where it happened.
It is also easier to identify bugs. Because Increasing it twice in a row looks wrong.
status.IncreaTotalZombies();
status.IncreaTotalZombies();
where following code can just look right
Status.TotalZombies += 2;
But if you do the above changes you will see your current Status.TotalZombies will not work anymore. You also have to change how to get an instance of your Status class. For this lets assume you create a GameObject in Unity named Status. Then in your Generate class you should add the following.
private Status status;
void Awake() {
this.status = GameObject.Find("Status").GetComponent<Status>();
}
Now you can replace the Status.TotalZombies++ and so on with status.IncreaseTotalZombies(). If you just want to get the values, you still just can write status.TimeLeft but setting the value status.TimeLeft -= Time.deltaTime will now throw an error. And you don't need to set it anymore because that is a behaviour that the Status class already handle in his Update Method.
Now additonally in your Generate class you had code like this.
Application.LoadLevel("level 2");
Status.TotalZombies=10;
Status.timeLeft=59.0f;
This didn't work as expected. Because when you call Application.LoadLevel() your new Scene gets called and the lines behind it was was never called. You could fix this be changing the order.
Status.TotalZombies=10;
Status.timeLeft=59.0f;
Application.LoadLevel("level 2");
Because your Status where static the value persists through loading. But the whole approach is still not really good. The Problem is you hardcode values in your code. And it seems you want different amount of Zombies and Time for every level. If you want that you can just add Attributes to your Status class that initialize your variables, And those variables are set-able through your Unity IDE. For example add the following attributes to your Status class.
public int _StartZombies = 5;
public float _StartTime = 25f;
If you add this to your Status class now in your IDE two TextBoxes will appear named Start Zombies and Start Time. In this boxes you can now enter how many Zombies or how much Start time your Level should have. The default values are 5 and 25 for those values. But those values didn't get applied on loading your level. To also apply those values when your level gets loaded change your Awake method to.
void Awake() {
this.TotalZombies = this._StartZombies;
this.TimeLeft = this._StartTime;
}
Now this.TotalZombies and this.TimeLeft always get the values that you have configured in your IDE. The only thing you now need to do is to write.
Application.LoadLevel("SomeLevel");
And you just can configure the amount of Zombies and time through your IDE! It also means you now have reusable Components. And you configure things where it belongs!
You also described that you want different conditions to loading a new Level. For example if a user is able to kill all the Zombies in a specific amount of time he directly jumps to Level 3 instead of Level 2 and so on. So how can you add this, without creating a lot of special classes?
At first you need a class on its own that just hold data. In your case you want a specific time and a definition which level gets loaded. So you could write something like this.
[System.Serializable]
public class LoadLevelData {
public float TimeLeft;
public string LoadLevel;
}
But in my opinion that logic belongs to the Status class, so what you now do is add the following to this class.
public LoadLevelData[] _NextLevels;
As soon as you add that to your code. In the Unity IDE you will see "Next Levels" with a "Cursor". You now can expand this cursor and a Size field will appear. You now can for example write 2 into it and it gives you Element 0 and Element 1. So Unity gives you the ability to create an Array of objects and you can create as many entries as you want from the IDE with any values that you want!
Now you could write a LoadNextLevel Method in such a way.
public void LoadNextLevel() {
foreach ( var level in this._NextLevels ) {
if ( level.TimeLeft > this.TimeLeft ) {
Application.LoadLevel(level.LoadLevel);
}
}
}
Now you can configure in the Unity IDE
Element 0:
Time Left -> 20
Next Level -> "Level 3"
Element 1:
Time Left -> 10
Next Level -> "Level 2"
You only need to call status.LoadNextLevel() when your game finished. And you can configure everything from the IDE. Also note. The order in which you fill your _NextLevel Array is important. In this case "Time Left" -> 20 must be before "10".

Categories

Resources