因为最近制作了一款儿童教育游戏,所以涉及到不少操作手势,其中比较有意思的是这样一个手势如图: 在游戏中这个画圈的手势往往位于一个可操作物体之上,并绕着中心顺时针旋转.这个物体可能是一个转盘,也可能是一个打蛋用搅拌器等等,意思是提示用户手指按照这个轨迹操作就可以得到正确的反馈,比如顺时针转动转盘实现开关…
在参考了unity商店的LogViewer插件以后(插件的开启手势正巧是在屏幕上画圈),我写了一个测试脚本如下:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class TestGestureAround : MonoBehaviour { public bool bIsClockWise; public Camera camGestureTest; public GameObject objRotateTarget; public float fGestureRadius = 50; public float fGestureRadiusFix = 10; public float fRotateFactor = 500f;//旋转系数 private Vector3 _v3AroundCenterPoint; private bool _bGesturing; private Vector3 _v3LastStarPoint; private float _fSampleDisThreshold; private List<Vector3> _inputGesturePhases = new List<Vector3>(); private float _fTotalRotate; System.Action OnRotateFinish; // Use this for initialization void Awake () { _fSampleDisThreshold = fGestureRadius / 4f;//(2*PI/24),PI~=3 _v3AroundCenterPoint = camGestureTest.WorldToScreenPoint(objRotateTarget.transform.position); _fTotalRotate = 0; } // Update is called once per frame void Update () { if (Input.GetMouseButtonDown(0)) { _bGesturing = true; _v3LastStarPoint = Input.mousePosition; } else if (Input.GetMouseButtonUp(0)) { _bGesturing = false; } if (_bGesturing) { //支持一个大概的圆形区,空心处理的小一点,就用修正值了 if (Vector3.Distance(_v3AroundCenterPoint, Input.mousePosition) < fGestureRadius + fGestureRadiusFix && Vector3.Distance(_v3AroundCenterPoint, Input.mousePosition) > fGestureRadiusFix) { var deltaVec = Input.mousePosition - _v3LastStarPoint; if (deltaVec.sqrMagnitude > _fSampleDisThreshold * _fSampleDisThreshold)//超过阈值,记录一下 { _inputGesturePhases.Add(deltaVec); if (_inputGesturePhases.Count > 1) { int curCount = _inputGesturePhases.Count; float multiDot = Vector3.Dot(_inputGesturePhases[curCount - 1], _inputGesturePhases[curCount - 2]); Vector3 multiCross = Vector3.Cross(_inputGesturePhases[curCount - 1], _inputGesturePhases[curCount - 2]); if (multiDot <= 0)//画圆只能是锐角 { _inputGesturePhases.Clear(); } else if (multiCross.z == 0 || (multiCross.z > 0 && !bIsClockWise) || (multiCross.z < 0 && bIsClockWise))//叉积右手法则,顺时针后一条叉前一条,z应该是正,z是0表示平行 { _inputGesturePhases.Clear(); } else { //通过上面几个条件测试表示是正在画一个圆,可以转动物体了 float rotateZ = bIsClockWise ? -1 * fRotateFactor * Time.deltaTime : 1 * fRotateFactor * Time.deltaTime; _fTotalRotate += rotateZ; objRotateTarget.transform.Rotate(new Vector3(0, 0, rotateZ)); _v3LastStarPoint = Input.mousePosition; if (Mathf.Abs(_fTotalRotate) > 360) { Debug.Log("Around callbakc here"); _fTotalRotate = 0; } } } } } else { _inputGesturePhases.Clear();//乱画就清除路径,重新来过 } } } }想要看上面代码的效果,只需要新建一个带摄像机的空场景,建立一个cube,放在屏幕可见的位置,然后脚本挂在摄像机上并拖上参数(如图)运行: 运行以后试着在物体的周围逆时针画圈,物体就能开始转动啦.
因为是测试,所以代码中只有mouse的检测而没有涉及到Input.Touch的逻辑,如果要支持移动端操作,这方面我在正式代码中采用了LeanTouch插件,是免费而且开源的,有兴趣的话可以检索unity商店下载并自行替换.
现在来解释一下代码的大概思路: 1.当点击屏幕且未松开的情况下,只要手指移动一段距离,就把这个路径记录在list内. 2.当list中有两个元素以后,计算这两条路径的角度和角度的方向. 3.如果满足期望,就认为是在画圈的操作中,否则就认为用户没有按照期望来画圈,清空所有路径.
看了上面三条以后,可以知道代码的核心算法其实就是求向量的点积和叉积:
float multiDot = Vector3.Dot(_inputGesturePhases[curCount - 1], _inputGesturePhases[curCount - 2]); Vector3 multiCross = Vector3.Cross(_inputGesturePhases[curCount - 1], _inputGesturePhases[curCount - 2]);点积可以判断向量的角度,而叉积可以知道这个角度的方向,也就是说是顺时针方向的角还是逆时针方向的角. 当我们的操作通过了距离,角度,方向的种种判断之后,可以认为用户的操作是正确的,因此,代码中将目标物体旋转了一个小小的角度.当用户持续操作,我们创建的cube就可以旋转完整的360度了.
在实际的开发中,可能我们遇到的情况会比这个更复杂,比如无论正逆都可以接受,或者逻辑和表现剥离后加入各类回调,或者是范围的控制这些其实都好解决,只要在对应的if判断前后做出修改即可.