1. 程式人生 > >在VR中模擬用鼠標操作電腦並實現簡單畫圖的小程序

在VR中模擬用鼠標操作電腦並實現簡單畫圖的小程序

npr 事件 line 屏幕 reset relative max using false

技術分享圖片

(圖沒有錄好,明天換一下)

一、概述

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 = (Action)Delegate.Combine(mouseController.ClickUp, new Action(MouseController_ClickUp));
        mouseController.ClickDown = (Action)Delegate.Combine(mouseController.ClickDown, new Action(MouseController_ClickDown));
    }

    void OnDisable()
    {
        mouseController.ClickUp = (Action)Delegate.Remove(mouseController.ClickUp, new Action(MouseController_ClickUp));
        mouseController.ClickDown = (Action)Delegate.Remove(mouseController.ClickDown, new Action(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 = (Action)Delegate.Combine(cursorController.OnMoved, new Action(CheckClickablesIsHoverByCursor));
        cursorController.ClickedDown = (Action)Delegate.Combine(cursorController.ClickedDown, new Action(ClickDown));
        cursorController.ClickedUp = (Action)Delegate.Combine(cursorController.ClickedUp, new Action(ClickUp));
    }


    void OnDisable()
    {
        cursorController.OnMoved = (Action)Delegate.Remove(cursorController.OnMoved, new Action(CheckClickablesIsHoverByCursor));
        cursorController.ClickedDown = (Action)Delegate.Remove(cursorController.ClickedDown, new Action(ClickDown));
        cursorController.ClickedUp = (Action)Delegate.Remove(cursorController.ClickedUp, new Action(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 = (Action)Delegate.Combine(computer.Clicked, new Action(OnMouseClick));
        computer.UnClicked = (Action)Delegate.Combine(computer.UnClicked, new Action(OnMouseUnClick));
    }

    void OnDisable()
    {
        computer.Clicked = (Action)Delegate.Remove(computer.Clicked, new Action(OnMouseClick));
        computer.UnClicked = (Action)Delegate.Remove(computer.UnClicked, new Action(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

在VR中模擬用鼠標操作電腦並實現簡單畫圖的小程序