How to repeat function after set time while in OnTriggerStay2D - c#

In my game I have a shooting enemy that I want to attack the player only when he's in range, and to attack him once every 3 seconds. This is the code I need repeated
Whatever I try doing just results in the enemy shooting a metric ton of bullets at the player to the point when it sometimes crashes my project(tried for loops, while loops, played with booleans and timing with Time.deltaTime). Can someone smarter than me give me some pointers how I could do this?

You'd probably want to do the check not every frame but whenever the state changes. So you cache the player.
Transform target;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
target = other.transform;
}
private void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Player"))
target = null;
}
Whenever target is not null, you may shoot to it.
private void Update()
{
if (target != null)
ShootAt(target.position);
}
private void ShootAt(Vector3 position)
{
// Spawn your stuff here to shoot at the given location
}
Now how about the timing? I am not a fan of coroutines in Unity. They tend to get messy, are overused and the "WaitForSeconds" is not very precise. It just waits until the time has passed, but not exactly. Maybe you want more precision, who knows?
So let's extend the Update call to have a small timer.
private float shootCooldown;
private void Update()
{
if (target != null)
{
shootCooldown -= Time.deltaTime;
if (shootCooldown <= 0.0f)
{
shootCooldown += 3.0f;
ShootAt(target.position);
}
}
}
One last thing: If you want the timer to reset at some point, just set it to 0.0f. A good location for that would be when the player leaves the trigger. Also, maybe you want the timer to continue to go down, even if the target is null, so move the shootCooldown -= Time.deltaTime line out of the if-scope but make sure it does not count down forever, but rests at 0.

I would avoid using OnTriggerStay2D() and use OnTriggerEnter2D and OnTriggerExit2D in this situation. You need to call the 'yield return' statement within a loop as shown below. The below code should do what you are asking.
private void OnTriggerEnter2D(Collider2D other) {
if (other.tag == "Player") {
StartCoroutine(ShootAtPlayer());
}
}
private void OnTriggerExit2D(Collider2D other) {
if (other.tag == "Player") {
StopAllCoroutines();
}
}
private IEnumerator ShootAtPlayer() {
while (true) {
Instantiate(enemyBullet, enemy.transform.localPosition, transform.LocalRotation);
yield return new WaitForSeconds(3.0f);
}
}

Related

I want to finish the level with 2 players at the door. How do I do it?

here is my code for the trigger
`
private AudioSource finishSound;
private bool levelCompleted = false; //play the audio once
void Start()
{
finishSound = GetComponent<AudioSource>();
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.name == "Player1" && !levelCompleted)
{
CompleteLevel();
levelCompleted = true;
Invoke("CompleteLevel", 1.5f); //delay finish
}
}
private void CompleteLevel()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}`
I know this code is only for one Player. How do I do it requiring 2 players at the door to finish the level?
An approach for this would be to cache the players and check against the total count. To see if it's a player, I'd advice to use tags instead of using the name of the object for a lot of reasons. I assume, the tag "Player" is used to identify the player.
List<GameObject> players = new List<GameObject>();
private void OnTriggerEnter2D(Collider2D collision)
{
// Add a new player if they enter the trigger
if (collision.gameObject.CompareTag("Player") && !players.Contains(collision.gameObject))
{
players.Add(collision.gameObject);
}
// Both players are in the trigger
if (players.Count == 2 && !levelCompleted)
{
CompleteLevel();
levelCompleted = true;
Invoke("CompleteLevel", 1.5f); //delay finish
}
}
private void OnTriggerExit2D(Collider2D collision)
{
// Remove a player if they leave the trigger again
if (collision.gameObject.CompareTag("Player") && players.Contains(collision.gameObject))
{
players.Remove(collision.gameObject);
}
}
If you have a varying count of players, you probably don't want to hard code the amount of players in a trigger. You should have a total count somewhere to check against. A game manager of sorts, that tracks the expected amount of players.
One way would be to create flags for each player, not one. When player one enters the trigger, you set playerOneCompleted = true. Then, you check if second player also completed. If that's the case finish the game if not, do nothing.
What you would need to add is the method OnTriggerExit2D to set flags of respective players to false when they exit the area.
Since the code you presented is inside the player, I would advise moving it to the door's trigger, so you can reference both players more easily.

How to not have my function executed in a single frame?

