How to make my stamina regenerate slowly? - c#

I need some help on my mobile open world game project. I have a player who can walk and runs when we press a button. I made a stamina wheel (like in zelda botw), and when my player run the stamina decreases. I have also put a regeneration coroutine that make the stamina regen. But because my maxStamina = 1, the time between it is empty and full is really fast. Here is my code :
public static StaminaUI instance;
private WaitForSeconds regenTick = new WaitForSeconds(0.1f);
private Coroutine regen;
private void Awake()
{
instance = this;
}
void Start()
{
fillAmount = maxFill;
}
void Update()
{
if (Ybot.MoveSpeed > 5f)
{
UseStamina(0.015f);
}
if (fillAmount < 0.01f)
{
Ybot.MoveSpeed = 0f;
Ybot.animator.SetBool("isRunning", false);
Ybot.animator.SetBool("isWalking", false);
}
}
public void UseStamina(float amount)
{
if (fillAmount - amount >= 0)
{
fillAmount -= amount;
if (regen != null)
{
StopCoroutine(regen);
}
regen = StartCoroutine(RegenStamina());
}
else
{
Debug.Log("Not enough stamina");
}
}
private IEnumerator RegenStamina()
{
yield return new WaitForSeconds(1);
while (fillAmount < maxFill)
{
fillAmount += maxFill/1;
yield return regenTick;
}
regen = null;
}
Hope someone can help me to this little problem.

Since you increment every 0.1 seconds I think it should be
fillAmount += maxFill / desiredDuration * 0.1f;
where desiredDuration is the time in seconds needed to completely fill the stamina if it is 0.
Or as an alternative for smooth Updates instead of the 0.1 second steps you could do
while (fillAmount < maxFill)
{
fillAmount += maxFill / desiredDuration * Time.deltaTime;
yield return null;
}

You need to use Time.time and then do the regen if the TIme.time is > than a fixed interval. You have to realise that Update is called once per frame. This could mean it can be called anywhere between 30-90 times a second depending on your device's refresh rate.
Update is called once per frame so in your code you have to do something similar to this:
lastRegenTime and regenIntervals can both be floats (which can be expensive), but in Update the logic would be ->
if (Time.time - lastRegenTime > regenInterval)
{
Regenerate();
}
Set regenInterval to something low, and then set the incrementor for life at life+=0.02f; You can even do it more incrementally, or *=0.00002f;

Related

Unity cant figure out how to fix the "still trying to access gameobject" error

