My slow-healing script gets faster and faster (C# / Unity) - c#

I am making a FPS zombie game that is essentially a rip off of COD Nazi Zombies. I am 2 months into self-learning Unity with C# and everything is going well, however this small issue is bugging me and I could use a hand.
The Problem : The players health heals far too fast nomatter what values I change healingAmount and healingFreq to. Furthermore, the health seems to increasingly get faster. Is there an better way of writing a self heal method than to use a Coroutine?
Relevent code below...
public int currentHealth = 100;
public int maxHealth = 100;
public int healingAmount = 1;
public int healingFreq = 1;
public void Start()
{
SetHealthBar(currentHealth);
}
public void Update()
{
StartCoroutine(SlowHeal());
}
IEnumerator SlowHeal()
{
while (currentHealth < maxHealth)
{
yield return new WaitForSeconds(healingFreq); // Stops loop for a desired amount of seconds at a time.
currentHealth += healingAmount; // Adds the desired healing amount to the players current health.
SetHealthBar(currentHealth); // Updates healthbar to the players current health.
if (currentHealth > maxHealth) // Limits the healing to the max in the event it may overflow
{
currentHealth = maxHealth;
}
}
}

You only need to call StartCoroutine() method once in your Start method. StartCoroutine() will handle regular calls to SlowHeal(). Try moving your StartCoroutine() call to your Start method and see how things pan out.

Once the zombie has healed once, there is no way to start the healing process again. I would propose the following.
public void Update()
{
if ( ( currentHealth < maxHealth ) && !healing )
{
healing = true;
StartCoroutine(SlowHeal());
}
}
IEnumerator SlowHeal()
{
while (currentHealth < maxHealth)
{
yield return new WaitForSeconds(healingFreq);
currentHealth += healingAmount;
if (currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
SetHealthBar(currentHealth);
}
healing = false;
}

Topher is basically correct in his assessment on what's happening, but I would write the code differently. Likewise Chris Walsh is correct in saying you only need to call StartCoroutine in Start.
public void Start()
{
SetHealthBar(currentHealth);
}
IEnumerator SlowHeal()
{
while (true)
{
yield return new WaitForSeconds(healingFreq);
currentHealth += healingAmount;
if (currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
SetHealthBar(currentHealth);
}
}
Because the zombie will always heal once it is not at full health, there is no reason to terminate the coroutine when the zombie reaches full health: the code is already there to insure the zombie doesn't heal above maximum, so further healing and no healing are effectively identical.
In this way the coroutine can be started once (in Start) and never needs to be messed with after that. No checking for whether or not the coroutine is already running, no more bools to insure we don't start a second cortoutine, nothing special is needed if we insure that the coroutine itself never exits (coroutines are automatically stopped when the script is disabled or destroyed*).
*This does not apply to coroutines started by other, non-self objects.

Related

Why is a console log going off when it's not supposed to?

Basically, I have a system set up where one file has a damage value, and another file has the health.
void Attack()
{
// Detect enemies in range of attack
Collider2D[] hitEnemies = Physics2D.OverlapCircleAll(attackPoint.position, attackRange, enemyLayers);
// Damage them
foreach(Collider2D enemy in hitEnemies)
{
Debug.Log("We hit " + enemy.name);
enemy.GetComponent<Enemy>().TakeDamage(attackDamage);
}
}
it is worth noting that attackDamage is set to 40.
for my enemy script, this is the important code
public class Enemy : MonoBehaviour
{
public int maxHealth = 100;
int currentHealth;
// Start is called before the first frame update
void Start()
{
currentHealth = maxHealth;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
// Play hurt animation
if (currentHealth <= 0);
{
Die();
}
}
void Die()
{
Debug.Log("Enemy died!");
// Die animation
// Disable the enemy
}
The problem here is that when I run the console and the enemy it says the enemy has 60 health, it still runs the 'Enemy died!', even though that should only happen if it's equal to or under 0. By the way i'm using unity on c#
I believe the most likely issue is hitEnemies contains duplicates or already dead enemies. You can check if the enemy is already dead or not to solve this. You could also use .Distinct to remove duplicates but it will take more time to run the attack method.
Remember, a debugger can be your best friend. Use it to see what is really going on in your game. It will let you trace and follow through your code to see what is happening and why it does things.

I have been coding a script that plays a hit animation when an enemy is hit but the enemy gets stuck in frame 1 of the animation

My code was working as intended until I added the lines defining current health (currentHealth = health;) and (anim.SetTrigger("attacked")) which
I then changed to what you see below to ensure the values were the same as they were not before.
Even after the change I had the same issue, my enemy would get stuck in frame 1 of the hit animation as soon as the game started. Damage and health all work fine so I'm completely stumped as to what could be the problem. Any advice would be appreciated and if needed I can add more info on my code in the comments.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyHealth : MonoBehaviour
{
public float health;
public float currentHealth;
private Animator anim;
// Start is called before the first frame update
void Start()
{
anim = GetComponent<Animator>();
currentHealth = 100;
health = 100;
}
// Update is called once per frame
void Update()
{
if (health < currentHealth)
currentHealth = health;
anim.SetTrigger("attacked");
if (health <= 0)
Debug.Log("Enemy is dead");
}
}
So you have a few problems. The body of your first guard statement should be enclosed in braces { } due to having multiple lines logically. The indent is not enough.
Change the lines:
if (health < currentHealth)
currentHealth = health;
anim.SetTrigger("attacked");
...to:
if (health < currentHealth)
{ // <--- new
currentHealth = health;
anim.SetTrigger("attacked");
} // <--- new
Also, you have a typo in the variable names - you want to compare the "current health" to the "maximum".
Change it to read like so:
if (currentHealth < health)
Going by your code I can't help but feel that you need some flag in your guard statement prior to playing an animation (in addition to checking health level), rather than simply going by the health level and resetting the enemy health to max health each time they are attacked. Otherwise, the animation will keep playing from the beginning and never advance.
Let us introduce the flag _isPlayingAnimation like so:
private bool _isPlayingAnimation;
void Update()
{
if (currentHealth < health && !_isPlayingAnimation) // <--- updated
{
currentHealth = health;
anim.SetTrigger("attacked");
_isPlayingAnimation=true; // <--- new
}
.
.
.
}
.
.
.
// elsewhere, when the animation completes
_isPlayingAnimation = false; // <--- new
Other tips
Consider renaming your variables. health is kinda ambiguous. Perhaps maxHealth would be better? This will avoid confusion in lines such as the original problem line health < currentHealth.

How to count down both cooldown and duration

In a small simulation game (A.I. spaceship shooter) that I am developing, I am trying to come up with an effective shield function or IEnumerator which can be called or started and do multiple things:
Count down the shield's cooldown if it is above zero
Activate the shield for the set duration (5 seconds) if the cooldown has ended
Deactivate the shield when the duration expires
However, I run into some problems when trying this using only an Ienumerator. I have been able to use IEnumerators to count down timers and cooldowns before but trying to do both a cooldown and duration doesn't seem to work as Unity does not let me WaitForSeconds twice without leaving the IEnumerator.
Similarly, each ship has a turret and inside of that turret is an IEnumerator which fires or counts down its cooldown, whichever is needed for the situation.
// Fire continuously if in range and we have more than 1 shot left
// Otherwise, reload for (rate) seconds and reset shots left
public IEnumerator Fire(Vector2 target) {
firing = true;
if (cooldown <= 0) {
if (bullets > 0) {
// Fire a bullet
bullets--;
// Instatiate the bullet
}
} else {
// Reload
cooldown = rate;
bullets = count;
}
} else {
yield return new WaitForSeconds(1);
cooldown--;
}
firing = false;
yield break;
}
The Fire Coroutine is called by using the firing flag to check whether it is running or not and if it is not call
var fire = turret.Fire(shootTarget + offset);
if (!turret.firing && InRange() == true) {
StartCoroutine(fire);
}
every second or so if the ship is alive and we have a target.
I do think that my current use of the IEnumerator is not recommended because it has to be called at least every second, but with how small the environment is at the moment, it doesn't appear to be an issue.
Any help is appreciated.
Quick code without coroutines, no idea if it works in game. Should give you some idea though, I hope.
public class Shield : MonoBehaviour {
public float duration;
public float cooldown;
public float lastActivated;
public bool IsActivated => (lastActivated - (Time.time - duration)) > 0;
public bool onCoolDown => (lastActivated - (Time.time - cooldown)) > 0;
public void Activate(){
if(onCoolDown) return;
lastActivated = Time.time;
}
void LateUpdate(){
if(IsActivated) //Show shield effects, blahblah
else //Do nothing, blahblah
}
}

Move GameObject to new position overtime [duplicate]

I am learning Unity from a Swift SpriteKit background where moving a sprite's x Position is as straight forward as an running an action as below:
let moveLeft = SKAction.moveToX(self.frame.width/5, duration: 1.0)
let delayAction = SKAction.waitForDuration(1.0)
let handSequence = SKAction.sequence([delayAction, moveLeft])
sprite.runAction(handSequence)
I would like to know an equivalent or similar way of moving a sprite to a specific position for a specific duration (say, a second) with a delay that doesn't have to be called in the update function.
gjttt1's answer is close but is missing important functions and the use of WaitForSeconds() for moving GameObject is unacceptable. You should use combination of Lerp, Coroutine and Time.deltaTime. You must understand these stuff to be able to do animation from Script in Unity.
public GameObject objectectA;
public GameObject objectectB;
void Start()
{
StartCoroutine(moveToX(objectectA.transform, objectectB.transform.position, 1.0f));
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
Similar Question: SKAction.scaleXTo
The answer of git1 is good but there is another solution if you do not want to use couritines.
You can use InvokeRepeating to repeatedly trigger a function.
float duration; //duration of movement
float durationTime; //this will be the value used to check if Time.time passed the current duration set
void Start()
{
StartMovement();
}
void StartMovement()
{
InvokeRepeating("MovementFunction", Time.deltaTime, Time.deltaTime); //Time.deltaTime is the time passed between two frames
durationTime = Time.time + duration; //This is how long the invoke will repeat
}
void MovementFunction()
{
if(durationTime > Time.time)
{
//Movement
}
else
{
CancelInvoke("MovementFunction"); //Stop the invoking of this function
return;
}
}
You can use co-routines to do this. To do this, create a function that returns type IEnumerator and include a loop to do what you want:
private IEnumerator foo()
{
while(yourCondition) //for example check if two seconds has passed
{
//move the player on a per frame basis.
yeild return null;
}
}
Then you can call it by using StartCoroutine(foo())
This calls the function every frame but it picks up where it left off last time. So in this example it stops at yield return null on one frame and then starts again on the next: thus it repeats the code in the while loop every frame.
If you want to pause for a certain amount of time then you can use yield return WaitForSeconds(3) to wait for 3 seconds. You can also yield return other co-routines! This means the current routine will pause and run a second coroutine and then pick up again once the second co-routine has finished.
I recommend checking the docs as they do a far superior job of explaining this than I could here

unity move object over time using Coroutine wont move the object precisely [duplicate]

I am learning Unity from a Swift SpriteKit background where moving a sprite's x Position is as straight forward as an running an action as below:
let moveLeft = SKAction.moveToX(self.frame.width/5, duration: 1.0)
let delayAction = SKAction.waitForDuration(1.0)
let handSequence = SKAction.sequence([delayAction, moveLeft])
sprite.runAction(handSequence)
I would like to know an equivalent or similar way of moving a sprite to a specific position for a specific duration (say, a second) with a delay that doesn't have to be called in the update function.
gjttt1's answer is close but is missing important functions and the use of WaitForSeconds() for moving GameObject is unacceptable. You should use combination of Lerp, Coroutine and Time.deltaTime. You must understand these stuff to be able to do animation from Script in Unity.
public GameObject objectectA;
public GameObject objectectB;
void Start()
{
StartCoroutine(moveToX(objectectA.transform, objectectB.transform.position, 1.0f));
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
Similar Question: SKAction.scaleXTo
The answer of git1 is good but there is another solution if you do not want to use couritines.
You can use InvokeRepeating to repeatedly trigger a function.
float duration; //duration of movement
float durationTime; //this will be the value used to check if Time.time passed the current duration set
void Start()
{
StartMovement();
}
void StartMovement()
{
InvokeRepeating("MovementFunction", Time.deltaTime, Time.deltaTime); //Time.deltaTime is the time passed between two frames
durationTime = Time.time + duration; //This is how long the invoke will repeat
}
void MovementFunction()
{
if(durationTime > Time.time)
{
//Movement
}
else
{
CancelInvoke("MovementFunction"); //Stop the invoking of this function
return;
}
}
You can use co-routines to do this. To do this, create a function that returns type IEnumerator and include a loop to do what you want:
private IEnumerator foo()
{
while(yourCondition) //for example check if two seconds has passed
{
//move the player on a per frame basis.
yeild return null;
}
}
Then you can call it by using StartCoroutine(foo())
This calls the function every frame but it picks up where it left off last time. So in this example it stops at yield return null on one frame and then starts again on the next: thus it repeats the code in the while loop every frame.
If you want to pause for a certain amount of time then you can use yield return WaitForSeconds(3) to wait for 3 seconds. You can also yield return other co-routines! This means the current routine will pause and run a second coroutine and then pick up again once the second co-routine has finished.
I recommend checking the docs as they do a far superior job of explaining this than I could here

Categories