I'm making a 3D roulette game, where when the player presses the 'bet' button the ball will be positioned in a given location and also a force will be added to the ball. I've added 37 individual box colliders to know where the ball lands.
My problem is that from my understanding the bet function gets executed in a single frame. This means that the script checks for the fallen number before the ball has finished moving and landed inside a box collider. So the for the first bet the number fallen will be 0 even if it lands on another number, and then on the 2nd bet it will have the value of the first fallen number, etc...
public void BetSpinWheel()
{
uiController.repeatButton.interactable = true;
Spin();
int earnings = CalculateEarnings(currentBet.ToArray(), lastNumbers);
UpdateBalance(earnings);
lastBet.Clear();
foreach(Bet bet in currentBet)
{
lastBet.Add(bet);
}
currentBet.Clear();
updateUI();
uiController.ChangeLastNumberText();
}
private void Spin()
{
audioSource.Stop();
audioSource.Play();
ballController.SpinBall();
while (ballController.isMoving)
{ random++; }
if (!ballController.isMoving && ballController.hasBallEnteredCollider)
{
lastNumbers = ballController.numberFallen;
print(lastNumbers);
}
}
and here is the ballController.SpinBall() function:
public void SpinBall()
{
rb.constraints = RigidbodyConstraints.None;
transform.position = ballStartPosition;
rb.velocity = Vector3.zero;
transform.rotation = Quaternion.Euler(0f, 0f, 0f);
rb.AddForce(forceToAdd, ForceMode.Impulse);
print(isMoving);
}
If you would like to view the whole project, you can find it here:
https://github.com/hamzaOud/Assignment02
Use a Coroutine:
// The button should call this
public void BetSpinWheel() {
StartCoroutine(BetSpinWhellCoroutine());
}
private IEnumerator BetSpinWheelCoroutine() {
// Your bet stuff here
}
You can also 'store' a coroutine, so that you can stop it if you need to:
private Coroutine betSpinWheelCoroutine
// The button should call this
public void BetSpinWheel() {
// Stop the coroutine first if it was running.
if (betSpinWheelCoroutine != null){
StopCoroutine(betSpinWheelCoroutine);
}
betSpinWheelCoroutine = StartCoroutine(BetSpinWhellCoroutine());
}
private IEnumerator BetSpinWheelCoroutine() {
// Your bet stuff here
}

Unity3d instantaneous speed up of player movement

in unity3d I have a player that moves constantly forward with a certain speed and I control only it's left or right position.
I want my player to speed up instantaneously when it encounters an object and a trigger is enabled.
Here is what I tried but it doesn't seem to work correctly. Any ideas?
void Update ()
{
GetComponent<Rigidbody>().velocity = new Vector3(Input.GetAxisRaw("Horizontal") * 4, 0, horizVel);
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "SpeedUp")
{
GetComponent<Rigidbody>().velocity = new Vector3(Input.GetAxisRaw("Horizontal") * 4, 0, horizVel * 10.0f);
}
}
horizVel is a public variable for my velocity set to 10.
Seems to be because you have hardcoded your speed variable OnTriggerEnter methods, instead of updating it.
Update is called once a frame. If your horizVel is set to 10, it will move at a speed of 10 once per frame.
When you hit OnTriggerEnter your horizVel gets updated to 10x that of what it was before, i.e: 100.
BUT, because you've not updated your speed variable, when you come back round to the Update method, your horizVel will be back at 10 again.
I think what you should be trying is this:
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "SpeedUp")
{
horizVel *= 10f;
}
}
That way your speed variable will stay at 10x that of what it was before INSTEAD of for just the collision period.
EDIT
"I tried this but the speed remains boosted not only in the collision period"
Then you can use a coroutine to reset the speed variable back to its original value:
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "SpeedUp")
{
horizVel *= 10f;
StartCoroutine(ResetSpeedAfterTime(5f));
}
}
// Resets the speed variable back to the original value after a set amount of time
private IEnumerator ResetSpeedAfterTime(float time)
{
yield return new WaitForSeconds(time);
horizVel = 10f; // the original speed value;
}
Though I am a beginner at C# and my answer may not be correct but have you tried assigning an id to the game object that is supposed to make the character speed up and then calling it in the statement
if (other.gameObject.tag == "SpeedUp")
It maybe because the engine is unable to calculate the exact moment the collision is taking place
if you just want the speed to be boosted when your object is in within the trigger bounds you can reverse the speed after your character leaves the collider.
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "SpeedUp")
{
horizVel *= 10f;
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "SpeedUp")
{
horizVel /= 10f;
}
}

Cancel delayed Destroy() call on a GameObject

I have create a gameObject inside a destroy and have called the Destroy() method on this object with a delay duration of 2 second.
Destroy(gameObject,2.0f);
What I want is, if another gameObject with a collider collides with this gameObject, I want to cancel the destroy call for this particular gameObject.
I tried to call destroy again on the same gameobject with new duration, but it still gets destroyed based on the old duration.
void OnCollisionEnter2D(Collision2D coll)
{
//Debug.Log("Inside Enter");
if (coll.gameObject.tag == "Ball")
{
Destroy(gameObject, reInitializeLifeOfLine);
Debug.Log("Inside Enter");
}
}
Can someone please suggest how to achieve this
There is no built-in way to prevent destruction of a GameObject after you have called Destroy(). However, there are a variety of workarounds that others have explored which achieve exactly what you need.
Here's a simple one (adapted from Unity Answers) using Invoke(), which you can cancel manually:
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "Ball")
{
CancelInvoke("DestroySelf");
Invoke("DestroySelf", reInitializeLifeOfLine);
}
}
void DestroySelf () {
Destroy(gameObject);
}
Hope this helps! Let me know if you have any questions.
I resolved the issue using below code
float reInitializeLifeOfObject = 5.0f;
float lifeTimeOfObject = 3f;
private float startTime;
void Start()
{
startTime = Time.time;
}
void FixedUpdate()
{
// This will destroy the gameObject in 5 second
if((Time.time - startTime) > lifeTimeOfObject)
{
Destroy(gameObject.transform.parent.gameObject);
}
}
void OnCollisionEnter2D(Collision2D coll)
{
if (coll.gameObject.tag == "Ball")
{
// Re-initializing the startTime so that Object is not abruptly destroy while ball is still
// interacting with game object
startTime = Time.time;
}
}
I created a float variable startTime and initialized it in Start() method.
Now in FixedUpdate, I am checking whether 5 seconds have passed. If yes, then destroy the object.
Now for the collision part, I am checking in OnCollisionEnter2D(). If object is interacting with a collider, I am re-assigning value to startTime with current time.
This solved my problem

Cannot use InvokeRepeating with method parameters. How do I work around this?

I'm trying to implement a damage over time system, but Unity keeps saying "Trying to Invoke method...Couldn't be Called." The method I want to call uses the parameters "Collider coll", but from my research you can't invoke if the method has said paremters.
Here is my code:
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
public class DamageOverTime : MonoBehaviour
{
public int PHP; //PHP = Player Health from PlayerHealth.cs script.
public int Damage; //Amount of damage.
public int DamageOverTime; //Damage over time.
public float DamageInterval_DOT = .25f; //Damage interval for damage over time.
public string Level;
PlayerHealth player;
void Start()
{
player = GameObject.Find("Player").GetComponent<PlayerHealth>();
InvokeRepeating("OnTriggerEnter", DamageInterval_DOT, DamageInterval_DOT);
}
void Update()
{
PHP = GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP;
if (PHP <= 0)
{
SceneManager.LoadScene(Level);
}
}
void OnTriggerEnter(Collider coll)
{
if (coll.gameObject.tag == "Player")
{
GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP = PHP - Damage;
}
if (coll.gameObject.tag == "Ball")
{
gameObject.SetActive(false);
SceneManager.LoadScene(Level);
}
}
}
My goal is to get the OnTriggerEnter function to loop ever 1/4 of a second (or lower possibly). Current upon entering a collider my health is drained by 60% in about a second which is far too fast. How should I work around this?
You can't use InvokeRepeating with OnTriggerEnter, because it's a trigger, which means it will trigger once when entrance of its holder occured.
Also InvokeRepeating means that you want to keep repeating an action continously which is not the case here. You want your trigger to occur once and then remove health points over time.
Solution - Coroutine
Unity3D makes custom usage of IEnumerable and yield keyword called Coroutine that always returns an IEnumerator. How it works? It will return control on every yield there is in our Coroutine and then will go back to exact point where it gave back control instead of starting function execution from scratch.
Code:
void OnTriggerEnter(Collider coll)
{
if (coll.gameObject.tag == "Player")
{
StartCoroutine("DamageOverTimeCoroutine");
}
if (coll.gameObject.tag == "Ball")
{
gameObject.SetActive(false);
SceneManager.LoadScene(Level);
}
}
public IEnumerator DamageOverTimeCoroutine()
{
var dotHits = 0;
while (dotHits < 4)
{
//Will remove 1/4 of Damage per tick
GameObject.Find("Player").GetComponent<PlayerHealth>().PlayerHP -= Damage / 4;
dotHits++;
//Will return control over here
yield return new WaitForSeconds(DamageInterval_DOT);
//And then control is returned back here once 0.25s passes
}
}
There's of course room for improvement in this Coroutine. You can pass parameters to it, same as you can to any other method in C#. Also you can implement other invervals that are not hardcoded. Code above is just a simple example on how to deal with such scenarios.
For continous damage over time
public IEnumerator DamageOverTimeCoroutine()
{
var dotHits = 0;
var player = GameObject.Find("Player").GetComponent<PlayerHealth>();
while (true)
{
//Stop removing damage, player is dead already
if (player.PlayerHP <= 0)
yield break;
//Will remove 5 Damage per tick
player.PlayerHP -= 5;
dotHits++;
//Will return control over here
yield return new WaitForSeconds(DamageInterval_DOT);
//And then control is returned back here once 0.25s passes
}
}
To stop Coroutine somewhere else from code use StopCoroutine("DamageOverTimeCoroutine") to stop certain coroutine type or StopAllCoroutines() to stop all coroutines that are active now.

Categories

Resources