Clarification using closures in IEnumerator coroutines - c#

I have 2 IEnumerator coroutines that are similar enough that they should be combined:
A:
IEnumerator CountDown_A() {
timeLeft_A = totalTime_A;
while (timeLeft_A > 0) {
yield return new WaitForSeconds(1);
timeLeft_A--;
}
}
B:
IEnumerator CountDown_B() {
timeLeft_B = totalTime_B;
while (timeLeft_B > 0) {
yield return new WaitForSeconds(1);
timeLeft_B--;
}
}
The only 2 differences are the variables totalTime_A vs totalTime_B and timeLeft_A vs timeLeft_B. These vars are from outside the scope of this function.
The problem I'm having modularizing this coroutine is that the incremented value of timeLeft_A and timeLeft_B needs to apply outside this function, so I need to pass a reference to them somehow.
User "Kurt-Dekker" posted a great solution in this thread but I'm having trouble applying it to my code. He says to "use a closure (functor) to allow the coroutine to modify it by callback":
IEnumerator CountDown( System.Action<int> callback){
....
}
which I think would be called like so:
StartCoroutine ( CountDown( (i) => { timeLeft_A = i; } ) );
StartCoroutine ( CountDown( (i) => { timeLeft_B = i; } ) );
What I don't understand is how to then reference/modify the actual value of the int being passed in, inside the IEnumerator, if all I have to work with is a callback function. For example to do the following:
while(callback > 0){
or:
callback--;
Any help appreciated.

I think this may answer your question, on how to use the System.Action<float> within a coroutine.
Basicaly, when you call StartCoroutine you give the maximum time for you counter as a normal parameter and a callback, this callback takes a float as argument (here it is i).
If you call callback(--timeLeft); in your coroutine, it will execute the System.Action you passed in.
Here it will set the timeLeft_A or timeLeft_B to the timeLeft variable of the corresponding coroutine.
public float timeLeft_A = 0f;
public float timeLeft_B = 0f;
public float totalTime_A = 15f;
public float totalTime_B = 20f;
// Start is called before the first frame update
void Start()
{
StartCoroutine(Cooldown(totalTime_A, (i) =>
{
timeLeft_A = i;
}));
StartCoroutine(Cooldown(totalTime_B, (i) =>
{
timeLeft_B = i;
}));
}
IEnumerator Cooldown(float totalTime, Action<float> callback)
{
var timeLeft = totalTime;
while (timeLeft > 0)
{
yield return new WaitForSeconds(1);
callback(--timeLeft);
}
}

Just putting my implementation here for posterity, based off Antoine Thiry's answer.
private static int totalTime_A = 45; // Num seconds
private int timeLeft_A = totalTime_A;
private static int totalTime_B = 30; // Num seconds
private int timeLeft_B = totalTime_B;
// Pass in totalTime separately from the callback (totalTime doesn't need to change)
IEnumerator CountDown(int totalTime, Action<int> callback){
int timeLeft = totalTime; // Create new local var here to operate on it
while (timeLeft > 0) {
yield return new WaitForSeconds(1);
callback(--timeLeft); // Then put the local var into callback after operating on it
}
}
CountDown() called like so:
// timeLeft_A and timeLeft_B change globally
IEnumerator Countdown_A = CountDown(totalTime_A, (i) => { timeLeft_A = i; });
IEnumerator Countdown_B = CountDown(totalTime_B, (i) => { timeLeft_B = i; });
StartCoroutine(Countdown_A);
StopCoroutine(Countdown_A);
StartCoroutine(Countdown_B);
StopCoroutine(Countdown_B);

Related

How to activate and deactivate GameObjects with a delay in Untiy

I am working on a game, that has a keypad puzzle. A specific key combination lights up one by one, which the player must repeat to solve that puzzle. I am going to let the player know what the combination is by activating and deactivating some GameObjects systematically, one by one. As it suggests, there is some time delay between the deactivation of one GameObject and the activation of another. The problem is, in my code, all the GameObjects activate simultaneously instead of one by one, after a delay.
Here is the code:
public string Generate(int length, float delay)
{
// Variables for logic
string combination = "";
int prev = -1; int current = 0;
int rnd = 0;
for (int i = 0; i < length; i++)
{
rnd = Random.Range(0, BUTTONS);
while (rnd == prev)
{
rnd = Random.Range(0, BUTTONS);
}
prev = current;
current = rnd;
combination += current.ToString();
// Activation and Deactivation
StartCoroutine(GenerateDelay(delay, current));
}
return combination;
}
IEnumerator GenerateDelay(float delay, int index)
{
ButtonClicks[index].SetActive(true);
yield return new WaitForSeconds(delay);
ButtonClicks[index].SetActive(false);
}
The loop counter specifies the length of the combination. I believe there is something wrong with the Coroutine I made? Since all the objects activate simultaneously.
Here is the result in the game as well:
We can see, only one button should turn green at a time, but all of them do in this case. Any solutions?
You start all your Coroutines parallel so things happen at the same time.
StartCoroutine does not delay the method which calls it (unless it is yielded as well.
You would need to run the entire loop within a Coroutine in order to delay it as well.
You could simply split up the creation of the combination and while you already return it you start the visualization in the background in parallel
public string Generate(int length, float delay, Action<string> onCombination)
{
// Variables for logic
var combination = List<int>();
var prev = -1;
for (var i = 0; i < length; i++)
{
int rnd;
do
{
rnd = Random.Range(0, BUTTONS);
}
while (rnd == prev);
prev = rnd;
combination.Add(rnd);
}
StartCorouine (ShowCombination(combination, delay));
return string.Join("", combination);
}
private IEnumerator ShowCombination (IEnumerable<int> combination, float delay)
{
foreach(var i in combination)
{
ButtonClicks[i].SetActive(true);
yield return new WaitForSeconds(delay);
ButtonClicks[i].SetActive(false);
}
}
something alot easier then a coroutine is invoking a function, basically create a function to activate/deactivate the wanted object and whenever you want to call it do:
Invoke("FUNCTIONNAME", TIME);
and it will run the function specified after the TIME.

Create a class/method for a time (start, reset, stop, get istant, get timerun)

I'm building a racing game and I'm working on race times.
I try to build a system to start an instance of a timer with various options.
My little experience is putting me in crisis ... would some good soul want to help me?
This was the idea:
public class Timer {
public float counter;
public bool reset;
public string runtime = "--:--:--";
public string istant = "not istant";
public void startTimer()
{
/* inupdatealternative: counter += Time.deltaTime; */
if(reset == true)
{
counter = 0;
}
else
{
counter = Time.time;
}
var minutes = counter/60; // divide guitime by sixty (minutes)
var seconds = counter%60; // euclidean division (seconds)
var fraction = (counter * 100) % 100; // get fraction of seconds
runtime = string.Format ( "{0:00}:{1:00}:{2:000}", minutes, seconds, fraction);
Debug.Log("in Start: "+runtime);
}
public void resetTimer()
{
reset = true;
}
public string getTimerRuntime()
{
return runtime;
}
public string getTimerIstant()
{
istant = runtime;
return istant;
}
}
in update, for exemple:
var lapTimer = new Timer(); // create a new timer
if(Lap < Pilot.pilotlap )
{
lapTimer.startTimer();
Lap++
}
else if(Lap==Pilot.pilotlap)
{
timerLabel.text = lapTimer.getTimerIstant();
lapTimer.resetTimer();
lapTimer.startTimer();
}
in my head I'm sure someone has already dealt with it ... surely there will be something that manages the times and returns values ​​in various ways: does it exist? or is there anyway how to make or build such a thing?
There is, it's called Stopwatch, it's THE class used in C# to use precise timers, and it's located in the System.Diagnostics namespace.
Using your Update() example, you can use it like this:
// Create a new stopwatch instance
// If the timer is used repeatedly, just instantiate one at start and re-use the same,
// to avoid garbage generation
Stopwatch lapTimer = new Stopwatch();
if(Lap < Pilot.pilotlap )
{
lapTimer.Start();
Lap++
}
else if(Lap==Pilot.pilotlap)
{
lapTimer.Stop();
// ElapsedMilliseconds returns exactly what it says, so you may need to format the value
// before passing it to the timerLabel.text
timerLabel.text = lapTimer.ElapsedMilliseconds.ToString();
lapTimer.Reset();
lapTimer.Start();
}
You can read about the class (its methods, fields and properties) here:
Stopwatch Class Documentation
You are doing a lot of unnecessary bool and local fields copiing and setting there. I would simply use something like
public class Timer
{
private float _startTime;
public bool IsRunning;
// you don't need an extra reset method
// simply pass it as a parameter
public void Start(bool reset = false)
{
if(IsRunning && !reset)
{
Debug.LogWarning("Timer is already running! If you wanted to restart consider passing true as parameter.");
return;
}
_startTime = Time.time;
Debug.Log("in Start: " + GetFormattedTime(_startTime));
IsRunning = true;
}
// depending what stop should do
// since this doesn't use any resources while running you could also simply
// only stick to the Start method and pass in true .. does basically the same
public void Stop()
{
IsRunning = false;
}
// I didn't see any difference between you two methods so I would simply use
public string GetCurrentTime()
{
if(!IsRunning)
{
Debug.LogWarning("Trying to get a time from a Timer that isn't running!");
return "--:--:---";
}
var timeDifference = Time.time - _startTime;
return GetFormattedTime(timeDifference);
}
private static string GetFormattedTime(float time)
{
// e.g. time = 74.6753
var minutes = Mathf.FloorToInt(time / 60f); // e.g. 1 (rounded down)
var seconds = Mathf.FloorToInt(time - 60f * minutes); // e.g. 14 (rounded down)
var fraction = Mathf.RoundToInt((time - seconds) * 1000f); // e.g. 676 (rounded down or up)
// Use a string interpolation for better readability
return $"{minutes:00}:{seconds:00}:{fraction:000}";
}
}
then in your Update you don't want to use
var lapTimer = new Timer(); // create a new timer
all the time since it would create a new timer and you wouldn't get any tracked time ... you rather would use it only once like
private Timer timer;
// just in case you want to keep track of needed times per lap
public List<string> lapTimes = new List<string>();
private void Awake()
{
timer = new Timer();
lapTimes.Clear();
}
private void Update()
{
...
if(Lap < Pilot.pilotlap)
{
timer.Start();
Lap++
}
else if(Lap == Pilot.pilotlap)
{
var currentTime = timer.GetCurrentTime();
timerLabel.text = currentTime;
lapTimes.Add(currentTime);
timer.Start(true)
}
...
}
Note that I don't know if this is all you have in Update or how you use it but you probably also do not want to (re)start the timer and count up the Lap every frame your conditions are true ... there should be more checks involved to make sure this can only be called once per lap ...

How to prevent same object from being picked from array twice in a row

I am having a bit of a problem figuring out how to randomly get an object out of a list that wasn't picked on the last update of a script. When this randomly instantiated object is spawned and reaches a certain y value, it will set itself to inactive. So when this object is not active, it will go through an array and pick another object at random. However, I do not want to include the previous active object.
example: blue ball was first active. Moves on the Y axis. Becomes inactive. Next object spawn should have no chance of being a blue ball. Any ideas and help will be greatly appreciated.
My code is below
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ballGeneratorShooter : MonoBehaviour
{
private int ballSelector;
private TagNumber theTagNumber;
public ObjectPooler[] theObjectballPools;//In my inspector, I have 5 prefab gameObjects attached
List<ObjectPooler> changingBalls;
public bool changeBalls;
public GameObject currentBall;
public GameObject newBall;
// Use this for initialization
void Start()
{
changingBalls = new List<ObjectPooler>();
currentBall = newBall;
}
// Update is called once per frame
void Update()
{
if (newBall == null)
{
ballSelector = Random.Range(0, theObjectballPools.Length);
newBall = theObjectballPools[ballSelector].GetPooledObject();
newBall.transform.position = transform.position;
newBall.transform.rotation = transform.rotation;
newBall.SetActive(true);
}
if (newBall.activeInHierarchy == false)
{
if (changeBalls)
{
for (int i = 0; i < theObjectballPools.Length; i++)
{
if (theObjectballPools[i].GetPooledObject().GetComponent<TagNumber>().tag != currentBall.GetComponent<TagNumber>().tag)
{
changingBalls.Add(theObjectballPools[i]);
}
//changingballs.Add(theObjectballPools[i]);
}
ballSelector = Random.Range(0, changingBalls.Count);
newBall = theObjectballPools[ballSelector].GetPooledObject();
Debug.Log(changingBalls.Count);
newBall.transform.position = transform.position;
newBall.transform.rotation = transform.rotation;
newBall.SetActive(true);
currentBall = newBall;
changeBalls = false;
changingBalls.Clear();
}
}
}
}
You need to store the random number to variable (lastRandomNum) each time you generate a random number. Now, use the function below that can generate a random number with exclusion.
int RandomWithExclusion(int min, int max, int exclusion)
{
var result = UnityEngine.Random.Range(min, max - 1);
return (result < exclusion) ? result : result + 1;
}
You pass in 0 to min, then theObjectballPools.Length or changingBalls.Count to the max, then finally, lastRandomNum value to the exclusion parameter.
You also need a boolean variable to determine if this is the first run. If this is the first run, use the Unity's Random.Range function then set the firstRun to false. If it is not the firstRun, use the RandomWithExclusion function from this answer and pass in the lastRandomNum value to exclude it. Also, store the generated random number to the lastRandomNum variable to be used next frame.
Below is a simplified version of what I said above. You have to incorporate that to your code.
GameObject[] yourItem = null;
bool firstRun = true;
int lastRandomNum = 0;
void Update()
{
if (firstRun)
{
firstRun = false;
//First run, use Random.Range
lastRandomNum = UnityEngine.Random.Range(0, yourItem.Length);
}
else
{
//Not First run, use RandomWithExclusion
lastRandomNum = RandomWithExclusion(0, yourItem.Length, lastRandomNum);
}
//Do something with the lastRandomNum value below
newBall = theObjectballPools[lastRandomNum].GetPooledObject();
}
int RandomWithExclusion(int min, int max, int exclusion)
{
var result = UnityEngine.Random.Range(min, max - 1);
return (result < exclusion) ? result : result + 1;
}
Try linq :
public class Ball
{
public static List<Ball> balls = new List<Ball>();
public int value { get; set; }
public Boolean active { get; set; }
public Ball() {}
public Ball(int size)
{
// initial class
Random rand = new Random();
for (int i = 0; i < size; i++)
{
balls.Add(new Ball(){ value = rand.Next(), active = false});
}
}
public Ball GetRandom()
{
Random rand = new Random();
Ball randomBall = balls.Where(x => x.active == false).Select((x) => new { value = x, randX = rand.Next() }).OrderBy(x => x.randX).FirstOrDefault().value;
randomBall.active = true;
return randomBall;
}
}

How to make coroutines run in order?

This is using Unity3D. I have three coroutines: GetJSONFromSelectedSubreddit(), LoadMoreMemes(), and a function in a separate script that needs to be able to access the array of memes through the GetNewMemes() function (must return type Meme[]). LoadNewMemes() produces. The thing is, LoadMoreMemes() requires the json to work, so they have to run in the mentioned order. If you need the functions, here they are:
public void GetNewMemes(string subReddit, int count)
{
SetSelectedSubreddit(subReddit);
memesAtATime = count;
subJSON = null;
StartCoroutine(GetJSONFromSelectedSubreddit());
StartCoroutine(LoadMoreMemes());
}
IEnumerator GetJSONFromSelectedSubreddit()
{
gettingJSON = true;
WWW requester = new WWW("https://www.reddit.com/r/" + selectedSub + "/new.json?sort=new&count=25&after=" + postIndex);
yield return requester;
subJSON = requester.text;
json = new JSONObject(subJSON);
gettingJSON = false;
}
IEnumerator LoadMoreMemes()
{
while (gettingJSON)
yield return new WaitForSeconds(0.1f);
for (int i = 0; i < memesAtATime; i++)
{
yield return StartCoroutine(GetUserPostKarma(json["data"]["children"][i]["data"]["author"].str));
string sourceURL = json["data"]["children"][i]["data"]["preview"]["images"][0]["source"]["url"].str;
sourceURL = sourceURL.Replace("&", "&");
yield return StartCoroutine(GrabImage(sourceURL));
Meme currentMeme = new Meme(
json["data"]["children"][i]["data"]["preview"]["images"][0]["source"]["url"].str,
authorPostKarma,
(int) json["data"]["children"][i]["data"]["score"].i,
json["data"]["children"][i]["data"]["permalink"].str,
json["data"]["children"][i]["data"]["title"].str,
currentBitmap
);
Debug.Log(currentMeme.cost);
memes[i] = currentMeme;
}
}
Here's the other script:
void Start ()
{
RedditCommunicator redditCommunicator = GetComponent<RedditCommunicator>();
redditCommunicator.GetNewMemes("me_irl", 1);
Meme[] memes = redditCommunicator.GetCurrentMemes();
Debug.Log(memes[0].currentScore);
redditCommunicator.SpawnOneMeme(memes[0]);
}
Each function works fine on its own, but they need to wait for each other to finish, as well as run in the correct order to work. I'd like the functions to stay separate so I can call them individually in the future. memes is a private variable, and the one I'd like to pass to the other script calling these functions. If you don't think I've tried my options Googling and solving this on my own, just believe me, I've done my best. Thanks for your help in advance. If you need more information, just ask me for it. The current state of this code is it returns memes to early, before the coroutines can finish, resulting in empty memes.
You can yield a Coroutine in an IEnumerator which will halt the progression of that Coroutine until that Coroutine is done. Like this:
void Start()
{
StartCoroutine(DoThings((text) => {
Debug.Log("Dothings told me: " + text);
}));
}
IEnumerator DoThings(Action<string>() callback)
{
yield return StartCoroutine(DoThisFirst());
callback("Returning a value mid-method!");
yield return StartCoroutine(ThenThis());
Debug.Log(3);
}
IEnumerator DoThisFirst()
{
yield return new WaitForSeconds(1);
Debug.Log(1);
}
IEnumerator ThenThis()
{
yield return new WaitForSeconds(1);
Debug.Log(2);
}
Problem is that GetJSONFromSelectedSubreddit and LoadNewMemes methods are called as two "parallel" coroutines in GetNewMemes method.
If you do not need to run a coroutine "asynchronously", you can just enumerate through it:
public void GetNewMemes(string subReddit, int count)
{
SetSelectedSubreddit(subReddit);
memesAtATime = count;
subJSON = null;
var enumerator = GetJSONFromSelectedSubreddit();
while (enumerator.MoveNext());
enumerator = LoadNewMemes();
while (enumerator.MoveNext());
}

Unity3D: Trying to make a custom Sleep/Wait function in C#

All this started yesterday when I use that code for make a snippet:
void Start() {
print("Starting " + Time.time);
StartCoroutine(WaitAndPrint(2.0F));
print("Before WaitAndPrint Finishes " + Time.time);
}
IEnumerator WaitAndPrint(float waitTime) {
yield return new WaitForSeconds(waitTime);
print("WaitAndPrint " + Time.time);
}
From: http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.StartCoroutine.html
The problem is that I want to set a static variable from a class of that type:
class example
{
private static int _var;
public static int Variable
{
get { return _var; }
set { _var = value; }
}
}
Which is the problem? The problem is that If put that variable in a parameter of a function this "temporal parameter" will be destroyed at the end of the function... So, I seached a little bit, and I remebered that:
class OutExample
{
static void Method(out int i)
{
i = 44;
}
static void Main()
{
int value;
Method(out value);
// value is now 44
}
}
From: http://msdn.microsoft.com/es-es/library/ms228503.aspx
But, (there's always a "but"), Iterators cannot have ref or out... so I decide to create my own wait function because Unity doesn't have Sleep function...
So there's my code:
Debug.Log("Showing text with 1 second of delay.");
float time = Time.time;
while(true) {
if(t < 1) {
t += Time.deltaTime;
} else {
break;
}
}
Debug.Log("Text showed with "+(Time.time-time).ToString()+" seconds of delay.");
What is the problem with that code? The problem is that It's very brute code, becuase It can produce memory leak, bugs and of course, the palayzation of the app...
So, what do you recomend me to do?
You could do something like this:
void Start()
{
print( "Starting " + Time.time );
StartCoroutine( WaitPrintAndSetValue( 2.0F, theNewValue => example.Variable = theNewValue ) );
print( "Before WaitAndPrint Finishes " + Time.time );
}
/// <summary>Wait for the specified delay, then set some integer value to 42</summary>
IEnumerator WaitPrintAndSetValue( float waitTime, Action<int> setTheNewValue )
{
yield return new WaitForSeconds( waitTime );
print( "WaitAndPrint " + Time.time );
int newValueToSet = 42;
setTheNewValue( newValueToSet );
}
If after your delay you need to both read and update a value, you could e.g. pass Func<int> readTheOldValue, Action<int> setTheNewValue and invoke with the following lambdas () => example.Variable, theNewValue => example.Variable = theNewValue
Here's more generic example:
void Delay( float waitTime, Action act )
{
StartCoroutine( DelayImpl( waitTime, act ) );
}
IEnumerator DelayImpl( float waitTime, Action act )
{
yield return new WaitForSeconds( waitTime );
act();
}
void Example()
{
Delay( 2, () => {
print( "After the 2 seconds delay" );
// Update or get anything here, however you want.
} );
}
Is this what you want?
Thread.Sleep(time)
What you're looking for here can be accomplished using Task.Delay:
void Start()
{
print("Starting " + Time.time);
WaitAndPrint(2.0F);
print("Before WaitAndPrint Finishes " + Time.time);
}
Task WaitAndPrint(float waitTime)
{
Task.Delay(TimeSpan.FromSeconds(waitTime))
.ContinueWith(t => print("WaitAndPrint " + Time.time));
}
You can then pass around lambdas/delegates, possibly closing over variables, to move data around your program.

Categories

Resources