I'm a beginner making my first game in Unity, following Unity's Create With Code course. I'm creating a shooter game that will use hand tracking. I haven't set up hand tracking yet so i created an OnTrigger input that explodes objects when I hit space. I created the spawn manager below to spawn waves of enemy attack, but they all the waves are spawning enemies too fast. It seems like they're spawning automatically instead of when the first wave has been destroyed.
Is there an easier way to spawn at a slower rate? Or spawn only when there are no more enemies alive?
EDIT: Added Attack script below
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnAttack : MonoBehaviour
{
public GameObject Trumps;
private float spawnRange = 9;
public int enemyCount;
public int waveNumber = 1;
void Start()
{
SpawnEnemyWave(waveNumber);
//InvokeRepeating("GenerateSpawnPosition", startDelay, randomInterval);
}
// Update is called once per frame
void Update()
{
enemyCount = FindObjectsOfType<Attack>().Length;
if(enemyCount == 0)
{
waveNumber++;
SpawnEnemyWave(waveNumber);
}
}
void SpawnEnemyWave(int enemiesToSpawn)
{
for (int i = 0; i < enemiesToSpawn; i++)
{
Instantiate(Trumps, GenerateSpawnPosition(), Trumps.transform.rotation);
}
}
private Vector3 GenerateSpawnPosition()
{
float spawnPosX = Random.Range(-spawnRange, spawnRange);
float spawnPosZ = Random.Range(-spawnRange, spawnRange);
Vector3 randomPos = new Vector3(spawnPosX, 0, spawnPosZ);
return randomPos;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Attack : MonoBehaviour
{
public float speed = 0.5f;
public GameObject Player;
private Rigidbody enemyRb;
// Start is called before the first frame update
void Start()
{
enemyRb = GetComponent<Rigidbody>();
Player = GameObject.Find("Player");
this.transform.LookAt(Player.transform);
}
// Update is called once per frame
void Update()
{
Vector3 lookDirection = (Player.transform.position - transform.position).normalized;
enemyRb.AddForce(lookDirection * speed);
transform.Translate(Vector3.forward * Time.deltaTime * speed);
}
private void OnTriggerEnter(Collider other)
{
Destroy(gameObject);
Debug.Log("Game Over");
}
}
I guess that you can use the invoke method:
Invoke("NameOfTheMethod", 1f)
What this method does is that it waits a certain amount of seconds before calling a method. You have to write the name of the method in quotation marks and then select how long you want to wait before the method is called (The "1f" represents the delay in seconds.) In your case, you can make the script wait before spawning enemies.
I don't know your Attack script but I would use something like
public class Attack : MonoBehaviour
{
public static readonly HashSet<Attack> AliveAttackInstances = new HashSet<Attack>();
private void Awake()
{
AliveAttackInstances.Add(this);
}
private void OnDestroy()
{
AliveAttackInstances.Remove(this);
}
}
So you can all the time in a cheaper way check how many and which enemies are alive like
public class SpawnAttack : MonoBehaviour
{
// I would change this type here to make sure your spawned prefab actually
// HAS an Attack attached .. otherwise your enemyCount will always be 0
public Attack Trumps;
...
void Update()
{
if(Attack.AliveAttackInstances.Count == 0)
{
waveNumber++;
SpawnEnemyWave(waveNumber);
}
}
Then in order to add a certain delay before spawning the next wave you could use a simple timer like
public class SpawnAttack : MonoBehaviour
{
public Attack Trumps;
[SerializeField] private float delay = 1f;
private float timer;
...
void Update()
{
if(Attack.AliveAttackInstances.Count == 0)
{
timer -= Time.deltaTime;
if(timer <= 0)
{
timer = delay;
waveNumber++;
SpawnEnemyWave(waveNumber);
}
}
}
Try to use Coroutine.
Here's a video about Coroutines: https://www.youtube.com/watch?v=qolMYyq0nX0
My example:
public class Spawn : MonoBehaviour {
private float TimeToWait = 3f;
public int enemyCount = 0;
public int waveNumber = 0;
public GameObject enemy;
void Start()
{
StartCoroutine(SpawnEnemyWave(waveNumber));
}
void Update()
{
if (enemyCount == 0)
{
waveNumber++;
StartCoroutine(SpawnEnemyWave(waveNumber));
}
if (Input.GetMouseButtonDown(0))
{
enemyCount--;
}
}
IEnumerator SpawnEnemyWave(int enemiesToSpawn)
{
//"Things to do before the seconds."
for (int i = 0; i < enemiesToSpawn; i++)
{
enemyCount++;
}
yield return new WaitForSeconds(TimeToWait); // at this example we wait 3 seconds (float TimeToWait = 3f;)
//"Things to do after the seconds."
for (int i = 0; i < enemiesToSpawn; i++)
{
Debug.Log("New Enemy!");
Instantiate(enemy, transform.position, Quaternion.identity);
}
}
}
Related
I am trying to update the lives of my player in a brick breaker game, however I find myself unable to update the text UI responsible for displaying the score (it always displays 3 lives remaining).
Is there any way to fix this issue?
Here is the relevant code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
// Start is called before the first frame update
int lives;
public bool isDead;
public GameObject[] bricks;
int numberBricks;
public Text livesText;
private void Start()
{
isDead = false;
lives = 3;
livesText.text = "lives left: " + lives.ToString();
Debug.Log(lives);
}
// Update is called once per frame
void Update()
{
livesText.text = "lives left: " + lives.ToString();
numberBricks = GameObject.FindGameObjectsWithTag("brick").Length;
if (numberBricks == 0) {
passLevel();
}
}
private void passLevel() {
}
public void ReduceHealth() {
lives--;
Debug.Log(lives);
}
public void CheckDeath() {
if (lives == 0) {
isDead = true;
}
}
}
the ReduceHealth() function is called at
Ball.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Ball : MonoBehaviour
{
// Start is called before the first frame update
Vector3 Direction;
int speedFactor;
float minX;
float maxX;
float additionalForce;
Rigidbody2D rb;
public GameObject paddle;
public AudioSource collisionSound;
public GameObject gameManager;
GameManager gm;
void Start()
{
float sign = Mathf.Sign(Random.Range(-1,1));
minX = -10f * sign;
maxX = -2f * sign;
Direction.x = Random.Range(minX,maxX) ;
Direction.y = -40f;
speedFactor = 1;
rb = GetComponent<Rigidbody2D>();
gm = gameManager.GetComponent<GameManager>();
}
// Update is called once per frame
void Update()
{
transform.position += Direction * speedFactor * Time.deltaTime;
}
void Reset() {
transform.position = new Vector3(0.14f,-4.58f,0f);
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.tag == "Player" || other.gameObject.tag == "border-vertical") {
Direction.y *=-1;
} else if(other.gameObject.tag=="border-bottom") {
gm.ReduceHealth();
Reset();
}else {
Direction.x *= -1;
}
}
}
and here is the Screenshot of my inspector (GameManager object)
any help would be appreciated.
It looks like the function ReduceHealth() has no references. I created a button to see if the lives are reduced or not, it's working. Here is an attached image of OnClick() in button inspector
A few tips
It is not a good practice to update the text in Update function, instead do it in the function where the value of lives is changing. Also check if the lives == 0 in the same function only, there is no need for the function CheckDeath().
public void ReduceHealth() {
lives--;
livesText.text = "lives left: " + lives.ToString();
if (lives == 0) {
isDead = true;
}
}
It is also not a good practice to keep checking the length of the number of bricks at every frame, instead have a check for it in whichever function the breaking of bricks is dealt with.
For eg when the health of the brick reaches zero, get a reference to the GameManager script and do the length check there.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShootBullets : MonoBehaviour
{
public float bulletDistance;
public bool automaticFire = false;
public float fireRate;
public Rigidbody bullet;
private float gunheat;
private bool shoot = false;
private GameObject bulletsParent;
private GameObject[] startpos;
// Start is called before the first frame update
void Start()
{
bulletsParent = GameObject.Find("Bullets");
startpos = GameObject.FindGameObjectsWithTag("Fire Point");
}
private void Fire()
{
for (int i = 0; i < startpos.Length; i++)
{
Rigidbody bulletClone = (Rigidbody)Instantiate(bullet, startpos[i].transform.position, startpos[i].transform.rotation, bulletsParent.transform);
bulletClone.velocity = transform.forward * bulletDistance;
Destroy(bulletClone.gameObject, 0.5f);
}
}
// Update is called once per frame
void FixedUpdate()
{
if (automaticFire == false)
{
if (Input.GetMouseButtonDown(0))
{
Fire();
}
}
else
{
if (shoot == true)
{
Fire();
shoot = false;
}
}
if (gunheat > 0) gunheat -= Time.deltaTime;
if (gunheat <= 0)
{
shoot = true;
gunheat = fireRate;
}
}
}
now the bullets firing up the air and i want the bullets to fire to a target with physics.
the main goal later is to make some kind of side mission where the player third person view should shoot on object and if and when hitting the object the object will fall down and then the player will be able to pick it up.
So, i created two scripts, one named "Stats.cs" registers the player stats and the other one named "PlayerHealth.cs" "makes" the player take damage on contact and updates the Hearts in the HUD. My problem is, whenever i collide with an object that has a tag named "Projectile" it simply doesn't work, my player doesn't take damage at all. The Stats.cs script isn't in any object, the PlayerHealth.cs is in my Player object.
Stats.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Stats{
private int health;
public int maxHP = 3;
public int Health
{
get
{
//Some code
return health;
}
set
{
//Some code
health = Mathf.Clamp(value, 0, maxHP);
}
}
public void SetHealth()
{
Health = maxHP;
}
}
PlayerHealth.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerHealth : MonoBehaviour
{
Stats playerStats = new Stats();
public int curHealth;
public int numOfHearts = 3;
public Image[] hearts;
public Sprite fullHeart;
public Sprite emptyHeart;
void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.CompareTag("Projectile"))
{
Debug.Log("Hello");
DamagePlayer(1);
Destroy(other.gameObject);
}
}
public void DamagePlayer(int damage)
{
playerStats.Health -= damage;
}
// Start is called before the first frame update
void Start()
{
playerStats.SetHealth();
curHealth = numOfHearts;
}
// Update is called once per frame
void Update()
{
curHealth = playerStats.Health;
numOfHearts = playerStats.maxHP;
if (curHealth>numOfHearts){
curHealth = numOfHearts;
}
if(curHealth <= 0){
Die();
}
for (int i = 0; i < hearts.Length; i++)
{
if(i < curHealth){
hearts[i].sprite = fullHeart;
} else
{
hearts[i].sprite = emptyHeart;
}
if(i < numOfHearts){
hearts[i].enabled = true;
} else {
hearts[i].enabled = false;
}
}
}
void Die(){
//Restart
Application.LoadLevel(Application.loadedLevel);
}
}
curHealth is updating so it will stay as the actual Health in Stats and will change the images in HUD.
The Player has a RigidBody2D on him two colliders, one is a box for the body, and the other is a circle collider, so when the player crouches, the circle collider disables.
The Projectiles also have and RigidBody2D with 0 gravity (so it won't fall in mid air) and a BoxCollider2D.
I would check and make sure that the projectile is tagged as Projectile and that the BoxCollider doesn't have "Is Trigger" checked.
I should also say, iterating with that for loop in the Update is very bad practice performance wise. That is happening literally as fast as the machine can loop it and it is doing that every time. I would look into updating it on an event.
Hope this helps!
I am creating a laser defender game in unity and I have this problem where when I shoot the first enemy it takes me directly to the NextLevelMenu scene but I want it to load when all the enemies are killed(on this level I have 5 enemies to kill). I have been told that I need to send a reference to the instance of its spawner to every spawned enemy but I did not quite understand. Can someone help, please?
EnemySpawner Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EnemySpawner : MonoBehaviour {
[SerializeField] GameObject EnemyPreFab;
[SerializeField] int MaxEnemies = 30;
[SerializeField] float EnemySpawnTime = 1.00001f;
[SerializeField] GameObject FirstWaypoint;
int CurrentNumOfEnemies = 0;
public LevelManager myLevelManager;
public int maximumnumberofhits = 0;
public static EnemySpawner Instance = null;
int timesEnemyHit;
IEnumerator SpawningEnemies()
{
while(CurrentNumOfEnemies <= MaxEnemies)
{
GameObject Enemy = Instantiate(EnemyPreFab, this.transform.position, Quaternion.identity);
CurrentNumOfEnemies++;
yield return new WaitForSeconds(EnemySpawnTime);
}
}
void Start()
{
if (Instance == null)
Instance = this;
StartCoroutine(SpawningEnemies());
timesEnemyHit = 0;
if (this.gameObject.tag == "EnemyHit")
{
CurrentNumOfEnemies++;
}
}
public void OnEnemyDeath()
{
CurrentNumOfEnemies--;
if (CurrentNumOfEnemies < 1)
{
// You killed everyone, change scene:
LaserLevelManager.LoadLevel("NextLevelMenu");
}
}
}
EnemyShooting script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyShooting : MonoBehaviour {
[SerializeField] float EnemyLaserSpeed = 10f;
[SerializeField] float EnemyLaserFireTime;
[SerializeField] GameObject LaserBulletEnemyPreFab;
[SerializeField] int MaxNumberOfHits = 1;
int CurrentNumberOfHits = 0;
Coroutine FireCoroutine;
void OnTriggerEnter2D(Collider2D collider)
{
if(collider.gameObject.tag == "PlayerLaser")
{
if(CurrentNumberOfHits < MaxNumberOfHits)
{
CurrentNumberOfHits++;
Destroy(collider.gameObject);
Score.ScoreValue += 2;//The user will be rewarded 1 point
}
}
}
void DestroyEnemy()
{
if(CurrentNumberOfHits >= MaxNumberOfHits)
{
Destroy(gameObject);
EnemySpawner.Instance.OnEnemyDeath(); // Tell the EnemySpawner that someone died
}
}
private void Fire()
{
FireCoroutine = StartCoroutine(ShootContinuously());
}
void BecomeVisible()
{
Fire();
}
IEnumerator ShootContinuously()
{
while (true)
{
GameObject LaserBulletEnemy = Instantiate(LaserBulletEnemyPreFab, this.transform.position, Quaternion.identity) as GameObject;
LaserBulletEnemy.GetComponent<Rigidbody2D>().velocity = new Vector2(0, EnemyLaserSpeed);
EnemyLaserFireTime = Random.Range(0.5f, 0.9f);
yield return new WaitForSeconds(EnemyLaserFireTime);
}
}
// Use this for initialization
void Start () {
BecomeVisible();
}
// Update is called once per frame
void Update () {
DestroyEnemy();
}
}
I would add a two fields to the spawner script. EnemiesToNextLevel and KilledEnemies. Then in the OnEnemyDeath() of your spawner, you may increase KilledEnemies everytime it is called, and then ask if KilledEnemies >= EnemiesToNextLevel, before changing the scene.
Sure there are a lot of other ways to do it, but for me thats the easiest.
So I put the object in the scene and then I made it "invisible" (deactivate if you will) from the inspector (the checkmark box next to the object's name) and after waiting 8 seconds it doesn't become visible. I am using Unity 2d and C#.
I have the game start paused for three seconds then plays after that which works. The first script is that one. The item is supposed to reappear after 8 seconds so after the game resumes, which doesn't work.
//delay before level starts script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class countDown : MonoBehaviour
{
public GameObject CountDown;
private void Start()
{
StartCoroutine("StartDelay");
}
void Update()
{
}
IEnumerator StartDelay()
{
Time.timeScale = 0;
float pauseTime = Time.realtimeSinceStartup + 3f;
while (Time.realtimeSinceStartup < pauseTime)
yield return 0;
CountDown.gameObject.SetActive(false);
Time.timeScale = 1;
}
{
//script for the flower to appear
IEnumerator Start()
{
print(Time.time);
yield return new WaitForSeconds(8);
print(Time.time);
flowerInScene.gameObject.SetActive(true);
}
[SerializeField] Transform flowerInScene;
}
I still don't really get your two methods called Start
You can simply call a StartCoroutine at the end of another Coroutine so you can chain them together (though there are surely better ways to do what you want in general):
using System.Collections;
using UnityEngine;
public class CountDown : MonoBehaviour
{
public GameObject CountDownObject;
public GameObject flowerObject;
private void Start()
{
StartCoroutine(Delay());
}
private IEnumerator Delay()
{
yield return new WaitForSeconds(3);
HideCountdown();
StartCoroutine(FlowerDelay());
}
private void HideCountdown()
{
CountDownObject.SetActive(false);
}
private IEnumerator FlowerDelay()
{
yield return new WaitForSeconds(8);
ShowFlower();
}
private void ShowFlower()
{
flowerObject.SetActive(true);
}
}
I personaly don't like Coroutines .. they are not so easy to debug sometimes. I would prefer doing something like this with simple timers (though in the first moment it does look worse). Advantage is I can now directly watch the timer count down in the inspector:
using UnityEngine;
public class SimpleCountDown : MonoBehaviour
{
[Header("The Objects")]
public GameObject CountDownObject;
public GameObject FlowerObject;
[Header("Settings")]
// Here you can adjust the delay times
public float StartOffset = 3;
public float FlowerOffset = 8;
[Header("Debug")]
public float startTimer;
public float flowerTimer;
public bool isStartDelay;
public bool isFlowerDelay;
private void Start()
{
startTimer = StartOffset;
flowerTimer = FlowerOffset;
isStartDelay = true;
}
private void Update()
{
if (!isStartDelay && !isFlowerDelay) return;
if (isStartDelay)
{
startTimer -= Time.deltaTime;
if (startTimer <= 0)
{
HideCountdown();
isStartDelay = false;
isFlowerDelay = true;
}
}
if (isFlowerDelay)
{
flowerTimer -= Time.deltaTime;
if (flowerTimer <= 0)
{
ShowFlower();
isFlowerDelay = false;
this.enabled = false;
}
}
}
private void HideCountdown()
{
CountDownObject.SetActive(false);
}
private void ShowFlower()
{
FlowerObject.SetActive(true);
}
}