Ruslan
Ruslan

Reputation: 1

How to correctly implement Coroutine and OnMouseDown method, in order to increase point for clicking on the objects for certain amount of time

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

Answers (2)

derHugo
derHugo

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

Tom Sebty
Tom Sebty

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

Related Questions