So i was writing code to simulate how animals would eat food and drink water around the map, but i keep getting an error. I believe its because multiple animals are trying to access one lake or one fruit, but i don't know how to avoid this from happening.
So here is my code, please help (i know my coding skills are quite bad, but i don't care)
public float hunger = 100;
public float speed;
public float thirst = 100;
// Start is called before the first frame update
void Start()
{
StartCoroutine(hungerdrop());
}
// Update is called once per frame
void Update()
{
if (hunger <= 20 && hunger < thirst)
{
gotoBush();
}
if (thirst <= 20 && thirst < hunger)
{
gotoLake();
}
if (hunger <= 0)
{
Destroy(gameObject);
}
}
IEnumerator hungerdrop()
{
yield return new WaitForSeconds(0.2f);
hunger -= 1;
thirst -= 2;
StartCoroutine(hungerdrop());
}
public void gotoBush()
{
GetClosestFruit();
}
public void gotoLake()
{
GetClosestLake();
}
GameObject GetClosestFruit()
{
GameObject tMin = null;
float minDist = Mathf.Infinity;
Vector3 currentPos = transform.position;
foreach (GameObject t in GameObject.FindGameObjectsWithTag("fruit"))
{
float dist = Vector3.Distance(t.transform.position, currentPos);
if (dist < minDist)
{
tMin = t;
minDist = dist;
}
}
if (tMin != null)
{
float step = speed * Time.deltaTime;
StartCoroutine(walkToBush());
IEnumerator walkToBush()
{
if (tMin != null)
{
while (transform.position != tMin.transform.position && tMin != null)
{
yield return new WaitForSeconds(0.1f);
transform.position = Vector3.MoveTowards(transform.position, tMin.transform.position, step);
}
hunger = 100;
StartCoroutine(bushEat());
}
IEnumerator bushEat()
{
yield return new WaitForSeconds(0.1f);
destroyBush();
}
}
void destroyBush()
{
Destroy(tMin);
}
}
return tMin;
}
GameObject GetClosestLake()
{
GameObject tMin = null;
float minDist = Mathf.Infinity;
Vector3 currentPos = transform.position;
foreach (GameObject t in GameObject.FindGameObjectsWithTag("lake"))
{
float dist = Vector3.Distance(t.transform.position, currentPos);
if (dist < minDist)
{
tMin = t;
minDist = dist;
}
}
if (tMin != null )
{
float step = speed * Time.deltaTime;
StartCoroutine(walkToBush());
IEnumerator walkToBush()
{
if (tMin != null)
{
while (transform.position != tMin.transform.position && tMin != null)
{
yield return new WaitForSeconds(0.1f);
if(tMin != null)
{
transform.position = Vector3.MoveTowards(transform.position, tMin.transform.position, step);
}
}
thirst = 100;
StartCoroutine(bushEat());
}
IEnumerator bushEat()
{
yield return new WaitForSeconds(0.1f);
destroyBush();
}
}
void destroyBush()
{
Destroy(tMin);
}
}
return tMin;
}
}
First: You should use while loops in coroutines instead of recursion like this:
IEnumerator hungerdrop()
{
while(true)
{
hunger -= 1;
thirst -= 2;
yield return new WaitForSeconds(0.2f);
}
}
Second: To check for gameObjects that have already ben destroyed, you can simply use an if statement like:
if(gameObject != null)
{
//do something
}
If I understand correctly you have multiple animal instances all running this component.
I see two huge issues here:
You might have concurrent Coroutines since you start a new routine every frame while your conditions are true! This might even mean that you try to move in two opposed directions => you never reach a target.
Multiple of your animals could target the very same resource => while the one is still moving towards it, the other one might already have eaten it and destroys it.
And then there is most probably this issue that you check the alive state of your resources after getting the position in
while (transform.position != tMin.transform.position && tMin != null)
where you should rather check
while (tMin && transform.position != tMin.transform.position)
However, this would still not prevent the rest of your code after the while from being executed even if tMin doesn't exist anymore!
What I would rather do is
Make sure only one Coroutine is running at a time
Make sure only one animal can target a resource at a time
Therefor instead of using tags I would rather have two actual components attached to the resources with a common base class:
public abstract class ConsumableResource : MonoBehaviour
{
public bool isOccupied;
}
and then these two go onto the according resource GameObjects:
public class Fruit : ConsumableResource
{
// Doesn't have to do anything else .. but could
}
and
public class Lake : ConsumableResource
{
// Doesn't have to do anything else .. but could
// You could e.g. consider to add a counter so that multiple animals can target this at a time
// and/or add another counter so multiple animals can consume this resource before it is destroyed
}
And then I would do
public class Animal : MonoBehaviour
{
public float hunger = 100;
public float speed;
public float thirst = 100;
// the currently executed coroutine
private Coroutine _currentRoutine;
// the currently targeted resource instance
private ConsumableResource _currentTargetResource;
// Update is called once per frame
private void Update()
{
// first of all I would rather drop the hunger and thirst like this
// This now means hunger drops 5 units per second and thirst drops 10 units per second
// What you had before was a complex way for writing basically the same
hunger -= 5f * Time.deltaTime;
thirst -= 10f * Time.deltaTime;
if (hunger <= 0)
{
Destroy(gameObject);
}
// In order to no end up with multiple concurrent routines check if there is already a routine running first
if (_currentRoutine == null)
{
if (hunger <= 20 && hunger < thirst)
{
// start our resource routine targeting the type Fruit and if we succeed to run the entire routine to and
// then call WhenConsumedFruit afterwards to reset the hunger
_currentRoutine = StartCoroutine(GetResourceRoutine<Fruit>(WhenConsumedFruit));
}
// make your cases exclusive!
else if (thirst <= 20 && thirst < hunger)
{
_currentRoutine = StartCoroutine(GetResourceRoutine<Lake>(WhenConsumedLake));
}
}
}
private void WhenConsumedFruit()
{
hunger = 100;
}
private void WhenConsumedLake()
{
thirst = 100;
}
// Use a generic method to not repeat the same implementation
// This now can be used to get the closest of any inherited type from ConsumableResource
private T FindClosestResource<T>() where T : ConsumableResource
{
// First get all instances in the scene of given resource type
var allFruits = FindObjectsOfType<T>();
// filter out those that are already occupied by another animal instance
var onlyNotOccupiedFruits = allFruits.Where(fruit => !fruit.isOccupied);
// sort the remaining instances by distance
var sortedByDistance = onlyNotOccupiedFruits.OrderBy(fruit => (fruit.transform.position - transform.position).sqrMagnitude);
// take the first one (= closest) or null if there was no remaining instance
return sortedByDistance.FirstOrDefault();
}
// Again use the most generic base class so you can reuse the same code for any inherited type of ConsumableResource
IEnumerator MoveTowardsResource(ConsumableResource target)
{
while (transform.position != target.transform.position)
{
transform.position = Vector3.MoveTowards(transform.position, target.transform.position, speed * Time.deltaTime);
yield return null;
}
transform.position = target.transform.position;
}
// and again do his only once
IEnumerator ConsumeResource(ConsumableResource target)
{
yield return new WaitForSeconds(0.1f);
// You might want to consider to rather move this into the resource itself
// so it could extend the behavior as said e.g. using counters
// without the need that your animal class has to be aware of what exactly
// this means for the resource. Maybe it doesn't destroy it but only disable
// for some time so it can grow back => unlimmitted options ;)
Destroy(target.gameObject);
}
private IEnumerator GetResourceRoutine<T>(Action whenConsumed) where T : ConsumableResource
{
_currentTargetResource = FindClosestResource<T>();
// did we find a closest fruit?
if (!_currentTargetResource)
{
// if not terminate this routine and allow the next one to start
_currentRoutine = null;
yield break;
}
// set closest fruit occupied so no animal can take it anymore
_currentTargetResource.isOccupied = true;
// Move towards the closest fruit
yield return MoveTowardsResource(_currentTargetResource);
// "Eat" the fruit
yield return ConsumeResource(_currentTargetResource);
// when all is done without this object dying before simply invoked the passed callback action
whenConsumed?.Invoke();
// Allow the next routine to start
_currentRoutine = null;
}
private void OnDestroy()
{
// in case we die for whatever reason make sure to release the occupied resources if there are any
// so if you die on the way towards it at least from now on another animal can pick it again as target
if (_currentTargetResource) _currentTargetResource.isOccupied = false;
}
}

