Homing Projectiles: Two Use Cases
There are two use cases for homing projectiles in my 2D Space Shooter. One is to follow the player, which is a unique game object. The other case is the player’s projectile that may destroy various enemy types, but also follows a particular type of enemy.
- Homing Beam from Enemy B
- Homing Missile from Player
Let’s start with the Enemy B Homing Beam which is a bit simpler.
In a previous article, the enemy B special ability was discussed and when it has an ID of 1, enemy B starts with a homing beam ready to be shot to the player whenever the enemy flies bellow -6.5 on the Y Axis. At this point the enemy calls the Public Void ActivateSeeking() seen bellow:
public class HomingBeam : MonoBehaviour
{
private bool _isSeeking;
private Player _player;
private Transform _target;
private Rigidbody2D _rigidBody;
private float _rotateSpeed = 200;
private float speed = 5f;
// Start is called before the first frame update
void Start()
{
_player = GameObject.Find("Player").GetComponent<Player>();
if (_player == null)
{
Debug.Log("Player is null");
}
_target = GameObject.FindGameObjectWithTag("Player").transform;
_rigidBody = GetComponent<Rigidbody2D>();
_isSeeking = false;
}
// Update is called once per frame
void FixedUpdate()
{
if (_isSeeking)
{
SeekTarget();
}
}
public void ActivateSeeking()
{
_isSeeking = true;
}
void SeekTarget()
{
if (_target != null)
{
Vector2 direction = (Vector2)_target.position - _rigidBody.position;
direction.Normalize();
float rotateAmount = Vector3.Cross(direction, transform.up).z;
_rigidBody.angularVelocity = -rotateAmount * _rotateSpeed;
_rigidBody.velocity = transform.up * speed;
} else
{
Destroy(this.gameObject);
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
_player.Damage();
Destroy(this.gameObject);
}
else if (other.tag == "Laser")
{
Destroy(this.gameObject);
}
}
}
This code makes the homing projectile start flying towards the player as soon as it is unparented and activated. It is a great enemy to kill as soon as possible since destroying the missile takes a lot of skill and an extra bullet; also, positioning the player in the right scenario to avoid the shot is a hard challenge and might end up in the player taking more damage.
The second case used in this game for a homing projectile is the player’s missile. The enemy’s homing beam is a prefab that is instantiated with every enemy B; for the player’s missile, I wanted to recycle the same missile, so a very different loop occurs when it hits an enemy or loses a target
public class HomingMissile : MonoBehaviour
{
private bool _isSeeking;
[SerializeField]
private GameObject _playerShip;
private GameObject _enemyTarget;
private Rigidbody2D _rigidBody;
private float _rotateSpeed = 200;
private float _speed = 5f;
private Transform _originalPosition;
// private bool _onboardingMessage;
[SerializeField]
private GameObject _missileThruster;
[SerializeField]
private bool _targetFound;
[SerializeField]
private SpriteRenderer _targetIndicator;
// Start is called before the first frame update
void Start()
{
// _onboardingMessage= true;
_targetFound = false;
_rigidBody = GetComponent<Rigidbody2D>();
_isSeeking = false;
OriginalPosition();
this.gameObject.SetActive(true);
}
// Update is called once per frame
void FixedUpdate()
{
if (!_targetFound && _isSeeking)
{
_enemyTarget = GameObject.FindGameObjectWithTag("Sweeper");
if (_enemyTarget == null)
{
Debug.Log("enemy not yet found");
}
else if (_enemyTarget != null)
{
_targetFound = true;
_targetIndicator.color = Color.green;
}
}
if (_isSeeking)
{
SeekTarget();
}
}
public void ActivateSeeking()
{
_isSeeking = true;
}
void SeekTarget()
{
_missileThruster.SetActive(true);
this.transform.SetParent(null);
if (!_targetFound)
{
transform.Translate(Vector3.up * _speed * Time.deltaTime);
if (transform.position.y > 8)
{
OriginalPosition();
}
}
else if (_targetFound)
{
if (_enemyTarget == null)
{
_targetFound = false;
_isSeeking = false;
OriginalPosition();
}
else
{
Vector2 direction = (Vector2)_enemyTarget.transform.position - _rigidBody.position;
direction.Normalize();
float rotateAmount = Vector3.Cross(direction, transform.up).z;
_rigidBody.angularVelocity = -rotateAmount * _rotateSpeed;
_rigidBody.velocity = transform.up * _speed;
}
}
}
public void OriginalPosition()
{
_missileThruster.gameObject.SetActive(false);
this.transform.SetParent(_playerShip.transform);
transform.position = _playerShip.transform.position;
_targetIndicator.color = Color.red;
_targetFound= false;
_enemyTarget = null;
_isSeeking= false;
this.gameObject.SetActive(false);
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Enemy Laser")
{
OriginalPosition();
}
}
}
Once it found a target, if the target becomes null. It comes back to its original position to avoid bugs where the missile would deactivate but stay amid flight.
Here, the missile’s red indicator, becomes green once the sweeper is found and it starts flying towards it.
In this other case, there is no sweeper found, but it still manages to kill an enemy Ram even if the indicator is red.
It is a very short shot, but you can tell how ram’s whole health is lowered and also we get to see both cases in the same GIF.
This functionalities are flexible and great for a fast and dynamic game where you might shoot an enemy to death before the missile even reaches it. These projectiles are very fun to play with and I love the challenge they were, but they satisfaction they leave for this game play.