coroutine calls following method too many times - c#

I'm trying out some code for an idea I had recently. But right now I'm stuck on trying to find a good way to add a delay. I've been trying to use coroutine, and I get a delay, but the method called after that gets called way too many times (I only want it to be called once)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System;
using Random = UnityEngine.Random;
public class YourHealth : MonoBehaviour
{
bool yourTurn = true;
public Button button1;
public Button button2;
public Text YH;
public Text EH;
private int yourHealth = 100;
private int enemyHealth = 100;
// Use this for initialization
void Start()
{
button1.onClick.AddListener(Heal20);
button2.onClick.AddListener(Damage40);
YH.text = Convert.ToString(yourHealth);
EH.text = Convert.ToString(enemyHealth);
}
public void Heal20()
{
yourHealth += 20;
yourTurn = false;
}
public void Damage40()
{
enemyHealth -= 40;
yourHealth -= 5;
yourTurn = false;
}
public void Update()
{
YH.text = Convert.ToString(yourHealth);
EH.text = Convert.ToString(enemyHealth);
if (yourTurn == false)
{
button1.interactable = false;
StartCoroutine(Wait(2));
}
else
{
button1.interactable = true;
}
}
public void EnemyTurn()
{
int roll = Random.Range(1 , 7);
switch (roll)
{
case 1:
yourHealth -= 10;
break;
case 2:
enemyHealth -= 10;
break;
case 3:
yourHealth -= 30;
break;
case 4:
yourHealth += 5;
break;
case 5:
break;
case 6:
enemyHealth += 10;
break;
}
yourTurn = true;
}
IEnumerator Wait(float time)
{
yield return new WaitForSecondsRealtime(time);
EnemyTurn();
}
}
I want EnemyTurn to be called only once after the delay. But instead it runs a bunch of times. I hope I can get a solution to this quickly because it has just brought a halt to my project and I can't figure it out myself.

That's because you're creating a Wait Coroutine on every Update call between the player's end and the enemy's turn. You need a way to only create it once per player turn end.
One way is adding a flag that you set when you create the Wait Coroutine and then unset it when the enemy's turn is done:
// If this is true, we are already waiting
// and don't want more Wait coroutines (yet)
private bool waiting;
void Start()
{
button1.onClick.AddListener(Heal20);
button2.onClick.AddListener(Damage40);
YH.text = Convert.ToString(yourHealth);
EH.text = Convert.ToString(enemyHealth);
waiting = false;
}
// ...
if (yourTurn == false)
{
if (!waiting)
{
// we only want to do this stuff the first frame we start waiting
button1.interactable = false;
waiting = true;
StartCoroutine(Wait(2));
}
}
else
{
button1.interactable = true;
}
// ...
IEnumerator Wait(float time)
{
yield return new WaitForSecondsRealtime(time);
EnemyTurn();
waiting = false;
}

Related

Cool down function for skill button in mobile game

I want to include a cool down function to my skill button in my mobile game that I am currently developing. So I wanted to allow the player to only be able to use the skill button once every few seconds.
Left button is my default skill button and right one is a duplicate. The default skill button will be placed over the duplicate so when I run the game, upon clicking on the default skill button, the duplicate will overlap the default skill button.
However, in my case the duplicate is not able to overlap the default skill button so it does not show the cool down timer.
I am wondering if I need to include a set of codes to allow the default skill button to turn inactive upon clicking or do I just have to sort the layers?
My current codes are as such:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Abilities : MonoBehaviour
{
public Image abilityImage1;
public float cooldown = 5;
bool isCooldown = false;
public Button ability1;
void Start()
{
abilityImage1.fillAmount = 0;
ability1.onClick.AddListener(AbilityUsed);
}
private void AbilityUsed()
{
if (isCooldown)
return;
isCooldown = true;
abilityImage1.fillAmount = 1;
StartCoroutine(LerpCooldownValue());
}
private IEnumerator LerpCooldownValue()
{
float currentTime = 0;
while (currentTime < cooldown)
{
abilityImage1.fillAmount = Mathf.Lerp(1, 0, currentTime /
cooldown);
currentTime += Time.deltaTime;
yield return null;
}
abilityImage1.fillAmount = 0;
isCooldown = false;
}
}
The first picture is my original skill button and on the bottom is a duplicate which I made to adjust the color to create an overlay effect for the cool down.
Thanks!
Use Lerp instead of changing fill amount in Update() method
void Start()
{
abilityImage1.fillAmount = 0;
ability1.onClick.AddListener(AbilityUsed);
}
private void AbilityUsed()
{
if (isCooldown)
return;
isCooldown = true;
abilityImage1.fillAmount = 1;
StartCoroutine(LerpCooldownValue());
}
private IEnumerator LerpCooldownValue()
{
float currentTime = 0;
while (currentTime < cooldown)
{
abilityImage1.fillAmount = Mathf.Lerp(1, 0, currentTime / cooldown);
currentTime += Time.deltaTime;
yield return null;
}
abilityImage1.fillAmount = 0;
isCooldown = false;
}