Trying to get enemy to deal damage every set seconds (Unity)

I am creating a 3D Shooter and I am trying to make an enemy deal damage to the player every set seconds. I have made the enemy deal damage with a raycast but it deals the damage way too fast.
I thought using yield return new WaitForSeconds(2) would take 1 damage away from the player every 2 seconds but it deals damage to the player a lot faster.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyMove : MonoBehaviour
{
public Transform target;
public Transform player;
public float enemySpeed;
public int moveTrigger = 1;
public int isAttacking;
public float distanceFromPlayer;
void Update()
{
distanceFromPlayer = Vector3.Distance(target.transform.position, player.transform.position);
if (distanceFromPlayer <= 10 && moveTrigger == 1)
{
transform.LookAt(target);
StartCoroutine(EnemyDamage());
}
if (distanceFromPlayer <10 && moveTrigger == 1 && distanceFromPlayer >3)
{
transform.Translate(Vector3.forward * enemySpeed * Time.deltaTime);
}
}
IEnumerator EnemyDamage()
{
RaycastHit PlayerHit;
if (Physics.Raycast(target.transform.position, target.transform.forward, out PlayerHit))
{
Debug.Log(PlayerHit.transform.name);
Target target = PlayerHit.transform.GetComponent<Target>();
if (target != null)
{
yield return new WaitForSeconds(2);
GlobalHealth.playerHealth -= 1;
yield return null;
}
}
}
}
You are starting a coroutine in every update loop so every frame your damage coroutine gets called and applies your 1 damage after 2 seconds.
If you want a damage over time effect the suggestion of using an game tick timer is correct, but if you want your enemy to only be able to attack every x seconds you need to implement some kind of cooldown on your damage function. For example add up Time.deltaTime until the time you want has past, and only then the enemy is able to deal damage again. You can do that with a boolean.
As mentioned in other answers, you are starting a new coroutine each frame. You should do all your waiting and looping inside your coroutine, as execution exits and re-enters your coroutine from the yeild statment. This is how you would write this to work
// in update
if (distanceFromPlayer <= 10 && moveTrigger == 1){
transform.LookAt(target);
if(!isAttacking)
StartCoroutine(EnemyDamage());
}
IEnumerator EnemyDamage()
{
isAttacking = true;
while(distanceFromPlayer <= 10){ // in range
RaycastHit PlayerHit;
if (Physics.Raycast(target.transform.position, target.transform.forward, out PlayerHit)){
Target target = PlayerHit.transform.GetComponent<Target>();
if (target != null){
GlobalHealth.playerHealth -= 1;
yield return new WaitForSeconds(2);
}
}
}
isAttacking = false; // out of range
yield return null;
}
You can use FixedUpdate → set a local variable bool alreadyShoot = false.
In your EnemyDamage() add a if(!alreadyShoot) before damages application and set alreadyShoot to true after damages application.
In FixedUpdate you can set alreadyShoot to false.
You can choose an other solution to set alreadyShoot to false instead of FixedUpdate (for example a coroutine that trigger each seconds, ...)
in update loop, check when two seconds have passed when the player is in range
float timeInRange = 0.0f;
bool inRange = false;
if (distanceFromPlayer <= 10 && moveTrigger == 1)
{
inRange = true;
}
else
{
inRange = false;
timeInRange = 0;
}
if (inRange)
{
timeInRange += Time.DeltaTime;
}
if (timeInRange > 2.0f)
{
GlobalHealth.playerHealth -= 1;
timeInRange = 0.0f;
}

