0
\$\begingroup\$

I am currently making a 2D top-down view shooter game for Android, where the game is played horizontally.

On the left side of the screen, you can touch anywhere to spawn in a joystick and aim by dragging your finger, and that works completely fine.

You can also swipe and it shoots. Swiping up shoots one type of bullet, whilst swiping down shoots another.

The problem is when doing both at the same time. If I start aiming, and then swipe, the swipe won't work, and the angle for aiming starts being read also from the touch position of the finger swiping, and this glitches the aiming.

Swipe Script:

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

public class Swipe : MonoBehaviour
{    
    private bool tap,swipeUp,swipeDown, swipeLeft, swipeRight;
    private bool isDraging = false;
    private Vector2 startTouch, swipeDelta;

    private void Update()
    {
        tap = swipeUp = swipeDown = swipeLeft = swipeRight = false;
    
        #region Touch Inputs
        if (Input.touches.Length > 0)
        {
            if (Input.touches[0].phase == TouchPhase.Began){
                isDraging = true;
                tap = true;
                startTouch = Input.touches[0].position;
            }
            else if (Input.touches[0].phase == TouchPhase.Ended || Input.touches[0].phase == TouchPhase.Canceled)
            {

                isDraging = false;
                Reset();
            }
        }
        #endregion
      
        #region Standalone Inputs
        if (Input.GetMouseButtonDown(0)){
            tap = true;
            isDraging = true;
            startTouch = Input.mousePosition;  
        }else if(Input.GetMouseButtonUp(0))
        {    
            isDraging = false;
            Reset();
        }
        #endregion
    
        // Calculate the distance
        swipeDelta = Vector2.zero;
        if(isDraging)
        {    
            if(Input.touches.Length > 0)
                swipeDelta = Input.touches[0].position - startTouch;
            else if (Input.GetMouseButton(0))
                swipeDelta = (Vector2)Input.mousePosition - startTouch;        
        }
    
    
        // Did we cross the deadzone?
        if (swipeDelta.magnitude > 125)
        {    
            // Which direction?
            float x = swipeDelta.x;
            float y = swipeDelta.y;
            if(Mathf.Abs(x) > Mathf.Abs(y))
            {    
                // left or right
                if (x < 0)
                    swipeLeft = true;

                else
                    swipeRight = true;
            }
            else{    
                // up or down
                if (y < 0)
                    swipeDown = true;

                else
                    swipeUp = true;                
            }    

            Reset();    
        }  
    }

    private void Reset()
    {

        startTouch = swipeDelta = Vector2.zero;
        isDraging = false;


    } 

    public Vector2 SwipeDelta { get { return swipeDelta;}}
    public bool SwipeUp { get { return swipeUp;}}
    public bool SwipeDown { get { return swipeDown;}}
    public bool SwipeLeft { get { return swipeLeft;}}
    public bool SwipeRight{ get { return swipeRight;}}
}

  

GestureDetection Script:

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

public class GestureDetector : MonoBehaviour
{
    public Shooting other;

    public Swipe swipeControls;

   
    private void Update()
    {    
        if (GameObject.Find("robot") != null) {
    
            if (swipeControls.SwipeLeft)
                 Debug.Log("LeftSwipe");
        
            if (swipeControls.SwipeRight)
                 Debug.Log("RightSwipe");
            
            
            if (swipeControls.SwipeUp)
                 other.ShootGreen();

            if (swipeControls.SwipeDown)
                 other.Shoot();
        }
    }
}
   

Joystick Script:

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

public class joystickShoot : MonoBehaviour {
    public Shooting other;
    public Rigidbody2D rb;
    
    private bool touchStart = false;

    private Vector2 pointA;
    private Vector2 pointB;
    public Transform player;
    public float speed = 15.0f;
    public GameObject bulletPrefab;

    public Transform circle;
    public Transform outerCircle;

    private Vector2 startingPoint;
    private int leftTouch = 99;


