Here is the updated system. The initial post is here
I’ve incorporated.
- Particle Effects
- Homing / Non-Homing Projectiles
- Random values for Arrow angle / Time between shots (for enemies)
- On collision system for all projectiles - the enemies can damage themselves now
Here’s how it looks in action
I had to switch the trigger system to the collision in order to detect when the non-homing arrows were landing on terrain or other objects in the Scene. Here’s how my prefabs look like.
Here’s my Projectile.cs code
using RPG.Core;
using UnityEngine;
using Random = UnityEngine.Random;
namespace RPG.Combat
{
public class Projectile : MonoBehaviour
{
[SerializeField] private float speed = 0.4f;
[SerializeField] private GameObject hitVFX;
[SerializeField] private bool isHoming = false;
[SerializeField] private GameObject[] destroyOnHit = null;
[SerializeField] private float lifeAfterImpact = 1;
private Health target = null;
private float damage = 0f;
private Vector3 initialPosition;
private Quaternion initialRotation;
private float angleMin = 5f;
private float angleMax=25f;
private float launchAngle;
private Rigidbody rigidbody;
private Vector3 lastPosition;
private float hitOffset = 0.4f;
private void Start()
{
rigidbody = GetComponent<Rigidbody>();
initialPosition = transform.position;
initialRotation = transform.rotation;
//Set the arrow angle a random value to create diversity in each shot
launchAngle = Random.Range(angleMin,angleMax);
//Get player position on Start - this returns the middle point of the collider
lastPosition = GetAimLocation();
}
void Update()
{
if(target==null) return;
if (isHoming && !target.IsDead())
{
transform.LookAt(GetAimLocation());
}
else
{
//We set the y axis to 0.1f - this will generate a collision with the terrain if the projectile doesnt hit the player
transform.LookAt(new Vector3(lastPosition.x,0.1f,lastPosition.z));
}
Launch();
//In case we dont collide with anything we destroy the object anyway.
Destroy(gameObject,lifeAfterImpact);
}
public void SetTarget(Health target,float damage)
{
this.target = target;
this.damage = damage;
}
private Vector3 GetAimLocation()
{
//This returns the center point on all axis of the Collider component
Vector3 aimLocation = target.GetComponent<Collider>().bounds.center;
return aimLocation;
}
private void OnCollisionEnter(Collision other)
{
//Here we get the collision point from the collider
ContactPoint contact = other.contacts[0];
Quaternion rotation = Quaternion.FromToRotation(Vector3.up, contact.normal);
Vector3 position = contact.point + contact.normal * hitOffset;
//This Debug checks to see if you collide with the enemy or player on instantiation.
//Debug.Log("Collided with"+ other.transform.name);
rigidbody.constraints = RigidbodyConstraints.FreezeAll;
speed = 0;
//We only take damage is the collision has a Health Component - this enables the enemies to attach each other
if (other.transform.GetComponent<Health>() != null)
{
other.transform.GetComponent<Health>().TakeDamage(damage);
}
//We instantiate the hit VFX and rotate.
Instantiate(hitVFX,position,rotation);
hitVFX.transform.LookAt(contact.point + contact.normal);
foreach (GameObject toDestroy in destroyOnHit)
{
Destroy(toDestroy);
}
Destroy(gameObject);
}
//Old Trigger Code from the Course
// private void OnTriggerEnter(Collider other)
// {
// if (other.GetComponent<Health>()!=target) return;
// if (target.IsDead()) return;
// Instantiate(hitVFX,GetAimLocation(),Quaternion.identity);
//
// target.TakeDamage(damage);
// foreach (GameObject toDestroy in destroyOnHit)
// {
// Destroy(toDestroy);
// }
// Destroy(gameObject,lifeAfterImpact);
// }
private void Launch()
{
Vector3 projectileXZPos = new Vector3(transform.position.x, 0.1f, transform.position.z);
float R = Vector3.Distance(projectileXZPos, GetAimLocation());
float G = Physics.gravity.y;
float tanAlpha = Mathf.Tan(launchAngle * Mathf.Deg2Rad);
float H = target.transform.position.y - transform.position.y;
float Vz = Mathf.Sqrt(G * R * R / (speed * (H - R * tanAlpha)));
float Vy = tanAlpha * Vz;
Vector3 localVelocity = new Vector3(0f, Vy, Vz);
Vector3 globalVelocity = transform.TransformDirection(localVelocity);
//This makes the projectile go forward
rigidbody.velocity = globalVelocity;
//This rotates the projectile correctly on the arc
transform.rotation = Quaternion.LookRotation(rigidbody.velocity) * initialRotation;
}
}
}
One issue I had with the collision system: dead enemies still have the colliders enabled once dead. This means the projectiles were still hitting them. I’ve decided to change them to triggers, rather than disable them completely. I figured maybe we will use the collider in the inventory system or other interaction.
I also took into account having separate Physics Layers for each projectile and character, but that would have been a nightmare to implement and keep track of.
Here is the small addition to the Die() function - in Heath.cs
void Die()
{
//if the target is already dead, dont play the animation again
if (isDead) return;
isDead = true;
GetComponent<Animator>().SetTrigger("die");
GetComponent<ActionScheduler>().CancelCurrentAction();
GetComponent<Collider>().isTrigger = true;
}