Need help with multiple updating transform positions
Hi all, So I'm trying to get my player in my game to attack the closest target within a distance when the player isn't moving. I'm new to programming, and I have mostly been using the Unity Learn site and googling to figure out solutions to the problems that arise. However I can't seem to figure out how to find the transform positions of multiple "Enemies" within my game. I can get it to work if I initialize my enemy gameobject constantly in the update method, but it only handles one random gameobjects position at a time. So the mechanic of getting the player to attack the closest enemy doesn't work, the player just attacks whatever gameobjects position was initialized first. How do I go about writing this code so that I can find all the positions of the enemies that spawn within a wave, and make it so my player attacks the closest one? Perhaps store multiple Vector3 variables somehow? Also if you have any suggestions or advice for my code, I'll happily take any criticism. I know it's probably sloppy and inefficient. Here is my code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Assertions.Must;
using UnityEngine.UIElements;
public class PlayerController : MonoBehaviour
{
protected Joystick joystick;
private Animator playerAnim;
private Rigidbody playerRb;
private GameObject[] enemy;
private GameObject firePos;
public GameObject arrowPrefab;
public HealthBar healthBar;
public int maxHealth = 100;
public int currentHealth;
private int enemyCount;
private float movementSpeed = 5.0f;
private float minAttackDistance = 8.0f;
private float rotationSpeed = 0.5f;
private float distance;
private float timer;
private float aimTime = 0.5f;
private float attackDelay = 1.0f;
private float attackTimer = 0f;
private float arrowDelay = 0.2f;
public bool gameOver;
void Start()
{
currentHealth = maxHealth;
healthBar.SetMaxHealth(maxHealth);
//Gets the fire position game object thats attached to the palyer
firePos = GameObject.Find("FirePos");
//Gets the Enemy Gameobject
enemy = GameObject.FindGameObjectsWithTag("Enemy");
//gets the animator on the player
playerAnim = GetComponentInChildren<Animator>();
//gets the Rigidbody on the player
playerRb = GetComponent<Rigidbody>();
//get the joystick asset from the scene
joystick = FindObjectOfType<Joystick>();
}
void Update()
{
timer += Time.deltaTime;
enemyCount = FindObjectsOfType<EnemyController>().Length;
if (enemyCount == 0)
{
enemy = GameObject.FindGameObjectsWithTag("Enemy");
}
//Code trying to get player to attack enemy within range
distance = Vector3.Distance(transform.position, enemy.transform.position);
//sets the animations to play for the player based on parameters
if (playerRb.velocity.magnitude > 0)
{
playerAnim.SetBool("Walk_b", true);
attackTimer = 0f;
}
else if (playerRb.velocity.magnitude == 0)
{
playerAnim.SetBool("Walk_b", false);
attackTimer += Time.deltaTime;
}
if (playerRb.velocity.magnitude > 3)
{
playerAnim.SetBool("Run_b", true);
}
else if (playerRb.velocity.magnitude < 3)
{
playerAnim.SetBool("Run_b", false);
}
if (currentHealth == 0)
{
playerAnim.SetBool("Death_b", true);
gameOver = true;
Debug.Log("Game Over!");
}
//Rotates the player towards enemy when within distance
if (distance < minAttackDistance)
{
//Rotates the player in the direction of the enemy
GenerateEnemyPos();
Quaternion targetRotation = Quaternion.LookRotation(GenerateEnemyPos() - transform.position);
targetRotation.x = 0;
targetRotation.z = 0;
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed);
}
//Statement that makes the player attack if within range and can attack
if (distance < minAttackDistance && playerRb.velocity.magnitude == 0 && timer >= attackDelay && attackTimer > aimTime)
{
playerAnim.SetBool("Attack_b", true);
InvokeRepeating("Attack", arrowDelay, attackDelay);
}
// gets the rigidbody component from the player
var rigidbody = GetComponent<Rigidbody>();
// adds force to the player in the direction of the joystick so the player moves if the game is not over
if (!gameOver)
{
rigidbody.velocity = new Vector3(joystick.Horizontal * movementSpeed, rigidbody.velocity.y, joystick.Vertical * movementSpeed);
//sets the joystick input for the for the rotation
Vector2 input = new Vector2(joystick.Horizontal, joystick.Vertical);
Vector2 inputDir = input.normalized;
//makes it so the players rotation doesnt snap back to the forward position
if (inputDir != Vector2.zero)
{
//Rotates the player based on the angle of the joystick
transform.eulerAngles = Vector3.up * Mathf.Atan2(inputDir.x, inputDir.y) * Mathf.Rad2Deg;
}
}
else if (gameOver)
{
movementSpeed = 0f;
rigidbody.velocity = new Vector3(movementSpeed, movementSpeed, movementSpeed);
}
}
private void OnCollisionEnter(Collision collision)
{
//When the player comes into contact with an enemy, hurt the player 20HP
if (collision.gameObject.CompareTag("Enemy"))
{
TakeDamage(20);
playerAnim.SetTrigger("Damage_trig");
if (currentHealth < 0)
{
currentHealth = 0;
}
}
}
//Method that decreases the health on the health bar
void TakeDamage(int damage)
{
currentHealth -= damage;
healthBar.SetHealth(currentHealth);
}
private void OnTriggerEnter(Collider collision)
{
//When the player collects a heartpickup heal the player 50HP
if (collision.gameObject.CompareTag("Heart"))
{
HealPlayer(50);
}
}
//Method that increases the health on the health bar
void HealPlayer(int health)
{
currentHealth += health;
healthBar.SetHealth(currentHealth);
if (currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
}
void Attack()
{
timer = 0f;
//spawns arrow to shoot at enemy
Instantiate(arrowPrefab, firePos.transform.position, transform.rotation);
CancelInvoke();
playerAnim.SetBool("Attack_b", false);
}
private Vector3 GenerateEnemyPos()
{
Vector3 enemyPos = enemy.transform.position;
return enemyPos;
}
}
Thanks in advance!
Answer by ShadyProductions · Sep 05, 2020 at 11:33 AM
When you instantiate your enemies keep them in a list
private List<GameObject> Enemies = new List<GameObject>();
public void SpawnWave()
{
// Eg:
var enemy = Instantiate(obj, transform, identity);
Enemies.Add(enemy);
}
public GameObject GetClosestEnemy(GameObject relativeTo)
{
GameObject closestEnemy = null;
float closestDistance = float.MaxValue;
foreach (var enemy in Enemies)
{
var distance = Vector3.Distance(relativeTo.transform.position, enemy.transform.position);
if (distance < closestDistance)
{
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
}
Then use like:
var closestEnemy = GetClosestEnemy(player);
Make sure to remove enemies that have died from the Enemies list.
Answer by SirTootall · Sep 05, 2020 at 09:20 PM
@ShadyProductions Thank you! This was very helpful.
So, since my SpawnWave(); method is controlled by a SpawnManager script, I put the list and the method in that script. Then I put the GetClosestEnemy(); method in my PlayerController script since the player is the thing looking for the closest enemy.
Here is my script for the SpawnManager:
using System.Collections;
using System.Collections.Generic;
using System.Security.Principal;
using UnityEngine;
public class SpawnManager : MonoBehaviour
{
public List<GameObject> enemies = new List<GameObject>();
public GameObject enemyPrefab;
private int enemyCount;
private int waveNumber = 1;
private float spawnRange = 9.0f;
// Start is called before the first frame update
void Start()
{
SpawnEnemyWave(waveNumber);
}
// Update is called once per frame
void Update()
{
enemyCount = FindObjectsOfType<EnemyController>().Length;
if (enemyCount == 0)
{
waveNumber++;
SpawnEnemyWave(waveNumber);
}
}
private Vector3 GenerateSpawnPos()
{
float spawnPosX = Random.Range(-spawnRange, spawnRange);
float spawnPosZ = Random.Range(-spawnRange, spawnRange);
Vector3 randomPos = new Vector3(spawnPosX, 0, spawnPosZ);
return randomPos;
}
public void SpawnEnemyWave(int enemiesToSpawn)
{
for (int i = 0; i < enemiesToSpawn; i++)
{
var enemy = Instantiate(enemyPrefab, GenerateSpawnPos(), enemyPrefab.transform.rotation);
enemies.Add(enemy);
}
}
}
I believe this script is good. Now my PlayerController script. I'm having trouble changing code I've written to work with this new code. First off, is there a way to reference the enemies List from another script? Second, these two if statements is how I was controlling the Player's rotation and projectile spawning:
//Rotates the player towards enemy when within distance
if (distance < minAttackDistance)
{
//Rotates the player in the direction of the enemy
var closestEnemy = GetClosestEnemy(player);
Quaternion targetRotation = Quaternion.LookRotation(closestEnemy.transform.position - transform.position);
targetRotation.x = 0;
targetRotation.z = 0;
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed);
}
//Statement that makes the player attack if (within range) and (can attack)
if (distance < minAttackDistance && playerRb.velocity.magnitude == 0 && timer >= attackDelay && attackTimer > aimTime)
{
playerAnim.SetBool("Attack_b", true);
InvokeRepeating("Attack", arrowDelay, attackDelay);
}
void Attack()
{
timer = 0f;
//spawns arrow to shoot at enemy
Instantiate(arrowPrefab, firePos.transform.position, transform.rotation);
CancelInvoke();
playerAnim.SetBool("Attack_b", false);
}
Is there a way to use the new var closestEnemy to get the player to rotate and fire at the closest enemy? And only when the closest enemy is within a certain distance, like attack range? Maybe with something like Transform.LookAt? Lastly, (if there is a way to reference lists from other scripts) the enemyController script is what controls the removal of enemies when their health reaches zero. So do I just put enemies.Remove(enemy) in the if statement to remove from List?
Again I want to thank you for your time. This is my first project and I'm still in the process of learning. So I apologize if these questions or my code seems redundant. :)
You could make Spawn$$anonymous$$anager a singleton, then you could access it in any class easily. However this requires there to be only 1 Spawn$$anonymous$$anager class in the scene.
private static Spawn$$anonymous$$anager _instance;
public static Spawn$$anonymous$$anager Instance { get { return _instance; } }
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(this.gameObject);
} else {
_instance = this;
}
}
And use like:
Spawn$$anonymous$$anager.Instance.enemies
You could use Transform.LookAt, and Vector3.$$anonymous$$oveTowards to do rotation and movement. And you can always use Vector3.Distance(pos1, pos1) to find the distance between the 2 And use if statement to check if it is below a certain threshold.
foreach (var enemy in Spawn$$anonymous$$anager.Instance.enemies)
{
if (currentHealth == 0)
{
Spawn$$anonymous$$anager.Instance.enemies.Remove(enemy);
}
}
if (currentHealth == 0)
{
Destroy(gameObject);
}
Would this work for removing an enemy from the list? I put this on the EnemyController script.
Also I am having problems with Null Reference errors when I destroy an enemy with the new code. It has to do with the distance var. I'm guessing it's because its not finding a new gameObjects distance when the gameObject it found the distance on initially is destroyed. This is on my PlayerController script in the update method.
distance = Vector3.Distance(GetClosestEnemy(player).transform.position, transform.position);
if (distance < $$anonymous$$AttackDistance)
{
var closestEnemy = GetClosestEnemy(player);
//Rotates the player in the direction of the enemy
Quaternion targetRotation = Quaternion.LookRotation(closestEnemy.transform.position - transform.position);
targetRotation.x = 0;
targetRotation.z = 0;
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed);
}
//Statement that makes the player attack if (within range) and (can attack)
if (distance < $$anonymous$$AttackDistance && playerRb.velocity.magnitude == 0 && timer >= attackDelay && attackTimer > aimTime)
{
playerAnim.SetBool("Attack_b", true);
InvokeRepeating("Attack", arrowDelay, attackDelay);
}
Other than that the code works perfectly! The player looks at and targets the closest enemy! $$anonymous$$aking the Spawn$$anonymous$$anager a singleton worked as well and enemies have no trouble spawning as they should. I can even kill the first enemy but when the second enemy dies that is when the errors pop up. Anyway thanks for your help! I'm learning so much from these tips and code lines! REALLY appreciate it :)
Forgot to mention this error as well:
InvalidOperationException: Collection was modified; enumeration operation may not execute. System.ThrowHelper.ThrowInvalidOperationException (System.ExceptionResource
To fix the NullReferneceExceptions, I ended up checking if the closestEnemy was null in the if statements. I don't know if that would work, but this is the only error that breaks the game now