Unity3D 用触摸和鼠标输入缩放相机

-回复 -浏览
楼主 2018-05-11 07:59:18
举报 只看此人 收藏本贴 楼主


当为Github Game Off 2016开发Byter时,我想允许玩家用触摸(点击或拖动来缩小,手指捏放来放大)设备包括及iOS,以及用桌面上的鼠标(点击和拖动来缩小,鼠标滚轮来放大)及游戏的WebGL版本来缩放照相机。


Byter的照相机有固定的角度,意味着玩家不能旋转,但对于在游戏中收集丢失的数据包来说,放大缩小很重要。相机的放大缩小对于大多数地静态点击风格游戏的交互来说也是意外的惊喜。


源码在开源库中 github.com/KyleBanks/ggo16-byter已经可用,普遍的版本在博客底部已经提供了,但我想它可能对别的Unity开发者有用,因此我喜欢通过代码来解释它是怎么运作的。


照相机设置


照相机安装很标准,我简单用固定角度的透视投影(这个对于我们处理放大很重要)来给一个稍微倾斜的视野。



显著的唯一的一件事是CameraHandler脚本附加于照相机上,这个脚本是通过输入处理来控制放大缩小的。


脚本


下面的代码包含了CameraHandler脚本,没必要在课程外使用逻辑学,除非你想个性化。


必需变量


我做的第一件事是定义缩放的边界,照相机移动和放大的速度。这让我限制照相机到场景固定的区域中,并限制我们能放大照相机多少。依据你的游戏和欲望,这些变量会不同,下面是我定义它们的方式:


private static readonly float PanSpeed = 20f;

private static readonly float ZoomSpeedTouch = 0.1f;

private static readonly float ZoomSpeedMouse = 0.5f;

   

private static readonly float[] BoundsX = new float[]{-10f, 5f};

private static readonly float[] BoundsZ = new float[]{-18f, -4f};

private static readonly float[] ZoomBounds = new float[]{10f, 85f};


边界被定义为浮点数组,每个的长度为2。第一个值代表边界的下限,第二个代表边界的上限。比如这个例子中的BoundsX,我保持照相机在X轴上的-10到5之间。简单而言,BoundsZ会控制Z轴,ZoomBounds用于放大。对Byter来说没有y轴上的移动,因此也没有必要为它定义边界。


接下来,我们需要一些示例变量来追踪帧之间的照相机状态。我们想要保持实际照相机的参照物,因此我们可以修改属性为用户的交互:


private Camera cam;

   

private Vector3 lastPanPosition;

private int panFingerId; // Touch mode only

   

private bool wasZoomingLastFrame; // Touch mode only

private Vector2[] lastZoomPositions; // Touch mode only


我们一个一个来分析这些属性:

cam存储照相机的参照物。

  • lastPanPosition是最后一帧之间用户缩小照相机的地方时,用户的手指或鼠标位置。

  • panFingerid跟踪手指的ID用来缩小照相机,仅仅用触摸模式。不能全部都用鼠标来控制,因为只有一个鼠标可以用。

  • wasZoomingLastFrame用触摸模式来决定照相机是否在最后一帧被放大。

  • lastZoomPositions,像lastPanPosition,追踪用户手指在最后一帧之间他们放大照相机的的位置。这个属性,不像lastPanPosition,仅仅是触摸模式可用,不能用鼠标控制。


唤醒(Awake)


我们用唤醒(Awake)函数,在这个函数中我们获取到照相机的参照物


void Awake() {

    cam = GetComponent<Camera>();

}


如果你之前用过unity就会觉得熟悉,我们仅仅是获取GameObject的Camera组件。你用cam=Camera.main来替换,但我选择用GetComponent你添加另一个照相机到你的游戏或从你的照相机移除MainCamera。以防万一,这是我们在Awake函数所需要的,我们准备写实际的逻辑。


更新(Update)


在更新循环中我们需要检查我们是否需要处理触摸或鼠标控制,我们定义两个空的函数(到目前为止),y以便在任何一个例子中可以调用:


void Update() {

    if (Input.touchSupported && Application.platform != RuntimePlatform.WebGLPlayer) {

        HandleTouch();

    } else {

        HandleMouse();

    }

}

   

void HandleTouch() {

   

}

   

void HandleMouse() {

   

}


