Nice work with the character sketch! And great choice with Unity. I've tried Unreal too and it felt a bit overwhelming, both in terms of performance drain and in terms of learning curve.
Also regarding the ranged system, you might be able to solve the issue by manually aiming at the target in front of you (if one is found), and otherwise just throwing it straight forward. You can detect targets in front of you regardless of elevation.
You can do this by parenting a BoxCollider (or any other collider of choide) to your character, making it a trigger, excluding all layers except the one where your targets exist. Also, make it really tall to deal with the elevation issues. Make it as long as the range the character has.
Add a new script to track targets, let's say, TargetManager to the character. This script has a OnTriggerEnter and OnTriggerExit callbacks which add valid targets to a list. On Update() you can pick the target from the list based on some criteria (closest, unobstructed, etc), save it to some variable, let's say GameObject target. Careful how you write this one, because it might have some performance concerns. Make it run on a less-frequent Coroutine if it does cause performance issues.
Then, on your object throw script, you check if you targetManager.target != null and then use this handy phisics calculation to tell exactly what force you need to apply to the throwable to make it land exactly on your target position:
using System; using UnityEngine; namespace Game.ProjectileSystem { public enum BallisticTrajectory { // Highest arc to hit from above Max, // Most direct arc to hit straight on Min, // Somewhere in between LowEnergy } public class Ballistics { public static Vector3 CalculateVelocityToLaunchToCameraDirection( GameObject camera, Transform source, float speed, LayerMask layerMask, float raycastRange = 15f )`{ // aim from camera infinitely far a our target var ray = new Ray(camera.transform.position, camera.transform.forward); if (Physics.Raycast(ray, out var hit, raycastRange, layerMask, QueryTriggerInteraction.Ignore)) { return CalculateVelocity(source, hit.point, speed).normalized * speed; } else return CalculateVelocity( source, (ray.origin + ray.direction * raycastRange), speed ).normalized * speed; } /// <summary> /// Returns the velocity needed to hit a target from a certain position with a certain speed. This 3d vector can also /// be used as the look direction for a projectile by using Quaternion.LookRotation(). /// </summary> /// <returns>The 3D velocity.</returns> public static Vector3 CalculateVelocity(Transform source, Vector3 target, float speed, BallisticTrajectory trajectoryType = BallisticTrajectory.Min) { speed = Mathf.Clamp(speed, 0, speed); Vector3 toTarget = target - source.position; // Set up the terms we need to solve the quadratic equations. float gSquared = Physics.gravity.sqrMagnitude; float b = speed * speed + UnityEngine.Vector3.Dot(toTarget, Physics.gravity); float discriminant = b * b - gSquared * toTarget.sqrMagnitude; // Check whether the target is reachable at max speed or less. if (discriminant < 0) { // Target is too far away to hit at this speed. // Abort, or fire at max speed in its general direction? return toTarget.normalized * speed; } float discRoot = Mathf.Sqrt(discriminant); // Highest shot with the given max speed: float T_max = Mathf.Sqrt((b + discRoot) * 2f / gSquared); // Most direct shot with the given max speed: float T_min = Mathf.Sqrt((b - discRoot) * 2f / gSquared); // Lowest-speed arc available: float T_lowEnergy = Mathf.Sqrt(Mathf.Sqrt(toTarget.sqrMagnitude * 4f / gSquared)); float T; switch (trajectoryType) { case BallisticTrajectory.Max: T = T_max; break; case BallisticTrajectory.Min: T = T_min; break; case BallisticTrajectory.LowEnergy: T = T_lowEnergy; break; default: throw new ArgumentOutOfRangeException(nameof(trajectoryType), trajectoryType, null); } // Convert from time-to-hit to a launch velocity: return toTarget / T - Physics.gravity * T / 2f; } } }
If you're curious where I got this function, the answer is a mix of physics knowledge and some experimentation plus some ChatGPT for translating said knowledge to C#.
If you're curious where I use this function, you can check out my game Recolonizer, which is out now as a browser game on open beta! I plan on fully releasing it to Steam and iOS in mid-2025! It is what drives the direction of the cannonballs, as they are all physics projectiles.
In any case, good luck on your project! Hope this helped a bit :)