This question already has answers here:
Detect swipe gesture direction
(5 answers)
Closed 5 years ago.
I'm implementing specific type of touch controller.
The player needs to hold their finger on screen in order to move.
Without lifting the finger, the player can swipe in different direction to change direction while still moving.
Once the finger is lifted, the player stops moving.
It's been a hard time isolating specific swipes (i.e. lines drawn on screen) ignoring any other movements that are not intended to draw a line.
For instance slight finger movements done when the player's finger is "stationary", they mess up my algorithm.
I thought about different approaches such as storing the last few touches and evaluate upon them to determine if there has been a swipe or not, but couldn't implement it properly.
Here's what I've tried so far. Most of the time it works fine, but often the player does erratic movement and goes absolutely the opposite direction to what I intended.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TouchInputHandler : AbstractInputHandler {
private const int SWIPE_MIN_DISTANCE = 35;
private Vector2 touchStartPoint;
private Vector2 touchMovePoint;
void Update () {
Touch[] touches = Input.touches;
if (touches.Length == 1) {
Touch firstTouch = touches [0];
if (firstTouch.phase == TouchPhase.Began) {
this.touchStartPoint = firstTouch.position;
fireNextDirectionChanged (currentDirection);
} else if (firstTouch.phase == TouchPhase.Moved) {
this.touchMovePoint = firstTouch.position;
if (Vector2.Distance(touchStartPoint, touchMovePoint) > SWIPE_MIN_DISTANCE) {
detectSwipeDirection ();
}
} else if (firstTouch.phase == TouchPhase.Stationary) {
touchStartPoint.x = touchMovePoint.x;
touchStartPoint.y = touchMovePoint.y;
} else if (firstTouch.phase == TouchPhase.Ended) {
fireNextDirectionChanged (Constants.Direction.NONE);
}
}
}
private void detectSwipeDirection() {
float xDiff = touchMovePoint.x - touchStartPoint.x;
float yDiff = touchMovePoint.y - touchStartPoint.y;
Constants.Direction nextDirection;
bool yGreater = Mathf.Abs(yDiff) >= Mathf.Abs(xDiff);
if (yGreater) {
// direction is up or down
nextDirection = yDiff < 0 ? Constants.Direction.DOWN : Constants.Direction.UP;
} else {
// direction is left or right
nextDirection = xDiff < 0 ? Constants.Direction.LEFT : Constants.Direction.RIGHT;
}
if (nextDirection != this.currentDirection)
{
fireNextDirectionChanged (nextDirection);
this.currentDirection = nextDirection;
}
}
}
I think you simply forgot a line setting the previous touch position to the current one in the TouchPhase.Moved block. Just add touchStartPoint = this.touchMovePoint; and it should work (only tested using mouse inputs but logic remains the same).
Also I commented the TouchPhase.Stationary block: I feel like it does what I suggested before but only when the finger is completely immobile. The final code looks like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TouchInputHandler : AbstractInputHandler
{
private const int SWIPE_MIN_DISTANCE = 35;
private Vector2 touchStartPoint;
private Vector2 touchMovePoint;
void Update()
{
Touch[] touches = Input.touches;
if(touches.Length == 1)
{
Touch firstTouch = touches[0];
if(firstTouch.phase == TouchPhase.Began)
{
this.touchStartPoint = firstTouch.position;
this.currentDirection = Constants.Direction.NONE;
fireNextDirectionChanged(currentDirection);
}
else if(firstTouch.phase == TouchPhase.Moved)
{
this.touchMovePoint = firstTouch.position;
if(Vector2.Distance(touchStartPoint, touchMovePoint) > SWIPE_MIN_DISTANCE)
{
detectSwipeDirection();
}
touchStartPoint = this.touchMovePoint; // <= NEW !
}
//else if(firstTouch.phase == TouchPhase.Stationary)
//{
// touchStartPoint.x = touchMovePoint.x;
// touchStartPoint.y = touchMovePoint.y;
//}
else if(firstTouch.phase == TouchPhase.Ended)
{
this.currentDirection = Constants.Direction.NONE;
fireNextDirectionChanged(Constants.Direction.NONE);
}
}
}
private void detectSwipeDirection()
{
float xDiff = touchMovePoint.x - touchStartPoint.x;
float yDiff = touchMovePoint.y - touchStartPoint.y;
Constants.Direction nextDirection;
bool yGreater = Mathf.Abs(yDiff) >= Mathf.Abs(xDiff);
if(yGreater)
{
// direction is up or down
nextDirection = yDiff < 0 ? Constants.Direction.DOWN : Constants.Direction.UP;
}
else
{
// direction is left or right
nextDirection = xDiff < 0 ? Constants.Direction.LEFT : Constants.Direction.RIGHT;
}
if(nextDirection != this.currentDirection)
{
fireNextDirectionChanged(nextDirection);
this.currentDirection = nextDirection;
}
}
}
Also when using a distance based on in game distance to detect swipe, I'd suggest adding a Screen.width/height ratio somewhere if you want to port your project to multiple resolutions devices (on Start() do something like SWIPE_MIN_DISTANCE *= Screen.width / BASE_SCREEN_WIDTH with private const int BASE_SCREEN_WIDTH = 1024;).
Hope this helps,
Related
In my 2D unity mobile game, when I touch and pull the ball it shows up a lineRenderer from the ball through the way I pull there is no problem with that but if I touch to screen while the ball is dynamic (before it stopped) the new lineRenderer take (0,0) point as the center instead of the location of the ball.
This is how it works properly when I touch the screen while the ball is not moving
This is the problematic version line renderer starts from the point (0,0) instead of the ball
void Update()
{
if (Input.touchCount > 0 && !hasMoved)
{
touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began)
{
dragStart();
}
if (touch.phase == TouchPhase.Moved)
{
Dragging();
}
if (touch.phase == TouchPhase.Ended)
{
DragRelease();
}
}
}
void dragStart()
{
dragStartposition = transform.position;
dragStartposition.z = 0;
gostergeLine.positionCount = 1;
gostergeLine.SetPosition(0,transform.position);
}
void Dragging()
{
Vector3 draggingPos = Camera.main.ScreenToWorldPoint(touch.position);
draggingPos.z = 0;
gostergeLine.positionCount = 2;
gostergeLine.SetPosition(1, draggingPos);
}
void DragRelease()
{
gostergeLine.positionCount = 0;
Vector3 dragReleasePos = Camera.main.ScreenToWorldPoint(touch.position);
dragReleasePos.z = 0;
Vector3 forceVec = dragStartposition - dragReleasePos;
forcePower = forceVec.magnitude;
if(forcePower > 45)
{
topRb.velocity = forceVec * 45;
}
else
{
topRb.velocity = forceVec * forcePower;
}
}
Even though it does not read my touch while the ball is moving (!hasmoved); if I touch the screen before it stopped, the new linerenderer shows up in wrong direction.
Update is called every frame.
The TouchPhase.Began is only true in the very first frame of a touch -> your ball was moving then so you ignored it
Now you continue touching so eventually TouchPhase.Moved is true at some point
=> Since the ball stopped in the meantime you now call Dragging without having called dragStart before.
You could make sure that the other cases are only called at all if dragStart was actually called first.
You could e.g. use a nullable
private Vector2? dragStartposition;
and then check for it in
void Dragging()
{
if(dragStartposition == null) return;
...
}
and also reset it in
void DragRelease()
{
if(dragStartposition == null) return;
...
Vector3 forceVec = dragStartposition.Value - dragReleasePos;
...
dragStartposition = null;
}
This way if a touch started to early it doesn't trigger the other methods, only if one started after the ball has stopped.
There is still a pitfall though with multiple touches
=> I would also use exactly one touch only
if(Input.touchCount == 1 && !hasMoved)
I am currently working on a 2D isometric game where the player will be able to control different units, and interact through them.
I created a Scriptable Object called UnitType. Thanks to this system I can define an action range and a moving range, that is the maximum distance in cells that the player can move to or interact with.
The problem is I don't know how to implement this through code. This is what I want to achieve, using an action range of 2 cells for the demonstration.
This is the goal
With a friend of mine, we thought about calculating the linear equation of those 4 lines to check if the raycast hit was within them, but it's not working right with negative x.
This is the current system
What would be the best practice ?
Thank you very much for your time and attention,
Mousehit is a Unity GameObject that is the child of the character. Its positions is bound to the center of the tilemap cells thanks to mousePosition that is a script attached to the GameManager.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class Interact : MonoBehaviour
{
[SerializeField] private Sprite upperLeft, upperRight, midLeft, midRight, bottomLeft, bottomMid, bottomRight;
[SerializeField] private GameObject mouseHit;
public UnitType unitType;
private GameObject gameManager;
private CheckMousePosition mousePosition;
private bool isInteracting, isAtGoodDistance;
private SpriteRenderer spriteRenderer;
private Tilemap tilemap;
private RaycastHit2D hit;
//private Transform mouseHit;
void Start()
{
tilemap = GameObject.Find("Tilemap").GetComponent<Tilemap>();
gameManager = GameObject.FindGameObjectWithTag("GameManager");
mousePosition = gameManager.GetComponent<CheckMousePosition>();
spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
isInteracting = false;
mouseHit.SetActive(false);
}
// Update is called once per frame
void Update()
{
if (isInteracting)
{
mouseHit.SetActive(true);
mouseHit.transform.position = new Vector3(mousePosition.tilemap.GetCellCenterLocal(mousePosition.cellPosition).x, mousePosition.tilemap.GetCellCenterLocal(mousePosition.cellPosition).y, 1);
if (Input.GetMouseButtonDown(0))
{
Interaction();
}
}
if (isInteracting == false)
{
spriteRenderer.sprite = null;
mouseHit.SetActive(false);
}
}
public void InitInteraction()
{
isInteracting = true;
transform.root.GetComponent<ContextualMenu>().CloseContextualMenu();
}
private void Interaction()
{
//When the player interacts, we cast a ray that will determine what he is interacting with.
hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero);
if (hit.collider != null)
{
if (isInteracting)
{
isInteracting = false; //the player is no longer interacting.
//If the player is interacting with a farmable cell using the worker.
if (transform.root.name == "Worker"
&& hit.collider.GetComponent<FarmableCell>() != null)
{
CheckInteractionDistance();
//hit.collider.GetComponent<FarmableCell>().CheckIfFarmed();
}
}
}
}
private void CheckInteractionDistance()
{
if (mouseHit.transform.localPosition.y <= (-0.5 * (mouseHit.transform.localPosition.x) + unitType.actionDistance / 2)
&& mouseHit.transform.localPosition.x >= (-0.5 * (mouseHit.transform.localPosition.x) - unitType.actionDistance / 2)
&& mouseHit.transform.localPosition.x >= (0.5 * (mouseHit.transform.localPosition.x) - unitType.actionDistance / 2)
&& mouseHit.transform.localPosition.x <= (0.5 * (mouseHit.transform.localPosition.x) + unitType.actionDistance / 2))
{
Debug.Log("ok");
}
else
{
Debug.Log("not ok");
}
}
}
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 3 years ago.
Improve this question
I've been trying to make a script for NPCs in a grid based game (the squares are 1 unit wide). The NPCs should be moving from 1 square (es. (1, 0)) to an adiacent one ((0, 0), (1, -1) (1, 1), (2, 0)), then he should stay idle for a certain amount of time before choosing another direction. Obviously, it already has a script that detects colliding objects in the adiacent squares.
The problem is that sometimes it moves to a position that isn't an integer (es. (0, 0,768)), but it corrects itself after a few steps. Could someone correct the code where it's needed?
P.S., the token is a GameObject that reserves the square for the NPC that created it, so to block any other entity from moving to those coordinates
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NPCMoveToPointScript : MonoBehaviour
{
public float speed = 2.0f; //movement speed
public Vector3 pos;
List<int> directions; //available directions
//where he's facing
public bool up;
public bool down;
public bool left;
public bool right;
public int dir; //value to use for animator
public float idleTime= 0.5f;
public bool idle; //false if a movement button is pressed
public bool canMove = true;
public GameObject token; //to avoid collisions between entities while moving towards a tile
CollisionScript cs;
void Start()
{
pos = transform.position;
down = true;
cs = GetComponent<CollisionScript>();
}
void Update() //move stickman in the direction of the keys and maintain the direction he's facing with animations
{
dir = ChooseDirection();
if (transform.position == pos) //go idle
{
StartCoroutine(StayIdle(idleTime));
}
//go in the chosen direction
if (!idle && dir == 0 && transform.position == pos)
{
AllFalse();
down = true;
pos += Vector3.down;
}
else if (!idle && (dir ==1) && (transform.position == pos))
{
AllFalse();
left = true;
pos += Vector3.left;
}
else if (!idle && (dir == 2) && (transform.position == pos))
{
AllFalse();
up = true;
pos += Vector3.up;
}
else if (!idle && dir == 3 && transform.position == pos)
{
AllFalse();
right = true;
pos += Vector3.right;
}
//if you just started moving towards a different tile, spawn a Token in that tile to avoid other npcs moving towards it
if (((transform.position.x - (int)transform.position.x == 0) && (transform.position.y - (int)transform.position.y == 0)) && pos != transform.position)
{
Instantiate(token, pos, Quaternion.identity);
}
transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * speed);
}
//avoid bug by deactivating all direction booleans before changing one
void AllFalse()
{
up = false;
down = false;
left = false;
right = false;
idle = false;
}
//checks how many directions are available and then chooses one of them randomly
int ChooseDirection()
{
directions = new List<int>();
//check the directions
if (!cs.hitDown)
{
directions.Add(0);
}
if (!cs.hitLeft)
{
directions.Add(1);
}
if (!cs.hitUp)
{
directions.Add(2);
}
if (!cs.hitRight)
{
directions.Add(3);
}
//turn directions into an array and pick a random direction
var array = directions.ToArray();
directions.Clear();
return array[Random.Range(0, (array.Length))];
}
//literaly what the name says (for some seconds) lol
public IEnumerator StayIdle(float idleTime)
{
yield return new WaitForSeconds(idleTime);
idle = true;
yield return new WaitForSeconds(4*idleTime);
idle = false;
}
}
I've found a solution that works perfectly (thanks to various suggestions in the comments). If anyone else needs a working casual grid movement script, here it is
public class NPCMovementNew : MonoBehaviour {
public float speed = 2.0f; //movement speed
public float idleTime = 1.0f; //how much time does he spend idling
public int dir; //where he's facing
public bool idle; //false if a movement button is pressed
public bool canMove = true; //to disable casual movement
public GameObject token; //to avoid collisions between entities while moving towards a tile
Vector3 pos;
List<int> directions; //available directions
CollisionScript cs;
// Use this for initialization
void Start () {
cs = GetComponent<CollisionScript>();
pos = transform.position;
dir = 0;
}
// Update is called once per frame
void Update () {
if(canMove && !idle) //check if he can move
{
if (transform.position != pos) //if he's not at the center of a square, he's not idling
{
idle = false;
transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * speed); //move to that position
}
else //here he decides to move
{
StartCoroutine(IdleThenMove(idleTime));
}
}
}
//function that goes idle for a certain time, then moves to an adiacent square
IEnumerator IdleThenMove(float t)
{
idle = true;
yield return new WaitForSeconds(t);
//here it randomly chooses a direction and translates it to a position vector
dir = ChooseDirection();
switch (dir)
{
case 0:
pos += Vector3.down;
break;
case 1:
pos += Vector3.left;
break;
case 2:
pos += Vector3.up;
break;
case 3:
pos += Vector3.right;
break;
default:
pos = transform.position;
break;
}
idle = false;
Instantiate(token, pos, Quaternion.identity); //spawn token to occupy square
transform.position= Vector3.MoveTowards(transform.position, pos, Time.deltaTime * speed); //move to that position
}
//checks how many directions are available and then chooses one of them randomly
int ChooseDirection()
{
directions = new List<int>();
//check the directions
if (!cs.hitDown)
{
directions.Add(0);
}
if (!cs.hitLeft)
{
directions.Add(1);
}
if (!cs.hitUp)
{
directions.Add(2);
}
if (!cs.hitRight)
{
directions.Add(3);
}
//turn directions into an array and pick a random direction
var array = directions.ToArray();
directions.Clear();
return array[Random.Range(0, (array.Length))];
}
}
I want to detect the mouse swipe up and swipe down, I tried the script below but it only works:
1 - if the finger has been released between any two swipes.
2 - if the finger hasn't been released, but only if the second swipe has exceed the original finger position (firstPressPos).
What I want exactly is:
For example, I put my finger on the screen and I swipe down, then after I swipe up (without releasing the finger between the two swipes), I want to detect the two swipes in the real time.
How can I do that?
Script:
if (Input.GetMouseButtonDown(0))
{
firstPressPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y);
}
if (Input.GetMouseButton(0))
{
secondPressPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y);
currentSwipe = new Vector3(secondPressPos.x - firstPressPos.x, secondPressPos.y - firstPressPos.y);
currentSwipe.Normalize();
if (currentSwipe.y > 0 && currentSwipe.x > -0.5f && currentSwipe.x < 0.5f)
{
//Swipe Up
}
if (currentSwipe.y < 0 && currentSwipe.x > -0.5f && currentSwipe.x < 0.5f)
{
//Swipe Down
}
}
You need to add a couple of flags to know which swipe status you're in if your are in one.
private bool _swiping;
private bool _swipingDown;
private Vector3 _previousSwipePosition;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
_previousSwipePosition = Input.mousePosition;
_swiping = true;
Debug.log("Started Swipe");
}
else if (Input.GetMouseButtonUp(0))
{
_swiping = false;
Debug.log("Finished Swipe");
}
if (_swiping)
{
Vector3 newPosition = Input.mousePosition;
if (newPosition.y < _previousSwipePosition.y)
{
if (!_swipingDown)
{
Debug.Log("Started Swipe Down");
_swipingDown = true;
}
}
if (newPosition.y> _previousSwipePosition.y)
{
if (_swipingDown)
{
Debug.Log("Started Swipe Up");
_swipingDown = false;
}
}
_previousSwipePosition = newPosition;
}
}
This will let you know when a Swipe is Started, Finished and Changes direction using flags.
flags - booleans to signify something
Using ARKit I can tap on the surface to place 3D object here. I also can move my finger and thus move the object along the surface.
How can I make an object to automatically appear and stick to the surface in front of the camera, with no need in touching the screen?
Here is the example script for placing 3D objects by finger tap:
using System;
using System.Collections.Generic;
namespace UnityEngine.XR.iOS
{
public class UnityARHitTestExample : MonoBehaviour
{
public Transform m_HitTransform;
bool HitTestWithResultType (ARPoint point, ARHitTestResultType resultTypes)
{
List<ARHitTestResult> hitResults = UnityARSessionNativeInterface.GetARSessionNativeInterface ().HitTest (point, resultTypes);
if (hitResults.Count > 0) {
foreach (var hitResult in hitResults) {
Debug.Log ("Got hit!");
m_HitTransform.position = UnityARMatrixOps.GetPosition (hitResult.worldTransform);
m_HitTransform.rotation = UnityARMatrixOps.GetRotation (hitResult.worldTransform);
Debug.Log (string.Format ("x:{0:0.######} y:{1:0.######} z:{2:0.######}", m_HitTransform.position.x, m_HitTransform.position.y, m_HitTransform.position.z));
return true;
}
}
return false;
}
// Update is called once per frame
void Update () {
if (Input.touchCount > 0 && m_HitTransform != null)
{
var touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Began || touch.phase == TouchPhase.Moved)
{
var screenPosition = Camera.main.ScreenToViewportPoint(touch.position);
ARPoint point = new ARPoint {
x = screenPosition.x,
y = screenPosition.y
};
// prioritize reults types
ARHitTestResultType[] resultTypes = {
ARHitTestResultType.ARHitTestResultTypeExistingPlaneUsingExtent,
// if you want to use infinite planes use this:
//ARHitTestResultType.ARHitTestResultTypeExistingPlane,
ARHitTestResultType.ARHitTestResultTypeHorizontalPlane,
ARHitTestResultType.ARHitTestResultTypeFeaturePoint
};
foreach (ARHitTestResultType resultType in resultTypes)
{
if (HitTestWithResultType (point, resultType))
{
return;
}
}
}
}
}
}
}
I've done something similar in Objective-C, so hopefully I can help with your unity example.
My logic was basically that placing the objects using a tap is based on the hitTest function with a point provided to it retrieved from the touch location. So I just created a CGPoint programmatically in the center of the screen and ran the hitTest function with this point. Had it hooked to a timer, and when a hit returned objects were added there.
CGPoint point = CGPointMake(self.sceneView.frame.size.width / 2, self.sceneView.frame.size.height / 2); //Get a point at the middle of the screen
NSArray <ARHitTestResult *> *hitResults = [_sceneView hitTest:point types:ARHitTestResultTypeFeaturePoint]; //Try to do a AR Hit Test on this point
if ([hitResults count] != 0) { //If have any results
//Perform desired operation
}