RPG Combat System - 11.3 :: OnTriggerEnter does not recognize my target

With a heavy, bruised ego I beseech advice again. I wanted to implement a few new features not covered (thus far) in the series. After refactoring keenly, I have somehow made a previously functional OnTriggerEnter to forget whom it answers to.

PREAMBLE / TLDR :: When I began this project I was using a UnitManager as an insulated container of common stats and pass through functions. I am just beginning to migrate that class towards BaseClass. It stands for now to avoid sledgehammer refactoring. So, That explains that.

My “ Projectile ” and “ Weapon “ classes are just ugly cousins to those in the course. They do work as intended other than this little time consumer of a headache. You see, I just had to have a throwable spear that can be retrieved and reused. And, an arced trajectory! No girl will ever talk to me if my arrows don’t simulate physics. After seriously crash coursing some trigonometry and calculus, I am now fully convinced that the operands used in both disciplines were taken directly from the Necronomicon. There is itching madness if you stare too long. But! I finally got it to work swimmingly* with some snooping and brain souping.

*It doesn’t work swimmingly.

PROBLEM

The snag points immediately to Ballistics.OnTriggerEnter. When the call to the enemy’s health is made, she nullifies. After that discovery I sprayed and prayed Debug bullets all over.

Code Blocks from CombatManager:

private void ContinueRangedAttack()
{
if (enemy.GetComponent().GetIsDead() == false)
{
Debug.Log(“CombatManager - ContinueRangedAttack() - Nobody dead.”);
equippedWeapon.MarkEnemy(enemy);
Debug.Log("CombatManager - ContinueRangedAttack() - Marked enemy = " + enemy.name.ToString());
firingRangedWeapon = true;
StartCoroutine(FireBow(enemy));
lastAttack = 0;
}
}

On To

IEnumerator FireBow(UnitManager target)
{
Debug.Log(“CombatManager, coroutine fire bow started”);
if (equippedWeapon == null || equippedWeapon.GetIsBow() == false)
{
firingRangedWeapon = false;
yield return null;
}

 /* Since the bow fires projectiles and is not a projectile itself
    the AttackRange of the bow is used instead of the seperate ProjectileRange
    used for how far a weapon can be thrown */

 scheduler.StartAction(this);
 movement.MoveToDestination(target.transform.position);
 float range = equippedWeapon.GetBowRange();

 if (Vector3.Distance(transform.position, target.transform.position) < range)
 { 
     firingRangedWeapon = true;
     movement.CancelAction();
     transform.LookAt(target.transform.position);
     handler.OnHandleBowAnimations();
     yield return new WaitForSeconds(animationDelay);

     equippedWeapon.FireProjectile(target);
 /* To avoid repeat fire */
     CancelAction();

 }

 firingRangedWeapon = false;

}

Next up is the Weapon Handler

public class WeaponHandler : ScriptableObject
{
    [Header("Weapon Components")]
    [SerializeField] AnimatorOverrideController overrideController = null;
    [SerializeField] Rigidbody equippedWeapon = null;
    [SerializeField] Ballistics projectile = null;
    [SerializeField] GameObject hitFX = null;
    [Header("Weapon Class")]
    [SerializeField] bool ranged = false;
    [SerializeField] bool majik = false;
    [SerializeField] bool canThrow = false;
    [Header("Ranged Attack")]
    [SerializeField] float attackRange = 0f;
    [SerializeField] float majikRange = 0f;
    [SerializeField] float throwRange = 0;
    [Header("Damage")]
    [SerializeField] int damage = 0;

    Transform handTransform = null;
    UnitManager enemy;

    const string storedWeaponData = "EquippedWeapon";

    #region Common Functions
    public void MarkEnemy(UnitManager enemyTarget)
    {
        enemyTarget = enemy;
    }

    public void DisplayHitFX(Vector3 position)
    {
        if (hitFX != null)
        {
            GameObject fx = Instantiate(hitFX, position, Quaternion.identity);
        }
    }
    #endregion Common Funcions

    #region Ranged Attack Functions
    public void FireProjectile(UnitManager enemy)
    {
        if (projectile == null)
        {
            return;
        }

        if (ranged == false && canThrow == false)
        {
            return;
        }

        Ballistics missle = Instantiate(projectile, handTransform.position, Quaternion.identity);
        missle.SetHandTarget(handTransform);
        missle.TargetEnemy(enemy, damage);
    }
    #endregion Ranged Attack Functions