我的目的,我简单检测下触摸是否用Input.touchSupported属性来支持,并确保游戏在webGl播放器中运行。这个很重要因为WebGL播放器支持触摸,但因为手机现在不能用webgl来支持Unity。我假设所有的webgl播放器在桌面浏览器中播放而不在手机上播放。


当菜单打开的时候,我想阻止照相机放大缩小,对于Byter来说也没什么,因此在Update函数的顶部来检查平台并调用Handle_函数,比如这样:


// OPTIONAL

if (isMenuOpen) {

    return;

}


这是完全选择性的,你也许对游戏有你的想法,你不想让照相机移动,所以如果这样的话,update函数的顶部是一个好地方来确认你是否处于照相机交互的状态。


处理鼠标


对了,是时候做些移动了。我们将拓展HandleMouse函数,更容易测试(Unity编辑器会用这个)很容易扩展触摸控制器。


void HandleMouse() {

    // On mouse down, capture it's position.

    // Otherwise, if the mouse is still down, pan the camera.

    if (Input.GetMouseButtonDown(0)) {

        lastPanPosition = Input.mousePosition;

    } else if (Input.GetMouseButton(0)) {

        PanCamera(Input.mousePosition);

    }

   

    // Check for scrolling to zoom the camera

    float scroll = Input.GetAxis("Mouse ScrollWheel");

    ZoomCamera(scroll, ZoomSpeedMouse);

}


首先我们检测鼠标是否用Input.GetMouseButtonDown点击这一帧并简单存储目前鼠标的位置作为LastPanPosition.如果鼠标不点击这帧,我们检测是否用Input.GetMouseBotton来点击并执行PanCamera方法。


接下来,我们用滚轮来处理放大。用Input.GetAxis并提供“Mouse ScroollWheel”做轴,我们得到距离,滚轮从最后一帧滚动。接下来我们用从Input.GetAxis输入返回的scroll来调用ZoomCameray并达到我们想放大的速度。这儿我用以上定义的ZoomSpeedMouse内容,你可能猜到我们用ZoomSpeedTouch常量。


放大缩小照相机


我们进入触摸输入前,我们拓展下PanCamera和ZoomCamera函数。这些都不是特别复杂,我们看下:


void PanCamera(Vector3 newPanPosition) {

    // Determine how much to move the camera

    Vector3 offset = cam.ScreenToViewportPoint(lastPanPosition - newPanPosition);

    Vector3 move = new Vector3(offset.x * PanSpeed, 0, offset.y * PanSpeed);

      

    // Perform the movement

    transform.Translate(move, Space.World); 

      

    // Ensure the camera remains within bounds.

    Vector3 pos = transform.position;

    pos.x = Mathf.Clamp(transform.position.x, BoundsX[0], BoundsX[1]);

    pos.z = Mathf.Clamp(transform.position.z, BoundsZ[0], BoundsZ[1]);

    transform.position = pos;

   

    // Cache the position

    lastPanPosition = newPanPosition;

}

   

void ZoomCamera(float offset, float speed) {

    if (offset == 0) {

        return;

    }

   

    cam.fieldOfView = Mathf.Clamp(cam.fieldOfView - (offset * speed), ZoomBounds[0], ZoomBounds[1]);

}


PanCamera获得鼠标(或手指)新的位置并创建一个基于之前的鼠标(或手指)的位置偏移量(offset)。自从上次PanCamera被调用后,鼠标(或手指)已经移动的距离,像之前的帧。接下来,一个move Vector3被创建获取偏移量的x,z坐标(没有y轴上的移动)并乘以PanSpeed。


接下来,我们用transform.Translate移动到照相机的世界坐标。一旦被执行,照相机实际被移动。但是,我们需要确保照相机保持在边界内,因此我们获取照相机的位置做参考并固定(Clamp)x,z坐标在合适的边界内。


比如,如果我们移动x轴到更低值BondsX[0].我们用BoundsX[0]来替代它确保不会向左走得更多,同样确保上限不会走得更远。相同的逻辑被用在z轴上。因为没有y轴上的移动,我们不需要担心这个。


这个固定的位置被直接用在照相机的变化,我们设置了最终的照相机的位置。剩下需要做的是抓取在lastPanPosition的newPanPosition变量被使用在下一个平移,我们全部设置。


对于放大照相机,我们有ZoomCamera函数,有偏移(offsret)和速度(speed)。我们有速度参数的理由是鼠标和触摸控制放大是很大不同的,每个用不同的速度。当从HandleMouse调用ZoomCamera时,你看到我们提供ZoomSpeedMouse常量。