triggerzone reads the same object every frame

Trigger-zone reads the same object every frame, it is not supposed to do that and it is messing up the game. It does not happen all the time and I don't know why
Sorry for the messy code, I am not too organized.
There are not many comments (if any) I always forget to put them in.
I am just typing now to try to get it to let me post
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class spacereader : MonoBehaviour
{
public int spacesmoved = 0, jiff;
void Start()
{
}
void Update()
{
sharedscript.nummoved = spacesmoved;
if(sharedscript.reset)
{
spacesmoved = 0;
jiff = 0;
}
}
private void OnTriggerEnter(Collider other)
{
Debug.Log(other.tag);
Debug.Log(other.name);
StartCoroutine(spotchecker(other));
if (jiff != 2)
jiff++;
else if (sharedscript.ismoving)
{
spacesmoved++;
}
}
private IEnumerator spotchecker(Collider other)
{
switch (other.tag)
{
case "GO":
break;
case "MEDAVE":
break;
case "CHEST":
break;
case "BALAVE":
break;
case "INCOME":
break;
case "READING":
break;
case "ORIENTAL AVE":
break;
case "CHANCE":
break;
case "VERMONT":
break;
case "CONNAVE":
break;
case "INJAIL":
break;
case "CHARPLACE":
break;
case "COMPANY":
break;
case "STATESAVE":
break;
case "VIRGAVE":
break;
case "PENNRAIL":
break;
case "JAMESPLACE":
break;
case "TENNAVE":
break;
case "NEWYORKAVE":
break;
case "FREEPARK":
break;
case "KENAVE":
break;
case "INDIAVE":
break;
case "ILLAVE":
break;
case "BORAIL":
break;
case "ATLAAVE":
break;
case "VENTAVE":
break;
case "WATERWORKS":
break;
case "MARVGARD":
break;
case "GOTOJAIL":
break;
case "PACAVE":
break;
case "NORTHAVE":
break;
case "PENNAVE":
break;
case "SHORTLINE":
break;
case "PARKPLACE":
break;
case "BOARDWALK":
break;
case "LUXTAX":
break;
}
yield return other;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class playercontroller : MonoBehaviour
{
public List<GameObject> targets;
public float speed, step;
bool canmove = false;
public List<Camera> cameras;
public Camera diecam;
public List<GameObject> players;
public List<GameObject> playerreaders;
public int[] turntarget = new int[] { 0, 0, 0, 0 };
public int turn = 1;
int lastturn = 0;
bool switchit = true;
Coroutine inin;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(canmove)
{
if(!switchit || sharedscript.doubles)
{
foreach(GameObject s in playerreaders)
{
s.SetActive(false);
}
foreach (Camera s in cameras)
{
s.gameObject.SetActive(false);
}
foreach(GameObject s in players)
{
Collider g = s.GetComponent<Collider>();
g.enabled = false;
Rigidbody w = s.GetComponent<Rigidbody>();
w.isKinematic = true;
}
lastturn = turn;
switchit = true;
}
Collider d = players[turn].GetComponent<Collider>();
d.enabled = true;
Rigidbody f = players[turn].GetComponent<Rigidbody>();
f.isKinematic = false;
playerreaders[turn].SetActive(true);
cameras[turn].gameObject.SetActive(true);
diecam.gameObject.SetActive(false);
step = speed * Time.deltaTime;
if (players[turn].transform.position.x <= targets[turntarget[turn]].transform.position.x + .003 && players[turn].transform.position.x >= targets[turntarget[turn]].transform.position.x - .003 && players[turn].transform.position.z <= targets[turntarget[turn]].transform.position.z + .03 && players[turn].transform.position.z >= targets[turntarget[turn]].transform.position.z - .03)
{
if (turntarget[turn] < 3)
{
turntarget[turn] ++;
players[turn].transform.Rotate(0, 90, 0);
}
else
{
turntarget[turn] = 0;
}
}
players[turn].transform.position = Vector3.MoveTowards(players[turn].transform.position, targets[turntarget[turn]].transform.position, speed);
}
if(sharedscript.nummoved >= sharedscript.total)
{
canmove = false;
sharedscript.ismoving = false;
}
else
{
canmove = true;
sharedscript.ismoving = true;
}
if(sharedscript.nummoved >= sharedscript.total && sharedscript.total != 0 && sharedscript.nummoved != 0 && !sharedscript.doubles && switchit)
{
if (turn < 3)
turn++;
else
turn = 0;
sharedscript.reset = true;
switchit = false;
Debug.Log("playedturn");
sharedscript.canroll = true;
}
}
}
You are starting your coroutine whenever something enters the trigger collider. Try checking if the object that entered is the object you are looking for. If it is your player and the object is tagged "Player" you could check for that:
private void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Player"))
{
StartCoroutine(YourCoroutine());
}
}
Another reason could be Unity collision detection. It can be wonky at times so most of the time you are better off checking if the objects collided already.
One way to solve this issue using coroutines in particular would be to store the coroutine in a variable in your script and check if it already ran. Something like this:
private Coroutine coroutine;
private void OnTriggerEnter(Collider other)
{
if(coroutine == null)
{
coroutine = StartCoroutine(YourCoroutine());
}
}
Then set the value to null in OnTriggerExit or something:
private void OnTriggerExit(Collider other)
{
coroutine = null;
}
I would recommend to also check for the tag in this implementation:
private Coroutine coroutine;
private void OnTriggerEnter(Collider other)
{
if(coroutine == null && other.CompareTag("Player"))
{
coroutine = StartCoroutine(YourCoroutine());
}
}