    #region Arm And Remove Equipment Functions
    private Transform GetHandTarget(Transform leftHandTarget, Transform rightHandTarget)
    {
        Transform selectedTarget;
        if (ranged == false)
        {
            selectedTarget = rightHandTarget;
        }
        else
        {
            selectedTarget = leftHandTarget;
        }

        return selectedTarget;
    }

    public void EquipWeapon(Transform leftHandTarget, Transform rightHandTarget, Animator animator)
    {
        ClearWeapon(leftHandTarget, rightHandTarget);

        if (equippedWeapon != null)
        {
            handTransform = GetHandTarget(leftHandTarget, rightHandTarget);
            Rigidbody weapon = Instantiate(equippedWeapon, handTransform);
            weapon.name = storedWeaponData;
            equippedWeapon.useGravity = false;
            equippedWeapon.isKinematic = true;
        }

        AnimatorOverrideController controller =
                overrideController.runtimeAnimatorController as AnimatorOverrideController;

        if (overrideController != null)
        {
            animator.runtimeAnimatorController = overrideController;
        }
        else if (controller != null)
        {
            animator.runtimeAnimatorController = controller.runtimeAnimatorController;
        }
    }

    private static void ClearWeapon(Transform rightHand, Transform leftHand)
    {
        Transform equippedWeapon = rightHand.Find(storedWeaponData);

        if (equippedWeapon == null)
        {
            equippedWeapon = leftHand.Find(storedWeaponData);
        }

        if (equippedWeapon == null) return;

        equippedWeapon.name = "GameObjectToDestroy";
        Destroy(equippedWeapon.gameObject);
    }

    public void DisconnectWeapon()
    {
        equippedWeapon.useGravity = true;
        equippedWeapon.isKinematic = false;
        equippedWeapon.GetComponent<Collider>().enabled = true;
        equippedWeapon.transform.parent = null;
    }
    #endregion Arm And Remove Equipment Functions

Another Heavy. Ballistics.

public class Ballistics : MonoBehaviour
{
UnitManager enemy = null;
[Header(“Class”)]
[SerializeField] bool tracking = false;
[SerializeField] bool thrown = false;
[SerializeField] bool majik = false;
[Header(“If thrown weapon, add this game object to recover after the throw.”)]
[SerializeField] Rigidbody recoveredWeapon = null;
[SerializeField] QuadraticCurve trajectory;
[SerializeField] GameObject hitFX = null;

 [SerializeField] float speed = 0f;
 [SerializeField] float flightRangeModifier = 0;

 [SerializeField] float lift = 0f;
 [SerializeField] float lifetime = 1f;
 [SerializeField] int damage = 0;
 [SerializeField] int burstForce = 8;
 
 Transform handTarget = null;
 int netDamage = 0;
 bool itemDropped = false;
 float evaluationTime = 0f;

 private void Start()
 {
     if (enemy == null) 
     {
         Destroy(gameObject);
     }

     SetFlightPath();
 }

 private void Update()
 {
     if (enemy == null)
     {
         return;
     }

     if (tracking)
     {
         trajectory.SetTarget(GetImpactZone());
     }

     evaluationTime += speed * Time.deltaTime;
     transform.position = trajectory.FollowTrajectory(evaluationTime);
     transform.forward = trajectory.FollowTrajectory(evaluationTime + 0.001f) - transform.position;
     Destroy(gameObject, lifetime);
 }

 #region Targeting Instructions
 public void TargetEnemy(UnitManager selectedTarget, int damage)
 {
     Debug.Log("Ballistics - TargetEnemy() = " + selectedTarget.name.ToString());
     Debug.Log("Ballistics - TargetEnemy() - damage input = " + damage);
     enemy = selectedTarget;
     netDamage = Random.Range(1, damage) + damage;
     Debug.Log("Ballistics - TargetEnemy() - net damage = " + netDamage);
 }

 /* <<<<< THIS RIGHT HERE >>>>> */
 Vector3 PeakOfFlight()
 {
     float halfway = 0.5f;
     Vector3 scaledPosition = halfway * Vector3.Normalize(enemy.transform.position - transform.position)
                              + transform.position;
     scaledPosition.y += lift;
     return scaledPosition;  
 }
 /* <<<<< IS SOLID GOLD >>>>> */
 /* NEVER REMOVE NEVER DESROY */

