1.需求
首先我们先来说说需求,在U3D的引擎中,做一个ARPG游戏的现多点触控。
a. 点击A点,人物向A点移动,A点手指未抬起,按下B点,角色立刻响应B点手指点击,向B点移动。如果按下B点的过程中A点抬起,则松开B点后不再响应A点。
c.点击A点,A点手指未抬起,同时点击(没有长按)敌方目标B,B处手指松开,则角色立刻攻击一下B目标,然后再向A点移动。
d.点击A点,同时点击技能图标,则角色释放技能,释放结束后向A点移动。
2.框架设计
首先我们要通过分析需求,找到问题的本质。
a.其实虽然是多点触摸,我们可以理解为2点触摸,也就是有第三点出现时,我们忽略第三点。
b.无论是两点还是多点,游戏角色,在某一瞬间仅仅响应一个触点控制,这就意味着我们某一时刻仅需要判断出,在多个触碰手指中,拿个手指是有效的。
c.角色有三种行为,移动,普通攻击,释放技能,其实这是一个优先级队列,优先级为 技能 > 普攻 > 移动
通过上面的分析,我们可以初步判做出设计为,底层组件和应用层组件。
底层设计:判断手指的触碰,来实现某一瞬间仅仅有一个有效手指触点动作,并将该动作报告上层组件,进而由应用层组件进行使用。
应用层组件:接收来自底层组件的消息,然后对角色行为的优先级队列进行操作,让角色做出正确的决定。
3.下层组件
下层组件是绑定到人物身上的,人物身上有各种组件,可以根据需要进行添加删除,以达到最大的灵活性。
UnitCompoent就是人物身上的组件。
a.检测组件
我们先看看执行具体检测人物的代码:我们先使用一个通过的接口,然后有两个类分别实现这个接口,一个类负责检测鼠标事件,另一个负责检测触摸事件,当然这两个组件不会同时使用。
public class IControlHandler
{
int ControlID;//检测组件的唯一编号
public virtual void updateControl(float deltaTime) { }//用于检测操作
Vector3 initPressPos;
float pressDeltaTime;
static float sPressDeltaTime;
protected void InitPressPos(Vector3 initPos)//保存鼠标或者手指按下时的一些信息(位置和时间)
{
initPressPos = initPos;
pressDeltaTime = Time.realtimeSinceStartup;
sPressDeltaTime = Time.realtimeSinceStartup;
//Debug.LogError("IControl Down Init" + Time.realtimeSinceStartup);
}
protected bool IsDrag(Vector3 curPos)//判断是否有过拖动
{
//Debug.LogError("IControl Down Init" + Time.realtimeSinceStartup + " - " + sPressDeltaTime + " = " + (Time.realtimeSinceStartup - sPressDeltaTime));
float dis = Vector3.Distance(initPressPos, curPos);
if (GlobalSettings.Instance.isLockView)
{
return true;
}
else
{
if (Time.realtimeSinceStartup - sPressDeltaTime < 0.3f && dis < 0.5) return false;
}
if (dis < 0.5f)
{
}
return true;
}
}
#endregion
#region 鼠标检测组件
public class MouseHandler : IControlHandler
{
public MouseHandler(int mouseIndex, ControlEventManager teManager)
{
tEventManager = teManager;
mouseID = mouseIndex;
Init();
}
public int ControlID
{
get
{
return mouseID;//0,1,表示左右键
}
}
void Init()
{
hasPressed = false;
downTimeStamp = 0;
}
ControlEventManager tEventManager;//消息管理器引用,用于发送消息
int mouseID = -1;
bool hasPressed = false;//是否长按过
float downTimeStamp = 0;
Vector2 mousePos;//点击位置
public override void updateControl(float deltaTime)
{
#if UNITY_EDITOR || UNITY_STANDALONE
AnalysisState(deltaTime);//每帧进行检测
#endif
}
void AnalysisState(float deltaTime)//检测函数
{
mousePos = Input.mousePosition;//更新鼠标位置
if (Input.GetMouseButtonDown(ControlID))//如果按下左键
{
Down(deltaTime);//进一步处理
}
else if (Input.GetMouseButtonUp(ControlID))//检测按键弹起
{
Up(deltaTime);
}
else if (Input.GetMouseButton(ControlID))//检测鼠标是否是按下状态
{
if (IsDrag(Input.mousePosition))
Press(deltaTime);
}
}
void Down(float deltaTime)//处理按下状态
{
//YJLog.log3("Down Event");
hasPressed = false;
InitPressPos(Input.mousePosition);//每次按下初始化位置和时间
downTimeStamp = Time.realtimeSinceStartup;
CreateTouchEvent(EControlType.eDown);//构造消息
}
void Up(float deltaTime)
{
if (hasPressed)
{
hasPressed = false;
//YJLog.log3("Move End Event");
CreateTouchEvent(EControlType.eMoveEnd);//长按结束的事件
}
else
{
//YJLog.log3("Up Event");
CreateTouchEvent(EControlType.eUp);//按下弹起的事件
}
downTimeStamp = 0;
}
void Press(float deltaTime)
{
hasPressed = true;
//YJLog.log3("Press Event");
CreateTouchEvent(EControlType.ePress);//长按事件
}
void CreateTouchEvent(EControlType tType)//事件构发送
{
tEventManager.AddEvent(new ControlEvent()//添加事件到列表
{
type = tType,
id = ControlID,
touchDownTimeStamp = downTimeStamp,
position = mousePos,
createTimeStamp = Time.realtimeSinceStartup
});
}
}
#endregion
#region 触摸检测组件
public class TouchHandler : IControlHandler
{
public TouchHandler(int fingerIndex, ControlEventManager teManager)
{
tEventManager = teManager;
fingerID = fingerIndex;
Init();
}
public int ControlID
{
get
{
return fingerID;//0,1分别代表按下的第1,2根手指的fingerID,A按下,ID为0,B按下ID为1,A抬起,B ID不变,A再次按下,ID还是0
}
}
void Init()
{
hasPressed = false;
downTimeStamp = 0;
}
ControlEventManager tEventManager;
int fingerID;
bool hasPressed = false;
float downTimeStamp = 0;
Touch touchInfo;//保存touchInfo
public override void updateControl(float deltaTime)
{
#if !UNITY_EDITOR && (UNITY_ANDROID || UNITY_IOS)
AnalysisState(deltaTime);
#endif
}
void AnalysisState(float deltaTime)//分析手指触控动作
{
for (int i = 0; i < Input.touchCount; ++i)
{
Touch t = Input.GetTouch(i);
if (t.fingerId == ControlID)//FingerID如果是当前组件的ID,说明是当前组件处理,这两个是一致的,
{
touchInfo = t;
DidpatchEvent(touchInfo.phase, deltaTime);
}
}
}
void DidpatchEvent(TouchPhase phase, float deltaTime)
{
switch (phase)
{
case TouchPhase.Began://开始按下
Down(deltaTime);
break;
case TouchPhase.Ended://抬起
Up(deltaTime);
break;
case TouchPhase.Moved://按下后移动
if (IsDrag(touchInfo.position))
Press(deltaTime);
break;
case TouchPhase.Stationary://按下后保持不动,这里其实不需要IsDrag,这个代码被人改过,我也不想说啥了,聪明人都懂得
if (IsDrag(touchInfo.position))
Press(deltaTime);
break;
default:
break;
}
}
void Down(float deltaTime)
{
hasPressed = false;
InitPressPos(touchInfo.position);
downTimeStamp = Time.realtimeSinceStartup;
CreateTouchEvent(EControlType.eDown);
}
void Up(float deltaTime)
{
if (hasPressed)
{
hasPressed = false;
//Debug.LogError("IControl MoveEnd");
CreateTouchEvent(EControlType.eMoveEnd);
}
else
{
//Debug.LogError("IControl Up");
CreateTouchEvent(EControlType.eUp);
}
downTimeStamp = 0;
}
void Press(float deltaTime)
{
//Debug.LogError("IControl Press");
hasPressed = true;
CreateTouchEvent(EControlType.ePress);
}
void CreateTouchEvent(EControlType tType)
{
tEventManager.AddEvent(new ControlEvent()
{
type = tType,
id = ControlID,
touchDownTimeStamp = downTimeStamp,
position = touchInfo.position,
createTimeStamp = Time.realtimeSinceStartup
});
}
}
#region ControlType
public enum EControlType//触碰事件类型
{
eNull = 0,
eDown = 1,
ePress = 2,
eUp = 3,
eMoveEnd = 4
}
#endregion
#region ControlEventManager
public class ControlEventManager//触碰检测管理
{
List rowEventList;//保存未经过筛选的触控事件
List listTouchHandler;//集合中包含触碰检测组件,多点触控2个,鼠标1个,不会同时加载
ControlEvent lastTriggerEvent = null;//最后一个触发的有效(传给高层组件)的事件
//Camera m_UICamera;
bool isDebug = false;//是否开启调试
TouchDebug tDebug;//DEBUG组件
private Camera mUICamera//获取摄像机主要是需要支持多点触摸
{
get
{
return UICamera.mainCamera;
}
}
public ControlEventManager()
{
}
public void OnInit()
{
rowEventList = new List();
listTouchHandler = new List();
#if (UNITY_ANDROID || UNITY_IPHONE) && (!UNITY_EDITOR)
listTouchHandler.Add(new TouchHandler(0, this));//加载触摸检测组件 0
listTouchHandler.Add(new TouchHandler(1, this));//家在触摸检测组件 1
#else
listTouchHandler.Add(new MouseHandler(0, this));//加载鼠标检测组件 0
#endif
if (isDebug)
{
tDebug = new TouchDebug();
tDebug.OnInit();
}
}
public void OnExit()
{
if (isDebug && null != tDebug)
{
tDebug.OnExit();
}
}
public void OpenMultiTouch(bool b)//打开关闭多点触控,这里要注意,Input这里有个开关, NGUI的UICamera有个开关(该开关仅影响同时点击UI)
{
Input.multiTouchEnabled = b;
if (null!=mUICamera)
{
UICamera c = mUICamera.GetComponent();
if (c != null)
{
c.allowMultiTouch = b;
}
}
}
void ClearEvent()
{
rowEventList.Clear();
}
public void AddEvent(ControlEvent newEvent)
{
if (null != newEvent)
{
rowEventList.Add(newEvent);
}
}
public ControlEvent UpdateControl(float deltaTime)//每帧调用每个触摸组件检测
{
ClearEvent();
for (int i = 0; i < listTouchHandler.Count; ++i)
{
listTouchHandler[i].updateControl(deltaTime);
}
return HandleEvent();//这个时候如果有检测到触摸事件,应该已经添加到事件管理类中,这是处理其中的事件
}
ControlEvent HandleEvent()
{
ControlEvent ce = GetFirstPirorityEvent();//获取优先级最高的事件
if (isDebug)
{
tDebug.UpdateList(rowEventList);
}
if (null!=ce && IsEventChanged(ce) && (!IsTouchUI(ce.position)))//检测事件是否是有意义的改变,同事是否是触碰UI
{
lastTriggerEvent = ce;//返回有效事件
return ce;
}
else
{
//TouchDebug.PrintDebugInfo("==>>touch event: ignore");
return null;
}
}
ControlEvent GetFirstPirorityEvent()
{
rowEventList.Sort((x, y) => ControlEvent.CompareTouchEvent(x,y));//按照游戏的需求,对事件进行排序,
if (rowEventList.Count > 0)
{
return rowEventList[0];//返回第一个,也就是优先级最高的事件
}
return null;
}
bool IsEventChanged(ControlEvent cEvent)//检测事件是否改变
{
bool flag = true;
do
{
if (null == cEvent)//没有事件,就没有改变
{
flag = false;
break;
}
if (null == lastTriggerEvent)//有事件,且上次有效事件为空,那这个就是新的事件,有效
{
break;
}
if (cEvent.type != EControlType.ePress || lastTriggerEvent.type != EControlType.ePress)//上次和这次只要有一个不是长按事件,有效
{
break;//非长按事件的优先级比较高,这个是从需求分析出来的
}
if (cEvent.id != lastTriggerEvent.id)//不是同一个手指,有效,这个不一定触发上层组建的优先级队列,但是对于底层组件,这是个有效事件
{
break;
}//位置间隔小于2,且事件间隔小于0.2不触发,减少同类型事件的触发频率(其实这里可以看出,这里仅仅是两次事件都是press的情况,且没有移动)
if (Mathf.Abs(cEvent.position.x - lastTriggerEvent.position.x) < 2f && Mathf.Abs(cEvent.position.y - lastTriggerEvent.position.y) < 2f)
{
if (cEvent.createTimeStamp - lastTriggerEvent.createTimeStamp < 0.2f)
{
flag = false;
}
}
} while (false);
return flag;
}
private int UIlayerMask = (1 << LayerMask.NameToLayer("PlayerUI")) |
(1 << LayerMask.NameToLayer("NGUI")) |
(1 << LayerMask.NameToLayer("UIEffect"));//UI屏蔽位
public bool IsTouchUI(Vector3 targetPoint)//检测是否是对UI的点击,其实这个感觉可以从NGUI层面进行记录保存,(没有尝试过,不确定)
{
if (mUICamera == null) return false;
Ray ray = UICamera.mainCamera.ScreenPointToRay(targetPoint);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f, UIlayerMask))
{
return true;
}
return false;
}
}
#endregion
#region ControlEvent
public class ControlEvent//事件类
{
public EControlType type = EControlType.eNull;
public int id = -1;
public float touchDownTimeStamp = 0;//按下操作的时间戳,抬起以后为0
public Vector2 position;
public float createTimeStamp = 0;//事件创建的时间戳
public override string ToString()//debug使用
{
return string.Format("figureID={0} type={1} position=({2},{3}) ",
id,
type.ToString(),
position.x,
position.y
);
}//因为我们处理多点触摸的思路一直是将其抽象成一个单一的指令,所以需要排序选出一个最优解来
static public int CompareTouchEvent(ControlEvent a, ControlEvent b)//比较事件的优先级
{
if (null == a && null == b)
{
return 0;
}
else if (null == a)
{
return 1;
}
else if (null == b)
{
return -1;
}
else
{
if (a.type == EControlType.eDown)//A按下
{
if (b.type == EControlType.eDown)//B同时按下,这个可能性较小,如果出现,那么fingerID(触摸)小的,表示“先”按下的
{
if (a.id > b.id)
{
return -1;
}
else
{
return 1;
}
}
else//按下操作的优先级是最高的,超过长按和抬起
{
return -1;
}
}
else if (a.type == EControlType.ePress)//A长按
{
if (b.type == EControlType.eDown)//B按下,B优先级高
{
return 1;
}
else if (b.type == EControlType.ePress)//B 长按
{
if (a.touchDownTimeStamp > b.touchDownTimeStamp)//比较A,B两个事件在各自序列中谁先按下的,后按下的(时间戳大)优先级高
{
return -1;
}
else if (a.touchDownTimeStamp < b.touchDownTimeStamp)
{
return 1;
}
else
{
return 0;
}
}
else if (b.type == EControlType.eUp)//B弹起,A优先级高
{
return -1;
}
else
{
return -1;
}
}
else if (a.type == EControlType.eUp)//A弹起
{
if (b.type == EControlType.eDown)//B按下,B优先级高
{
return 1;
}
else if (b.type == EControlType.ePress)//B长按 B优先级高
{
return 1;
}
else if (b.type == EControlType.eUp)//B弹起,也就是同时弹起
{
if (a.id > b.id)//fingerID大的优先级高
{
return -1;
}
else
{
return 1;
}
}
else
{
return 0;
}
}
else
{
if (b.type == EControlType.eNull)
{
return 0;
}
else
{
return 1;
}
}
}
}
}
#endregion
c.下层组件
public class BattleTouchController:UnitComponent
{
//激活控制
protected bool isActive = true;//表示是否启用该组件
ControlEventManager tEventManager; //分别管理
List listTrigger;//消息监听,这个我们可以回头再讲讲
bool inital = false;
#region 组件生命周期
public BattleTouchController()
{
}
public BattleTouchController(Units self)
: base(self)
{
}
public void Enable(bool b)//组件的启用/禁用
{
isActive = b;
}
public override void OnInit()
{
if (!inital)
{
Init();
inital = true;
}
}
public override void OnStart()
{
if (RecManager.CheckControl())//非录像播放状态才需要启用组件,其实这个应该放在组件外判断,
{
isActive = true;
}
}
public void OnUpdateByFrame(float delta = 0)//每帧调用组件检测
{
if (isActive)
{
TriggerEvent(tEventManager.UpdateControl(delta));//调用组件检测
DebugInput();//检测辅助按键,也就是在电脑上模拟多点触控
}
}
public override void OnUpdate(float delta = 0)
{
}
public override void OnStop()
{
isActive = false;
}
public override void OnExit()//退出时
{
tEventManager.OpenMultiTouch(false);//关闭多点触控
isActive = false;
UnRegisterEvent();//取消事件监听,
tEventManager.OnExit();
}
#endregion
#region 触发消息
void TriggerEvent(ControlEvent tEvent)//
{
if (null == tEvent)
return;
TriggerParamTouch param = new TriggerParamTouch();
param.EventID = (int)tEvent.type;
param.Trigger = this;
param.Pos = tEvent.position;
param.FingerID = tEvent.id;
TriggerManager2.Instance.Trigger2(param);
}
#endregion
void Init()
{
listTrigger = new List();
tEventManager = new ControlEventManager();
tEventManager.OnInit();
tEventManager.OpenMultiTouch(true);
RegisterEvent(); //TriggerParam_touchController
}
void RegisterEvent()//注册事件
{
TriggerCreateParam_touchController param = new TriggerCreateParam_touchController();//该事件的作用是控制组件enable和disable
param.EventID = (int)EEventID2_touchController.eStop;
param.TriggerID = TriggerManager2.assign_trigger_id();
param.Func_actions = OnStopOrStart;
param.Func_conditions = null;
TriggerEvent2 trigger = TriggerManager2.CreateTriggerEvent2(param);
TriggerManager2.Instance.AddListener(trigger);
listTrigger.Add(trigger);
}
void UnRegisterEvent()//取消注册
{
TriggerManager2.Instance.RemoveListner(ETriggerType2.TriggerEvent2_manulController);//取消注册消息
foreach(var it in listTrigger)
{
TriggerManager2.Instance.RemoveListner(it);//
}
}
void OnStopOrStart(ITriggerDoActionParam param)//事件监听函数,用于开启/禁用组件
{
TriggerParam_touchController info = param as TriggerParam_touchController;
if (null == info)
return;
if (info.Start)
{
OnStart();
}
else
{
OnStop();
}
}
void DebugInput()//用两个按键模拟触发多点触控事件,方便电脑上调试。
{
#if UNITY_EDITOR
if (Input.GetKey(KeyCode.S))
{
TriggerParamTouch param = new TriggerParamTouch();
param.EventID = (int)EControlType.ePress;
param.Trigger = this;
param.Pos = new Vector2(123, 276);
param.FingerID = 1;
TriggerManager2.Instance.Trigger2(param);
}
if (Input.GetKey(KeyCode.F))
{
TriggerParamTouch param = new TriggerParamTouch();
param.EventID = (int)EControlType.ePress;
param.Trigger = this;
param.Pos = new Vector2(600, 276);
param.FingerID = 1;
TriggerManager2.Instance.Trigger2(param);
}
#endif
}
d. 下层debug组件
#region 触摸点调试
public class TouchDebug
{
string ret1 = "";
string ret2 = "";
string ret = "";
int testTouchNum = 0;
int createEventTimes = 0;
int callTimes = 0;
int updateTimes = 0;
Touch pointA;
Touch pointB;
UILabel labelRet1;
UILabel labelRet2;
UILabel labelRet;
UILabel eventLabel;
UILabel labelA;
UILabel labelB;
List eList;
GameObject root;
bool valid = false;
public TouchDebug()
{
}
public void OnInit()
{
do
{
GameObject obj = null;
obj = Resources.Load("Prefab/Debug/touchDebug") as GameObject;
if (null == obj)
break;
GameObject target = GameObject.Find("ViewRoot/Camera") as GameObject;
if (null == target)
break;
root = NGUITools.AddChild(target, obj);
if (null == root)
break;
root.name = "TouchDebug";
Transform trans = null;
trans = root.transform.FindChild("ret1");
if(null==trans)
break;
labelRet1 = trans.gameObject.GetComponent();
if (null == labelRet1)
break;
trans = root.transform.FindChild("ret2");
if (null == trans)
break;
labelRet2 = trans.gameObject.GetComponent();
if (null == labelRet2)
break;
trans = root.transform.FindChild("ret");
if (null == trans)
break;
labelRet = trans.gameObject.GetComponent();
if (null == labelRet)
break;
trans = root.transform.FindChild("event");
if (null == trans)
break;
eventLabel = trans.gameObject.GetComponent();
if (null == eventLabel)
break;
trans = root.transform.FindChild("pointA");
if (null == trans)
break;
labelA = trans.gameObject.GetComponent();
if (null == labelA)
break;
trans = root.transform.FindChild("pointB");
if (null == trans)
break;
labelB = trans.gameObject.GetComponent();
if (null == labelB)
break;
valid = true;
}
while (false);
}
public void OnExit()
{
if (valid && null != root)
{
GameObject.Destroy(root);
root = null;
valid = false;
}
}
public void UpdateList(List list)
{
if (valid)
{
eList = list;
RefreshDebugInfo_touchPoint();
RefreshDebugInfo_ret();
}
}
void RefreshDebugInfo_ret()
{
for (int i = 0; i < eList.Count; ++i)
{
ControlEvent tEvent = eList[i];
if (null != tEvent)
{
if (tEvent.id == 0)
{
ret1 = tEvent.ToString();
labelRet1.text = ret1;
}
else
{
ret2 = tEvent.ToString();
labelRet2.text = ret2;
}
}
}
if (eList.Count > 0 && eList[0] != null)
{
ret = "ret= " + eList[0].ToString();
labelRet.text = ret;
}
}
void RefreshDebugInfo_touchPoint()
{
testTouchNum = Input.touchCount;
if (testTouchNum > 0)
{
pointA = Input.GetTouch(0);
labelA.text = string.Format("Touch 1: Phase={0},Id = {1},pos={2}", pointA.phase.ToString(), pointA.fingerId, pointA.position);
}
if (testTouchNum > 1)
{
pointB = Input.GetTouch(1);
labelB.text = string.Format("Touch 2: Phase={0},Id = {1},pos={2}", pointB.phase.ToString(), pointB.fingerId, pointB.position);
}
}
public static void PrintDebugInfo(string str)
{
#if UNITY_EDITOR
ClientLogger.Debug(LogTagNames.TAG_NULL, str);
#endif
}
}
#endregion