这个方法比PanCamera更简单因为我们只需要修改一个简单的属性,是照相机的视场角。视场角的定义是照相机能看到多少度。更有用的是什么,正如你在unity编辑器中实验得到的通过拖动照相机的fieldofview滑条,是更低的fileofview意味着照相机能看到更少垂直方向的,所以会移动的更近。相反,fieldofview越高,照相机离得越远。正如下面的GIF图片所示,效果很强大,但照相机绝不是真的在动。



不管怎样,我们简单固定fieldOfView一个新的值,这个新值是,如果新的值超出边界值,目前的fieldofView减去偏移量乘以速度或者低于或高于放大的边界得到的。


对了,这一点你应该准备在unity编辑器中测试鼠标控制。运行游戏并点击并拖动来移动照相机,滚动滑轮来放大缩小照相机,修改顶部的常量来适应你们的需要。


处理触摸


最后我们将拓展触摸控制。我最后保存这,个因为很难测试。因为你需要一个真正的设备来测试,需要整个应用构造并导致一个更慢的进程。因为我们知道放大缩小能很好作用于桌面,我们假设我们已经开发严密的逻辑来拓展触摸,不会犯很多错。


来看下函数:


void HandleTouch() {

    switch(Input.touchCount) {

   

    case 1: // Panning

        wasZoomingLastFrame = false;

          

        // If the touch began, capture its position and its finger ID.

        // Otherwise, if the finger ID of the touch doesn't match, skip it.

        Touch touch = Input.GetTouch(0);

        if (touch.phase == TouchPhase.Began) {

            lastPanPosition = touch.position;

            panFingerId = touch.fingerId;

        } else if (touch.fingerId == panFingerId && touch.phase == TouchPhase.Moved) {

            PanCamera(touch.position);

        }

        break;

   

    case 2: // Zooming

        Vector2[] newPositions = new Vector2[]{Input.GetTouch(0).position, Input.GetTouch(1).position};

        if (!wasZoomingLastFrame) {

            lastZoomPositions = newPositions;

            wasZoomingLastFrame = true;

        } else {

            // Zoom based on the distance between the new positions compared to the

            // distance between the previous positions.

            float newDistance = Vector2.Distance(newPositions[0], newPositions[1]);

            float oldDistance = Vector2.Distance(lastZoomPositions[0], lastZoomPositions[1]);

            float offset = newDistance - oldDistance;

   

            ZoomCamera(offset, ZoomSpeedTouch);

   

            lastZoomPositions = newPositions;

        }

        break;

          

    default:

        wasZoomingLastFrame = false;

        break;

    }

}


这个函数基于触摸屏幕的手指可以分为两部分。如果仅仅单个手指触摸,我们处理缩小。如果是两个手指触摸,我们处理放大。我们先拆解下缩小的逻辑,然后再拆解放大逻辑。


对于缩小,我们首先设置wasZoomingLastFrame布尔变量为false,我们很快会变回原值。接下来,我们会检查单个Touch的状态并执行。如果开始这个帧触摸,当手指移动的时候,我们存储位置和手指ID用在连续的帧。


说到这个,我们接下来检测是否现在的单个触摸手指匹配用来缩小照相机的手指。如果可以移动,我们调用PanCamera,传递手指的位置。


接下来放大的部分(这里是两个手指),我们在vector2[]中存储两个手指的位置叫做newPositions.如果我们在最后一帧期间不放大,在lastZoomPositions数组中存储newPositions数组,并设置wasZoomingLastFrame为真。


如果你调用我们的设置为假,当我们缩小。因此当有两个手指在屏幕的时候,我们知道开始放大。如果我们在最后一帧放大,我们计算当前帧的手指之间的距离以及前一帧的手指之间的距离,基于两个距离之间的offset,告诉我们手指的指向的移动(它们会变得更亲近或更疏远)。


我们为ZoomSpeedTouch提供oofset与ZoomCamera,处理fieldofView变化。最终,我们缓存在lastZoomPositions的变量newPositions,我们做放大的逻辑。


最终在default例子中,意味着有0个手指或多余两个手指在屏幕上,我们简单设置wasZoomingLastFrame为假。


然后在一个触摸设备上运行游戏(Android,iphone,ipad)你应该发现,以熟悉的很时尚的方法,你可以通过拖动你的手指来缩小照相机或者放大。