How to add a resume countdown to a game in unity?

So I am making a 2D app in Unity similar to any "dodge the falling objects". I have added a pause function, but I want to add a countdown timer after you exit the pause menu. What I have right now is something like this:
public void Pressed () {
if (Time.timeScale == 0) {
pauseMenu.SetActive(false);
countdown.text = "3";
yield WaitForSeconds(1);
// repeat for 2 and 1
Time.timeScale = currentTimeScale;
} else {
Time.timeScale = 0;
pauseMenu.SetActive(true);
}
}
Any suggestions on what the proper way to code this particular countdown is, or how to just fix mine in general would be very helpful.
Thanks!
-Brandon
There are already many implementations of count down timers. For example, see this and this.
The general idea is to use a int field to save the remaining time. Or use yield WaitForSeconds(1) inside a coroutine to save the state.
using UnityEngine;
using System.Collections;
public class WaitForSecondsExample : MonoBehaviour {
void Start() {
StartCoroutine(CountDown());
}
IEnumerator CountDown() {
countdown.text = "3";
yield return new WaitForSeconds(1);
countdown.text = "2";
yield return new WaitForSeconds(1);
countdown.text = "1";
yield return new WaitForSeconds(1);
//time up, now resume the app
}
}
UPDATE
As for how to pause the app, see this and this.
Time.realtimeSinceStartup returns the time since startup, not affected by Time.timeScale.
public static class CoroutineUtilities
{
public static IEnumerator WaitForRealTime(float delay){
while(true){
float pauseEndTime = Time.realtimeSinceStartup + delay;
while (Time.realtimeSinceStartup < pauseEndTime){
yield return 0;
}
break;
}
}
}
Usabilty:
yield return StartCoroutine(CoroutineUtilities.WaitForRealTime(1));
Then we use yield return StartCoroutine(CoroutineUtilities.WaitForRealTime(1)) instead of yield return new WaitForSeconds(1) in the code above:
using UnityEngine;
using System.Collections;
public class WaitForSecondsExample : MonoBehaviour {
void Start() {
StartCoroutine(CountDown());
}
IEnumerator CountDown() {
countdown.text = "3";
yield return StartCoroutine(CoroutineUtilities.WaitForRealTime(1));
countdown.text = "2";
yield return StartCoroutine(CoroutineUtilities.WaitForRealTime(1));
countdown.text = "1";
yield return StartCoroutine(CoroutineUtilities.WaitForRealTime(1));
//time up, now resume the app
}
}

Showing a Text layer for a short amount of time

