Reputation: 1
I have a game like Fruit Ninja, made in Unity. Fruits spawning in the scene, when you click them, they're being destroyed and you get 1 point for each fruit, which is shown on the score pannel. I want to implement some score boost mechanics, like such: When you click on the object with tag "scoreBoost", for the next 5 second every click on any fruit will give you additional 1 point.
I have to main scripts: gameManager and target. In the gameManager scripts there is method UpdateScore, which is showing the current score on the score pannel. I've tryed many things with coroutines and loops, end up with this one. Here is OnMouseDown method for target srcipt:
private void OnMouseDown()
{
if (gameManager.isGameActive)
{
if (gameObject.CompareTag("scoreBoost"))
{
StartCoroutine(ApplyScoreBonus());
}
while (scoreBonus == true)
{
Destroy(gameObject);
Instantiate(exposionParticle, transform.position, Quaternion.identity);
gameManager.UpdateScore(pointValue + 1);
}
Destroy(gameObject);
Instantiate(exposionParticle, transform.position, Quaternion.identity);
gameManager.UpdateScore(pointValue);
}
}
Here is the coroutine ApplyScoreBoost:
IEnumerator ApplyScoreBonus()
{
scoreBonus = true;
yield return new WaitForSeconds(5);
scoreBonus = false;
}
When i click on the object with tag "scoreBoost", the game just freezes
Upvotes: 0
Views: 70
Reputation: 90724
Couple of issues in your code and the How to correctly
very much depends on your exact desired behavior.
First and most importantly you have an infinite loop. A Coroutine is not asynchronous but (using the default yield instructions) runs within the Update
loop.
=> Your while
loop once entered can never finish since within the loop nothing will ever change scoreBonus
Further you immediately Destroy
this gameObject
.. which is running the Coroutine due to StartCoroutine
=> This routine will not run anymore since the GameObject
and accordingly also this component is already destroyed.
[opinion] And then your check via tag
is ok but why not simply have a bool
flag on the object itself? That would be saver against and mistakes and typos or later modifications (like e.g. have different objects with different boost and durations etc)
And finally - we don't see your code. Is scoreBonus
actually something static? Because if not only this very same instance of your component will be aware of it and other fruits not be aware at all.
=> I would rather move such a flag into the gameManager
itself to have it at a central place and there, where it is actually relevant. And then also run according routine there instead so it isn't affected by the mentioned Destroy
.
So something like e.g.
[SerializeField] private int points;
[SerializeField] private bool boost;
[SerializeField] private float boostFactor;
[SerilaizeField] private float boostDuration;
private void OnMouseDown()
{
if (!gameManager.isGameActive) return;
Destroy(gameObject);
Instantiate(exposionParticle, transform.position, Quaternion.identity);
gameManager.AddScore(points);
if(boost)
{
gameManager.ApplyBoost(boostFactor, boostDuration);
}
}
and then in your manager e.g.
private int totalPoints;
public void AddScore(int points)
{
totalPoints += Mathf.RoundToInt(points * currentBoostMultiplier);
// Update Display etc
}
And then from here on it depends a bit whether you want your boosts to
stack meaning you can have multiple active at a time
=> add them all to a list, remove them after their duration and when needed sum them up)
private float currentBoostMultiplier
{
get
{
var result = 1f; // by default normal points
foreach(var boost in m_ActiveBoosts)
{
// now again depends on your use case - either sum
result += boost.factor;
// or multiply
//result *= boost.factor;
}
}
}
private class Boost
{
public float Time;
public float Factor;
public Boost(float duration, float factor)
{
Time = duration;
Factor = factor;
}
}
private readonly List<Boost> m_ActiveBoosts = new ();
public void ApplyBoost(float multiplier, float duration)
{
m_ActiveBoosts.Add(new Boost(duration, multiplier));
if(!isBoost) StartCoroutine(BoostRoutine());
}
private bool isBoost;
private IEnumerator BoostRoutine()
{
if(isBoost) yield break;
isBoost = true;
while(m_ActiveBoosts.Count > 0)
{
foreach(var boost in m_ActiveBoosts)
{
boost.Time -= time.deltaTime;
}
m_ActiveBoosts.RemoveAll(boost => boost.Time <= 0f);
yield return null;
}
isBoost = false;
}
or, if you anyway only want only a single boost you can simply apply it and again decide
does a new boost overrule - or basically simply extend the current one
private float currentBoostMultiplier = 1f;
private float boostTimer;
public void ApplyBoost(float multiplier, float duration)
{
m_ActiveBoosts.Add(new Boost(duration, multiplier));
currentBoostMultiplier = multiplier; // or 1 + multiplier depedning on your use case
if(boostTimer > 0f)
{
// simply use the current routine
boostTimer = duration;
}
else
{
boostTimer = duration;
StartCoroutine(ResetBoostRoutine());
}
}
private IEnumertaor ResetBoostRoutine()
{
while(boostTimer > 0f)
{
boostTimer -= Time.deltaTime;
yield return null;
}
currentBoostMultiplier = 1f;
}
or is it simply not possible to activate a new boost while you already got one
private float currentBoostMultiplier = 1f;
private float boostTimer;
public void ApplyBoost(float multiplier, float duration)
{
m_ActiveBoosts.Add(new Boost(duration, multiplier));
if(boostTimer > 0f) return;
currentBoostMultiplier = multiplier; // or 1 + multiplier depedning on your use case
boostTimer = duration;
StartCoroutine(ResetBoostRoutine());
}
private IEnumertaor ResetBoostRoutine()
{
while(boostTimer > 0f)
{
boostTimer -= Time.deltaTime;
yield return null;
}
currentBoostMultiplier = 1f;
}
Upvotes: 0
Reputation: 302
You are probably entering an infinite while loop for 5 seconds. The while loop is running while the scoreBonus
variable is true, which is the case for 5 whole seconds, essentially blocking everything else from running. Change the while to an if (since the boost is per click and the mechanic is controlled with the coroutine, and it should fix it.
Upvotes: 0