Move Camera Over Terrain Using Touch Input in Unity 3D
Asked Answered
C

6

6

I am new to Unity and I am trying to figure out how to move the camera over a map/terrain using touch input. The camera would be looking down at the terrain with a rotation of (90,0,0). The terrain is on layer 8. I have had no problem getting it moving with keyboard, now I am trying to move to touch and it is very different if you want to keep expected usage on iOS.

The best example I can think of on a built in iOS app is Maps where the user would touch the screen and that point on the map would stay under the finger as long as the finger stayed on the screen. So as the user moves their finger the map appears to be moving with the finger. I have not been able to find examples that show how to do it this way. I have seen may examples of moving the camera or character with the mouse but they don't seem to translate well to this style.

Also posted on Unity3D Answers:

https://mcmap.net/q/43263/unity-move-camera-over-terrain-using-touch-input

Coryden answered 16/7, 2012 at 1:20 Comment(2)
Just for clarification, are you trying to mimic the 'Maps' app behavior or do you just need a script to handle touch based movement?Conni
Essentially yes, if a user puts their finger on the screen the area of terrain/map under the finger should stay under their finger as they move it around.Coryden
C
19

Below should be what you need. Note that it's tricky to get a 1 to 1 correspondence between finger/cursor and the terrain when using a perspective camera. If you change your camera to orthographic, the script below should give you a perfect map between finger/cursor position and map movement. With perspective you'll notice a slight offset.

You could also do this with ray tracing but I've found that route to be sloppy and not as intuitive.

Camera settings for testing (values are pulled from the inspector so apply them there):

  1. Position: 0,20,0
  2. Orientation: 90,0,0
  3. Projection: Perspective/Orthographic

using UnityEngine;
using System.Collections;



public class ViewDrag : MonoBehaviour {
    Vector3 hit_position = Vector3.zero;
    Vector3 current_position = Vector3.zero;
    Vector3 camera_position = Vector3.zero;
    float z = 0.0f;
    
    // Use this for initialization
    void Start () {
        
    }
    
    void Update(){
        if(Input.GetMouseButtonDown(0)){
            hit_position = Input.mousePosition;
            camera_position = transform.position;
            
        }
        if(Input.GetMouseButton(0)){
            current_position = Input.mousePosition;
            LeftMouseDrag();        
        }
    }
    
    void LeftMouseDrag(){
        // From the Unity3D docs: "The z position is in world units from the camera."  In my case I'm using the y-axis as height
        // with my camera facing back down the y-axis.  You can ignore this when the camera is orthograhic.
        current_position.z = hit_position.z = camera_position.y;
        
        // Get direction of movement.  (Note: Don't normalize, the magnitude of change is going to be Vector3.Distance(current_position-hit_position)
        // anyways.  
        Vector3 direction = Camera.main.ScreenToWorldPoint(current_position) - Camera.main.ScreenToWorldPoint(hit_position);
        
        // Invert direction to that terrain appears to move with the mouse.
        direction = direction * -1;
        
        Vector3 position = camera_position + direction;
        
        transform.position = position;
    }
}
Conni answered 16/7, 2012 at 3:7 Comment(2)
That works fairly close. The longer the movement in a left or right the more the ground under the finger/mouse drifts in the direction of the drag. Its noticeable, and is a great starting point. Thanks!Coryden
@bagusflyer Yes. (Input.GetMouseButtonDown(0)) is equivalent to (Input.touchCount>0 && Input.GetTouch(0).phase == TouchPhase.Began), or it was with Unity 3.5. Haven't tried in version 4.xConni
E
6

I've come up with this script (I have appended it to the camera):

private Vector2 worldStartPoint;

void Update () {

    // only work with one touch
    if (Input.touchCount == 1) {
        Touch currentTouch = Input.GetTouch(0);

        if (currentTouch.phase == TouchPhase.Began) {
            this.worldStartPoint = this.getWorldPoint(currentTouch.position);
        }

        if (currentTouch.phase == TouchPhase.Moved) {
            Vector2 worldDelta = this.getWorldPoint(currentTouch.position) - this.worldStartPoint;

            Camera.main.transform.Translate(
                -worldDelta.x,
                -worldDelta.y,
                0
            );
        }
    }
}

