Ground offset for projectile doesn't work as expected

So concerning this code in the DirectionalTargeting:

        public override void StartTargeting(AbilityData data, Action finished)
        {
            RaycastHit raycastHit;
            Ray ray = PlayerController.GetMouseRay();
            if (Physics.Raycast(ray, out raycastHit, 1000, layerMask))
            {
                data.SetTargetedPoint(raycastHit.point + ray.direction * groundOffset / ray.direction.y);
            }
            finished();
        }

The ground offset doesn’t seem correct. At least not for me, granted my camera angle is different than Sam/Rick’s game. I’ve got my camera closer to an isometric ARPG angle. Either way, when I shoot a projectile ability, as the cursor get’s closer to my player character, the projectile starts hitting the ground earlier. It never manages to travel just straight without a rotation in the Y. Regardless of how close the cursor is to the spawn point I would like it to travel at a constant Y height, AND pass through the where the cursor is clicked. Any thoughts or ideas?


Further away, projectile stays “above ground” longer

Closer to player, and projectile crosses ground plane sooner (obfuscated by the gate there).

I’ve tried getting the angle some other ways, like more explicitly stepping through the trig functions, but it leads to no different results at all. Just wondering if anyone has any thoughts.

Here’s the spawnprojectile ability

          public class SpawnProjectileEffect : EffectStrategy
    {
        [SerializeField] Projectile projectileToSpawn;
        [SerializeField] float damageAmount;
        [SerializeField] bool isRightHand = true;
        [SerializeField] bool useTargetPoint = true;
        [SerializeField] bool isWeaponBased = true;
        [SerializeField] float weaponDamageFraction = 0.25f;
        public override void StartEffect(AbilityData data, Action finished)
        {
            Fighter fighter = data.GetUser().GetComponent<Fighter>();
            Vector3 spawnPosition = fighter.GetHandTransform(isRightHand).transform.position;
            if(useTargetPoint)
            {
                SpawnProjectileForTargetPoint(data, spawnPosition);
            }
            else
            {
                SpawnProjectilesForTargets(data, spawnPosition);
            }
            finished();
        }

        private void SpawnProjectileForTargetPoint(AbilityData data, Vector3 spawnPosition)
        {
            if (isWeaponBased)
            {
                PlayerController playerController = data.GetUser().GetComponent<PlayerController>();
                damageAmount = GetWeaponDamage(playerController) * weaponDamageFraction;
            }
            Projectile projectile = Instantiate(projectileToSpawn);
            projectile.transform.position = spawnPosition;
            projectile.SetTarget(data.GetTargetedPoint(), data.GetUser(), damageAmount);
        }

        private void SpawnProjectilesForTargets(AbilityData data, Vector3 spawnPosition)
        {
            foreach (var target in data.GetTargets())
            {
                Health health = target.GetComponent<Health>();
                if (health)
                {
                    Projectile projectile = Instantiate(projectileToSpawn);
                    projectile.transform.position = spawnPosition;
                    projectile.SetTarget(health, data.GetUser(), damageAmount);
                }

            }
        }

        private float GetWeaponDamage(PlayerController playerController)
        {
            return playerController.GetComponent<Fighter>().GetCurrentWeaponDamage();
        }
    }

and the projectile class’ Update method, and settarget:

       void Update()
        {
            if (target != null && isHoming && !target.IsDead())
            {
                transform.LookAt(GetAimLocation());
            }
            transform.Translate(Vector3.forward * speed * Time.deltaTime);
            // MoveProjectile();
        }

    public void SetTarget(Health target, GameObject instigator, float damage)
        {
            SetTarget(instigator, damage, target);
        }

        public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage)
        {
            SetTarget(instigator, damage, null, targetPoint);
        }

        public void SetTarget(GameObject instigator, float damage, Health target = null, Vector3 targetPoint = default)
        {
            this.target = target;
            this.targetPoint = targetPoint;
            this.damage = damage;
            this.instigator = instigator;
            Destroy(gameObject, maxLifeTime);
        }

Any ideas about what could be causing this issue?

I’ve gone over the math several times, and it seems sound. I actually run this in a different manner, but the result should be the same. Sam’s just tried to simplifiy the math.

                Vector3 hitToCamera = (ray.origin - raycastHit.point).normalized; //reverses the ray direction and normalizes it
                float ratio = 1 / hitToCamera.y; //how many y to make one unit in the air
                data.SetTargetedPoint(raycastHit.point + hitToCamera * ratio * groundOffset); 

