I want to be able to flash stuff at a certain frequency. For an example, let's say 2Hz. I also want to be able to specify a ratio, where I can have the thing displayed for let's say 2/3 of the cycle and have it hidden for 1/3, so the ratio would be 2:1. It's a wild bunch of flashing, so I Need to stay flexible in the way I do it. There might be some flashing with a ratio of 3:5 and a frequency of 2Hz, and some other flashing at 4Hz with ratio 1:1, and so on.
Also, I need to be able to flash in sync. So if one object is flashing already and I start flashing another one, they need to be in sync (or rather their cycles need to be in sync, the flashing may vary as the ratio may be different). But if at the same frequency, they need to "turn on" at the same time, even if their ratios are different. Also, they all need to turn on at the same time the slowest turns on.
My current approach: I have a GameObject FlashCycle, that essentially in it's update method calculates a progress for the 3 frequency's I have (2Hz, 4Hz and 8Hz).
float time = Time.time;
twoHerzProgress = (time % twoHerzInSeconds) / twoHerzInSeconds;
fourHerzProgress = (time % fourHerzInSeconds) / fourHerzInSeconds;
eightHerzProgress = (time % eightHerzInSeconds) / eightHerzInSeconds;
I have tried different times, but that didn't really matter so let's just stick to that one if you don't think it's a bad idea!
Now, whenever I want to flash an object, in it's own Update() I do this:
switch (flashRate.herz)
{
case FlashRateInterval.twoHerz:
show = flashCycle.oneHerzProgress <= onTimePercentage;
case FlashRateInterval.fourHerz:
show =flashCycle.twoHerzProgress <= onTimePercentage;
case FlashRateInterval.eightHerz:
show =flashCycle.fourHerzProgress <= onTimePercentage;
default:
show =true;
}
and then just continue and have the object displayed if show == true.
Unfortunately this doesn't flash the objects at a nice smooth and regular interval. I measured the 2Hz interval and got differences in the ratio of up to 48ms, and though it seems like not much it really makes a difference on the screen.
So the question boils down to: How can I get quick, reqular flashes while maintaining the flexibility (ratio and frequency wise) and have a syncronized flash?
Thanks for your help!
You could use Coroutines and WaitForSeconds to achieve that
// onRatio and offRatio are "optional" parameters
// If not provided, they will simply have their default value 1
IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{
float cycleDuration = 1.0f / frequency;
float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration;
while(true)
{
show = true;
yield return new WatForSeconds(onDuration);
show = false;
yield return new WatForSeconds(offDuration);
}
}
so you can call it either with a frequency e.g. 8Hz
StartCoroutine(Flash(8.0f));
this is actually equal to any call where you set onRatio = offRatio e.g.
StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 1));
StartCoroutine(Flash(8.0f, onRatio = 2, offRatio = 2));
....
or with a frequency and ratios e.g. 1(on):2(off) with 8Hz
StartCoroutine(Flash(8.0f, onRatio = 1, offRatio = 2));
With this setup the Coroutine runs "forever" in the while(true)-loop. So, don't forget before you start a new Coroutine with different parameters to first stop all routines with
StopAllCoroutines();
Now if you want to change that dynamically in an Update method, you would have to add some controll flags and additional variables in roder to make sure a new Coroutine is only called when something changed:
FlashRateInterval currentInterval;
float currentOnRatio = -1;
float currentOffRatio = -1;
void Update()
{
// if nothing changed do nothing
if(flashRate.herz == currentInterval
//todo && Mathf.Approximately(<yourOnRatio>, currentOnRatio)
//todo && Mathf.Approximately(<yourOffRatio>, currentOffRatio)
) return;
StopAllCoroutines();
currentInterval = flashRate.herz;
//todo currentOnRatio = <yourOnRatio>;
//todo currentOffRatio = <yourOffRatio>;
switch (flashRate.herz)
{
case FlashRateInterval.twoHerz:
StartCoroutine(2.0f);
//todo StartCoroutine(2.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
case FlashRateInterval.fourHerz:
StartCoroutine(4.0f);
//todo StartCoroutine(4.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
case FlashRateInterval.eightHerz:
StartCoroutine(8.0f);
//todo StartCoroutine(8.0f, onRatio = <yournRatio>, offRatio = <yourOffRatio>);
default:
show =true;
}
}
Notes:
I dont know your FlashRateInterval but if you need to use it for some reason you could make it like
public enum FlashRateInterval
{
AllwaysOn,
twoHerz = 2,
fourHerz = 4,
eightHerz = 8
}
in order to directly use the correct values.
I would call a frequency variable flashRate.herz. You also wouldn't call a size value cube.meters. I'ld recommend to rename it to flashRate.frequency.
To archieve that syncing you would somehow need access to all Behaviours and compare their values (so I'ld say some static List<YourBehavior>) and than e.g. in the Coroutine wait until all bools are e.g. set to true before continuing with your own one. For that you would need an additional bool since it is possible that show is true permanently on one component.
public bool isBlinking;
IEnumerator Flash(float frequency ,float onRatio = 1, float offRatio = 1)
{
//todo: You'll have to set this false when not blinking -> in Update
isBlinking = true;
float cycleDuration = 1.0f / frequency;
float onDuration = (onRatio/ (onRatio + offRatio)) * cycleDuration;
float offDuration = (offRatio/ (onRatio + offRatio)) * cycleDuration;
// SYNC AT START
show = false;
// wait until all show get false
foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
{
// skip checking this component
if(component == this) continue;
// if the component is not running a coroutine skip
if(!component.isBlinking) continue;
// Now wait until show gets false
while(component.show)
{
// WaitUntilEndOfFrame makes it possible
// for us to check the value again already before
// the next frame
yield return new WaitForEndOfFrame;
}
}
// => this line is reached when all show are false
// Now lets just do the same but this time wating for true
// wait until all show get false
foreach(var component in FindObjectsOfType<YOUR_COMPONENT>())
{
// skip checking this component
if(component == this) continue;
// if the component is not running a coroutine skip
if(!component.isBlinking) continue;
// Now wait until show gets false
while(!component.show)
{
// WaitUntilEndOfFrame makes it possible
// for us to check the value again already before
// the next frame
yield return new WaitForEndOfFrame;
}
}
// this line is reached when all show are getting true again => begin of loop
while(true)
{
.........
Instead of using FindObjectsOfType<YOUR_COMPONENT>() which is kind of slow you could also do something like
public static List<YOUR_COMPONENT> Components = new List<YOUR_COMPONENT>();
private void Awake()
{
if(!Components.Contains(this)){
Components.Add(this);
}
}
so you also get currently disabled components and objects
You got some diferences because you are doing everything in an Update() cycle with <= condition. On slower/faster machines you will have more/less differences because the frame's duration will never be equal to your frequency.
Try doing everything in a Corotine: unity coroutine docs
//bad code below but i think its more understandable like this
IEnumerator Flash()
{
while(true)
{
BlinkOn();
Sync();//sync here another cicle if you want to sync when on starts
yield return new WaitForSeconds(yourDuration);// yourDuration*multiplier/something+0.5f....ecc
BlinkOff()
Sync();//sync here another cicle if you want to sync when of starts
yield return new WaitForSeconds(yourDuration);
}
}
Related
I have a joystick display picture for my game. Currently, when the player touches the screen the image disappears and when the player is not touching the screen, it reappears. I wrote that using an if else statement.
if (indicator.inputIndicator.x != 0)
{
joystick.SetActive(false);
}
else
{
joystick.SetActive(true);
}
The problem is, I want the image to reappear after some time like 2 seconds. I want to delay the "else", but I do not want to use a coroutine. I want "else" to work after 2 seconds since the player takes his hand off the screen but I couldn't figure out how to do it. any help will be great.
Setting a timer is a pretty common problem you have to solve in Unity. One basic approach is to have a variable that you add Time.deltaTime every update. That way you can tell how long it has been since some condition was met.
Every Update iteration that meets the condition, add Time.deltaTime to the variable. If at some point the condition fails, reset the variable to 0. Then you can just base your joystick.SetActive() call on the value of your variable.
For example, your script might become:
float thresholdTimeToShowPrompt = 2;
// By starting at the threshold, the image is hidden at the start until a touch
float timeSincePlayerTouch = 2;
void Update()
{
// Rather than calling SetActive directly, just update the timer
if (indicator.inputIndicator.x != 0)
{
timeSincePlayerTouch = 0;
}
else
{
timeSincePlayerTouch += Time.deltaTime;
}
// Now we can base visibility on the time since the last user touch
bool shouldShowIcon = timeSincePlayerTouch >= thresholdTimeToShowPrompt;
// Only call SetActive when needed, in case of overhead
if (shouldShowIcon && !joystick.activeSelf)
{
joystick.SetActive(true);
}
else if (!shouldShowIcon && joystick.activeSelf)
{
joystick.SetActive(false);
}
}
I am coding a stamina bar in my game and have had it all hooked up, though the tutorial I followed for it was specifyed for a burst of stamina used. I was hoping I would be able to slowly deminish it until there is none left. I tried to apply a while loop hooked up to a coroutine though it freezes unity. I just want way to lose stamina consistantly while a button is held. Its unable to be put under the void update because the code to check if shift is activated only plays it once. I activate the script by using "stamina.instance.UseStamina(5);". Heres the code.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class stamina : MonoBehaviour
{
public Slider staminabar;
private int maxstamina = 100;
private int currentstamina;
public static stamina instance;
private WaitForSeconds regenTick = new WaitForSeconds(0.1f);
private Coroutine regen;
private void Awake()
{
instance = this;
}
// Start is called before the first frame update
void Start()
{
currentstamina = maxstamina;
staminabar.maxValue = maxstamina;
staminabar.value = maxstamina;
}
// Update is called once per frame
void Update()
{
}
public void UseStamina(int amount)
{
if (currentstamina - amount >= 0)
{
currentstamina -= amount;
staminabar.value = currentstamina;
if (regen != null)
StopCoroutine(regen);
regen = StartCoroutine(regenstamina());
}
else
{
Debug.Log("out of stamina!");
}
}
private IEnumerator regenstamina()
{
yield return new WaitForSeconds(2);
while (currentstamina < maxstamina)
{
currentstamina += maxstamina / 100;
staminabar.value = currentstamina;
yield return regenTick;
}
regen = null;
}
}
I was expecting with a while loop for a smooth way for the slider to go down instead it just froze with me needing to go into taskmanager to close unity. I also tried putting a small wait for seconds hoping that it was a issue with lag.
If I'm not mistaken, there's a couple of issues that I see.
Firstly, your stamina bar value should be a value between 0f and 1f. But your code has set a maxValue, which I'm not sure where that's coming from. So maybe you're using a different stamina bar? Not sure. But the default Slider uses a value between 0f and 1f.
Moving on to the second issue. You're doing divisions on integers. If you use any stamina, and your current stamina is less that the max stamina (which it almost always will be), then your code is always going to get stuck. For example, you use 1 stamina, then your calculation is 99 / 100 = 0. YHour current stamina will never be increased. That's just the way integer calculations work.
I also noted your UseStamina method reduces the stamina by the correct amount, but then your co-routine is trying to do modify the current stamina again. It's doing the caluclations twice, and only the first is the correct one.
You can cast to floats first, then back to an int. Or you could use a different measurement. For example:
public bool UseStamina ( int amount )
{
if ( currentstamina - amount <= 0 )
{
Debug.Log ( "No enough stamina!" );
return false;
}
if ( regen != null )
StopCoroutine ( regen );
currentstamina -= amount;
regen = StartCoroutine ( regenstamina ( ) );
return true;
}
private IEnumerator regenstamina ( )
{
yield return new WaitForSeconds ( 2 );
var timer = 0f;
while ( timer < 1f )
{
timer += Time.deltaTime;
staminabar.value = Mathf.Lerp ( staminabar.value, ( float ) currentstamina / maxstamina, timer );
yield return null;
}
regen = null;
}
Not that using Lerp like this is often seen as an error. You generally want set starting and ending points, but by having the starting point being updated to the current value, you end up with a bit of an 'easing' effect. Play around with that if the effect isn't what you're looking for.
Also note, the code was written but not tested... but, I stand by my previous comments.
I just want way to lose stamina consistantly while a button is held
I had this exact problem, when I started Unity development two years ago.
Perhaps, as stated by someone, this answer might not directly help OP. But, the general community of stackoverflow might benefit from this answer. I'm sharing this because it's the most readable and performant solution I'm currently using. If you have other approaches or better solutions readability/performance wise, please let me know.
We should refrain from using coroutines due to reasons stated in Why Rx?:
Using Coroutine is not good practice for asynchronous operations for the following (and other) reasons:
Coroutines can't return any values, since its return type must be IEnumerator.
Coroutines can't handle exceptions, because yield return statements cannot be surrounded with a try-catch construction.
Also stated that:
UniRx helps UI programming with uGUI. All UI events (clicked, valuechanged, etc) can be converted to UniRx event streams. And streams are cheap and everywhere.
You can achieve on hold operation without coroutine if you use UniRx. For example:
button.OnPointerDownAsObservable()
.Throttle(TimeSpan.FromSeconds(0.4f)) // How many seconds to hold to trigger
.SelectMany(_ => Observable.Interval(TimeSpan.FromSeconds(0.04f))) // Diminishing speed
.TakeUntil(button.OnPointerUpAsObservable()) // Execute until button is released
.RepeatUntilDestroy(button)
.Subscribe(_ =>
{
if ((currentstamina - amount) > 0)
{
currentstamina -= amount;
staminabar.value = currentstamina;
}
else{
currentstamina = 0;
staminabar.value = currentstamina;
}
});
The above code might not meet your exact needs if you just copy-paste. It's just an example but I really hope it gives you a general idea and knowledge about other ways of achieving what you're looking for.
My coroutine to fade the player out after they die,
// Fade out ragdoll
IEnumerator RagdollFade()
{
yield return new WaitForSeconds(3f);
while (startingColour.a > 0.0f)
{
headSR.color = new Color(headSR.color.r, headSR.color.g, headSR.color.b, headSR.color.a - (Time.deltaTime / 1.5f));
bodySR.color = new Color(bodySR.color.r, bodySR.color.g, bodySR.color.b, bodySR.color.a - (Time.deltaTime / 1.5f));
leftArmSR.color = new Color(leftArmSR.color.r, leftArmSR.color.g, leftArmSR.color.b, leftArmSR.color.a - (Time.deltaTime / 1.5f));
rightArmSR.color = new Color(rightArmSR.color.r, rightArmSR.color.g, rightArmSR.color.b, rightArmSR.color.a - (Time.deltaTime / 1.5f));
leftLegSR.color = new Color(leftLegSR.color.r, leftLegSR.color.g, leftLegSR.color.b, leftLegSR.color.a - (Time.deltaTime / 1.5f));
rightLegSR.color = new Color(rightLegSR.color.r, rightLegSR.color.g, rightLegSR.color.b, rightLegSR.color.a - (Time.deltaTime / 1.5f));
yield return null;
}
}
speeds up after each execution.
For example, this first time the coroutine is called everything works fine and after 3 seconds the player is faded out. However, the next time it is called 3 seconds don't pass before the fade, the next time even less time, etc.
startingColour is set in the Start() function.
It seems that your startingColour.a value is always bigger then 0 so the while loop never finishes and your coroutine just runs forever. Hard to tell without seeing the rest of your code.
So if you start it a second time you now have both routines running parallel => now each frame you decrease the alphas by the double amount .. then triple .. etc. and it also doesn't wait the 3 seconds before the first called routines are already running the while loop so they continue to decrease the alphas.
You could use StopAllCoroutines or StopCoroutine in order to interrupt any still running routines when starting a new one. But that's actually more a kind of dirty workaround.
I would rather takle the actual issue and make sure your while loop returns which is currently unlikely to happen since you seem to not change startColor.a anywhere.
Or add a flag not allowing parallel routines at all like e.g.
private bool isFading;
IEnumerator RagdollFade()
{
if(isFading) yield brake;
// prevents other routines
isFading = true;
...
// reset the flag once routine is finished
isFading = false;
}
Then I would also rather suggest to have one single float value you use for fading using Color.Lerp like
private bool isFading;
// you can also use a fixed duration and not pass it as parameter
// but this way you are even more flexible
IEnumerator RagdollFade(float duration)
{
if(isFading) yield brake;
// prevents other routines
isFading = true;
yield return new WaitForSeconds(3f);
// it is more performant to gather all required information beforehand
headStartColor = headSR.color;
bodyStartColor = bodySR.color;
leftArmStartColor = leftArmSR.color;
rightArmStartColor = rightArmSR.color;
leftLegStartColor = leftLegSR.color;
rightLegStartColor = rightLegSR.color;
headTargetColor = new Color(headStartColor.r, headStartColor.g, headStartColor.b, 0f);
bodyTargetColor = new Color(bodyStartColor.r, bodyStartColor.g, bodyStartColor.b, 0f);
leftArmTargetColor = new Color(leftArmStartColor.r, leftArmStartColor.g, leftArmStartColor.b, 0f);
rightArmTargetColor = new Color(rightArmStartColor.r, rightArmStartColor.g, rightArmStartColor.b, 0f);
leftLegTargetColor = new Color(leftLegStartColor.r, leftLegStartColor.g, leftLegStartColor.b, 0f);
rightLegTargetColor = new Color(rightLegStartColor.r, rightLegStartColor.g, rightLegStartColor.b, 0f);
var passedTime = 0f;
while (passedTime < duration)
{
// get the interpolation factor from 0 to 1
var factor = passedTime / duration;
// for adding additional ease-in and ease-out
// factor = Mathf.SmoothStep(0, 1, factor);
headSR.color = Color.Lerp(headStartColor, headTargetColor, factor);
bodySR.color = Color.Lerp(bodyStartColor, bodyTargetColor, factor);
leftArmSR.color = Color.Lerp(leftArmStartColor, leftArmTargetColor, factor);
rightArmSR.color = Color.Lerp(rightArmStartColor, rightArmTargetColor, factor);
leftLegSR.color = Color.Lerp(leftLegStartColor, leftLegTargetColor, factor);
rightLegSR.color = Color.Lerp(rightLegStartColor, rightLegTargetColor, factor);
// avoid overshooting
passedTime += Mathf.Min(Time.deltatime, duration - passedTime);
yield return null;
}
// reset the flag once routine is finished
isFading = false;
}
This is more flexible and you can add ease-in and ease-out using whatever simple math you like.
Try call StopCoroutine() before starting a new Coroutine. Possibly you have a few coroutines working at the same time.
Ok, I've solved the problem, and it's completely my bad.
I realized that ANOTHER coroutine I had was interfering with the current one, so that's why StopCoroutine() and adding a fade check wasn't working.
Sorry guys for not including that in my question post, you would probably have been able to help me out more effectively.
So for anyone who encounters strange routine behavior in the future, make sure two coroutines are not interfering with each other.
This question already has answers here:
Coroutines and while loop
(3 answers)
Closed 6 years ago.
I wish to move an object over time using coroutines. I wanted to get an object from point A to point B over say 2 seconds. To achieve this I used the following code:
IEnumerator _MoveObjectBySpeed()
{
while (TheCoroutine_CanRun)
{
if(myObject.transform.position.y <= UpperBoundary.position.y)
{
myObject.transform.position = new Vector3(myObject.transform.position.x,
myObject.transform.position.y + Step, myObject.transform.position.z);
}
yield return new WaitForSeconds(_smoothness);
}
TheCoroutine_CanRun = true;
moveAlreadyStarted = false;
}
and the Step is calculated like
private void CalculateSpeed()
{
Step = _smoothness * allDistance / timeToReachTop;
}
where allDistance is the distance between the bottom and the top boundary.
_smoothness is a fix value. The thing is, the bigger I get this value, the more accurate the time gets to get from bottom to up. Note that a small value here means smoother movement. This smoothness is the time the coroutine waits in between moving the myObject.
The time is measured like this:
void FixedUpdate()
{
DEBUG_TIMER();
}
#region DEBUG TIME
public float timer = 0.0f;
bool allowed = false;
public void DEBUG_TIMER()
{
if (Input.GetButtonDown("Jump"))
{
StartTimer();
}
if (myObject.transform.position.y >= UpperBoundary.position.y)
{
StopTimer();
Debug.Log(timer.ToString());
//timer = 0.0f;
}
if (allowed)
{
timer += Time.fixedDeltaTime;
}
}
void StartTimer()
{
timer = 0;
allowed = true;
}
void StopTimer()
{
allowed = false;
}
#endregion
The results were:
When I wanted the object to reach the top under 1 second and set the _smoothness to 0.01, the time the myObject took to get to the top was 1.67 seconds. When _smoothness was 0.2s, the time to actually reach the top was 1.04s.
So why is this so inaccurate and how to make it work fine?
This smoothness is the time the coroutine waits in between moving the myObject
The mistake you're making is assuming that a co-routine waits the perfect time before executing. Rather, it probably executes on the next frame after the timeout has finished.
Assuming you want smooth motion, you want to move the object every frame (e.g. in Update or co-routine that uses 'yield return null').
Note: Each frame may take a different duration (consider 144fps vs 15fps), and you can discover this in Time.deltaTime . https://docs.unity3d.com/520/Documentation/ScriptReference/Time-deltaTime.html
I have a "datapack" hidden in each of my levels, the system remembers per level if the artifact has been taken and if so it disables the artifact when a player replays it but i also want to keep track of the total amount taken, the system almost works there's just 1 small issue:
When i finish the level with the first datapack, in the menu it says: 1, this is correct.
but when i do it with the 2nd, it says 3, with the 3rd it says 4, 4th says 5 etc
i don't understand why and thus can't fix it
here's the code:
function OnTriggerEnter(other : Collider) {
if(other.CompareTag("DataPacket")){
pickedUpDataPacket = 1;
Destroy(other.gameObject);
gameObject.Find("DatapackFound").guiText.enabled = true;
yield WaitForSeconds (1.5);
gameObject.Find("DatapackFound").guiText.enabled = false;
}
if(other.CompareTag("Finish") && pickedUpDataPacket == 1){
PlayerPrefs.SetInt("DataPackLevel" + levelindex.ToString(), 1);
//if(!PlayerPrefs.HasKey("totalDatapacks")){
//PlayerPrefs.SetInt("totalDatapacks", 1);
//} else {
PlayerPrefs.SetInt("totalDatapacks", (PlayerPrefs.GetInt("totalDatapacks")+1));
}
}
//}
i already commented a part out, i believe this was also part of the issue.
and part of a 2nd script:
if(datapacktotal){
if(PlayerPrefs.GetInt("totalDatapacks") > 0){
findText.text = "Collected:" + PlayerPrefs.GetInt ("totalDatapacks");
}
Thanks in advance :)
From the described behaviour it seems that, in some occasions, the collision is triggered multiple times, before the datapack is being destroied. As in level 2, where the counter passes from 1 to 3.
My though is that your collider has multiple contact points, so that - if you touches n of them - the collision is triggered n times.
I would try a simple experiment, just use a flag to determine if that's the first time you "touch" the collider; then, you'll update the PlayerPref just in that case:
function OnTriggerEnter (other : Colliderstrong text){
if (collisionAlreadyConsidered) return;
collisionAlreadyConsidered = true;
// your code here...
}
function Update(){
collisionAlreadyConsidered = false;
}
, where collisionAlreadyConsidered is a global variable.