Selectively switching targets with right analogue

Hi,

I’ve finished all the lectures for 3rd person combat and traversal and now I’m trying to implement target switching whilst in the targeting state. I’ve managed to get the basic functionality working by following this topic Toggle between different lock on targets but have hit a couple of issues:

The first one is with using the right stick, I’ve done this by adding a new CycleTargetEvent event to the onlook method I’ve managed to get it to switch targets but it starts jumping back and forth and the camera flickers alot, I think this is due to the right stick also being used for looking around in the free look state and the fact that the analogue input in a vector2 rather than a button press on the action map. I’ve proved that the code works when on a new button input (e.g. North gamepad button). Do I need to alter how the right analogue behaves while targeting?

Input Reader:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class InputReader : MonoBehaviour, Controls.IPlayerActions
{
    public bool IsAttacking {get; private set; }
    public bool IsBlocking { get; private set; }
    public bool IsAiming {get; private set; }
    
    public Vector2 MovementValue {get; private set; }
    public Vector2 MousePosition; 
    public Vector2 look;
    public event Action JumpEvent;
    public event Action DodgeEvent;
    public event Action TargetEvent;
    public event Action CycleTargetEvent;


    private Controls controls;

    private void Start()
    {
        controls = new Controls();
        controls.Player.SetCallbacks(this);

        controls.Player.Enable();
    }

    private void onDestroy()
    {
        controls.Player.Disable();
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        if(!context.performed) { return; }
        
        JumpEvent?.Invoke();
    }

    public void OnDodge(InputAction.CallbackContext context)
    {
        if(!context.performed) { return; }
        
        DodgeEvent?.Invoke();
    }

    public void OnMove(InputAction.CallbackContext context)
    {
        MovementValue = context.ReadValue<Vector2>();        
    }

    public void OnLook(InputAction.CallbackContext context)
    {
        look = context.ReadValue<Vector2>();
        look.y = (look.y * -1);

        CycleTargetEvent?.Invoke();
    }

    public void OnTarget(InputAction.CallbackContext context)
    {
        if(!context.performed) { return; }
        
        //if(!stateMachine.PlayerTargetingState())

        TargetEvent?.Invoke();          
    }

    public void OnAttack(InputAction.CallbackContext context)
    {
        if(context.performed) 
        { 
            IsAttacking = true;
        }
        else if(context.canceled) 
        { 
            IsAttacking = false;
        }
    }

    public void OnAim(InputAction.CallbackContext context)
    {
        if(context.performed) 
        { 
            IsAiming = true;            
        }
        else if(context.canceled) 
        { 
            IsAiming = false;
        }
    }

    public void OnBlock(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            IsBlocking = true;
        }
        else if (context.canceled)
        {
            IsBlocking = false;
        }
    }
    
}

PlayerTargetingState:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerTargetingState : PlayerBaseState
{   

    private readonly int TargetingBlendTreeHash = Animator.StringToHash("TargetingBlendTree");
    private readonly int TargetingForwardHash = Animator.StringToHash("TargetingForward");
    private readonly int TargetingRightHash = Animator.StringToHash("TargetingRight");

    private const float CrossFadeDuration = 0.1f;
    
    public PlayerTargetingState(PlayerStateMachine stateMachine) : base(stateMachine) { }

    public override void Enter()
    {
        stateMachine.InputReader.TargetEvent += OnTarget;
        stateMachine.InputReader.DodgeEvent += OnDodge;
        stateMachine.InputReader.JumpEvent += OnJump;
        stateMachine.InputReader.CycleTargetEvent += OnCycleTarget;

        stateMachine.Animator.CrossFadeInFixedTime(TargetingBlendTreeHash, CrossFadeDuration);
    }

    public override void Tick(float deltaTime)
    {
        if(stateMachine.InputReader.IsAttacking)
        {
            stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
            return;
        }

        if (stateMachine.InputReader.IsBlocking)
        {
            stateMachine.SwitchState(new PlayerBlockingState(stateMachine));
            return;
        }

        if (stateMachine.Targeter.CurrentTarget == null)
        {
            stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            return;
        }

        Vector3 movement = CalculateMovement(deltaTime);
        
        Move(movement * stateMachine.TargetingMovementSpeed, deltaTime);

        UpdateAnimator(deltaTime);

        FaceTarget();
                
    }

    public override void Exit()
    {
        stateMachine.InputReader.TargetEvent -= OnTarget;
        stateMachine.InputReader.DodgeEvent -= OnDodge;
        stateMachine.InputReader.JumpEvent -= OnJump;
        stateMachine.InputReader.CycleTargetEvent -= OnCycleTarget;
    }

    private void OnCycleTarget()
    {
        stateMachine.Targeter.SelectNextTarget();
    }

    private void OnTarget()
    {
        stateMachine.Targeter.Cancel();

        stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
    }

    private void OnDodge()
    {
        stateMachine.SwitchState(new PlayerDodgingState(stateMachine, stateMachine.InputReader.MovementValue));
    }

    private void OnJump()
    {
        stateMachine.SwitchState(new PlayerJumpingState(stateMachine));
    }

    private Vector3 CalculateMovement(float deltaTime)
    {
        Vector3 movement = new Vector3();
                
        movement += stateMachine.transform.right * stateMachine.InputReader.MovementValue.x;
        movement += stateMachine.transform.forward * stateMachine.InputReader.MovementValue.y;
                
        return movement;
    }

    private void UpdateAnimator(float deltaTime)
    {
        if(stateMachine.InputReader.MovementValue.y == 0)
        {
            stateMachine.Animator.SetFloat(TargetingForwardHash, 0, 0.1f, deltaTime);
        }
        else
        {
            float value = stateMachine.InputReader.MovementValue.y > 0 ? 1f : -1f;
            stateMachine.Animator.SetFloat(TargetingForwardHash, value, 0.1f, deltaTime);
        }

        if(stateMachine.InputReader.MovementValue.x == 0)
        {
            stateMachine.Animator.SetFloat(TargetingRightHash, 0, 0.1f, deltaTime);
        }
        else
        {
            float value = stateMachine.InputReader.MovementValue.x > 0 ? 1f : -1f;
            stateMachine.Animator.SetFloat(TargetingRightHash, value, 0.1f, deltaTime);
        }
        
    }

}

Targeter:

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Cinemachine;
using UnityEngine;

public class Targeter : MonoBehaviour
{
    [SerializeField] private CinemachineTargetGroup cineTargetGroup;
    
    private Camera mainCamera;

    public List<Target> targets = new List<Target>();

    public Target CurrentTarget { get; private set; }

    private void Start()
    {
        mainCamera = Camera.main;
    }
    
    private void OnTriggerEnter(Collider other)
    {
        if (!other.TryGetComponent<Target>(out Target target)){ return; }
      
        targets.Add(target);
        target.OnDestroyed += RemoveTarget;
    }

    private void OnTriggerExit(Collider other)
    {
        if (!other.TryGetComponent<Target>(out Target target)){ return; }

        RemoveTarget(target);       
    }

    public bool SelectTarget()
    {
        if (targets.Count == 0) { return false; }

        Target closestTarget = null;
        float closestTargetDistance = Mathf.Infinity;

        foreach (Target target in targets)
        {
            Vector2 viewPos = mainCamera.WorldToViewportPoint(target.transform.position);

            if(!target.GetComponentInChildren<Renderer>().isVisible)
            {
                continue;
            }

            Vector2 toCenter = viewPos - new Vector2(0.5f, 0.5f);
            if(toCenter.sqrMagnitude < closestTargetDistance)
            {
                closestTarget = target;
                closestTargetDistance = toCenter.sqrMagnitude;
            }
        }

        if(closestTarget == null) { return false; }

        CurrentTarget = closestTarget;
        cineTargetGroup.AddMember(CurrentTarget.transform, 1f, 2f);

        return true;
    }

    public void SelectNextTarget()
    {
        if (targets.Count == 0) return;

        // Find the current target's index
        var currentIndex = targets.IndexOf(CurrentTarget);
        if (currentIndex == -1)
        {
            // The current target is no longer in the list. Just select the closest target
            SelectTarget();
            return;
        }

        // move to the next index, wrapping around if we're at the end
        var nextIndex = (currentIndex + 1) % targets.Count;
        SetCurrentTarget(targets[nextIndex]);
    }

    public void Cancel()
    {
        if (CurrentTarget == null) { return; }
        
        cineTargetGroup.RemoveMember(CurrentTarget.transform);
        CurrentTarget = null;
    }

    private void SetCurrentTarget(Target newCurrent)
    {
        // Remove current target from cm target group
        cineTargetGroup.RemoveMember(CurrentTarget.transform);

        // Set the new current target
        CurrentTarget = newCurrent;

        // Add new target to cm target group
        cineTargetGroup.AddMember(CurrentTarget.transform, 1f, 2f);
    }

    private void RemoveTarget(Target target)
    {
        if(CurrentTarget == target)
        {
            cineTargetGroup.RemoveMember(CurrentTarget.transform);
            CurrentTarget = null;
        }

        target.OnDestroyed -= RemoveTarget;
        targets.Remove(target);
    }      
    
}

The second is that ideally what I want is to be able to switch targets based on left right analogue inputs, so right will find the closest next target on the right and left will find the next closest target on the left, so far all I’ve manged is to get it switching to the next closest.

I’ve tried a few things to get this working, all of them required a lot of googling, but I’ve ended up with a similar result every time. In the end I removed all the code and went with the method described in the mentioned topic as it was the cleanest, but I’m still unsure how to handle the behaviour I want.

With the code here as written, the CycleTarget will be called… pretty much any time the stick is touched. This is because OnLook fires any time something is different about the control.

What you want to do is set up a dedicated CycleTarget button in your InputActions. Using the Listen feature, press down on the Gamepad right stick (in addition to capturing input as a joystick, it also has a button press. Look for GamePad|Right Stick Press. Your OnCycleTarget method that you’ll have to create after you add a Cycle Target action should be like Ontarget and OnDodge, firing only when if(context.Performed) is true.

Hi Brian

Thank you for your response, it has solved one of the problems I was having however, I’m still struggling with the logic to switch targets based on which is the closest in a certain direction from the target.

What I have now is a lock on system that locks on to a target when clicking in the right analogue, clicking it again cancels the lock on. I have also implemented the buttons for left and right on the right stick to switch targets and I have managed to get right on the analogue to switch to targets on the right. the issue I’m having now is getting the closest target on the right of the current target. Currently it does switch to the right but seems to chose the target at random. I’ve pasted my method below.

public void SelectNextTargetRight()
    {
        if (targets.Count == 0) return;

        float nextClosestTargetDistance = Mathf.Infinity;
        int nextIndex = 0;
        

        foreach (Target target in targets)
        {
            Vector3 NextTargetPosition = target.transform.InverseTransformPoint(CurrentTarget.transform.position);
           
            if (NextTargetPosition.x <= 0) 
            {
                continue;
            }
            else
            {
                Vector3 DifferenceToTarget = CurrentTarget.transform.position - NextTargetPosition;
                float DistanceToTarget = DifferenceToTarget.sqrMagnitude;

                if (DistanceToTarget < nextClosestTargetDistance)
                {
                    nextClosestTargetDistance = DistanceToTarget;
                    nextIndex = targets.IndexOf(target);
                }
                                
            }

        }
                
        SetCurrentTarget(targets[nextIndex]);
    }

I’ve also tried the following, which seems to pass all the correct values when stepping through the code but, I’m still not getting the desired behaviour.

public void SelectNextTargetRight()
    {
        if (targets.Count == 0) return;

        Vector3 CurrentTargetPosition = CurrentTarget.transform.InverseTransformPoint(this.transform.position);
        float nextClosestTargetDistance = Mathf.Infinity;
        int nextIndex = 0;

        CurrentTargetPosition = CurrentTargetPosition.normalized;



        foreach (Target target in targets)
        {
            Vector3 NewTargetPosition = target.transform.InverseTransformPoint(this.transform.position);
            NewTargetPosition = NewTargetPosition.normalized;
            
            if (NewTargetPosition.x <= CurrentTargetPosition.x) 
            {
                continue;
            }
            else
            {
                Vector3 DifferenceToTarget = CurrentTarget.transform.position - target.transform.position;
                float DistanceToTarget = DifferenceToTarget.sqrMagnitude;

                if (DistanceToTarget < nextClosestTargetDistance)
                {
                    nextClosestTargetDistance = DistanceToTarget;
                    nextIndex = targets.IndexOf(target);
                }
                                
            }

        }
        if (nextIndex != 0)
        {
            SetCurrentTarget(targets[nextIndex]);
        }        
        
    }

Neither of these methods is considering the actual angle of the character, only the distance from the targeter.
Try something along these lines:

    public bool SelectClosestTargetToRight()
    {
        if (targets.Count <= 1)
        {
            return false;
        }
        Target oldTarget = CurrentTarget;
        Target newTarget = null;
        float angle = 90;
        foreach (Target target in targets)
        {
            if (target == oldTarget) continue;
            float newAngle = Vector3.SignedAngle(Vector3.up, target.transform.position - oldTarget.transform.position, Vector3.up);
            if (newAngle>0 && newAngle < angle)
            {
                newTarget = target;
                angle = Vector3.SignedAngle(transform.forward, target.transform.position - transform.position, Vector3.up);
            }
        }

        if (newTarget != null)
        {
            CurrentTarget = newTarget;
            return true;
        }
        else
        {
            return false;
        }
    }

This will go through each target, only considering targets where the angle is between 0 and 90 degrees, ruling out the CurrentTarget.

Thanks again for your quick response Brian.

While I see where you are coming from logically with the code snippet you suggested, unfortunately I’m having trouble getting it to work. I’m not sure If I’m misunderstanding how the Signed angle function works.

I’ve disabled any actual target switching for the time being and I’m just outputting the angles to the console. I’ve noticed that none of them are what I I would expect from the 3 points I’m trying to gather the angle from. Focusing on the current target (Target 2) and Target 4, I would expect this to be close to 90 degrees but its showing as only a 20 degree angle.

Code and screenshot below:

public bool SelectClosestTargetToRight()
    {
        if (targets.Count <= 1)
        {
            return false;
        }

        Target oldTarget = CurrentTarget;
        Target newTarget = null;

        Debug.Log("Current Target: " + oldTarget);

        float angle = 90;

        foreach (Target target in targets)
        {
            if (target == oldTarget) continue;

            float newAngle = Vector3.SignedAngle(oldTarget.transform.position, target.transform.position, this.transform.position);
            Debug.Log(target.name + ", Angle from current target: " + newAngle);

            if (newAngle > 0 && newAngle < angle)
            {
                Debug.Log("will switch to target: " + target);
                //newTarget = target;
                //angle = Vector3.SignedAngle(transform.forward, target.transform.position - transform.position, Vector3.up);
            }
        }

        if (newTarget != null)
        {
            //CurrentTarget = newTarget;
            SetCurrentTarget(newTarget);
            return true;
        }
        else
        {
            return false;
        }
        

    }

You’re right, those aren’t the values I’d expect either (although in this case, it does appear to have selected the correct character…

Your TargetFinder is a child of the Player object, so ideally should have the same transform.forward as the player… The first thing I would do is head into the TargetFinder’s inspector and make sure that the transform is reset, in other words, the position and rotation should both be zero. (The position in the inspector is relative to the position of the parent object).

Vector3.SignedAngle simply returns the same angle that Vector3.Angle does, but Vector3.Angle will always return Mathf.Abs(angleResult), so if the character is at -45 degrees, then it will be returned as 45 degrees. Vector3.Angle is useful as a fast way of determining if a character is in front of you or behind you (if it’s over 90 degrees, it should be considered out of sight), but it doesn’t tell you if the character is to the left or right of you. SignedAngle will return negative numbers for items to the left of transform.forward, and positive values for items to the right of transform forward.

That makes sense, thanks for taking the time to explain that. I have it working how I want now so thanks for you help. For your curiosity or anyone else who wants to do a similar thing I will put the code below.

I used two methods that are subscribed to when entering the targeting state that are linked to left/right analogue inputs on the RS as button presses.

private void OnCycleTargetRight()
    {
        if (targetSwitchCooldown > 0f)
        {
            Debug.Log("Target Switch Cooling Down");
        }
        else
        {
            Debug.Log("Cycling Right");
            stateMachine.Targeter.SelectClosestTargetToRight();
            //Set a short cooldown to stop analog input from entering the state multiple times 
            targetSwitchCooldown = stateMachine.TargetSwitchCooldownDuration;            
        }            
    }

    private void OnCycleTargetLeft()
    {
        if (targetSwitchCooldown > 0f)
        {
            Debug.Log("Target Switch Cooling Down");
        }
        else
        {
            Debug.Log("Cycling Left");
            stateMachine.Targeter.SelectClosestTargetToLeft();
            //Set a short cooldown to stop analog input from entering the state multiple times 
            targetSwitchCooldown = stateMachine.TargetSwitchCooldownDuration;
        }
    }

Those methods link respectively to their own methods in the targeter script:

public bool SelectClosestTargetToRight()
    {
        if (targets.Count <= 1)
        {
            return false;
        }

        Target oldTarget = CurrentTarget;
        Target newTarget = null;

        Debug.Log("Current Target: " + oldTarget);

        float angle = 90;

        // Work out whether each target is on the left or the right of the original using angles and take the smallest angle as the closest
        foreach (Target target in targets)
        {
            if (target == oldTarget) continue;

            float newAngle = Vector3.SignedAngle(oldTarget.transform.position, target.transform.position, this.transform.position);
            Debug.Log(target.name + ", Angle from current target: " + newAngle);

            if (newAngle > 0 && newAngle < angle)
            {
                Debug.Log("will switch to target: " + target);
                newTarget = target;
                angle = newAngle;
            }
        }

        if (newTarget != null)
        {            
            SetCurrentTarget(newTarget);
            return true;
        }
        else
        {
            return false;
        }
        

    }

    public bool SelectClosestTargetToLeft()
    {
        if (targets.Count <= 1)
        {
            return false;
        }

        Target oldTarget = CurrentTarget;
        Target newTarget = null;

        Debug.Log("Current Target: " + oldTarget);

        float angle = -90;

        // Work out whether each target is on the left or the right of the original using angles and take the smallest angle as the closest
        foreach (Target target in targets)
        {
            if (target == oldTarget) continue;

            float newAngle = Vector3.SignedAngle(oldTarget.transform.position, target.transform.position, this.transform.position);
            Debug.Log(target.name + ", Angle from current target: " + newAngle);

            if (newAngle < 0 && newAngle > angle)
            {
                Debug.Log("will switch to target: " + target);
                newTarget = target;
                angle = newAngle;
            }
        }

        if (newTarget != null)
        {
            SetCurrentTarget(newTarget);
            return true;
        }
        else
        {
            return false;
        }


    }```

Thanks again for your help!

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

Privacy & Terms