Updated - Projectile Parabola Motion and Rotation (Projectiles fly on an arc)

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;
        }

3 Likes

You’ve put a lot of work into this. Well done!

1 Like

I see that you did your best to achieve the next level and, sincerely, I’ve no doubt about that! The mechanic is fluid and amazingly beautiful.
Well done! :clap: :clap: :clap:
Your dedication is fantastic @mihaivladan !

1 Like

Thanks, Phillipe! Appreciate the kind words. Hope it helps current/future students of this great course! And as always - if you have any suggestions on how to improve it, or you found/fixed some bugs please share :slight_smile: :sweat_smile:

1 Like