    void Update () {
        int i = 0;
        while(i < Input.touchCount){
            Touch t = Input.GetTouch(i);
            Vector2 touchPos = getTouchPosition(t.position); // * -1 for perspective cameras
            if(t.phase == TouchPhase.Began){
                if(t.position.x > Screen.width / 2) {
                } else {    
                    touchStart = true;
                    leftTouch = t.fingerId;
                      pointA = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.transform.position.z));
                    circle.transform.position = pointA * 1;
                    outerCircle.transform.position = pointA * 1;
                    circle.GetComponent<SpriteRenderer>().enabled = true;
                    outerCircle.GetComponent<SpriteRenderer>().enabled = true;    
                    startingPoint = touchPos;
                    
                }
            }else if(t.phase == TouchPhase.Moved && leftTouch == t.fingerId){
               touchStart = true;
                Vector2 offset = touchPos - startingPoint;
                Vector2 direction = Vector2.ClampMagnitude(offset, 1.0f);
                
                 pointB = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.transform.position.z));
               
                aimCharacter(direction);
                circle.transform.position = new Vector2(pointA.x + direction.x, pointA.y + direction.y) * 1;
                circle.transform.position = new Vector2(outerCircle.transform.position.x + direction.x, outerCircle.transform.position.y + direction.y);

            }else if(t.phase == TouchPhase.Ended && leftTouch == t.fingerId){
                
                circle.GetComponent<SpriteRenderer>().enabled = false;
            outerCircle.GetComponent<SpriteRenderer>().enabled = false;
                leftTouch = 99;
                circle.transform.position = new Vector2(outerCircle.transform.position.x, outerCircle.transform.position.y);
            }else{

                touchStart = false;
                
            }
            ++i;
        }    
    }

    Vector2 getTouchPosition(Vector2 touchPosition){
        return GetComponent<Camera>().ScreenToWorldPoint(new Vector3(touchPosition.x, touchPosition.y, transform.position.z));
    }

    private void FixedUpdate() {
          if (touchStart) {
            Vector2 offset = pointB - pointA;                
            Vector2 direction = Vector2.ClampMagnitude(offset, 1.0f);
            aimCharacter(direction * 1);    
          }
    }
    
    
    void aimCharacter(Vector2 direction){           
        Vector2 lookDir = pointB - pointA;
        float angle = Mathf.Atan2(lookDir.y, lookDir.x) * Mathf.Rad2Deg;
        rb.rotation = angle;            
    }    
}
\$\endgroup\$
0

1 Answer 1

0
\$\begingroup\$

Two problems:

  • Your Swipe script only looks at touch 0. Subsequent touches get ignored, not what you want.

  • Your Swipe script doesn't check to see if the touch is already handled by the joystick (ie. on the left side of the screen), so it can ignore it.

I'd rewrite the swipe script to look something more like this instead. Advantages of this approach:

  • We can now track multiple swipe attempts in progress from multiple touches. So if you're using the joystick or accidentally touched the screen with another finger while gripping your phone, this won't interrupt swipe tracking.

  • We can specify a rectangular region of the screen where swipes are allowed to originate. This lets you ignore the left half of the screen that's owned by the joystick. But since this is just data, you can change it for different instances of the script: say if you had a minigame that was all about swiping and had no joystick to avoid, you could let that one use the whole screen with the same code, just different Inspector configuration.

  • Instead of setting bools, I've used UnityEvent variables to let you wire up what should happen from each swipe. Now you don't need your GestureDetector script at all - the OnSwipeUp event can call ShootGreen() directly, without coupling in your code.

  • I added a small deadzone close to the 45-degree lines, where it's ambiguous whether the player meant to swipe left/right or up/down. That way, we wait and see if the touch continues to veer further horizontally or vertically, rather than firing off our event prematurely with maybe the wrong swipe detected.

I've commented the code sample, but please feel free to comment below if anything is unclear.

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

public class SwipeDetector : MonoBehaviour
{
    // To keep our code paths unified, we'll pretend the mouse is a finger
    // and just choose an ID for it that's obviously not one Unity uses for fingers.
    const int MOUSE_ID = -42;

    [Tooltip("Fraction of the screen where a swipe can start, where 0 is the bottom/left and 1 is top/right.")]
    public Rect swipeRange = new Rect(0.5f, 0f, 0.5f, 1f);
    [Tooltip("Fraction of the screen diameter a touch has to travel to be considered a swipe.")]
    public float deadZone = 0.1f;

    // We'll use events to fire off responses to swipes/taps instead of bools,
    // because we could hypothetically have two taps/swipes in the same frame from different fingers.
    // This also reduces coupling: your gameplay code doesn't need to know about swiping.
    public UnityEvent OnTouch;
    public UnityEvent OnSwipeUp;
    public UnityEvent OnSwipeDown;
    public UnityEvent OnSwipeLeft;
    public UnityEvent OnSwipeRight;

    // We'll use a dictionary to be able to track multiple swipes happening at the same time.
    Dictionary<int, Vector2> _swipeStartPos = new Dictionary<int, Vector2>();
    
    // Check if a touch at the given position is allowed to start a swipe.
    bool IsTouchStartValid(Vector2 position) {
        Vector2 normalized = new Vector2(position.x / Screen.width, position.y / Screen.height);
        return swipeRange.Contains(normalized);
    }