 Vector3 GetImpactZone()
 {
     BoxCollider targetCollider = enemy.GetComponent<BoxCollider>();
     if (targetCollider == null)
     {
         return enemy.transform.position;
     }
     Debug.Log("Ballistics - GetImpactZone() - enemy = " + enemy.name.ToString());
     Debug.Log("Ballistics - GetImpactZone() - enemy position = " + enemy.transform.position);
     return enemy.transform.position + Vector3.up * targetCollider.size.y / 2;
 }
 #endregion Targeting Instructions

 #region Impact Instuctions
 void OnTriggerEnter(Collider collider)
 {
     Debug.Log("OnTriggerEnter() - collider = " + collider.gameObject.name.ToString());
     StartCoroutine(HandleImpact(collider));
 }

 IEnumerator HandleImpact(Collider collider)
 {
     if (enemy == null || collider.GetComponent<UnitManager>() != enemy)
     {
         yield return null;
     }

     /* THINGS GO CLUNK HERE */
     Debug.Log("HandleImpact() enemy is identified as " + enemy.name.ToString());

     if (enemy.GetComponent<Health>().GetIsDead())
     {
         tracking = false;
         Destroy(gameObject);
         yield return null;
     }

     if (thrown && recoveredWeapon != null && itemDropped == false)
     {
         if (enemy.GetComponent<Health>().GetIsDead())
         {
             Rigidbody droppedWeapon = Instantiate(recoveredWeapon, enemy.transform.position, transform.rotation);
             Debug.Log("Ballistics - HandleImpact() - enemy supposedly died. Weapon dropped.");
         }
         else
         {
             Rigidbody droppedWeapon = Instantiate(recoveredWeapon, GetImpactZone(), transform.rotation);
             Debug.Log("Ballistics - HandleImpact() - Weapon dropped.");
         }

         itemDropped = true;
     }

     if (collider.GetComponent<UnitManager>() == enemy)
     {
         Debug.Log("Ballistics - HandleImpact() - DamageEnemy(). The enemy is " + enemy.name.ToString());
         DamageEnemy();
     }

     Destroy(gameObject, lifetime);
 }

 public void DamageEnemy()
 {
     Debug.Log("DamageEnemy() - enemy == " + enemy.name.ToString());
     int evasion = enemy.GetEvasion();
     Debug.Log("DamageEnemy() - evasion = " + evasion);
     int hitRoll = Random.Range(1, 21);
     Debug.Log("DamageEnemy() - hitroll = " + hitRoll);
    
     if (hitRoll > evasion)
     {
         Debug.Log("Ballistics - hit roll = " + hitRoll + " Vs. evasion of " + evasion);
         int attackDamage = Random.Range(1, netDamage);
         enemy.GetComponent<Health>().TakeDamage(attackDamage);
         Debug.Log("Ballistics - DamaegeEnemy() - TakeDamage(" + attackDamage + ")");

         if (hitFX != null)
         {
             Debug.Log("Ballistics - DamageEnemy() - hit fx called for.");

             GameObject fx = Instantiate(hitFX, GetImpactZone(), transform.rotation);
         }

         if (burstForce >= 0)
         {
             Debug.Log("Ballistics - DamageEnemy() - burst force = " +  burstForce);
             Push(enemy);
         }
     }
 }

 void Push(UnitManager target)
 {
     Vector3 heading = enemy.transform.position - transform.position;
     float distance = heading.magnitude;
     Vector3 direction = heading / distance;
     enemy.GetComponent<Rigidbody>().AddForce(direction * burstForce);
 }

 public int GetProjectileDamage()
 {
     return damage;
 }
 #endregion Impact Instructions

 #region FlightInstructions
 public void SetFlightPath()
 {
     trajectory.SetLaunchPoint(handTarget.position);
     trajectory.SetControlPoint(PeakOfFlight());
     trajectory.SetTarget(enemy.transform.position);
     Debug.Log("Ballistics - SetFlightPath() - " + enemy.name.ToString() + " at " + enemy.transform.position);
 }

 public float GetRangeModifier()
 {
     return flightRangeModifier;
 }

 public void SetHandTarget(Transform target)
 {
     Debug.Log("Ballistics - SetHandTarget(" + target.name.ToString() + ")");
     handTarget = target;
 }
 #endregion FlightInstructions

Throwing actions are handled just a little differently so that logic can be ignored for now.

With all that I have a console full of:

My Inspector worn out from double checking. I have moved things all around to no avail. I have tried removing Cancel calls, being crazy person redundant with identifying colliders and game objects (like every other line sort of OCD). I am somehow missing the Big Simple.

I am positive that there is much to sneer at with my code efficiency and logic. Be gentle kind friends.

Your first problem is in the HandleImpact coroutine. If your enemy is null or the enemy is not the one collided with, you yield return null. This only lets the coroutine wait a frame before continuing on with the rest of the code. If enemy was null, you’d be getting NRE’s from there onwards - as you do. To exit the coroutine instead, you need to use yield break. This will exit the coroutine at that point. I haven’t read most of the post (sorry, I am lazy today) so if there are more issues after that, let me know and we can take a look

First thing that literally just jumped out and screamed at me. I have a method just at the top of WeaponHandler.

public void MarkEnemy(UnitManager enemyTarget)
{
enemyTarget = enemy;
}

Oops. This makes more sense.

public void MarkEnemy(UnitManager enemyTarget)
{
enemy = enemyTarget;
}

SO when I fixed THAT… I fixed nothing.

Also, 'bout the yield return null business. A big “Um, yeah, I know that,” dribbled from my slacked jaw. Actually, that function did not begin life as a coroutine. The yield return nulls were just hypno-typing to dodge the little red, wavey snakes that remind you of all of the errors in your own life. That oversight is akin to rearranging the furniture in the den and covering up a couple of electrical outlets.

I’ll keep digging. I don’t smell brimstone yet.

The same thing is happening in the FireBow coroutine. If there’s no weapon, or the weapon is not a bow, you still try to do bow-related things.

With very few and slight changes I got it all to work. It feels a bit amateurish but if I just check at the very beginning of OnTriggerEnter with a simple…

if (collider.Tag == “Enemy”)
{
enemy = collider.GetComponent<Health();
}

… I am once again raining death upon BadMonkey(s) everywhere. I am steering my project more towards this course’s tutelage but as I am ever a curious student and a feature creature, I am bound to butt against more than a few obstacles.

Oh, Bixarrio, I haven’t seen you about for a minute. I haven’t been stalking the forums for ya’ll either. Anyway, your input is always welcome and respected. By pointing out the mindless “return nulls” you helped me free the tires from the rut.

I am always open for criticism and pointers. They help me improve. Then one day, when I have learned enough, I can lord my knowledge over the lesser beings and make them my slaves. Thank you.

Good stuff. I was trying to follow the path of the code but I wasn’t seeing much. Based on the console entries, I noticed that the HandleImpact would be called for EquippedWeapon as well as pf_player_master before it gets to the BadMonkey. The NRE also occurs before that, so I was trying to see of something in the coroutine wouldn’t be setting the enemy to null before it even gets to the actual enemy but nothing jumped out at me. Glad you solved it, though

This may be the best reason to fix a feature in a game I have ever seen!

Pages 199 to be specific.

Oh beware ye master of the dark arts, and understand the principle that what goes up must go down. Commit yourself to the sin of the acute angle of thy shot and know that whilst it rises, it shall also fall in a gracious arc.

This is the way

Wait, what? Unity doesn’t have a Quadratic curve…

For arced trajectories, you might find a great deal more simplicity in an AnimationCurve.

        private AnimationCurve animCurve;
        private float distance;
        private void Start()
        {
            if(target)
            {
                targetPoint = GetAimLocation();
                distance = Vector3.Distance(transform.position, targetPoint);
                float height = (targetPoint.y + transform.position.y) / 2 + 10f;
                transform.LookAt(targetPoint);
                animCurve.AddKey(0, transform.position.y);
                animCurve.AddKey(distance / 2f, height);
                animCurve.AddKey(distance, targetPoint.y);
            }
        }

Finally, in Update()

            distance += Time.deltaTime;
            transform.Translate(Vector3.forward * speed * Time.deltaTime);
            transform.position = new Vector3(transform.position.x, animCurve.Evaluate(distance), transform.position.z);

Of course, this doesn’t account for the proper arc of the arrow in terms of direction it should be facing (in other words, it’s always going to be pointing to the target even as it’s rising… That’s a set of calculations for another day.

On a more serious note, good job on this!

For the quadratic curve, it was a simple mashing together of a few examples I found out and about. I ran tests and eventually succeeded. Further study confirmed most of my logic (test runs made sense on notepad but never looked correct, I knew there was more to touch on). A more elegant prose presented itself. I made it work. The calculations remain in their own separate script that just rides along with the projectile.

using UnityEngine;

namespace ThirdLevelAccess
{
/* Tip of the Day:
Don’t pet burning dogs */
public class QuadraticCurve : MonoBehaviour
{
public Transform x1;
public Transform x2;
public Transform crest;

    public Vector3 FollowTrajectory(float time)
    {
        Vector3 x1y = Vector3.Lerp(x1.position, crest.position, time);
        Vector3 yx2 = Vector3.Lerp(crest.position, x2.position, time);
        return Vector3.Lerp(x1y, yx2, time);
    }

    private void OnDrawGizmos()
    {
        if (x1 == null || x2 == null || crest == null)
        {
            return;
        }

        Gizmos.color = Color.cyan;

        for (int i = 0; i < 20; i++)
        {
            Gizmos.DrawWireSphere(FollowTrajectory(i / 20f), 0.1f);
        }
    }

    public void SetLaunchPoint(Vector3 startPosition)
    {
        x1.position = startPosition;
    }

    public void SetControlPoint(Vector3 elevation)
    {
        crest.position = elevation;
    }

    public void SetTarget(Vector3 targetPosition)
    {
        x2.position = targetPosition;
    }
}

}

In Ballistic.cs - Start() the path is assigned its values.

public void SetFlightPath()
{
trajectory.SetLaunchPoint(handTarget.position);
trajectory.SetControlPoint(PeakOfFlight());
trajectory.SetTarget(enemy.transform.position);
}

Then in Update it is business (almost) as usual.

float evaluationTime += speed * Time.deltaTime;
transform.position = trajectory.FollowTrajectory(evaluationTime);
transform.forward = trajectory.FollowTrajectory(evaluationTime + 0.001f) - transform.position;

But this, this right here I am proud of. I was so “preferred explicative here” frustrated with my projectile defying time / space. Then I fell off the toilet, hit my head, bought a Delorian and invented

Vector3 PeakOfFlight()
{
float halfway = 0.5f;
Vector3 scaledPosition = halfway * Vector3.Normalize(enemy.transform.position - transform.position)
+ transform.position;
scaledPosition.y += lift;
return scaledPosition;
}

Ta-Freakin’-Da.

Now, what really got me headed after the white rabbit was that I wondered if instantiating and destroying so frequently would ultimately add up to too much processing when I have several controllable characters and enemies trading fire. The architecture is there for mini-armies (RTS minded but more humble) complete with tactical pausing (I figure that I can leverage the down time to clean up some messes behind the curtain). So, I began on this path with this in mind: When a ranged weapon is equipped; instantiate into a list x number of projectiles with the mesh renderer off, and then deactivate it. The premise is to activate the arrow, remove from quiver, beam it undetected to its specified launch point, turn on the lights, shoot the bad guy. Then all of the previous posts happened concurrent with dinking around on this. I had to commit to that task first or suffer the temptation to turn my workstation into impulse trebuchet ammunition.

Are all those steps really going to return a net gain or is it better to K.I.S.S.

Thoughts?

why do I have to see amazing ideas when I’m fixing bugs in completely unrelated systems… I’m bookmarking this

(I was planning to go for an entire physics projectiles lesson down the line for that one, until it didn’t matter)

1 Like

BTW. With the method that I use the lift variable is easily scaled to show a greater parabola for long distances. Simple as:

scaledPosition.y += lift * Vector3.Distance(enemy.transform.position - transform.position).

Set lift to a visually pleasing incline. For me it is one unit of vertical rise over ten units in linear distance; thus, lift is 0.1f.

And, me amigo, now I got me some boomerangs. But I’ve digressed from topic.

I tend to want to apply the KISS principle as often as possible, though sometimes, that’s not necessarily possible. Just be careful of getting so complex that a small change here disrupts a major system there or that your code base gets so girthy, you start to lose track of it’s details.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms