I was using a spherecastboxcast. But before that I was actually using an invisible mesh collider that was shaped like a tall and narrow wedge. Similar I think to what you had considered. I ended up switching to a spherecastboxcast because it was easier to configure and was one less thing taking up space in the editor screen. it was also simpler to code. But if you really want a steep but narrow detection field it’s probably better to go with the mesh.
I don’t have a ton of time to explain so I’ll just give you a basic idea and post the code (including all of the stuff that will have zero relevance to any project but my own lol). Also, bare with me. Been about three years since I did any of this.
So each entity, player and NPCs alike, consists of a master gameobject and several child objects. One such object represents the weapon they are holding. Normally it faces straight forward relative to the parent. As the player moves or aims a weapon the parent object is rotated accordingly and the weapon fires in that direction. But when the aim assist feature detects a lock it begins to drift the weapon in a direction that differs from the parent. Every NPC in the game has a rather large hitbox that this spherecastboxcast checks against to see where to aim. It decides the final aim vector as a combination of the weapon position, the detected hitbox position, and then a couple configurable offsets that are stored on the aim-assist component itself (though if you want to get better results it would be best to store the target offsets on the enemy and somehow gather that data during the detection phase). There is also a sprite that represents the targeting reticule that smoothly moves to the target detected and then snaps back to a fixed position relative to it’s parent weapon. Probably not useful to you but I figured I’d mention it so you know what that’s all about. In my case, the aim-assist is off by default and only activates when the player is actually firing the weapon so as soon as they stop shooting, I reset everything, the weapon snaps back to the forward direction relative to it’s parent, and the target reticule is returned to it’s default position. You can see an example of it here:
And here’s the relevant (sorta) code:
using Toolbox;
using Toolbox.Behaviours;
using Toolbox.Graphics;
using Toolbox.Math;
using UnityEngine;
namespace WoP
{
/// <summary>
/// Attach this to anything that has a component that implements
/// IWeapon and it will gain an aim assist ability.
///
/// NOTE: Don't forget to place this is at an earlier execution time than all other movement for your entity.
/// </summary>
[DisallowMultipleComponent]
public class WeaponAimAssist : LocalListenerMonoBehaviour
{
[Tooltip("The transform that will act as the default 'compass' when aim assist is not applied. It cannot be the same transform that we are changing.")]
public Transform AimTrans;
[Header("Aim Assist Options")]
public bool AimAssist = true;
public bool RequireLos = true;
public float AimSpeed = 1000;
[Tooltip("The max range at which aim-assist will target.")]
public float Range = 10;
[Tooltip("The half-width and half-height of the raycast to perform.")]
public Vector2 HalfSize;
[Tooltip("Offset applied to the weapon in the LoS check for aim assist.")]
public Vector3 AssistWeaponOffset = Vector3.up;
[Tooltip("Offset applied to the target in the LoS check for aim assist.")]
public Vector3 AssistTargetOffset = Vector3.up;
[Tooltip("The maximum angle of LoS allowed to consider aim-assist valid.")]
public float AssistAngle = 66f;
public LayerMask AssistLayers;
public LayerMask LosBlockers;
public SpriteRenderer ReticuleSprite;
public float RotateSpeed = 100;
public float BounceSpeed = 1;
public float BounceScale = 0;
InterpolateChild Inter;
BillboardAdvanced RetFreeze;
Transform MyTrans;
Transform ReticuleTrans;
Vector3 OriginalRetOffset;
Vector3 DefaultAngleOffset;
Bounds LastTargetBounds;
public float ResetSpeedX = 50, ResetSpeedY = 50, ResetSpeedZ = 50;
public float AimSpeedX = 25, AimSpeedY = 10, AimSpeedZ = 10;
Vector3 Last;
/// <summary>
/// Returns true if the aim-assist is currently adjusting the agent's aim.
/// </summary>
public bool IsAiming { get; private set; }
Transform _LastTarget;
float LastTargetTime;
/// <summary>
///
/// </summary>
public Transform LastTarget
{
get
{
if (Time.time - LastTargetTime > 0.5f || _LastTarget == null || !_LastTarget.gameObject.activeInHierarchy)
_LastTarget = null;
return _LastTarget;
}
set
{
_LastTarget = value;
LastTargetTime = Time.time;
}
}
void Awake()
{
MyTrans = transform;
ReticuleSprite.gameObject.SetActive(false);
ReticuleTrans = ReticuleSprite.transform;
OriginalRetOffset = ReticuleTrans.localPosition;
RetFreeze = ReticuleTrans.GetComponent<BillboardAdvanced>();
Inter = ReticuleTrans.GetComponent<InterpolateChild>();
}
protected override void OnDestroy()
{
base.OnDestroy();
}
/// <summary>
///
/// </summary>
/// <param name="agentForward"></param>
/// <param name="agentPos"></param>
/// <returns></returns>
public Vector3 ForwardToLastTarget(Vector3 agentForward, Vector3 agentPos)
{
if (LastTarget == null) return agentForward;
else
{
var targetCenter = LastTargetBounds.center + AssistTargetOffset;
return targetCenter - (agentPos + AssistWeaponOffset);
}
}
/// <summary>
/// Helper for getting the final rotation that must be applied to the agent in order to perform the aim-assist.
/// </summary>
/// <param name="agentOriginalForward"></param>
/// <param name="agentPos"></param>
/// <param name="targetPos"></param>
/// <returns></returns>
public Quaternion RotateToLastTarget(Vector3 agentOriginalForward, Vector3 agentPos)
{
//Vector3 forwardToTarget = targetPos - agentPos;
Vector3 forwardToTarget = ForwardToLastTarget(agentOriginalForward, agentPos);
var assistV = Vector3.RotateTowards(agentOriginalForward, forwardToTarget.normalized, float.MaxValue, float.MaxValue);
#if UNITY_EDITOR
Debug.DrawRay(agentPos + AssistWeaponOffset, assistV * 10, new Color(1f, 0.5f, 0.75f, 1f), 2);
#endif
return Quaternion.LookRotation(assistV);
}
/// <summary>
///
/// </summary>
void Update()
{
ApplyAssist();
}
public void ApplyAssist()
{
//always be sure to reset orientation before adjusting
MyTrans.rotation = Quaternion.LookRotation(AimTrans.forward, Vector3.up);
var hit = Toolbox.SharedArrayFactory.Hit1;
var forward = MyTrans.forward;
var myPos = MyTrans.position + AssistWeaponOffset;
if(!IsAiming)
DefaultAngleOffset = RetFreeze.AngleOffset;
Transform CurrentTarget = null;
LastTargetBounds = new Bounds();
if(Physics.BoxCastNonAlloc(myPos, new Vector3(HalfSize.x, HalfSize.y, .1f), forward, hit, MyTrans.rotation, Range, AssistLayers, QueryTriggerInteraction.Collide) > 0)
{
CurrentTarget = hit[0].transform;
LastTarget = CurrentTarget;
LastTargetBounds = hit[0].collider.bounds;
}
if (CurrentTarget != null)
{
if (!CurrentTarget.gameObject.activeInHierarchy)
CurrentTarget = null;
}
if (CurrentTarget != null)
{
LockToTarget(LastTargetBounds, myPos, forward);
}
else ResetAim();
}
/// <summary>
///
/// </summary>
/// <param name="targetBounds"></param>
/// <param name="agentPos"></param>
/// <param name="agentForward"></param>
void LockToTarget(Bounds targetBounds, Vector3 agentPos, Vector3 agentForward)
{
var targetCenter = targetBounds.center + AssistTargetOffset;
var forwardToTarget = (targetCenter - agentPos);
var distToEnemy = forwardToTarget.magnitude;
float distToWall = float.MaxValue;
forwardToTarget.Normalize();
#if UNITY_EDITOR
Debug.DrawLine(agentPos, targetCenter, Color.green);
#endif
float viewAngle = Toolbox.Math.MathUtils.AngleOnY(agentPos, agentForward, targetCenter);
//perform a quick raycast to ensure we have los before making final adjustment
var hit = Toolbox.SharedArrayFactory.Hit1;
int wallCount = Physics.RaycastNonAlloc(agentPos, forwardToTarget, hit, Range, LosBlockers, QueryTriggerInteraction.Collide);
if (wallCount > 0)
distToWall = (hit[0].point - agentPos).magnitude;
if (viewAngle <= AssistAngle && (!RequireLos || distToEnemy < distToWall))
{
IsAiming = true;
var assistV = Vector3.RotateTowards(agentForward, forwardToTarget, float.MaxValue, float.MaxValue);
MyTrans.rotation = Quaternion.LookRotation(assistV);
ReticuleSprite.gameObject.SetActive(true);
if (Inter != null) Inter.enabled = false;
ReticuleTrans.position = SetRetPos(targetCenter, AimTrans.forward, Vector3.zero, AimSpeedX, AimSpeedY, AimSpeedZ);
RetFreeze.AngleOffset = new Vector3(RetFreeze.AngleOffset.x, RetFreeze.AngleOffset.y, RetFreeze.AngleOffset.z + (RotateSpeed * Time.deltaTime));
}
else
{
#if UNITY_EDITOR
Debug.DrawLine(agentPos, hit[0].point, Color.red);
#endif
ResetAim();
}
}
/// <summary>
/// Resets the orignal aim of the agent from before the aim-assist adjustment.
/// </summary>
public void ResetAim()
{
IsAiming = false;
MyTrans.forward = AimTrans.forward;
ReticuleTrans.position = SetRetPos(AimTrans.position, MyTrans.forward, OriginalRetOffset, ResetSpeedX, ResetSpeedY, ResetSpeedZ);
RetFreeze.AngleOffset = DefaultAngleOffset;
ReticuleSprite.gameObject.SetActive(false);
}
public Vector3 SetRetPos(Vector3 parentPos, Vector3 parentForward, Vector3 offset, float xSpeed, float ySpeed, float zSpeed)
{
var pos = MathUtils.ForwardSpaceOffset(parentPos, parentForward, offset);
float x = MathUtils.SmoothApproach(Last.x, pos.x, pos.x, xSpeed);
float y = MathUtils.SmoothApproach(Last.y, pos.y, pos.y, ySpeed);
float z = MathUtils.SmoothApproach(Last.z, pos.z, pos.z, zSpeed);
Last = new Vector3(x, y, z);
return Last;
}
public Vector3 SetRetPos(Vector3 target, float xSpeed, float ySpeed, float zSpeed)
{
var pos = target;
float x = MathUtils.SmoothApproach(Last.x, pos.x, pos.x, xSpeed);
float y = MathUtils.SmoothApproach(Last.y, pos.y, pos.y, ySpeed);
float z = MathUtils.SmoothApproach(Last.z, pos.z, pos.z, zSpeed);
Last = new Vector3(x, y, z);
return Last;
}
}
}
Hopefully that all helps a bit. Good luck!