Now this should yield the exact same result as Sam’s calculation(since ray.direction.y is -, it will reverse the direction of the vector just like I do in hitToCamera.

See if this helps.

Thanks for the tip, unfortunately the result is the same. If I click close to the player, it dives through the terrain just after the cursor. The further from the player I click, it does start to behave correctly though. I think at this point I’m going to give up on it. Revisit it later, maybe!

One more thing to check, this all assumes that your fireball is being launched from one unit off of the ground. This may not always be the case. Supposing the height of the right hand transform is actually higher than the groundOffset used in directional targeting, then the angle would push downward, and the closer the target point is to the player, the steeper that angle would become.
Try this:

        public override void StartTargeting(AbilityData data, Action finished)
        {
            RaycastHit raycastHit;
            Ray ray = PlayerController.GetMouseRay();
            if (Physics.Raycast(ray, out raycastHit, 1000, layerMask))
            {
                Fighter fighter = data.GetUser().GetComponent<Fighter>(); //Discard this change, see next reply
                groundOffset = fighter.GetHandTransform(true).position.y - data.GetUser().transform.position.y; //Discard this change, see next reply
                Vector3 hitToCamera = (ray.origin - raycastHit.point).normalized; //reverses the ray direction and normalizes it
                float ratio = 1 / hitToCamera.y; //how many y to make one unit in the air
                data.SetTargetedPoint(raycastHit.point + hitToCamera * ratio * groundOffset); 
                
                data.SetTargetedPoint(raycastHit.point + ray.direction * groundOffset / ray.direction.y);
            }
            finished();
        }
    

Actually, that still may not work, because at the targetting phase, the Right Hand Transform may not be at the same relative y position as when the release happens…

It might be helpful to log the delta in the y in SpawnProjectile, and then go back and set the groundOffset instead…
In SpawnProjectile.StartEffect, add this Debug

        public override void StartEffect(AbilityData data, Action finished)
        {
            Fighter fighter = data.GetUser().GetComponent<Fighter>();
            Vector3 spawnPosition = fighter.GetHandTransform(isRightHand).position;
            Debug.Log($"groundOffset should be {spawnPosition.y - fighter.transform.position.y}");

Then you can take this number and plug it in to groundOffset. This should adjust the targetted point to be the exact same distance from the ground to the cursor as the right hand transform is from the ground (because Fighter.transform.position should be at the character’s feet).

Ok, so originally I had tried to do all the programmatically in the StartTargeting method itself (trying to offset for spawn position) but for some reason it didn’t quite work how I expected either and I guess looking back that makes sense because it would have got the offset from the hand before it was starting to spawn the attack. Debugging it’s spawn position fixed this. Thanks so much my guy!

I was considering ways this could be accomplished at spawn time (advantage: If you change animations you don’t have to go through this all over again). Here’s what I came up with:

First, in AbilityData, I added

     Vector3 hitToCamera;
     public void HitToCamera(Vector3 direction) => hitToCamera = direction;
    public Vector3 GetHitToCamera() => hitToCamera;

Then in DirectionalTargeting, I did this:

        public override void StartTargeting(AbilityData data, Action finished)
        {
            RaycastHit raycastHit;
            Ray ray = PlayerController.GetMouseRay();
            if (Physics.Raycast(ray, out raycastHit, 1000, layerMask))
            {
                Vector3 hitToCamera = (ray.origin - raycastHit.point).normalized; //reverses the ray direction and normalizes it
                data.SetTargetedPoint(raycastHit.point);
                data.HitToCamera(hitToCamera);
                //data.SetTargetedPoint(raycastHit.point + ray.direction * groundOffset / ray.direction.y);
            }
            finished();
        }

and finally, in SpawnProjectileEffect

        private void SpawnProjectileForTargetPoint(AbilityData data, Vector3 spawnPosition)
        {
            Projectile projectile = Instantiate(projectileToSpawn);
            projectile.transform.position = spawnPosition;
            float deltaY = spawnPosition.y - data.GetUser().transform.position.y;
            float ratio = 1 / data.GetHitToCamera().y;
            projectile.SetTarget(data.GetTargetedPoint() +data.GetHitToCamera() * ratio , data.GetUser(), damage);
        }

What this does is calculate the offset at the exact spawn time, meaning you can swap out animations and use the directional targeting for other types of effects that might use a different animation/effect combo.

1 Like

This was close, but it got me down the right path. Doing it the way as you have it above yields the same result, with the closer the cursor is to the player object, the faster the projectile falls.

However I ended up making these modifications, and this has the anticipated result:

AbilityData.cs

using System;
using System.Collections;
using System.Collections.Generic;
using RPG.Core;
using UnityEngine;

public class AbilityData : IAction
{
    GameObject user;
    Vector3 targetedPoint;
    IEnumerable<GameObject> targets;
    bool cancelled = false;
    Vector3 hitToCamera;
    float ratio;
    public void HitToCamera(Vector3 direction) => hitToCamera = direction;
    public Vector3 GetHitToCamera() => hitToCamera;

    public AbilityData(GameObject user)
    {
        this.user = user;
    }

    public GameObject GetUser()
    {
        return user;
    }

    public IEnumerable<GameObject> GetTargets()
    {
        return targets;
    }
    public void SetTargets(IEnumerable<GameObject> targets)
    {
        this.targets = targets;
    }

    public void SetTargetedPoint(Vector3 targetPoint)
    {
        this.targetedPoint = targetPoint;
    }

    public Vector3 GetTargetedPoint()
    {
        return targetedPoint;
    }

    public void StartCouroutine(IEnumerator coroutine)
    {
        user.GetComponent<MonoBehaviour>().StartCoroutine(coroutine);
    }

    public void Cancel()
    {
        cancelled = true;
    }

    public bool IsCancelled()
    {
        return cancelled;
    }

    public void SetRatio(float ratio)
    {
        this.ratio = ratio;
    }

    public float GetRatio()
    {
        return ratio;
    }
}

StartTargetingMethod:

        public override void StartTargeting(AbilityData data, Action finished)
        {
            RaycastHit raycastHit;
            Ray ray = PlayerController.GetMouseRay();
            if (Physics.Raycast(ray, out raycastHit, 1000, layerMask))
            {
                Fighter fighter = data.GetUser().GetComponent<Fighter>();
                Vector3 spawnPosition = fighter.GetHandTransform(isRightHand).transform.position;
                var offset = spawnPosition.y - fighter.transform.position.y;
                Vector3 hitToCamera = (ray.origin - raycastHit.point).normalized;
                float ratio = 1/hitToCamera.y;
                data.SetTargetedPoint(raycastHit.point);
                data.HitToCamera(hitToCamera);
                data.SetRatio(ratio);                
            }
            finished();
        }

SpawnProjectile (only the relevant methods)

        public override void StartEffect(AbilityData data, Action finished)
        {
            Fighter fighter = data.GetUser().GetComponent<Fighter>();
            Vector3 spawnPosition = fighter.GetHandTransform(isRightHand).transform.position;
            float offset = spawnPosition.y - fighter.transform.position.y;
            if(useTargetPoint)
            {
                SpawnProjectileForTargetPoint(data, spawnPosition, offset);
            }
            else
            {
                SpawnProjectilesForTargets(data, spawnPosition);
            }
            finished();
        }

        private void SpawnProjectileForTargetPoint(AbilityData data, Vector3 spawnPosition, float groundOffset)
        {
            if (isWeaponBased)
            {
                PlayerController playerController = data.GetUser().GetComponent<PlayerController>();
                damageAmount = GetWeaponDamage(playerController) * weaponDamageFraction;
            }
            Projectile projectile = Instantiate(projectileToSpawn);
            projectile.transform.position = spawnPosition;
            projectile.SetTarget((data.GetTargetedPoint() + data.GetHitToCamera() * data.GetRatio() * groundOffset), data.GetUser(), damageAmount);
        }

Basically the ground offset has to be calculated at the StartEffect method, as that one starts 0.53 seconds into the animation, and that’s where the “hand” that launches it is at the correct height.

1 Like

Yes, because I forgot one silly detail… to multiply the result times delta y… it should have been

data.GetTargetedPoint() + data.GetHitToCamera() * ratio * deltaY

An excellent job taking it to that step! My reading of your code suggests that you should be nailing the cursor every time (unless, of course, you move it after you click)

Privacy & Terms