    // Handle initial touches in our range, and start tracking them for future swipes.
    void TryStartSwipe(int id, Vector2 startPosition) {       

        if (IsTouchStartValid(startPosition)) {            
            // Debug.Log("Swipe started at " + startPosition);
            _swipeStartPos[id] = startPosition;
            OnTouch?.Invoke();
        }
    }

    // Check if a moved touch should count as a swipe, and consume it if so.
    void CheckSwipe (int id, Vector2 currentPosition, float thresholdSquared) {
        // If this isn't a finger we're tracking, abort.
        if (!_swipeStartPos.TryGetValue(id, out Vector2 startPosition))
            return;

        // If the touch hasn't moved far enough to count yet, abort.
        Vector2 delta = currentPosition - startPosition;
        if (delta.sqrMagnitude < thresholdSquared)
            return;

        Vector2 absoluteDelta = new Vector2(Mathf.Abs(delta.x), Mathf.Abs(delta.y));

        // This adds a little extra dead zone around the 45-degree line, where it's
        // ambiguous whether the player was trying to swipe vertically or horizontally.
        // 1 = no deadzone, 0.2 = swiped axis must be 20% more than the perpendicular.
        float angularDeadzone = 1f + 0.2f;

        // If we've swiped in a particular direction, fire off the event callback,
        // and stop tracking this finger. (Need to tap again for a new swipe)
        if (absoluteDelta.x >= angularDeadzone * absoluteDelta.y) {
            if (delta.x > 0) {
                OnSwipeRight?.Invoke();
            } else {
                OnSwipeLeft?.Invoke();
            }
            EndSwipe(id);
        } else if (absoluteDelta.y >= angularDeadzone * absoluteDelta.x) {
            if (delta.y > 0) {
                OnSwipeUp?.Invoke();
            } else {
                OnSwipeDown?.Invoke();
            }
            EndSwipe(id);
        }        
    }

    // Don't call this "Reset" - that's a special function name the Unity Editor
    // uses to wipe a component's data clean. If that's not what the function
    // is for, use a different name so it's not called at the wrong time.
    void EndSwipe(int id) {
        // Technically, removing touches from tracking is not necessary,
        // since we'll overwrite the start position on the next Began.
        // I just like to do it for neatness, so the state in the dictionary
        // closely matches the state of active swipes in flight.
        _swipeStartPos.Remove(id);
    }

    void Update() {
        // Convert our normalized deadzone threshold into an absolute pixel size,
        // accounting for the possibility that our window size changed recently.
        float deadZoneThreshold = deadZone * Mathf.Min(Screen.width, Screen.height);
        deadZoneThreshold *= deadZoneThreshold;

        // Check all touches, not just the first one.
        for (int i = 0; i < Input.touchCount; i++) {
            var touch = Input.GetTouch(i);

            switch (touch.phase) {
                case TouchPhase.Began:
                    TryStartSwipe(touch.fingerId, touch.position);
                    break;

                case TouchPhase.Moved:
                    CheckSwipe(touch.fingerId, touch.position, deadZoneThreshold);
                    break;
                
                case TouchPhase.Ended:
                case TouchPhase.Canceled:
                    EndSwipe(touch.fingerId);
                    break;
            }
        }

        // Repeat the swipe lifecycle tracking for our mouse in standalone mode.
        if(Input.GetMouseButton(0)) {
            if(Input.GetMouseButtonDown(0)) {
                TryStartSwipe(MOUSE_ID, Input.mousePosition);
            } else {
                CheckSwipe(MOUSE_ID, Input.mousePosition, deadZoneThreshold);
            }
        } else if (Input.GetMouseButtonUp(0)) {
            // Technically a mouse can be clicked and released in the same frame.
            // We'll handle the release for the click-release case.
            // For the release-then-click case, the swipe start just gets
            // overwritten above, so it's safe to ignore.
            EndSwipe(MOUSE_ID);
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ First of all, thanks ALOT! And also thank you for explaining step by step what each part of the code does! I can finally swipe whilst holding down the other finger to use the joystick! The problem is that for some reason, if I don't have any finger on the screen, and I go to swipe, 2 bullets spawn, and they bump in each other so nothing happens (I put the shooting methods (from another script) inside the UnityEvents, I don't know if that's what I should have done). Also, the aiming angle is still being read from the finger swiping. Any idea regarding what I can do? Thanks! \$\endgroup\$ Commented Sep 28, 2020 at 16:28

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .