【前序】
那么,首先各位2019新年快乐吧。跨年之际,一位学土木的业余游戏程序员,在这里为大家带来他的第一篇文章(并希望以后就把知乎当作自己博客算了)。知乎的App做的倒是挺不错的说,博客写在这里可以随时在手机上看到,而且突然发现自己竟然注册过(虽然也被推荐过去CSDN一类的专业IT站去写博客),嘛不过既然注册了知乎能用就用便是了,博客又不是非得写给谁看得,更多的是为自己写的就是了。而且其实我也不是专攻编程软件开发方面的,一方面是被分到了土木专业,更多的是有一个成为全栈独立游戏制作者的梦想(或者说努力挣扎的方向),比起那些专业的IT站或许知乎这里更适合分享各方面的知识。相信这里也有一些真正想学技术的人,嘛也不乏一些吃瓜的人,不过都欢迎评论就是了,大家一起笑着讨论知识便是
【2D虚拟手柄】
首先来说说2D虚拟手柄吧。就是你玩碧蓝航线的时候左下角的那个控制舰娘方向的手柄。也是你玩第五人格,手游吃鸡的时候左下角那个控制方向的手柄
2D虚拟手柄个人给予它的定义是:在手机游戏中,通过触摸并滑动,在以初始触摸点为中心参考,结合当前滑动的触摸点,以完成对于方向的控制的一种实现方式
它的原理基于实体手柄中的摇杆所设计
以下对于如何实现虚拟手柄进行泛型的大致方法总结,以及如何在unity3d中实现虚拟手柄进行详细的阐述
【定义与揭示】
以下对于【2D虚拟手柄】的一些相关概念进行个人的定义和解释,引入一些新的定义名词,以便更好的,详细的阐述【2d虚拟手柄】
----【锁定式】与【非锁定式】:这是两种【2d虚拟手柄】的常见分类形式,区别在于【2d虚拟手柄】是否被固定在一个位置并处于始终可见的状态(【锁定式】),还是在一定的范围内,当你摁下时以你的初始触摸点为中心生成手柄(【非锁定式】)
----【触摸点】:即用户手指触摸手机屏幕并滑动的过程中,手指的当前位置
----【参考中心】:通过与【触摸点】进行相应的运算,来完成【2d虚拟手柄】主要功能的一个重要的中心参考位置。无论何时【手柄圆】都应以【参考中心】为圆心。【锁定式】中的锁定也是【参考中心】的锁定,而【非锁定式】中,则以用户当前摁下时的初始触摸位置为参考中心来生成手柄
---- 【触点圆】:【2d虚拟手柄】中反应当前触摸位置的一个圆形,用于告知操作者当前的触摸相对位置。但【触点圆】的位置应被限制在【手柄圆】中,即当前若【触摸点】在【手柄圆】中,【触点圆】的位置就是【触摸点】的位置,若当前【触摸点】不在【手柄圆】中,【触点圆】的位置应是【触摸点】相对【参考中心】的向量与【手柄圆】边界相交的位置
----【手柄圆】:【2d虚拟手柄】中以【参考中心】为圆心,一定半径的一个大圆形。用于限制【触点圆】,也对于实现【2d虚拟手柄】的相关功能有一定的作用
----【启发域】:即【2d虚拟手柄】接受用户初始触摸输入的一个限制范围,由于不可能将整个触摸屏的范围都交由【2d虚拟手柄】来使用,因此【2d虚拟手柄】的触摸接收就需要设置一定的初始触摸接收范围。在捕捉到一个符合要求的【触摸点】后,将其传入【2d虚拟手柄】的接收方法中,并对其进行持续的“关注”,以其对应的当前【触摸点】结合【参考中心】进行相应的运算,以完成【2d虚拟手柄】相应功能的实现。在【锁定式】中【启发域】就是【手柄圆】,在【非锁定式】中【启发域】则是一个屏幕内的限制范围
【unity3d的实现】
那么终于到了万众期待的实现环节,且听我慢慢道来其结合【unity3d】引擎的实现方法
好吧其实代码早就写好了,也被用在了其他许多项目中了,只是一直没有好好总结一下,并整理出一个可用的Package罢了,下面也是进行了一些总结和更正
以下是对于【非锁定式】的实现,完成后其实插入一个Flag变量并区分一下OnPointerDown中的触摸位置,以及在抬起和摁下时一些方法就可用自然的完成【锁定式】了
首先先让我们看看场景中需要的对象和的层级关系
>【启发域】用一个Panel来实现并自然作为画布的子对象
>>【手柄圆】用一张Image并作为【启发域】Panel的子对象
>>>【触点圆】用一张Image并作为【手柄圆】Image的子对象
之后是要求RectTransform中的锚点为x y 均拉开且Left Right Top Bottom数据均为0,如下图中的Panel锚点设置
这里是要用到UnityEngine.EventSystems;中的一些接口,来完成对于【触摸点】的捕捉·,那么自然脚本就需要挂在【启发域】的那张Panel上,让我们创建一个名为Button_Handle的脚本并挂在Panel上吧
之后进入脚本环节
首先让我们先引入命名空间,并实现几个需要被使用到的接口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public class Button_Handle : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler, IBeginDragHandler
{
//接口
public void OnBeginDrag(PointerEventData eventData)//滑动开始
{
}
public void OnDrag(PointerEventData eventData)//滑动中
{
}
public void OnPointerDown(PointerEventData eventData)//触摸开始
{
}
public void OnPointerUp(PointerEventData eventData)//触摸结束
{
}
//Base
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
之后我们来声明以下需要被用到的变量,public变量通过场景中拖拽完成赋值。
这里public Camera mcma;//摄像机 获取了摄像机是由于eventData.position 是一个ScreenPoint 而不同分辨率下相同的相对位置的ScreenPoint是不同的,即800*600中屏幕的最右上角点是(800,600)而1920*1080中则是(1920,1080)。需要通过ViewPoint来进行统一,而unity中管理UI对象位置的RectTransform也是使用ViewPoint所以,需要利用摄像机的ScreenToViewportPoint方法进行转换
不过这里做一个更正,应使用Camera.main的方法来获取,而不是public Camera mcma;并在场景中拖拽,因为只是需要利用ScreenToViewportPoint方法罢了,画布也被设置为Screen Spack - Overlay 所以用Camera.main的方法来获取显然更好一些,所以之后的代码中都使用了Camera.main的方法
没有声明出【参考中心】是由于button_Img的中心,以及相对button_Img的ViewPoint坐标(0.5,0..5)代表就代表了【参考中心】的位置
//UI_Button 控制
public GameObject button_Img;//手柄圆
public GameObject button_Point;//触摸圆
public Camera mcma;//摄像机
bool isonCon = false;//是否正在控制
之后一步步按照【2d虚拟手柄】的设计输入操作步骤,和相应的实现功能来一步步完成这个【2d虚拟手柄】
首先,脚本被挂在Panel上,并实现相应的接口来完成输入,所以当被实现的接口方法被调用时,就已经保证了触摸点一定在【启发域】中了
那么当用户摁下时,我们希望显示【手柄圆】,抬起时则取消显示(【非锁定式】的特性)
public void OnPointerDown(PointerEventData eventData)//触摸开始
{
button_Img.SetActive(true);//显示
}
public void OnPointerUp(PointerEventData eventData)//触摸结束
{
button_Img.gameObject.SetActive(false);//取消显示
}
ok这样就好了
之后我们希望在用户摁下时的初始触摸位置为中心设置【手柄圆】的位置,并将【触点圆】置于【手柄圆】的中心,也就是初始触摸位置
我们在OnPointerDown中来完成这两步操作
public void OnPointerDown(PointerEventData eventData)//触摸开始
{
button_Img.SetActive(true);//显示
//根据初始点击点 设置【手柄圆】的位置
Vector3 viewPoint = Camera.main.ScreenToViewportPoint(new Vector3(eventData.pressPosition.x,eventData.pressPosition.y, 0));
setToViewPoint(viewPoint, gameObject, button_Img, true);
//根据初始点击点 设置【触点圆】的位置
setToViewPoint(new Vector2(0.5f, 0.5f), button_Img, button_Point, false);
}
这里我们声明了一个setToViewPoint的方法,根据传入的ViewPoint来调整第三参数中传入的GameObjecr的RectTransform的位置,并根据第四参数,一个布尔型来判断是否考虑第二参数的【转入偏移量】
下面来说说【转入偏移量】吧
首先我们再回忆一下现在场景物体的层级关系(你可以翻到前面去看看,这里不再过多赘述)
ok,脚本是被挂在Panel上的,【手柄圆】是Panel的子对象,如果你知道RectTransform的特性,那么你就应该知道,此时【手柄圆】Image的RectTransform坐标和Panel的坐标已经不一样了
Panel是画布的子对象,对于它来说ViewPoint的点(1,1)就是画布的最右上角(也是屏幕的最右上角)
而【手柄圆】的Image是Panel的子对象,对于它来说ViewPoint的点(1,1)就是Panel的最右上角
在实际使用中由于Panel实现的是【启发域】,你可以再读一读我对【启发域】的定义和解释,就知道画布的最右上角和Panel的最右上角是不一样的(见下图,常见的以左1/4屏幕作为【启发域】时)
而此时你使用ScreenToViewportPoint转换eventData.pressPosition的ScreenPoint数据后得到的ViewPoint数据是相对于画布中的ViewPoint数据,而不是相对于Panel的,即(1,1)点对应画布最右上角
而由于【手柄圆】的Image是【启发域】的那张Panel的子对象,所以你需要用对应Panel中的ViewPoint来设置【手柄圆】的Image的位置,所以需要考虑一个将高一级(画布中)的ViewPoint转入低一级(Panel中)的一个偏移转换问题
但在设置【触点圆】的Image时,【触点圆】一定需要被设置在【手柄圆】的中心,由于【触点圆】的Image是【手柄圆】的Image的子对象,所以显然ViewPoint数据应是(0.5,0.5)且不考虑偏移
之后的位置设置就是根据初始锚点的长宽,将锚点中心至于最终需要的位置坐标上
以下是setToViewPoint的代码
void setToViewPoint(Vector2 viewPoint, GameObject objbase, GameObject objset, bool model)
//根据base中view点 设置set RectTrans位置 model true:由点击触发考虑baseView偏移值(进行ViewPoint转入) false:不考虑baseView偏移值(硬性设置)
{
//获取RectTransform
RectTransform objset_rt = objset.GetComponent();
//获取Img Anchor占比距离 Panel Anchor占比距离
float objset_x = objset_rt.anchorMax.x - objset_rt.anchorMin.x;
float objset_y = objset_rt.anchorMax.y - objset_rt.anchorMin.y;
Vector2 topanel_Point;//最终需要的坐标位置
if (model)
{
topanel_Point = changeInBase(viewPoint, objbase);//将高一级的ViewPoint转入低一级中
}
else
{
topanel_Point = viewPoint;
}
objset_rt.anchorMin = new Vector2(topanel_Point.x - objset_x / 2, topanel_Point.y - objset_y / 2);
objset_rt.anchorMax = new Vector2(topanel_Point.x + objset_x / 2, topanel_Point.y + objset_y / 2);
}
这里在model为true时使用了一个changeInBase的转换方法来完成将高一级的ViewPoint转入低一级中
以下是changeInBase的代码
Vector2 changeInBase(Vector2 viewPoint, GameObject objbase)//将高一级的ViewPoint 转入低一级
{
//获取RectTransform
RectTransform objbase_rt = objbase.GetComponent();
float objbase_x = objbase_rt.anchorMax.x - objbase_rt.anchorMin.x;
float objbase_y = objbase_rt.anchorMax.y - objbase_rt.anchorMin.y;
//获取ViewPoint转换至objbase中的对应的View点
return new Vector2((viewPoint.x - objbase_rt.anchorMin.x) / objbase_x, (viewPoint.y - objbase_rt.anchorMin.y) / objbase_y);
}
转换原理是首先根据objbase的RectTransform最左下角锚点,将ViewPoint的原点先变换为objbase的最左下角,在将其位置坐标的x y分别对应除以objbase的RectTransfor的x y锚点长宽(相当于是根据objbase的锚点宽高进行了一定比例的收缩)
ok,那么现在我们还需要在用户滑动过程中设置【触点圆】Image的位置
我们在OnDrag所实现的方法中,完成用户触摸后滑动的相关控制
那么我们首先需要把ScreenToViewportPoint转换eventData.pressPosition的ScreenPoint数据后得到的ViewPoint数据再做两次转换,先转入【启发域】的Panel中,再转入【手柄圆】的Image中,以设置作为【手柄圆】Image子对象的【触点圆】Image的位置
而根据【触摸点】是否在【手柄圆】中,【触点圆】的设置分为两种(如果不记得了,你可以往前翻一翻,看看我之前对【触点圆】的定义和解释)
下面是onDrag中的代码
public void OnDrag(PointerEventData eventData)//滑动中
{
//获取ViewPoint
Vector3 viewPoint = Camera.main.ScreenToViewportPoint(new Vector3(eventData.position.x, eventData.position.y, 0));
Vector2 panelViewPoint = changeInBase(viewPoint, gameObject);
//
Vector2 button_base_ViewPoint = changeInBase(panelViewPoint, button_Img.gameObject);
Vector2 toMiddlePoint = new Vector2(button_base_ViewPoint.x - 0.5f, button_base_ViewPoint.y - 0.5f);
float disToMiddlePoint = Mathf.Sqrt(toMiddlePoint.x * toMiddlePoint.x + toMiddlePoint.y * toMiddlePoint.y);
if (disToMiddlePoint - 0.5 < 0.001)//在范围内
{
//按键设置
setToViewPoint(panelViewPoint, button_Img.gameObject, button_Point.gameObject, true);
}
else//不在范围内
{
//按键设置
Vector2 goToViewPoint = toMiddlePoint.normalized * 0.5f;
setToViewPoint(new Vector2(goToViewPoint.x + 0.5f, goToViewPoint.y + 0.5f), button_Img.gameObject, button_Point.gameObject, false);
}
}
好的至此,我们的【2d虚拟手柄】的基础操作接收部分算是完成了
那么我们再来插入一个 isLock的布尔型变量,并进行相关的设置,来完成【锁定式】的【2d虚拟手柄】的实现
以下是更改过后的相关代码
相关设置讲解见注释
public bool isLock = false;//是否是【锁定式】
bool isStartInHandlePoint = false;//触摸点一开始是否在【手柄圆】中
//接口
public void OnDrag(PointerEventData eventData)//滑动中
{
if (isLock&&!isStartInHandlePoint)//【锁定式】且初始触摸点不在【手柄圆】中,不进行onDrag
{
return;
}
//获取ViewPoint
Vector3 viewPoint = Camera.main.ScreenToViewportPoint(new Vector3(eventData.position.x, eventData.position.y, 0));
Vector2 panelViewPoint = changeInBase(viewPoint, gameObject);
//
Vector2 button_base_ViewPoint = changeInBase(panelViewPoint, button_Img.gameObject);
Vector2 toMiddlePoint = new Vector2(button_base_ViewPoint.x - 0.5f, button_base_ViewPoint.y - 0.5f);
float disToMiddlePoint = toMiddlePoint.magnitude;
if (disToMiddlePoint - 0.5 < 0.001)//在范围内
{
//按键设置
setToViewPoint(panelViewPoint, button_Img.gameObject, button_Point.gameObject, true);
}
else//不在范围内
{
//按键设置
Vector2 goToViewPoint = toMiddlePoint.normalized * 0.5f;
setToViewPoint(new Vector2(goToViewPoint.x + 0.5f, goToViewPoint.y + 0.5f), button_Img.gameObject, button_Point.gameObject, false);
}
}
public void OnPointerDown(PointerEventData eventData)//触摸开始
{
if (!isLock)//【非锁定式】
{
button_Img.SetActive(true);//显示
//根据初始点击点 设置【手柄圆】的位置
Vector3 viewPoint = Camera.main.ScreenToViewportPoint(new Vector3(eventData.pressPosition.x, eventData.pressPosition.y, 0));
setToViewPoint(viewPoint, gameObject, button_Img, true);
//根据初始点击点 设置【触点圆】的位置
setToViewPoint(new Vector2(0.5f, 0.5f), button_Img, button_Point, false);
}
else
//【锁定式】需要根据初始触摸点是否在【手柄圆】中设置flag告知onDrag是否需要触发,以及在范围内时立刻完成对于【触摸点】位置的设置
{
Vector3 viewPoint = Camera.main.ScreenToViewportPoint(new Vector3(eventData.position.x, eventData.position.y, 0));
Vector2 panelViewPoint = changeInBase(viewPoint, gameObject);
//
Vector2 button_base_ViewPoint = changeInBase(panelViewPoint, button_Img.gameObject);
Vector2 toMiddlePoint = new Vector2(button_base_ViewPoint.x - 0.5f, button_base_ViewPoint.y - 0.5f);
float disToMiddlePoint = toMiddlePoint.magnitude;
if (disToMiddlePoint - 0.5 < 0.001)//在范围内
{
isStartInHandlePoint = true;
//按键设置
setToViewPoint(panelViewPoint, button_Img.gameObject, button_Point.gameObject, true);
}
else
{
isStartInHandlePoint = false;
}
}
}
public void OnPointerUp(PointerEventData eventData)//触摸结束
{
if (isLock)//【锁定式】抬起时【触点圆】回归中心,而不是取消显示
{
setToViewPoint(new Vector2(0.5f, 0.5f), button_Img, button_Point, false);
return;
}
button_Img.gameObject.SetActive(false);//取消显示
}
void Start () {
if (isLock)//【锁定式】一开始就需要被设置在中心
{
setToViewPoint(new Vector2(0.5f, 0.5f), button_Img, button_Point, false);
}
else//【非锁定式】一开始保证不现实【手柄圆】
{
button_Img.SetActive(false);
}
}
下面是一个实际中使用的例子:
1.控制角色的移动:
这里是之前写好的使用了【非锁定式】的代码,所以没有呼应上面的【锁定式】代码的更正
onDrage代码更改为:
public void OnDrag(PointerEventData eventData)
{
//获取ViewPoint
Vector3 viewPoint = mcma.ScreenToViewportPoint(new Vector3(eventData.position.x, eventData.position.y, 0));
Vector2 panelViewPoint = changeInBase(viewPoint, gameObject);
//
Vector2 button_base_ViewPoint = changeInBase(panelViewPoint, button_Img.gameObject);
Vector2 toMiddlePoint = new Vector2(button_base_ViewPoint.x - 0.5f, button_base_ViewPoint.y - 0.5f);
float disToMiddlePoint = toMiddlePoint.magnitude;
if (disToMiddlePoint - 0.5 < 0.001)//在范围内 比例速度
{
//按键设置
setToViewPoint(panelViewPoint, button_Img.gameObject, button_Point.gameObject, true);
//速度设置
nowSpeed= maxSpeed * (disToMiddlePoint / 0.5f);
movetoPos = toMiddlePoint;
//float onConSpeed = maxSpeed * (disToMiddlePoint / 0.5f);
//setPlayerSpeed(toMiddlePoint, onConSpeed);
}
else//最大速度
{
//setPlayerSpeed(toMiddlePoint, maxSpeed);
//按键设置
Vector2 goToViewPoint = toMiddlePoint.normalized * 0.5f;
setToViewPoint(new Vector2(goToViewPoint.x+0.5f,goToViewPoint.y+0.5f), button_Img.gameObject, button_Point.gameObject, false);
//速度设置
nowSpeed = maxSpeed;
movetoPos = toMiddlePoint;
}
}
关于速度设置
这里根据触摸点是否在【手柄圆】中,还将速度的设置分为在【手柄圆】内的比例速度(按触摸点与【参考中心】的距离占【手柄圆】半径的百分比)和不在【手柄圆】内时的最大速度。
变量toMiddlePoint就是触摸点到【参考中心】的相对ViewPoint向量
变量disToMiddlePoint则是toMiddlePoint的向量模长
之后在开始滑动时设置一个flag变量为在控制,抬起时设置为不在控制并将角色速度归0(停下),不断更新nowSpeed速度和movetoPos方向并根据flag在Update中根据当前nowSpeed和movetoPos的来设定角色的速度
这里不在onDrag中设置速度的原因是由于滑动中停下时,onDrag就不会再调用了,会出现控制时速度设定的中断,因而需要转到Update中在检测到进行了滑动后每一帧都绝对的去设置速度
以下是其他相关的代码更改
public void OnPointerUp(PointerEventData eventData)
{
button_Img.gameObject.SetActive(false);
isonCon = false;
//ClearSpeed
onControlPlayer.velocity = new Vector3(0f, onControlPlayer.velocity.y, 0f);
}
Vector2 speedDir;
Vector3 baseSpeedDir;
Vector3 basesDir;
public Transform cmabaseTrans;
//受控Player
void setPlayerSpeed(Vector2 dir,float speed)
{
speedDir = dir.normalized * speed;
if (playerMod == PlayerMod.ThirdView_UnLock)
{
basesDir = new Vector3(speedDir.x, 0f, speedDir.y);
basesDir = cmabaseTrans.TransformVector(basesDir);
speedDir = new Vector2(basesDir.x, basesDir.z);
}
baseSpeedDir= new Vector3(speedDir.x, onControlPlayer.velocity.y, speedDir.y);
onControlPlayer.velocity = onControlPlayer.transform.TransformVector(baseSpeedDir);
}
// Use this for initialization
void Start()
{
}
Vector3 lookDir;
Quaternion now;
Quaternion lookto;
// Update is called once per frame
void Update()
{
if (isonCon)
{
if (playerMod == PlayerMod.ThirdView_UnLock)
{
cmabaseTrans.position = new Vector3(mcma.transform.position.x, mcma.transform.position.y - mcma.transform.localPosition.y, mcma.transform.position.z);
cmabaseTrans.LookAt(onControlPlayer.gameObject.transform);
Vector3 lookDir = basesDir;
Quaternion now = onControlPlayer.transform.Find("Body").rotation;
Quaternion lookto = Quaternion.LookRotation(lookDir, Vector3.up);
onControlPlayer.transform.Find("Body").rotation = Quaternion.RotateTowards(now, lookto,270* Time.deltaTime);
}
setPlayerSpeed(movetoPos, nowSpeed);
}
}
public void OnBeginDrag(PointerEventData eventData)
{
isonCon = true;
}
关于人物朝向的设置
这里还可以看到上面的代码Update中有一个
if(playerMod == PlayerMod.ThirdView_UnLock)
的判定,并根据这个判定(当前是【第三人称非锁定旋转型】的角色控制模式,就是第五人格的那种人物控制模式)还需要在检测【2d虚拟手柄】的输入控制时进行人物朝向的一些相关设定操作
这里关于【第三人称非锁定旋转型】以及包括这种控制模式的总共三种3D游戏角色控制模式,我将在接下来的“【unity3d模块总结】-3D游戏角色控制 ”一文中进行详细的归纳、阐述和总结。此处不再赘述。
ok,那么打错字的地方见谅,技术尚浅,不对的地方欢迎评论指正,欢迎评论交流
这里是Randy ---一位学土木的游戏程序员
再次祝各位新年快乐