C# how to Make function/Method Wait without blocking program (Unity)

I want to simulate health regeneration in my game in unity, in the function RestoreHealth().
Am I overthinking it by wanting to create a child process, so when I call wait, it won't affect any current running process or thread and the child process will die when the function is done.
public void RestoreHealth() {
if (health >= MaxHealth) return; // health and MaxHealth are Class variables
if (health % 10 != 0) { // if health is not dividable by 10
int temp = health % 10; // then we round it to the closest
//tenth
temp = 10 - temp;
health += temp;
}
int i = health;
for (; i < MaxHealth; i += 10) { // where the health grows till 100
health += 10;
// sleep(1000); // make function wait for '1 second' to iterate again
Debug.Log("Health: " + health);
}
}
How do I create a child process in C# or unity in this case and cause it to wait?
Is there an equivalent to Fork(); like in C?
Also, this function is called when the player initially receives damage.
Solution:
note: I changed Health to Armour
public IEnumerator RestoreArmour() {
while (_Armour < _MaxArmour) {
_Armour++;
Debug.Log("Health: " + _Armour);
yield return new WaitForSeconds(ArmourRegenRate); // ArmourRegenRate is a
// float for the seconds
}
}
and use this to initiate the coroutine
void Start(){
StartCoroutine(player.RestoreArmour());
}
Basic Coroutine concept
In Unity you work with Coroutines to achieve this asychronous "threaded" behaviour
IEnumerator RestoreHealth() {
while (health != MaxHealth) {
health++;
yield return new WaitForEndOfFrame();
}
}
and then invoke it with
StartCoroutine(RestoreHealth());
Restarting a Coroutine
In order to stop an existing Coroutine from running and start a new one, this is how you would achieve that:
private Coroutine _myCoroutine = null;
void SomeMethod()
{
if (_myCoroutine != null)
StopCoroutine(_myCoroutine);
_myCoroutine = StartCoroutine(SomeOtherMethod());
}
Pausing armor restoration for X seconds when player has taken damage
A common functionality is to have something restore armor when player hasn't taken damage for X seconds:
private bool _shouldRestoreArmour = true;
private Coroutine _pauseArmorCoroutine = null;
void Update()
{
if (_shouldRestoreArmour)
Armor += ArmorRegenerationPerSecond * Time.deltaTime;
}
void PlayerTakeDamage()
{
if (_pauseArmorCoroutine != null)
StopCoroutine(_pauseArmorCoroutine);
_pauseArmorCoroutine = StartCoroutine(PauseRestoreArmor());
// Take damage code
}
IEnumerator PauseRestoreArmor()
{
_shouldRestoreArmor = false;
yield return new WaitForSeconds(RESTORE_ARMOR_DELAY_TIME);
_shouldRestoreArmor = true;
}
Here the player will regenerate armor at all times, except for X seconds after the player has taken damage. If the player takes damage multiple times we will simply abort the previous coroutine and start a new one, so that it will be a fresh X seconds from the last hit.
You can create a timer assuming that you run this in the update method
private float timer = 1;
private float timerReset = 1;
private void Update(){
RestoreHealth();
}
public void RestoreHealth() {
if (health >= MaxHealth) return; // health and MaxHealth are Class variables
if (health % 10 != 0) { // if health is not dividable by 10
int temp = health % 10; // then we round it to the closest tenth
temp = 10 - temp;
health += temp;
}
int i = health;
for (; i < MaxHealth; i += 10) { // where the health grows till 100
if(timer > 0){
timer -= 1 * time.deltaTime;
}else{
health += 10;
Debug.Log("Health: " + health);
timer = timerReset;
}
}
}
or you can simply just give your player 1 health every second or whatever amount for instance
public void RestoreHealth() {
if (health >= MaxHealth) return; // health and MaxHealth are Class variables
if (health % 10 != 0) { // if health is not dividable by 10
int temp = health % 10; // then we round it to the closest tenth
temp = 10 - temp;
health += temp;
}
int i = health;
for (; i < MaxHealth; i += 10) { // where the health grows till 100
health += 10 * 1 * time.deltaTime;
Debug.Log("Health: " + health);
timer = timerReset;
}
}
So in this scenario your player recieves 10 health every second but the value will always go up so in the matter of a second the player health would go up 1 health every 100ms