i've got a player and an enemy. When i rightclick the enemy his HP goes down and a hitcounter goes up. I want to make it like when you hit the enemy the text label becomes visible and when you stop attacking it stays visible for a couple more seconds and then hides and sets the hitcounter back to 0.
This is what i have at the moment.
public Text GUIHit;
public int HitCounter = 0;
void OnMouseOver()
{
if (Input.GetMouseButtonDown(1))
{
HitCounter++;
StartCoroutine(ShowHitCounter(HitCounter.ToString(), 2));
}
}
IEnumerator ShowHitCounter(string message, float delay)
{
GUIHit.text = message;
GUIHit.enabled = true;
yield return new WaitForSeconds(delay);
HitCounter = 0;
GUIHit.enabled = false;
}
What happens is that it works for 2 seconds, but even when im still attacking it goes invisible and the hit counter goes back to 0, the coroutine does not get reset back to a starting point.
Lets analyze your code:
void OnMouseOver()
{
if (Input.GetMouseButtonDown(1)) //you get passed that if when you hit first time
{
HitCounter++;
StartCoroutine(ShowHitCounter(HitCounter.ToString(), 2)); //you call your label with delay of 2 sec
}
}
IEnumerator ShowHitCounter(string message, float delay)
{
GUIHit.text = message;
GUIHit.enabled = true;
yield return new WaitForSeconds(delay); // still on your first hit you get to here and wait 2 seconds
HitCounter = 0; //after 2 seconds you reset hitcounter and disable label
GUIHit.enabled = false;
}
To fix it you need to know when you stopped hitting, and then reset hitcounter and disable label.
I would change showhitcounter to below:
IEnumerator ShowHitCounter(string message)
{
GUIHit.text = message;
GUIHit.enabled = true;
}
void ClearLabel()
{
HitCounter = 0;
GUIHit.enabled = false;
}
}
I made clearLabel to have separate method that clears label. Your logic will have to be in different places and call this method.
One place would onmouseleave event.
Other place would be in your onmouseover and added a property
public static DateTime TimeLeft { get; set; }
void OnMouseOver()
{
TimeSpan span = DateTime.Now - TimeLeft;
int ms = (int)span.TotalMilliseconds;
if (ms > 2000)
{
ClearLabel();
}
if (Input.GetMouseButtonDown(1))
{
HitCounter++;
StartCoroutine(ShowHitCounter(HitCounter.ToString(), 2));
}
}
Also you need to initialize TimeLeft somewhere before
Just finished with my solution and realized there is an answer already. Can't discard it. Just putting it as a solution with no memory allocation.
You don't need to start Coroutine each time right mouse is clicked like you did in the code in your question. I say this because of constant memory allocation when StartCoroutine() is called after each mouse click. Timer in the code below is based on frame-rate but can be easily changed to real-time by using DateTime.Now. You can also put the code in a while loop in a Coroutine then call it once from Start function.
public Text GUIHit;
public int HitCounter = 0;
bool firstRun = true;
float waitTimeBeforeDisabling = 2f;
float timer = 0;
void Update()
{
//Check when Button is Pressed
if (Input.GetMouseButtonDown(1))
{
//Reset Timer each time there is a right click
timer = 0;
if (!firstRun)
{
firstRun = true;
GUIHit.enabled = true;
}
HitCounter++;
GUIHit.text = HitCounter.ToString();
}
//Button is not pressed
else
{
//Increement timer if Button is not pressed and timer < waitTimeBeforeDisabling
if (timer < waitTimeBeforeDisabling)
{
timer += Time.deltaTime;
}
//Timer has reached value to Disable Text
else
{
if (firstRun)
{
firstRun = false;
GUIHit.text = HitCounter.ToString();
HitCounter = 0;
GUIHit.enabled = false;
}
}
}
}
Awh, okay then, here's another concept, just for the sake of it :)Did not test it and such so handle with care, but the thing is, starting a coroutine, etc looks too much (and too expensive) for me for something as little as what you want.
private float holdOutTime = 2.0f;
private float lastHitTime = 0.0f;
void OnMouseOver() {
if (Input.GetMouseButtonDown(1)) { IncHitAndShowUI() } //compacted
}
private void Update() {
if (GUIHit.enabled) { TestAndDisableHitUI(); } //compacted
}
#region priv/helper methods
//would force it inline if it was possible in Unity :)
private void IncHitAndShowUI() {
HitCounter++;
lastHitTime = Time.time;
GUIHit.text = HitCounter.ToString();
GUIHit.enabled = true;
}
//same here :)
private void TestAndDisableHitUI() {
if (lastHitTime + holdOutTime >= Time.time) {
GUIHit.enabled = false;
}
}
#endregion

