[SOLVED] Moving GameObject along X and Z Axis by drag and drop using X and Y from ScreenSpace

Hello,

I have a 3d space, with a camera that can have different angles to view at a plane.
Now I want to be able to drag and drop a game object on the X- and Z-axis on the plane, with the y-axis (height) being locked, so that it looks like I move a chess piece on a board.

Currently my game object starts to fly, but when locking the Y-axis the mouse is faster than the game object.

What I want is what you can see in this example:

I have searched this forum and stack overflow but I could not find a working example or an Idea how to make that happen.

This post describes my problem, that my mouse is not staying on the object when moving my mouse on the y-axis(screen space), it moves faster than my object on the Z-axis (in world space)
https://gamedev.stackexchange.com/questions/134213/mouse-position-to-world-position-help

When the camera looks 90degree downwards on the plane, the translation is correct, so I assume I need to take the angle in account to calculate the correct z value in world space to align with my mouse position? Or a way to know the Z position where I want my object to drag to, but also not sure how to retrieve that.

Is there any Idea how I could accomplish this?

This is the current code I use with the Y-axis being locked to the dragging object

private GameObject _drag;
private Vector3 screenPosition;
private Vector3 offset;

private void Update()
        {
            if (_drag == null && Input.GetMouseButtonDown(0))
            {
                RaycastHit hit;
                Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
                if (Physics.Raycast(ray, out hit, 100.0f))
                {
                    if (gameObject == hit.transform.gameObject)
                    {
                        _drag = hit.transform.gameObject;
                        screenPosition = Camera.main.WorldToScreenPoint(_drag.transform.position);
                        offset = _drag.transform.position - Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, screenPosition.z));
                    }
                }
            }

            if (Input.GetMouseButtonUp(0))
            {
                _drag = null;
            }

            if (_drag != null)
            {
                Vector3 currentScreenSpace = new Vector3(Input.mousePosition.x, Input.mousePosition.y, screenPosition.z);
                Vector3 currentPosition = Camera.main.ScreenToWorldPoint(currentScreenSpace) + offset;
                _drag.transform.position = new Vector3(currentPosition.x, _drag.transform.position.y, currentPosition.z);
            }
        }

Maybe something like this could work for you.

float planeY = 0;
Transform draggingObject = transform;

Plane plane = new Plane(Vector3.up, Vector3.up * planeY); // ground plane

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

float distance; // the distance from the ray origin to the ray intersection of the plane
if(plane.Raycast(ray, out distance)) 
{
    draggingObject.position = ray.GetPoint(distance); // distance along the ray
}
16 Likes

Thank you that works perfect!

And i don’t even need anything else (e.g. WorldToScreenPoint, ScreenToWorldPoint multiple times), do I understand correctly that it will put the GameObject always at the position where the ray hits the plane?

Would you suggest to do the Raycast every update or skip some frames, just curious if you know by any chance if there is any noticeable performance difference.

And do you think a solution exists by calculating the Z-WorldPoint and how would it perform in comparison?

Thank you again, very glad with your provided Solution!. I will mark this Thread as solved and if you or anyone reading by any chance know further answers to my question I’m very curious.

I think that for the best user experience, you should update the position with a raycast every frame that the user is dragging the object. You could add a check to make sure the mouse moved enough since last frame if you want some optimization, but it’s a pretty cheap raycast because it’s only checking against one Plane object that exists only as a logical object. You can create that plane and store it on Awake instead of creating it for every raycast, but I doubt you will see any impact whatsoever from those optimizations, but it’s good practice i guess.

I don’t know of any other way except for a raycast. You have to query the position on the surface under the mouse somehow, and considering the angle of the camera etc, I don’t see any other way.

1 Like

I was looking for something else but after I saw this post I needed to changed my working code.
Amazing and clean solution.

1 Like

Is there a way to use this method to move an object over a varying terrain? This worked beautifully for me on a consistent plane - Thanks

If you have a terrain object, you can do the same type of thing by raycasting against the TerrainCollider:

TerrainCollider terrain; // set in inspector, or passed in, or otherwise referenced
Transform draggingObject = transform;

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hitInfo;
if(terrain.Raycast(ray, out hitInfo, Mathf.Infinity)) // using infinity for the ray length for example
{
    draggingObject.position = hitInfo.point;
}

If you want to be able to raycast against anything you deem the “ground”, then you can do something like this provided all your “ground” objects are on a layer configured in the “groundLayers” LayerMask:

LayerMask groundLayers; // expose this in the inspector, set to "ground" layers
Transform draggingObject = transform;

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hitInfo;
if(Physics.Raycast(ray, out hitInfo, Mathf.Infinity, groundLayers)) // using infinity for the ray length for example
{
    draggingObject.position = hitInfo.point;
}

If you want your object to orient to the surface (in a pretty basic way, just setting one axis), add draggingObject.up = hitInfo.normal;after the position is set.

Thank you for this post, Can I also ask how can I apply this to my object like move only in you local Y,X or Z axis?

No. screenpointtoray is very unnecessary here, you just need to say that pos.z is equal to pos.x…

like so…

Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - dragOrigin);
       pos.z = pos.x;
       Vector3 move = new Vector3(0, pos.y * dragSpeed, pos.z * -dragSpeed);

always a pleasure x

Awesome, I thought I needed something else, but turns out this is ideal!

Well, Mr LitterallyJeff. you have saved me a lot of head scratching.
I’ve added a little snapping feature for placing “Buildings” down on a grid.

private Color mouseOverColor = Color.red;
    private Color originalColor = Color.green;
    private bool dragging = false;

    [SerializeField] int gridSnapSize = 5;
    [SerializeField] int fixedHieght = 5;

    private void Update()
    {
        if (dragging)//_drag == null &&
        {
            Transform draggingObject = transform;

            Plane plane = new Plane(Vector3.up, Vector3.up * fixedHieght); // ground plane

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            float distance; // the distance from the ray origin to the ray intersection of the plane
            if (plane.Raycast(ray, out distance))
            {
                Vector3 rayPoint = ray.GetPoint(distance);
                Vector3 snappedRayPoint = rayPoint;
                snappedRayPoint.x = (Mathf.RoundToInt(rayPoint.x / gridSnapSize) * gridSnapSize);
                snappedRayPoint.z = (Mathf.RoundToInt(rayPoint.z / gridSnapSize) * gridSnapSize);
                draggingObject.position = snappedRayPoint;
            }
        }
    }
    void OnMouseDown()
    {
        dragging = true;
    }

    void OnMouseUp()
    {
        dragging = false;
    }
    void OnMouseEnter()
    {
        GetComponent<Renderer>().material.color = mouseOverColor;
    }

    void OnMouseExit()
    {
        GetComponent<Renderer>().material.color = originalColor;
    }
2 Likes

This code is very good, its exactly the behaviour I need. Is there a way to make sure the object does not move when you first click down to drag?

This part right here

draggingObject.position = ray.GetPoint(distance); // distance along the ray

Everytime we try to drag the object again it’s initial position is set to 0,0,0. Is there a way we can retain its initial position when we start dragging?

1 Like

Hi!
Im newby, this scripts work perfectly but i would like to trigger it on GetMouseButtonDown(1) so that i can drop the gameObjects and use the left mouse button to rotate the camera. Could you please update this code?

Tnx!!