全部资源


正如我们开始说的,这是CameraHandler脚本的全部代码。把这个脚本插入到Unity3d项目中,附加到你的照相机,并调整边界和速度来适合你的需要。


using UnityEngine;

using System.Collections;

   

public class CameraHandler : MonoBehaviour {

   

    private static readonly float PanSpeed = 20f;

    private static readonly float ZoomSpeedTouch = 0.1f;

    private static readonly float ZoomSpeedMouse = 0.5f;

      

    private static readonly float[] BoundsX = new float[]{-10f, 5f};

    private static readonly float[] BoundsZ = new float[]{-18f, -4f};

    private static readonly float[] ZoomBounds = new float[]{10f, 85f};

      

    private Camera cam;

      

    private Vector3 lastPanPosition;

    private int panFingerId; // Touch mode only

      

    private bool wasZoomingLastFrame; // Touch mode only

    private Vector2[] lastZoomPositions; // Touch mode only

   

    void Awake() {

        cam = GetComponent<Camera>();

    }

      

    void Update() {

        if (Input.touchSupported && Application.platform != RuntimePlatform.WebGLPlayer) {

            HandleTouch();

        } else {

            HandleMouse();

        }

    }

      

    void HandleTouch() {

        switch(Input.touchCount) {

      

        case 1: // Panning

            wasZoomingLastFrame = false;

              

            // If the touch began, capture its position and its finger ID.

            // Otherwise, if the finger ID of the touch doesn't match, skip it.

            Touch touch = Input.GetTouch(0);

            if (touch.phase == TouchPhase.Began) {

                lastPanPosition = touch.position;

                panFingerId = touch.fingerId;

            } else if (touch.fingerId == panFingerId && touch.phase == TouchPhase.Moved) {

                PanCamera(touch.position);

            }

            break;

      

        case 2: // Zooming

            Vector2[] newPositions = new Vector2[]{Input.GetTouch(0).position, Input.GetTouch(1).position};

            if (!wasZoomingLastFrame) {

                lastZoomPositions = newPositions;

                wasZoomingLastFrame = true;

            } else {

                // Zoom based on the distance between the new positions compared to the

                // distance between the previous positions.

                float newDistance = Vector2.Distance(newPositions[0], newPositions[1]);

                float oldDistance = Vector2.Distance(lastZoomPositions[0], lastZoomPositions[1]);

                float offset = newDistance - oldDistance;

      

                ZoomCamera(offset, ZoomSpeedTouch);

      

                lastZoomPositions = newPositions;

            }

            break;

              

        default:

            wasZoomingLastFrame = false;

            break;

        }

    }

      

    void HandleMouse() {

        // On mouse down, capture it's position.

        // Otherwise, if the mouse is still down, pan the camera.

        if (Input.GetMouseButtonDown(0)) {

            lastPanPosition = Input.mousePosition;

        } else if (Input.GetMouseButton(0)) {

            PanCamera(Input.mousePosition);

        }

      

        // Check for scrolling to zoom the camera

        float scroll = Input.GetAxis("Mouse ScrollWheel");

        ZoomCamera(scroll, ZoomSpeedMouse);

    }

      

    void PanCamera(Vector3 newPanPosition) {

        // Determine how much to move the camera

        Vector3 offset = cam.ScreenToViewportPoint(lastPanPosition - newPanPosition);

        Vector3 move = new Vector3(offset.x * PanSpeed, 0, offset.y * PanSpeed);

          

        // Perform the movement

        transform.Translate(move, Space.World); 

          

        // Ensure the camera remains within bounds.

        Vector3 pos = transform.position;

        pos.x = Mathf.Clamp(transform.position.x, BoundsX[0], BoundsX[1]);

        pos.z = Mathf.Clamp(transform.position.z, BoundsZ[0], BoundsZ[1]);

        transform.position = pos;

      

        // Cache the position

        lastPanPosition = newPanPosition;

    }

      

    void ZoomCamera(float offset, float speed) {

        if (offset == 0) {

            return;

        }

      

        cam.fieldOfView = Mathf.Clamp(cam.fieldOfView - (offset * speed), ZoomBounds[0], ZoomBounds[1]);

    }

}


感谢阅读



点击下方“阅读原文”更多精彩等你来!
↓↓↓
我要推荐
转发到