Unity C# timer should trigger function every 1.5 min but doesn't

I'm trying to write a script that turns all lights off after 1.5 minutes for 10 seconds, then turns them back on.
Right now, it seems like the timer is getting bypassed. I realize that the reason for this is probably because the time will never be exactly 90 or so.
That being said, I have no idea how to get the result I want.
I thought of using InvokeRepeating instead (as commented out) but then that would mean that the lights would be off longer every time.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LightsTimerTrigger : MonoBehaviour {
private GameObject[] allLights;
private float time = 0.0f;
private float secondTimer = 0.0f;
void Start () {
// Create array of lights
allLights = GameObject.FindGameObjectsWithTag("riddleLights");
//InvokeRepeating("lightsOn", 60.0f, 120.0f);
//InvokeRepeating("lightsOff", 60.10f, 120.10f); // Exponential
}
// Update is called once per frame
void Update () {
time += Time.deltaTime;
if(time%90 == 0)
{
secondTimer = time;
lightsOff();
Debug.Log("Lights off");
}
if (time == secondTimer + 10)
{
// Turn lights back on after 10 seconds
lightsOn();
Debug.Log("Lights back on");
}
}
void lightsOff()
{
foreach (GameObject i in allLights)
{
i.SetActive(false);
}
}
void lightsOn()
{
foreach (GameObject i in allLights)
{
i.SetActive(true);
}
}
}
if(time%90 == 0)
This will (almost certainly) never be true.
What happens if the time is 90.000000001? Well, divide off the 90 part and check if(0.000000001 == 0) which is false.
You should correct your code thusly:
if(time >= 90) {
time -= 90;
//the rest of your code
}
You'll have to do something similar for your 10 second delay.
The coroutine option:
void Start () {
//existing code
StartCoroutine(FlickerLights());
}
IEnumerator FlickerLights() {
while(true) {
yield return new WaitForSeconds(90);
lightsOff();
Debug.Log("Lights off");
yield return new WaitForSeconds(10);
lightsOn();
Debug.Log("Lights back on");
}
}

Increase and decrease light intensity overtime