// convert screen point to world point
private Vector2 getWorldPoint (Vector2 screenPoint) {
    RaycastHit hit;
    Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
    return hit.point;
}
Effeminize answered 8/1, 2015 at 9:27 Comment(1)
This is good for e. g. keeping a plane under your "cursor"/touch.Seedman
M
1

Pavel's answer helped me a lot, so wanted to share my solution with the community in case it helps others. My scenario is a 3D world with an orthographic camera. A top-down style RTS I am working on. I want pan and zoom to work like Google Maps, where the mouse always stays at the same spot on the map when you pan and zoom. This script achieves this for me, and hopefully is robust enough to work for others' needs. I haven't tested it a ton, but I commented the heck out of it for beginners to learn from.

using UnityEngine;

// I usually attach this to my main camera, but in theory you can attach it to any object in scene, since it uses Camera.main instead of "this".
public class CameraMovement : MonoBehaviour
{
    private Vector3 MouseDownPosition;

    void Update()
    {
        // If mouse wheel scrolled vertically, apply zoom...
        // TODO: Add pinch to zoom support (touch input)
        if (Input.mouseScrollDelta.y != 0)
        {
            // Save location of mouse prior to zoom
            var preZoomPosition = getWorldPoint(Input.mousePosition);

            // Apply zoom (might want to multiply Input.mouseScrollDelta.y by some speed factor if you want faster/slower zooming
            Camera.main.orthographicSize = Mathf.Clamp(Camera.main.orthographicSize + Input.mouseScrollDelta.y, 5, 80);

            // How much did mouse move when we zoomed?
            var delta = getWorldPoint(Input.mousePosition) - preZoomPosition;

            // Rotate camera to top-down (right angle = 90) before applying adjustment (otherwise we get "slide" in direction of camera angle).
            // TODO: If we allow camera to rotate on other axis we probably need to adjust that also.  At any rate, you want camera pointing "straight down" for this part to work.
            var rot = Camera.main.transform.localEulerAngles;
            Camera.main.transform.localEulerAngles = new Vector3(90, rot.y, rot.z);

            // Move the camera by the amount mouse moved, so that mouse is back in same position now.
            Camera.main.transform.Translate(delta.x, delta.z, 0);

            // Restore camera rotation
            Camera.main.transform.localEulerAngles = rot;
        }

        // When mouse is first pressed, just save location of mouse/finger.
        if (Input.GetMouseButtonDown(0))
        {
            MouseDownPosition = getWorldPoint(Input.mousePosition);
        }

        // While mouse button/finger is down...
        if (Input.GetMouseButton(0))
        {
            // Total distance finger/mouse has moved while button is down
            var delta = getWorldPoint(Input.mousePosition) - MouseDownPosition;

            // Adjust camera by distance moved, so mouse/finger stays at exact location (in world, since we are using getWorldPoint for everything).
            Camera.main.transform.Translate(delta.x, delta.z, 0);
        }
    }

    // This works by casting a ray.  For this to work well, this ray should always hit your "ground".  Setup ignore layers if you need to ignore other colliders.
    // Only tested this with a simple box collider as ground (just one flat ground).
    private Vector3 getWorldPoint(Vector2 screenPoint)
    {
        RaycastHit hit;
        Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
        return hit.point;
    }
}
Moton answered 14/10, 2016 at 2:33 Comment(1)
Using Camera.main inside of the update function is inefficient because it calls FindObjectWithTag() every time you call it. You would get much better performance if you cached a reference member variable to it in your start function.Antidote
K
0

Based on the answer from Pavel, I simplified the script and removed the unlovely "jump" when touch with more then one finger and release the second finger:

private bool moreThenOneTouch = false;
private Vector3 worldStartPoint;

