Home > Blockchain >  How to detect objects entering and leaving an aircraft's "missile lock" cone?
How to detect objects entering and leaving an aircraft's "missile lock" cone?

Time:12-29

As may be surmised, I am making a small 3d air combat-type game in Unity. One of the player's abilities (and quite possibly of the enemies') that I am trying to develop, is the ability to "missile lock" onto an opposing plane. It should go like this:

  • An enemy plane enters lock zone (a small cone in front of the player)
  • After the enemy plane has remained in zone for x seconds (or a crosshairs is "walked onto" the target, etc., choose your passive target lock mode of choice), the Player acquires target lock. The Player can attack one or multiple locked enemies at a time.
  • If an enemy plane leaves the zone, target lock for that plane is reset and must be re-acquired.

My question is, what is the best way to go about this?

Presumably, this would involve maintaining a list of currently targeting-in-progress and target-acquired enemies. I currently have a function which returns a list of all enemies in the zone in a given frame, however, comparing the results of the previous and current frames seems a bit awkward since I would have to find which list elements are new, which are old and which have been removed, then transfer over their current time(?). Alternatively I could increment all items currently in list, then run the mentioned function and add any new entries with timer = 0, while removing any which have left the zone. Can anyone advise on what is the proper structure for handling this interaction?

Unity provides Collider events for collision entry and exit, so I could possibly attach a cone collider, however the cone is an improper shape (flat on the side furthest away from the player) so this is not ideal unless I were to keep the cone very small and hope players don't notice?

I used some parts of locking example


Approach 2

Using a different approach, this modified code can recognise "attackers" and "targetables". Each "attacker" can record all of the "targetables" that are currently being targeted or locked. And each "targetable" is aware of the "attackers" that are targeting it. The code is similar to that above, but this code set more accurately suits the requirement outlined in the OP:

"One of the player's abilities (and quite possibly of the enemies') that I am trying to develop, is the ability to "missile lock" onto an opposing plane."

So, here's a modified solution making use of Dictionary collections:

public enum TargetState
{
    None,
    Targeting,
    Locked
}

public interface IAttacker
{
    // Nothing required just yet, but this would undoubtedly change.
}

public interface ITargetable
{
    void Targetted ( IAttacker attacker );
    void Locked ( IAttacker attacker );
    void Clear(IAttacker attacker);
}

Now for an example implementation of an Enemy. It should be stressed that if the game was looking at a battlefield, then it would likely be inappropriate to assign materials based on the last target state by an attacker (noted in the code comments). But suits the immediate requirements.

public class Enemy : MonoBehaviour, ITargetable
{
    private Renderer _renderer;
    private readonly Dictionary <IAttacker, TargetState> _attackers = new();


    private void Start ( )
    {
        _renderer = GetComponent<Renderer>();
        _renderer.material = Test.instance.TargetStateMaterial ( TargetState.None );
    }


    // The following won't be appropriate if an ITargetable is not targetted by one IAttacker, but Targetted by another.
    public void Clear ( IAttacker attacker )
    {
        _attackers.Remove ( attacker );
        _renderer.material = Test.instance.TargetStateMaterial ( TargetState.None );
    }

    // The following won't be appropriate if an ITargetable is !Locked by one IAttacker, but Locked by another.
    public void Locked ( IAttacker attacker )
    {
        _attackers [ attacker ] = TargetState.Locked;
        _renderer.material = Test.instance.TargetStateMaterial(TargetState.Locked );
    }

    // The following won't be appropriate if an ITargetable is !Targeting by one IAttacker, but only Targeting by another.
    public void Targetted ( IAttacker attacker )
    {
        _attackers [ attacker ] = TargetState.Targeting;
        _renderer.material = Test.instance.TargetStateMaterial ( TargetState.Targeting );
    }
}

And an test implementation of an attacker.

public class Test : MonoBehaviour, IAttacker
{
    public static Test instance { get; private set; }

    [SerializeField] private float lockTime = 2f;
    [SerializeField] private Material normal;
    [SerializeField] private Material targeting;
    [SerializeField] private Material locked;

    [SerializeField] private float mouseSensitivity = 10f;

    private float _xAngle;
    private float _yAngle;

    private Dictionary <int, ( float targetTime, TargetState state)> _targetted = new();


    private void Awake ( )
    {
        instance = this;
    }

    private void Start ( )
    {
        UnityEngine.Cursor.lockState = CursorLockMode.Locked;
    }

    public Material TargetStateMaterial ( TargetState s ) => s switch
    {
        TargetState.None => normal,
        TargetState.Targeting => targeting,
        TargetState.Locked => locked,
        _ => normal
    };


    void Update ( )
    {
        var d = Time.deltaTime * mouseSensitivity; ;
        _xAngle = Mathf.Clamp ( _xAngle  = Input.GetAxisRaw ( "Mouse X" ) * d, -90f, 90f );
        _yAngle = Mathf.Clamp ( _yAngle -= Input.GetAxisRaw ( "Mouse Y" ) * d, -90f, 90f );
        transform.rotation = Quaternion.Euler ( _yAngle, _xAngle, 0 );
    }

    private void OnTriggerEnter ( Collider other )
    {
        if ( other.gameObject.TryGetComponent<ITargetable> ( out var target ) )
        {
            if ( _targetted.ContainsKey ( other.gameObject.GetInstanceID ( ) ) ) return;
            _targetted.Add ( other.gameObject.GetInstanceID ( ), (0, TargetState.Targeting) );
            target.Targetted ( this );
        }
    }

    private void OnTriggerStay ( Collider other )
    {
        if ( other.gameObject.TryGetComponent<ITargetable> ( out var target ) )
        {
            var id = other.gameObject.GetInstanceID ( );
            if ( !_targetted.TryGetValue ( id, out var targetInfo ) )
            {
                targetInfo = (0, TargetState.Targeting);
                _targetted.Add ( other.gameObject.GetInstanceID ( ), targetInfo ); // Check to see if OnTriggerEnter was never called.
            }
            targetInfo.targetTime = targetInfo.targetTime   Time.deltaTime;
            if ( targetInfo.targetTime > lockTime )
                targetInfo.state = TargetState.Locked;
            _targetted [ id ] = targetInfo;

            Debug.Log ( $"OnTriggerStay ({targetInfo.targetTime}, {targetInfo.state})" );

            // A late check to see whether to inform the other ITargetable that they've been locked.
            if ( targetInfo.state == TargetState.Locked )
                target.Locked ( this );
        }
    }

    private void OnTriggerExit ( Collider other )
    {
        if ( other.gameObject.TryGetComponent<ITargetable> ( out var target ) )
        {
            var id = other.gameObject.GetInstanceID ( );
            if ( !_targetted.TryGetValue ( id, out var targetInfo ) )
                return; // Check to see if OnTriggerEnter was never called.
            if ( targetInfo.state == TargetState.Locked )
                return;
            _targetted.Remove ( id );

            // Inform the other ITargetable that they're not being targetted by this IAttacker anymore.
            target.Clear ( this );
        }
    }
}

It should be noted that an "attacker" could also be a "targetable" simply by adding both interfaces to the class declaration.

A quick test showed it to behave exactly as the GIF above.

  • Related