一、概述
1.實現的基本操作是:
1)用手柄抓住黃色的方塊代表手抓住鼠標。
2)通過移動手柄模擬鼠標移動,電腦屏幕上的光標跟着移動。
3)當光標移動到一個Button上時,Button高亮,離開時Button取消高亮,點擊Button觸發點擊事件。
4)當點擊Button之后,打開一個畫圖程序,可以用光標在顏色選擇區選擇一種顏色,然后在畫圖區根據光標的移動軌跡,畫出選擇顏色的光標移動路徑的曲線;
2.腳本
1)ComputerController掛在代表電腦的Canvas上,本例掛在Computer上;
2)MouseController掛在一個代表鼠標的物體上,本例掛在Mouse上;
3)ComputerCursorController 掛在表示光標的一個Image上,本例掛在Cursor上;
4) ComputerClickable掛在所有可點擊的應用程序圖標上,在本例中只有一個應用程序,掛在PaintProgramIcon上;
5)PaintProgram一個畫圖程序,在本例中掛在PaintProgram這個Panel上
3.場景的Hierachy面板
二、實現
1.手柄的操作的鼠標設置:
由於VRTK這個插件集成了很好的物理交互功能,所以就手柄與場景物體交互方面選擇用VRTK這個插件。
下面是代表鼠標的黃色的Cube的設置
首先需要掛上如上面圖中的所有組件:
1)Rigidbody需要設置約束:禁止Y方向的移動,以及任意方向的旋轉;
2)將VRTK_TrackObjectGrabAttach這個腳本拖到VRTK_InteractableObject的Grab Attach Mechanic上;
3)將Grab Override Button選擇一個手柄上不存在鍵,HTC VIVE中這個鍵是Button One,我們將在代碼中設置抓取;
4)將VRTK_InteractableObject這個組件上的IsGrabable和IsUsable打上勾;
5)將VRTK_Interact Controller Apperance的Hide Controller On Grab打勾;
Mouse Controller這個腳本就是控制鼠標移動的;
using System; using UnityEngine; using VRTK; [RequireComponent(typeof(VRTK_InteractableObject))] public class MouseController : MonoBehaviour { public Transform mousePad;//鼠標墊 public Action ClickDown;//當手柄上的use鍵按下的時候,引發的事件(Trigger鍵); public Action ClickUp;//當手柄上的use鍵抬起的時候,引發的事件; private Rect mappingRect;//這個鼠標的移動范圍(用來映射電腦屏幕上的光標的位置) private Vector2 mouseReletiveToMousePadPostion;//鼠標相對於鼠標墊的位置 Rigidbody rig; BoxCollider boxCollider; VRTK_InteractableObject mouse; VRTK_InteractGrab controller;//當前正在使用鼠標的手柄 bool isUsedToGrab;//鼠標是不是被手柄抓住 float releaseDistance;//手柄離開鼠標的距離(不再抓住鼠標了) void Start() { //初始化一些數據 mappingRect = GetMatchRect(mousePad); CalculateMouseReletivePos(); mouse = GetComponent<VRTK_InteractableObject>(); rig = GetComponent<Rigidbody>(); boxCollider = GetComponent<BoxCollider>(); releaseDistance = boxCollider.bounds.size.y * 2f; //監聽手柄觸碰到鼠標的事件 mouse.InteractableObjectTouched += Mouse_InteractableObjectTouched; //監聽手柄不再觸碰鼠標事件 mouse.InteractableObjectUntouched += Mouse_InteractableObjectUntouched; } /// <summary> /// //當手柄觸碰到鼠標時,執行的事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Mouse_InteractableObjectTouched(object sender, InteractableObjectEventArgs e) { //監聽手柄使用鍵按下時的事件 mouse.InteractableObjectUsed += Mouse_InteractableObjectUsed; } /// <summary> /// 當手柄use鍵按下時,執行的事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Mouse_InteractableObjectUsed(object sender, InteractableObjectEventArgs e) { isUsedToGrab = true;//設置抓取住了鼠標 controller = e.interactingObject.GetComponent<VRTK_InteractGrab>();//設置當前抓取鼠標的手柄 controller.AttemptGrab();//強制手柄抓住鼠標 mouse.InteractableObjectUsed -= Mouse_InteractableObjectUsed;//不再監聽use鍵按下事件 //監聽當前操作的手柄use鍵按下時的事件 controller.GetComponent<VRTK_InteractUse>().UseButtonPressed += MouseController_UseButtonPressed; //監聽當前操作的手柄use鍵抬起時的事件 controller.GetComponent<VRTK_InteractUse>().UseButtonReleased += MouseController_UseButtonReleased; } private void MouseController_UseButtonReleased(object sender, ControllerInteractionEventArgs e) { //引發事件 if (ClickUp != null) { ClickUp(); } } private void MouseController_UseButtonPressed(object sender, ControllerInteractionEventArgs e) { if (ClickDown != null) { ClickDown(); } } private void Mouse_InteractableObjectUntouched(object sender, InteractableObjectEventArgs e) { rig.velocity = Vector3.zero;//一旦手柄不再和鼠標有碰撞,這時要讓鼠標的速度為0 if (isUsedToGrab)//如果之前是抓住了鼠標的 { isUsedToGrab = false; controller.GetComponent<VRTK_InteractUse>().UseButtonPressed -= MouseController_UseButtonPressed; controller.GetComponent<VRTK_InteractUse>().UseButtonReleased -= MouseController_UseButtonReleased; controller.ForceRelease();//強制松開 } else { mouse.InteractableObjectUsed -= Mouse_InteractableObjectUsed; } controller = null; } void Update() { if (isUsedToGrab)//只有當鼠標被抓住時才執行 { //根據鼠標移動的速度,手柄相應程度的振動 VRTK_ControllerReference controllerReference = VRTK_ControllerReference.GetControllerReference(controller.gameObject); float force = VRTK_SDK_Bridge.GetControllerVelocity(controllerReference).sqrMagnitude; VRTK_SDK_Bridge.HapticPulse(controllerReference, force / 3); CalculateMouseReletivePos(); //判定當前手柄是不是離開了鼠標 if ((transform.position - controller.transform.position).sqrMagnitude > releaseDistance * releaseDistance) { controller.ForceRelease();//強制松開 } } } public Vector2 MouseReletiveToTablePos { get { return mouseReletiveToMousePadPostion; } } /// <summary> /// 計算鼠標相對於鼠標墊的位置,x和y都是0-1 ; /// </summary> void CalculateMouseReletivePos() { float x = Mathf.InverseLerp(mappingRect.xMin, mappingRect.xMax, transform.position.x); float y = Mathf.InverseLerp(mappingRect.yMin, mappingRect.yMax, transform.position.z); mouseReletiveToMousePadPostion.Set(x, y); } /// <summary> /// 設置鼠標的移動范圍 /// </summary> /// <param name="content"></param> /// <returns></returns> public Rect GetMatchRect(Transform content) { Vector3 contentSize = content.GetComponent<MeshRenderer>().bounds.size; Vector3 selfSize = GetComponent<MeshRenderer>().bounds.size; //讓Rect的position是在鼠標墊的左下角 Vector2 pos = new Vector2(content.localPosition.x - contentSize.x * 0.5f + selfSize.x * 0.5f, content.localPosition.z - contentSize.z * 0.5f + selfSize.z * 0.5f); //設置Rect的長度和寬度(應該減去鼠標自身的長寬) Vector2 size = new Vector2(contentSize.x - selfSize.x, contentSize.z - selfSize.z); Rect rect = new Rect(pos, size); return rect; } }
2.電腦屏幕光標的設置:
需要注意的是:錨點在左下角,pivot在自身矩形的左上角(鼠標的尖端的位置)
ComputerCursorController是實現鼠標和光標位置映射的類
using System; using UnityEngine; public class ComputerCursorController : MonoBehaviour { public MouseController mouseController; public Action OnMoved; public Action ClickedDown; public Action ClickedUp; private Rect rect;//表示電腦屏幕范圍的Rect private RectTransform rectTransform;//這個光標的RectTransform void Start() { //代表電腦屏幕范圍的Rect的位置計算 rect = transform.parent.GetComponent<RectTransform>().rect; rect.position += new Vector2(rect.width / 2f, rect.height / 2f); rectTransform = transform as RectTransform; } void OnEnable() { mouseController.ClickUp += MouseController_ClickUp); mouseController.ClickDown += MouseController_ClickDown; } void OnDisable() { mouseController.ClickUp -= MouseController_ClickUp; mouseController.ClickDown -= MouseController_ClickDown; } private void MouseController_ClickDown() { if (ClickedDown != null) { ClickedDown(); } } private void MouseController_ClickUp() { if (ClickedUp != null) { ClickedUp(); } } void Update() { Vector2 parameter = mouseController.MouseReletiveToTablePos; Vector2 vector = new Vector2(Mathf.Lerp(rect.xMin, rect.xMax, parameter.x), Mathf.Lerp(rect.yMin, rect.yMax, parameter.y)); //當鼠標映射的位置和光標的位置不等的時候,說明這個時候鼠標是在移動的 if (rectTransform.anchoredPosition != vector && OnMoved != null) { OnMoved(); } rectTransform.anchoredPosition = vector; } }
3.響應光標的點擊事件
在電腦中點擊桌面上的一個圖標的時候,是可以進入應用程序的,在VR中直接單擊進入程序就好,ComputerController這個類用來控制光標引發的一些事件
using System; using UnityEngine; public class ComputerController : MonoBehaviour { public ComputerClickable[] clickables;//桌面上所有可點擊的應用程序圖標 public ComputerCursorController cursorController;//光標 private ComputerClickable currentClickable;//當前光標所在圖標 private ComputerClickable cacheClickedClickable;//緩存的圖標 [SerializeField] private Canvas canvas;//代表Computer的畫布 public Action Clicked; public Action UnClicked; void OnEnable() { cursorController.OnMoved += CheckClickablesIsHoverByCursor; cursorController.ClickedDown += ClickDown; cursorController.ClickedUp += ClickUp; } void OnDisable() { cursorController.OnMoved -= CheckClickablesIsHoverByCursor; cursorController.ClickedDown -= ClickDown; cursorController.ClickedUp -= ClickUp; } /// <summary> /// 檢查光標是不是移動到應用程序圖標上 /// </summary> private void CheckClickablesIsHoverByCursor() { for (int i = 0; i < clickables.Length; i++) { if (clickables[i].CheckHoverByCursor(CursorPosition)) { currentClickable = clickables[i]; return; } } currentClickable = null; } private void ClickDown() { if (currentClickable != null) { currentClickable.Click(); cacheClickedClickable = currentClickable; currentClickable = null; } if (Clicked != null) { Clicked(); } } private void ClickUp() { if (cacheClickedClickable != null) { cacheClickedClickable.UnClick(); cacheClickedClickable = null; } if (UnClicked != null) { UnClicked(); } } public Canvas Canvas { get { return canvas; } } /// <summary> /// 光標的位置 /// </summary> public Vector2 CursorPosition { get { return RectTransformUtility.WorldToScreenPoint(canvas.worldCamera, cursorController.transform.position); } } }
4.可點擊的應用程序圖標
using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; public class ComputerClickable : MonoBehaviour { RectTransform rectTransform; Canvas canvas; Button button; bool isHighlighter;//當前Button是否高亮 void Start() { //初始化字段 rectTransform = transform as RectTransform; canvas = GetComponentInParent<Canvas>(); button = GetComponent<Button>(); button.onClick.AddListener(() => Debug.Log("Clicked")); } /// <summary> /// 光標是否移動到自身上 /// </summary> /// <param name="cursorPos">光標的位置</param> /// <returns>True 當前光標在自身上</returns> public bool CheckHoverByCursor(Vector2 cursorPos) { //檢查一個RectTransform是不是包含一個點 bool isHorver = RectTransformUtility.RectangleContainsScreenPoint(rectTransform, cursorPos, canvas.worldCamera); PointerEventData eventData = new PointerEventData(EventSystem.current); if (isHorver && !isHighlighter)//如果包含,且當前Button沒有高亮 { //引發Button高亮 ExecuteEvents.Execute(gameObject, eventData, ExecuteEvents.pointerEnterHandler); isHighlighter = true; } else if (!isHorver && isHighlighter)//如果沒有包含,但是Button高亮,說明光標已經離開 { isHighlighter = false; ExecuteEvents.Execute(gameObject, eventData, ExecuteEvents.pointerUpHandler); ExecuteEvents.Execute(gameObject, eventData, ExecuteEvents.pointerExitHandler); ExecuteEvents.Execute(gameObject, eventData, ExecuteEvents.deselectHandler); } return isHorver; } public void Click() { ExecuteEvents.Execute(gameObject, new PointerEventData(EventSystem.current), ExecuteEvents.pointerDownHandler); button.onClick.Invoke(); } public void UnClick() { ExecuteEvents.Execute(gameObject, new PointerEventData(EventSystem.current), ExecuteEvents.pointerUpHandler); // button.OnPointerUp(new PointerEventData(EventSystem.current)); } }
至此基本功能已經實現,接下來可以寫一個小程序來試試;
三、簡單的畫圖小程序
畫圖小程序主要包含3個部分
1)顏色選擇區(ColorPick)
2)顏色選擇展示區(SelectColor)
3)畫圖區(PaintImage)
腳本功能實現:
using UnityEngine.UI; using UnityEngine; using System; using System.Linq; public class PaintProgram : MonoBehaviour { public ComputerController computer; public int pictureWidth;//畫圖區的寬度 public int pictureHeight;//畫圖區的高度 public RawImage pictureImage;//畫圖區 public RawImage colorPickImage;//顏色選擇區 public Image selectedColorDisplay;//顏色選擇展示區 private Texture2D pictureTex;//賦值給畫圖區的Texture private Texture2D colorPickTex;//顏色選擇區的Texture private Rect pictureRect;//畫圖區區域 private Rect colorPickerRect;//顏色選擇區區域 private bool canOperate;//當前是否可以操作(畫圖或者選擇顏色) private bool isInDrawArea;//光標是否在畫圖區 private bool isInPickArea;//光標是否在顏色選擇區域 private Color selectedColor = Color.red;//當前從顏色選擇區選擇的顏色 //在本例中是給每個像素賦值,用這兩個參數,可以給一個區域內的像素賦值 private Color[] c;//色塊的顏色 private Vector2 lineSize;//畫圖區線的大小 void Awake() { pictureTex = new Texture2D(pictureWidth, pictureHeight); pictureTex.filterMode = FilterMode.Point; pictureTex.wrapMode = TextureWrapMode.Clamp; pictureImage.texture = pictureTex; ResetPixes(Color.white); //設置畫圖區域矩形的位置 pictureRect = pictureImage.GetComponent<RectTransform>().rect; pictureRect.position = computer.Canvas.transform.InverseTransformPoint(pictureImage.transform.position); pictureRect.center = pictureRect.position; //設置顏色選擇區域矩形的位置 colorPickerRect = colorPickImage.GetComponent<RectTransform>().rect; colorPickerRect.position = computer.Canvas.transform.InverseTransformPoint(colorPickImage.transform.position); colorPickerRect.center = colorPickerRect.position; colorPickTex = colorPickImage.texture as Texture2D; } void Start() { lineSize = 5 * new Vector2(pictureRect.width / (float)pictureWidth, pictureRect.height / (float)pictureHeight); c = new Color[(int)lineSize.x * (int)lineSize.y]; c = Enumerable.Repeat<Color>(selectedColor, c.Length).ToArray<Color>(); } void OnEnable() { computer.Clicked += OnMouseClick; computer.UnClicked += OnMouseUnClick; } void OnDisable() { computer.Clicked -= OnMouseClick; computer.UnClicked -= OnMouseUnClick; } void Update() { Vector2 relativeDrawPosition = GetRelativePosition(pictureRect); Vector2 relativePickPosition = GetRelativePosition(colorPickerRect); isInDrawArea = relativeDrawPosition.x >= 0f && relativeDrawPosition.x < 1f && relativeDrawPosition.y >= 0f && relativeDrawPosition.y < 1f; isInPickArea = relativePickPosition.x >= 0f && relativePickPosition.x < 1f && relativePickPosition.y >= 0f && relativePickPosition.y < 1f; if (isInDrawArea)//如果在畫圖區 { if (canOperate)//鼠標點擊了 { SetPixel(relativeDrawPosition.x, relativeDrawPosition.y); } } else if (isInPickArea)//如果在顏色選擇區 { if (canOperate)//鼠標點擊了 { PickColor(relativePickPosition.x, relativePickPosition.y); } } else { if (canOperate) { canOperate = false; } } } /// <summary> /// 選取顏色 /// </summary> /// <param name="x"></param> /// <param name="y"></param> private void PickColor(float x, float y) { selectedColor = colorPickTex.GetPixel((int)(x * colorPickTex.width), (int)(y * colorPickTex.height));//獲取選擇的顏色 selectedColorDisplay.color = selectedColor;//把選擇的顏色展示出來 c = Enumerable.Repeat<Color>(selectedColor, c.Length).ToArray<Color>(); } /// <summary> /// 獲取光標相對於指定rect的位置 /// </summary> /// <param name="rect">指定的rect</param> /// <returns></returns> private Vector2 GetRelativePosition(Rect rect) { Vector2 cursorPos = computer.Canvas.transform.InverseTransformPoint(computer.cursorController.transform.position); Vector2 result = cursorPos - rect.position; result.x /= rect.width; result.y /= rect.height; return result; } /// <summary> /// 鼠標點擊 /// </summary> private void OnMouseClick() { Vector2 relativeDrawPosition = GetRelativePosition(pictureRect); Vector2 relativePickPosition = GetRelativePosition(colorPickerRect); isInDrawArea = relativeDrawPosition.x >= 0f && relativeDrawPosition.x < 1f && relativeDrawPosition.y >= 0f && relativeDrawPosition.y < 1f; isInPickArea = relativePickPosition.x >= 0f && relativePickPosition.x < 1f && relativePickPosition.y >= 0f && relativePickPosition.y < 1f; if (isInPickArea || isInDrawArea) { canOperate = true; } } /// <summary> /// 鼠標點擊后抬起 /// </summary> private void OnMouseUnClick() { canOperate = false; } /// <summary> /// 是否打開畫圖程序 /// </summary> /// <param name="isActive"></param> public void Show(bool isActive) { gameObject.SetActive(isActive); } /// <summary> /// 畫圖 /// </summary> /// <param name="relX"></param> /// <param name="relY"></param> private void SetPixel(float relX, float relY) { Color[] pixels = pictureTex.GetPixels(); int num = Mathf.Clamp((int)(relX * (float)pictureWidth), 0, pictureWidth - 1); int num2 = Mathf.Clamp((int)(relY * (float)pictureHeight), 0, pictureHeight - 1); if (pixels[num2 * pictureWidth + num] != selectedColor) { pixels[num2 * pictureWidth + num] = selectedColor; pictureTex.SetPixels(pixels); pictureTex.Apply(); } //pictureTex.SetPixels(num, num2, (int)lineSize.x, (int)lineSize.y, c); } /// <summary> /// 重置圖片 /// </summary> /// <param name="color"></param> private void ResetPixes(Color color) { Color[] array = new Color[pictureWidth * pictureHeight]; for (int i = 0; i < array.Length; i++) { array[i] = color; } pictureTex.SetPixels(array); pictureTex.Apply(); } }
最后為PaintProgramIcon的Button組件添加OnClick事件;
最后附上工程:鏈接:https://pan.baidu.com/s/1jJkaVEu 密碼:mdwh