I have met a strange problem when using Coroutine in Unity. Before modification, my code is as the following:
IEnumerator Destory()
{
yield return new WaitForSeconds(destoryDelay);
yield return StartCoroutine(Timer.Start(0.5f, false, gameManager.EnableBtnSummon));
GameObject.Destroy(this.gameObject);
}
Time.Start() is an utility written by myself and used for delay invoke.
public static IEnumerator Start(float duration, bool repeat, Action callback)
{
do
{
yield return new WaitForSeconds(duration);
if (callback != null)
callback();
} while (repeat);
}
Because Time.Start() includes WaitForSeconds(), so I decided to modify above code as the following:
IEnumerator Destory()
{
//yield return new WaitForSeconds(destoryDelay);
yield return StartCoroutine(Timer.Start(destoryDelay+0.5f, false, gameManager.EnableBtnSummon));
GameObject.Destroy(this.gameObject);
}
Unfortunately, console throw an error:
ArgumentException: Value does not fall within the expected range.
gameManager.EnableBtnSummon is just an Action processing game logic. After debug, i make sure that error occurred before this function run. But i will show it for more clues.
public void EnableBtnSummon()
{
//will not reach this!
print("Enable Button Summon");
//if detecting monster, change relative sprite of monster medal
if (currentMonsterIndex != -1)
{
Image captureMonsterSprite = monsterMedalList.transform.GetChild(currentMonsterIndex).GetComponent<Image>();
captureMonsterSprite.sprite = mosnterExplicitMedalList[currentMonsterIndex];
Image gameOverMonsterSprite = gameOverMonsterList.transform.GetChild(currentMonsterIndex).GetComponent<Image>();
gameOverMonsterSprite.sprite = mosnterExplicitMedalList[currentMonsterIndex];
currentMonsterIndex = -1;
captureMonsterCount++;
}
if (captureMonsterCount == monsterIndexDictionary.Count) return;
var summonAnimator = btnSummon.GetComponent<Animator>();
summonAnimator.SetBool("isSearch", false);
btnSummon.enabled = true;
btnExit.enabled = true;
fogParticleSystem.Play();
}
I cannot understand it, could someone tell me what happens? thx...
The exception:
ArgumentException: Value does not fall within the expected range.
is happening on this line of code:
yield return StartCoroutine(MoveTowards.Start(destoryDelay + 0.5f, false, gameManager.EnableBtnSummon));
This has nothing to do with StartCoroutine as the title of the question says. The source of the problem is the MoveTowards.Start coroutine function. The third parameter(Action callback) that is passed into it is the issue.
The issue is that you are passing null to the third parameter of the MoveTowards.Start function. Since you are passing gameManager.EnableBtnSummon to that third parameter, this means that the gameManager variable is null.
You can verify this by adding Debug.Log(gameManager) before that line of code. The output should be "null" in the Console tab.
FIX:
Initialize the gameManager variable:
Name the GameObject your GameManager script is attached to "ManagerObj" then use the simple code below to initialize the gameManager variable.
GameManager gameManager;
void Awake()
{
gameManager = GameObject.Find("ManagerObj").GetComponent<GameManager>();
}
Note:
Rename your Start function to something else as there is already Unity built in function named "Start" and "Awake". You need to change the name to something else but this is not the problem.
Related
I have two scripts. The first script gets called in the beginning as follows:
Script1.cs
private Script2 script2;
void Start () {
script2= (Script2) GameObject.FindObjectOfType (typeof (Script2));
StartCoroutine("CallTrigger");
}
IEnumerator CallTrigger() {
while(script2.hasTriggered == true){
Debug.Log("Success");
script2.hasTriggered = false;
}
yield return 0;
}
And my script 2 is as follows:
public bool hasTriggered = false;
Since my 1st script gets called first, I want the CallTrigger() function to wait till the bool in script2 is set to true. Unfortunately while() is not the right way I suppose since it is not working for me. I know the best way is to use Update() but I am using multiple instances of this script from which only some get called in the beginning.
So how do I make my CallTrigger await till the hasTriggered in script2 is set to true?
I want the CallTrigger() function to wait till the bool in script2 is set to true
You can simply use WaitUntil
IEnumerator CallTrigger()
{
yield return new WaitUntil(() => script2.hasTriggered);
Debug.Log("Success");
}
which basically equals doing something like
IEnumerator CallTrigger()
{
// As long as the flag is NOT set
while(!script2.hasTriggered)
{
// wait a frame
yield return null;
}
Debug.Log("Success");
}
You could also btw directly make it
// Yes, if Start returns IEnumerator Unity automatcally runs it as a coroutine
IEnumerator Start()
{
// Rather use the generic versions
script2 = GameObject.FindObjectOfType<Script2>();
yield return new WaitUntil(()=> script2.hasTriggered);
Debug.Log("Success");
}
I have a recursive IEnumerator which looks like this :
IEnumerator Spawn()
{
if(canSpawn)
{
Vector3 offset = new Vector3(example.transform.position.x + offsetVar, example.transform.position.y, example.transform.position.z);
Instantiate(someObject, offset,Quaternion.identity);
canSpawn = false;
yield return new WaitForSeconds(cooldown);
canSpawn = true;
StartCoroutine(Spawn());
}
}
And I use GameObject.FindGameObjectWithTag(string s) in Start() method to locate the example game object. I call this IEnumerator once with a copy of this IEnumerator which just has different variables. It works as expected but the distance between the object this IEnumerator instantiates and the another one keeps getting smaller and smaller until they both get instantiated at the same position. What could be the problem here ?
It was because that I started both of the coroutines in a method and I was calling that method in Update() method. Now I call them only once and this problem does not occur.
I'm using the IEnumerator function and have had some issues with my if statement working:
IEnumerator Spawning()
{
Debug.Log("Spawning being called...");
if (GameObject.Find("FPSController").GetComponent<BoxCollide>().hitTrigger == true)
{
Debug.Log("CUSTOMER SHOULD SPAWN!");
while (countStop == false) {
yield return new WaitForSeconds(2);
Debug.Log("Fisher Spawned!");
counter++;
spawnNewCharacter.SpawnCharacter();
if (counter >= 3){
countStop = true;
}
}
}
}
After some debugging, it turns out that my if statement actually works. This issue is actually the IEnumerator function being called as soon as I run my game. I need a way to call this IEnumerator function only when my hitTrigger == true, but I can't seem to get anything to work.
I've tried this on top of the IEnumerator function:
void Update()
{
if (GameObject.Find("FPSController").GetComponent<BoxCollide>().hitTrigger == true)
{
Debug.Log("Spawning now...");
StartCoroutine(Spawning());
}
}
But still can't even get any of the Debug.Log's to come through. Would appreciate some help on this!
Side Information
Find() and GetComponent()
You don't want to use GameObject.Find(...) in the Update method, as it's an expensive call. The Update method is called each frame, so you'd call GameObject.Find(...) 60 times in 1 second at 60fps.
So when you use GetComponent() or Find() you want to save a reference to these objects like shown in the snippets below.
Better locations to use methods like GetComponent() or GameObject.Find() are the Awake() and Start() methods.
Awake
Awake is used to initialize any variables or game state before the
game starts. Awake is called only once during the lifetime of the
script instance. Awake is called after all objects are initialized so
you can safely speak to other objects or query them using eg.
GameObject.FindWithTag. [...]
Explanation is taken from the linked documentation.
Start
Start is called on the frame when a script is enabled just before any
of the Update methods is called the first time. Like the Awake
function, Start is called exactly once in the lifetime of the script.
However, Awake is called when the script object is initialised,
regardless of whether or not the script is enabled. Start may not be
called on the same frame as Awake if the script is not enabled at
initialisation time.
Explanation is also taken from the linked documentation
Possible Solution
Add the first Component (FPSControllerCollission) onto the object that holds your FPSController.
It makes use of unities OnTriggerEnter & OnTriggerExit methods.
This script is gonna set the IsTriggered bool to true, when a trigger entered the space of the box collider.
Note: A collider acts as a trigger, when the "Is Trigger" checkbox on
the component is checked.
You can do similar with OnCollisionEnter/Exit to recognize, when a Collider enters the space of the box collider.
Note that the following is only an example and you'll have to tweak /
integrate it into your code
[RequireComponent(typeof(BoxCollider))]
public class FPSControllerCollission : MonoBehaviour {
public bool IsTriggered;
private void OnTriggerEnter(Collider other) {
this.IsTriggered = true;
}
private void OnTriggerExit(Collider other) {
//Maybe check the tag/type of the other object here
this.IsTriggered = false;
}
}
The following SpawnController class could be integrated in the class you allready have.
public class SpawnController : MonoBehaviour {
private FPSControllerCollission _fpsControllerCollission;
private void Awake() {
this._fpsControllerCollission = FindObjectOfType<FPSControllerCollission>();
}
private void Update() {
if (this._fpsControllerCollission.IsTriggered) {
StartCoroutine(nameof(Spawning));
}
}
IEnumerator Spawning() {
Debug.Log("Spawning being called...");
if (this._fpsControllerCollission == true) {
Debug.Log("CUSTOMER SHOULD SPAWN!");
bool countStop = false;
int counter;
while (countStop == false) {
yield return new WaitForSeconds(2);
Debug.Log("Fisher Spawned!");
counter++;
spawnNewCharacter.SpawnCharacter();
if (counter >= 3) {
countStop = true;
}
}
}
}
}
How can I make the name of the Coroutine dynamic?
I use this to make targets die automatically after a few seconds:
void InitiateKill(int i)
{
//i is the number of the target
StartCoroutine(TargetDie(i, timeAlive/1000));
//some other stuff
}
When the target is killed before this timer ends, I obviously get an error because it can't kill the target again.
That's why I want to stop the coroutine of that specific target, but I don't know how.
I tried:
Coroutine b[i] = StartCoroutine(TargetDie(i, timeAlive/1000));
But that gives a syntax error. b[i] can't be used for Coroutines.
How to do this the proper way?
Update:
This is (the relevant part of) my TargetDie function:
IEnumerator TargetDie(int i, float delayTime)
{
yield return new WaitForSeconds(delayTime);
Destroy(targets[i]);
}
When the player kills the target, I do:
void Damage(int i)
{
// at this time, the first Coroutine, started in InitiateKill, should stop, because otherwise it tries to destroy the target twice
StartCoroutine(TargetDie(i, 0));
}
So most simple way, move the coroutine on the object itself.
public class DieScript: MonoBehaviour
{
private Manager manager;
public void StartDeathProcess(Manager manager)
{
this.manager = manager;
StartCoroutine(DieAsync(manager));
}
private IEnumerator DieAsync(Manager manager)
{
yield return new WaitForSeconds(timer);
Destroy(this.gameObject);
}
public void Dmaage() // This is register as listener for death of the object
{
this.manager.PropagateDeath(this);
Destroy(this.gameObject);
}
}
public class Manager:MonoBehaviour
{
private List<DieScript> die;
void InitiateKill(int i)
{
die[i].StartDeathProcess(this);
}
}
Trying to keep the control on the controller is about to bring more pain than solution.
You could have a list of IEnumerator to keep track of the running coroutines. But you still need a message from the object to inform the controller that it is dead. So you are missing this part.
You need a script on the target so the controller knows about this type.
Controller runs the coroutine with a reference to that script asking each frame are you dying. When the target is meant to die, it sets a boolean to inform. Using Destroy keeps the object until end of frame so it would work out.
But this is doom to fail later on. It is kinda against programming concept to have a controller doing everything. You should see it more as a bypass for information.
You can simply check for null before destroying it so that you won't get any error:
if (targets != null)
Destroy(targets[i]);
But below is the recommended way if you want to stop the old coroutine.
You can use Dictionary to handle it. Use the int i as the key and Coroutine as the value.
When you call InitiateKill, add the int i to the dictionary and when you start the coroutine, add the Coroutine as the value in the dictionary too.
When Damage is called, check if that int value exist in the Dictionary. If it does, use it to retrieve the Coroutine value and stop the old coroutine. If it doesn't exit, start a new coroutine and then add it to that dictionary.
The dictionary should look like this:
Dictionary<int, Coroutine> dyingTarget = new Dictionary<int, Coroutine>();
Your new InitiateKill function which adds to the Dictionary:
void InitiateKill(int i)
{
//i is the number of the target
Coroutine crt = StartCoroutine(TargetDie(i, timeAlive / 1000));
//Add to Dictionary
dyingTarget.Add(i, crt);
}
Your new Damage function now checks if the item is already in the dictionary then retries it, stops the coroutine and removes it from the Dictionary.
void Damage(int i)
{
// at this time, the first Coroutine, started in InitiateKill, should stop, because otherwise it tries to destroy the target twice
StopIfAlreadyRunning(i);
Coroutine crt = StartCoroutine(TargetDie(i, 0));
//Add to Dictionary
dyingTarget.Add(i, crt);
}
void StopIfAlreadyRunning(int i)
{
Coroutine crtOut;
//Retrieve and stop old coroutine if it exist then removes it
if (dyingTarget.TryGetValue(i, out crtOut))
{
StopCoroutine(crtOut);
dyingTarget.Remove(i);
}
}
The new TargetDie function which removes it from the Dictionary after killing it. It also checks for null before destroying it:
IEnumerator TargetDie(int i, float delayTime)
{
yield return new WaitForSeconds(delayTime);
if (targets != null)
Destroy(targets[i]);
//Remove from Dictionary
dyingTarget.Remove(i);
}
I'm somewhat new to PUN and Unity so hopefully, I am making some kind of beginner mistake.
void Update(){
if(launch == true){
Debug.Log("launched");
myPhotonView.RPC("ChangeScene", PhotonTargets.All);
}
//myPhotonView.RPC("ChangeScene", PhotonTargets.All);
}
When I run the code above, I can see launched in the console but I get
"NullReferenceException: Object reference not set to an instance of an
object" on this line: myPhotonView.RPC("ChangeScene",
PhotonTargets.All);
When I run it with that line commented out and the line that's outside the if statement uncommented, there is no null reference exception and the method executes. Is there a mistake in the above code, or is this a bug in Photon?
Any help at all is much appreciated. I can post the full code or error messages if anyone thinks it's necessary.
Also, this is less important, but myPhotonView is null in any methods I create that aren't regular MonoBehaviour methods like Start or Update even though I set it as a global variable and filled it in Start before I try to access it. This seems like it might be tied to the other problems I'm having, and it's the only reason I'm using Update for this.
Edit: Here's the entire file:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class NetworkedPlayer : Photon.MonoBehaviour{
public GameObject avatar;
public PhotonView myPhotonView;
public bool launch = false;
public string destination = "";
//Responsible for avatar movements on the plane
public Transform playerGlobal;
//Responsible for headmovements
public Transform playerLocal;
public Transform playerRotation;
void Start (){
//launch is successfuly set to false
//if(!launch) Debug.LogError("banana");
Debug.Log("Instantiated");
//Necessary for photon voive to work
var temp1 = PhotonVoiceNetwork.Client;
myPhotonView = this.photonView;
//this ensures that only you can control your player
if (photonView.isMine){
Debug.Log("Controlling my avatar");
playerGlobal = GameObject.Find("OVRPlayerController").transform;
playerLocal = playerGlobal.Find("OVRCameraRig/TrackingSpace/CenterEyeAnchor/Pivot");
playerRotation = playerGlobal.Find("OVRCameraRig/TrackingSpace/CenterEyeAnchor");
this.transform.SetParent(playerLocal);
this.transform.localPosition = Vector3.zero;
this.transform.rotation = playerRotation.transform.rotation;
// avatar.SetActive(false);
//Throws null
//GetComponent<PhotonVoiceRecorder> ().enabled = true;
}
}
void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info){
//This sends your data to the other players in the same room
if (stream.isWriting){
stream.SendNext(playerGlobal.position);
stream.SendNext(playerGlobal.rotation);
stream.SendNext(playerLocal.localPosition);
stream.SendNext(playerLocal.localRotation);
}
//This recieves information from other players in the same room
else{
this.transform.position = (Vector3)stream.ReceiveNext();
this.transform.rotation = (Quaternion)stream.ReceiveNext();
avatar.transform.localPosition = (Vector3)stream.ReceiveNext();
avatar.transform.localRotation = (Quaternion)stream.ReceiveNext();
}
}
public void ChangeAndSyncScene(string dest){
Debug.LogError("Dest selected: " + dest);
launch = true;
destination = dest;
Update();
}
void Update(){
if(launch == true){
Debug.LogError("launched");
myPhotonView.RPC("ChangeScene", PhotonTargets.All);
ChangeScene();
}
myPhotonView.RPC("ChangeScene", PhotonTargets.All);
}
[PunRPC]
void ChangeScene(){
Debug.LogError("scene changed");
Application.LoadLevel (destination);
}
}
Edit: I never managed to fix this, but I did get around it. I created gameobjects to give values to other players in the game. PM me for more info