I want to create some fireflies in Unity. I want to Increase light intensity then wait some seconds and then decrease it in Unity. When they get spawned, I want them increasing their light intensity, wait some seconds and then fade out. How can I create this "process" in a clean way?
private Light pointLight; // The light component of the firefly
private float minLuminosity = 0; // min intensity
private float maxLuminosity = 1; // max intensity
private float luminositySteps = 0.005f; // factor when increasing / decreasing
private float shineDuration = 3; // wait 3 seconds when faded in
private void Start()
{
pointLight = GetComponent<Light>();
pointLight.intensity = Random.Range(minLuminosity, maxLuminosity); // start with a random intensity
StartCoroutine(ChangeIntensity()); // start the process
}
private IEnumerator ChangeIntensity()
{
pointLight.intensity += luminositySteps; // increase the firefly intensity / fade in
yield return new WaitWhile(() => pointLight.intensity >= maxLuminosity); // wait for the maximum intensity
yield return new WaitForSeconds(shineDuration); // wait 3 seconds
pointLight.intensity -= luminositySteps;
yield return new WaitWhile(() => pointLight.intensity <= maxLuminosity); // wait for the minimum intensity
StartCoroutine(ChangeIntensity()); // do it again
}
So obviously the coroutine stops forever at the first WaitWhile() How can I create such a code chain? When fading in or out, I just mean changing the light intensity.
Even though this has been solved, the current solutions are just decrementing the variable and also creates new object (WaitForSeconds) every frame.
The proper way of doing this in Unity is using Mathf.Lerp and Time.deltaTime. This type of operation is what Mathf.Lerp is made for which is to go from your minLuminosity to maxLuminosity. You can read more about this in my other question for fading out/in GameObject using its alpha component here.
I took the fadeInAndOut function from that answer and ported it to work with the Light component. Here is a simple light fade in/out function:
IEnumerator fadeInAndOut(Light lightToFade, bool fadeIn, float duration)
{
float minLuminosity = 0; // min intensity
float maxLuminosity = 1; // max intensity
float counter = 0f;
//Set Values depending on if fadeIn or fadeOut
float a, b;
if (fadeIn)
{
a = minLuminosity;
b = maxLuminosity;
}
else
{
a = maxLuminosity;
b = minLuminosity;
}
float currentIntensity = lightToFade.intensity;
while (counter < duration)
{
counter += Time.deltaTime;
lightToFade.intensity = Mathf.Lerp(a, b, counter / duration);
yield return null;
}
}
Now, to create the exact effect you want which is to increase light intensity then wait some seconds and then decrease it, create another coroutine function that calls the function above and waits for it to finish. You can do that by yielding the fadeInAndOut function. Notice how WaitForSeconds is declared outside the while loop so that it does not create new Object each time.
//Fade in and out forever
IEnumerator fadeInAndOutRepeat(Light lightToFade, float duration, float waitTime)
{
WaitForSeconds waitForXSec = new WaitForSeconds(waitTime);
while (true)
{
//Fade out
yield return fadeInAndOut(lightToFade, false, duration);
//Wait
yield return waitForXSec;
//Fade-in
yield return fadeInAndOut(lightToFade, true, duration);
}
}
USAGE:
public Light lightToFade;
public float eachFadeTime = 2f;
public float fadeWaitTime = 5f;
void Start()
{
StartCoroutine(fadeInAndOutRepeat(lightToFade, eachFadeTime, fadeWaitTime));
}
The problem in your code was that you applied your luminosity change only once, thus your WaitWhile condition would never be reached. I would change both WaitWhile into simple while loops, and then use WaitForEndOfFrame:
private IEnumerator ChangeIntensity()
{
while(true)
{
while(pointLight.intensity <= maxLuminosity)
{
pointLight.intensity += luminositySteps; // increase the firefly intensity / fade in
yield return new WaitForEndOfFrame();
}
yield return new WaitForSeconds(shineDuration); // wait 3 seconds
while(pointLight.intensity > minLuminosity)
{
pointLight.intensity -= luminositySteps;
yield return new WaitForEndOfFrame();
}
}
}
So I got it by using the following while loop
private IEnumerator ChangeIntensity()
{
while (true)
{
pointLight.intensity += isIncreasing ? luminositySteps : -luminositySteps;
if (pointLight.intensity <= minLuminosity)
isIncreasing = true;
if (pointLight.intensity >= maxLuminosity)
{
isIncreasing = false;
yield return new WaitForSeconds(shineDuration);
}
yield return null;
}
}

Categories

Resources