2D Games Core Programming: Enemy Pickup Behavior
Objective: If a pickup is in front of an enemy, the enemy will fire its weapon at the pickup to destroy it before the player can get it.
I have decided to give this new functionality to the standard enemy type, not to the Laserbeam enemy and not to the Smart enemy (rear-firing).
Currently, the powerups spawn in the main game scene as the parent of the powerup. But the enemies spawn with the game object “Enemy_Container” as their parent game object. The Enemy_Container is a child of the Spawn_Manager.
I created a child game object “Powerup_Container” inside of the Spawn_Manager, to serve as the parent for all the instantiated powerups.
In the SpawnManager.cs script, in the variable declarations in the beginning of the script, I made a new variable for the Powerup_Container.
public GameObject powerupContainer;
In the Start() method I added a null check for the powerupContainer
void Start()
{
_gameManager = GameObject.Find("Game_Manager").GetComponent<GameManager>();
if (_gameManager == null)
{
Debug.LogError("SpawnManager::Start(). Game Manager is NULL");
}
if (_enemyContainer == null)
{
Debug.LogError("SpawnManager::Start(). Enemy Container is NULL");
}
if (powerupContainer == null) // NEW
{
Debug.LogError("SpawnManager::Start(). Powerup Container is NULL"); // NEW
}
}
In the coroutine SpawnPowerupRoutine(), after the powerup is instantiated, I made this new powerup a child of the powerupContainer.
IEnumerator SpawnPowerupRoutine(GameObject[] _spawnList, float _waitTimeMin, float _waitTimeMax)
{
int _randomPowerUpIndex = 0;
while ((_stopSpawning == false) && (_gameManager._isGameOver == false))
{
_waitTimePowerups = Random.Range(_waitTimeMin, _waitTimeMax);
yield return new WaitForSeconds(_waitTimePowerups);
if ((_stopSpawning == false) && (_gameManager._isGameOver == false))
{
// Instantiate prowerup prefab
_randomX = Random.Range(-_xPositionLimit, _xPositionLimit);
Vector3 spawnPosition = new Vector3(_randomX, _yPositionLimit, 0);
_randomPowerUpIndex = Random.Range(0, _spawnList.Length);
GameObject newPowerup = Instantiate(_spawnList[_randomPowerUpIndex], spawnPosition, Quaternion.identity);
newPowerup.transform.parent = powerupContainer.transform; // NEW
}
}
}
Because the Enemy_Container and Powerup_Container share the same parent, Spawn_Manager, making a reference to the powerups from the enemy script becomes easier.
In the Enemy.cs script, I made a method DetermineIfPowerupInfront() in which an array of powerups is made and populated by every powerup in the level. But this enemy script does not have a direct reference to the powerups. I referenced the parent objects and then down to the second child object which should always be the “Powerup_Container”.
For each powerup in the level, the Dot Product function was used to determine if the powerup is in front of this enemy. If it is, a laser is fired.
private void DetermineIfPowerupInfront()
{
// Make array of powerups. Second child of the Spawn_Manager will always be the "Powerup_Container" game object
Powerup[] _allPowerUps = transform.parent.parent.GetChild(1).GetComponentsInChildren<Powerup>();
for (int i = 0; i < _allPowerUps.Length; i++)
{
Vector3 directionPowerup = _allPowerUps[i].transform.position - transform.position;
directionPowerup.Normalize();
// Check if the powerup is in front of this enemy
float checkFront = Vector3.Dot(directionPowerup, -transform.up);
if ((checkFront > -0.75f) & (checkFront < 0.0f))
{
Invoke("FireLaserNormal", 0.1f);
}
}
}
The method DetermineIfPowerupInfront() was called when the enemy is initially spawned and when it wraps around the end of the screen.
void Start()
{
_player = GameObject.Find("Player").GetComponent<Player>();
if (_player == null)
{
Debug.LogError("Enemy::Start() No _player");
}
_gameManager = GameObject.Find("Game_Manager").GetComponent<GameManager>();
if (_gameManager == null)
{
Debug.LogError("Enemy::Start(). Game Manager is NULL");
}
_enemyAnimator = GetComponent<Animator>();
if (_enemyAnimator == null)
{
Debug.LogError("Enemy::Start() No _enemyAnimator");
}
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
{
Debug.LogError("Enemy::Start() _audioSource is NULL.");
}
if (_laserPrefab == null)
{
Debug.LogError("Enemy::Start() _laserPrefab is NULL. Add prefab in Inspector.");
}
_laserSpawnPoint = this.gameObject.transform.GetChild(0).gameObject; // First child object is the front spawn point
if ((_laserSpawnPoint == null) | (this.gameObject.transform.GetChild(0).name != "Laser_Spawn_Front"))
{
Debug.LogError("Enemy::Start() _laserSpawnPoint is NULL or not Laser_Spawn_Front.");
}
switch (_enemyID)
{
case _enemyIDs.LaserBeam:
_explosionAnimLength = 0.0f;
break;
case _enemyIDs.SmartRearLaser:
_explosionAnimLength = 0.0f;
_laserSpawnPointBack = this.gameObject.transform.GetChild(1).gameObject; // Second child object is the front back point
if ((_laserSpawnPointBack == null) | (this.gameObject.transform.GetChild(1).name != "Laser_Spawn_Back"))
{
Debug.LogError("Enemy::Start() _laserSpawnPoint is NULL or not Laser_Spawn_Back.");
}
break;
case _enemyIDs.Standard:
default:
if (_enemyShieldsOnEnemy == null)
{
Debug.LogError("Enemy::Start: No Enemy Shields Game Object");
}
_explosionAnimLength = 2.6f;
Invoke("SheildsInitialize", 0.1f);
DetermineIfPowerupInfront(); // NEW
break;
}
}
private void CalcMovementAtScreenLimits()
{
float _randomXPos = Random.Range(-_horizontalLimit, _horizontalLimit);
//if bottom of screen
if (transform.position.y <= -_verticalLimit)
{
//move to top with a new random x position
transform.position = new Vector3(_randomXPos, _verticalLimit, 0);
DetermineIfPowerupInfront(); // NEW
}
// if left or right side of screen
if (Mathf.Abs(transform.position.x) > _horizontalLimit)
{
float _randomYPos = Random.Range(-_verticalLimit, _verticalLimit);
transform.position = new Vector3(-transform.position.x, _randomYPos, 0);
DetermineIfPowerupInfront();// NEW
}
}
When the level is tested, all the powerups can be destroyed and the Standard Enemy fires at a powerup if it is in front of the enemy.