My Unity Script Wont work?

Hi I have a script attached to the main camera, and in this script I want to choose a number between 0 to 5 . And depending on what number I get, I want a script to run. Hers my script that is attached to the main camera. I keep getting this error
NullReferenceException: Object reference not set to an instance of an object RandomFunction.Start () (at Assets/Resources/Scripts/RandomFunction.cs:22)
using UnityEngine;
using System.Collections;
public class RandomFunction : MonoBehaviour {
int n;
void Awake()
{
GetComponent<RandomFunction> ().enabled = true;
}
void Start ()
{
n=Random.Range(0,5);
if(n==0)
{
GetComponent<BlueGoUp> ().enabled = true;
}
else if(n==1)
{
GetComponent<RedGoUp> ().enabled = true;
}
else if(n==2)
{
GetComponent<GreenGoUp> ().enabled = true;
}
else if(n==3)
{
GetComponent<OrangeGoUp> ().enabled = true;
}
else if(n==4)
{
GetComponent<YellowGoUp> ().enabled = true;
}
else if(n==5)
{
GetComponent<PurpleGoUp> ().enabled = true;
}
}
}
Remove Awake() method because it is really unnecessary to active RandomFunction script from here. Active that in inspector.
I'm not sure if use component like this was correct. I always use gameObject.GetComponent
if(n==5) will never be true because Random.Range(0,5) return 0,1,2,3 or 4 .
from the document:
Range(int min, int max), Returns a random integer number between min [inclusive] and max [exclusive] (Read Only).
Use switch case statement instead of If else
Your final code should look like this:
using UnityEngine;
using System.Collections;
public class RandomFunction : MonoBehaviour {
int n;
void Start ()
{
n=Random.Range(0,6);
switch (n)
{
case 0:
gameobject.GetComponent<BlueGoUp> ().enabled = true;
break;
case 1:
gameObject.GetComponent<RedGoUp> ().enabled = true;
break;
case 2:
gameObject.GetComponent<GreenGoUp> ().enabled = true;
break;
case 3:
gameObject.GetComponent<OrangeGoUp> ().enabled = true;
break;
case 4:
gameObject.GetComponent<YellowGoUp> ().enabled = true;
break;
case 5:
gameObject.GetComponent<PurpleGoUp> ().enabled = true;
break;
default:
break;
}
}
}
So first of all, you don't need to do what you're doing in the Awake function because:
The MonoBehaviour object being used is the one attached to the GameObject. If you don't get what I mean, supposed you attached RandomFunction to a CameraObj. Now, that instance of RandomFunction (this pointer in script) is equal to or is same as CameraObj.GetComponent<RandomFunction>(), since you can't add more than one of the same component to a GameObject.
The Awake() method won't be executed till the object is actually enabled. Thus you won't need to re-enable the object in the script, because if it is executing, the object is enabled already.
Remove the Awake method and it should also remove the NullReferenceException. If it still doesn't, make sure that the same object (which has the RandomFunction component) also has the components BlueGoUp, RedGoUp, YellowGoUp, GreenGoUp, OrangeGoUp and PurpleGoUp.
Oh, and your if (n==5) condition will never be true, since 5 will never be returned by Random.Range(). The max parameter is always exclusive for integers in the Random.Range() function, thus the value returned will always be min <= value < max.
Most probably and I am hoping that all the GoUp scripts are not attached to the Camera. So find script in another way. Try to replace your code by this,
using UnityEngine;
using System.Collections;
public class RandomFunction : MonoBehaviour {
int n;
void Awake()
{
GetComponent<RandomFunction> ().enabled = true;
}
void Start ()
{
n=Random.Range(0,6);
switch(n){
case 0:
FindObjectOfType<BlueGoUp>().enabled = true;
break;
case 1:
FindObjectOfType<RedGoUp>().enabled = true;
break;
case 2:
FindObjectOfType<GreenGoUp>().enabled = true;
break;
case 3:
FindObjectOfType<OrangeGoUp>().enabled = true;
break;
case 4:
FindObjectOfType<YellowGoUp>().enabled = true;
break;
case 5:
FindObjectOfType<PurpleGoUp>().enabled = true;
break;
}
}
}
If this code will not work then there is no way other than that your scripts are not dropped on any gameObjects.

Categories