void Update() {

    Touch currentTouch;
    // only work with one touch
    if (Input.touchCount == 1 && !moreThenOneTouch) {
        currentTouch = Input.GetTouch(0);

        if (currentTouch.phase == TouchPhase.Began) {
            this.worldStartPoint = Camera.main.ScreenToWorldPoint(currentTouch.position);
        }

        if (currentTouch.phase == TouchPhase.Moved) {
            Vector3 worldDelta = Camera.main.ScreenToWorldPoint(currentTouch.position) - this.worldStartPoint;
            
            Camera.main.transform.Translate(
                -worldDelta.x,
                -worldDelta.y,
                0
            );
        }
    
    }

    if (Input.touchCount > 1) {
        moreThenOneTouch = true;
    } else {
        moreThenOneTouch = false;
        if(Input.touchCount == 1)
            this.worldStartPoint = Camera.main.ScreenToWorldPoint(Input.GetTouch(0).position);
    }
}
Kennard answered 22/2, 2021 at 20:58 Comment(0)
K
0

I've used the first code you guys wrote, and its worked but, i want the terrain or map to not stop instantly when i remove finger, i want it to slide a bit and stops, like maybe a second or two, so if i slide the terrain with my finger, it will stay under my finger, but if i slide and let go, the terrain will continue sliding for a some second? is this possible

Kiker answered 1/9, 2023 at 2:45 Comment(2)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Tomcat
If you have a new question, please ask it by clicking the Ask Question button. Include a link to this question if it helps provide context. - From ReviewBascule
I
-1
using UnityEngine;

// I usually attach this to my main camera, but in theory you can attach it to any object in scene, since it uses Camera.main instead of "this".
public class CameraMovement : MonoBehaviour
{
    private Vector3 MouseDownPosition;

    void Update()
    {
        // If mouse wheel scrolled vertically, apply zoom...
        // TODO: Add pinch to zoom support (touch input)
        if (Input.mouseScrollDelta.y != 0)
        {
            // Save location of mouse prior to zoom
            var preZoomPosition = getWorldPoint(Input.mousePosition);

            // Apply zoom (might want to multiply Input.mouseScrollDelta.y by some speed factor if you want faster/slower zooming
            Camera.main.orthographicSize = Mathf.Clamp(Camera.main.orthographicSize + Input.mouseScrollDelta.y, 5, 80);

            // How much did mouse move when we zoomed?
            var delta = getWorldPoint(Input.mousePosition) - preZoomPosition;

            // Rotate camera to top-down (right angle = 90) before applying adjustment (otherwise we get "slide" in direction of camera angle).
            // TODO: If we allow camera to rotate on other axis we probably need to adjust that also.  At any rate, you want camera pointing "straight down" for this part to work.
            var rot = Camera.main.transform.localEulerAngles;
            Camera.main.transform.localEulerAngles = new Vector3(90, rot.y, rot.z);

            // Move the camera by the amount mouse moved, so that mouse is back in same position now.
            Camera.main.transform.Translate(delta.x, delta.z, 0);

            // Restore camera rotation
            Camera.main.transform.localEulerAngles = rot;
        }

        // When mouse is first pressed, just save location of mouse/finger.
        if (Input.GetMouseButtonDown(0))
        {
            MouseDownPosition = getWorldPoint(Input.mousePosition);
        }

        // While mouse button/finger is down...
        if (Input.GetMouseButton(0))
        {
            // Total distance finger/mouse has moved while button is down
            var delta = getWorldPoint(Input.mousePosition) - MouseDownPosition;

           // Adjust camera by distance moved, so mouse/finger stays at exact location (in world, since we are using getWorldPoint for everything).
           Camera.main.transform.Translate(delta.x, delta.z, 0);
        }
    }

    // This works by casting a ray.  For this to work well, this ray should always hit your "ground".  Setup ignore layers if you need to ignore other colliders.
    // Only tested this with a simple box collider as ground (just one flat ground).
    private Vector3 getWorldPoint(Vector2 screenPoint)
    {
        RaycastHit hit;
        Physics.Raycast(Camera.main.ScreenPointToRay(screenPoint), out hit);
        return hit.point;
    }
}
Iniquity answered 15/3, 2018 at 10:58 Comment(2)
down-voted because this is simply a re-post of the script posted by @eselk.Eardrum
@Moton prove itChobot

© 2022 - 2024 — McMap. All rights reserved.