본문 바로가기
개발/Unity

유니티 - 드래그로 평면 위의 오브젝트 움직이기 (Drag and Move on the Plane)

by 피로물든딸기 2022. 5. 14.
반응형

Unity 전체 링크

 

드래그로 오브젝트 Y축 회전하기 (Drag to Rotate Object in Y-axis)
드래그로 오브젝트 위, 아래로 움직이기 (Drag Object in Y-Axis)

드래그로 오브젝트 움직이기 (Move GameObject with Drag)
드래그로 땅 위의 오브젝트 움직이기 (Drag and Move on the Ground)
드래그로 평면 위의 오브젝트 움직이기 (Drag and Move on the Plane)

드래그로 블럭 옆으로 한 칸 움직이기 (Drag GameObject Snapped to a Grid)


이전 글에서는 땅 위에서 닮음을 이용해 오브젝트를 움직였다.

 

이번에는 오브젝트를 평면 위에 붙여서 움직이도록 해보자.

최종 결과를 미리보면 아래와 같다.


먼저 기본 이동 아이디어는 드래그로 땅 위의 오브젝트를 움직이는 것과 같다.

카메라에서 쏜 Raycast가 object의 hit point를 움직인다.

따라서 OnMouseDown, OnMouseUp은 그대로 사용한다.

    RaycastHit hit;
    GameObject objectHitPosition;
    
    void OnMouseDown()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if(Physics.Raycast(ray, out hit))
        {
            objectHitPosition = new GameObject("Empty");
            objectHitPosition.transform.position = hit.point;
            this.transform.SetParent(objectHitPosition.transform);
        }
    }

    void OnMouseUp()
    {
        this.transform.parent = null;
        Destroy(objectHitPosition);
    }

 

이제 왼쪽의 오브젝트를 드래그로 오른쪽으로 옮긴다고 가정해보자.

카메라에서 Raycast를 쏴서 맞은 오브젝트의 위치를 옆으로 오른쪽으로 옮겼다. 

(실제로는 매우 작은 간격으로 움직인다.)

 

즉, objectHitPosition의 위치가  ○(이동 전)에서 ○(이동 후)가 되도록 해야 한다. 

 

objectHitPosition은 아래의 검은색 평면에서 움직인다고 볼 수 있다.

 

시점을 바꿔보면 아래와 같은 영역이 된다.

 

오브젝트를 향해 Raycast를 쏠 때, OnMouseDrag에서는 평면의 hit point를 구해야 한다.

Plane에는 Layer를 "Plane"으로 설정해두고, LayerMask를 이용해서 평면의 hit point를 구한다.

 

따라서 OnMouseDrag에 Raycast + LayerMask를 이용해 평면을 얻는다.

    RaycastHit hitLayerMask;

    void OnMouseDrag()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(ray.origin, ray.direction * 100.0f, Color.red);

        int layer = 1 << LayerMask.NameToLayer("Plane");
        if(Physics.Raycast(ray, out hitLayerMask, Mathf.Infinity, layer))
        {
            /* 이동 처리 */
        }
    }

 

다시 그림으로 돌아가보자.

검은색 평면의 노멀 벡터는 Plane의 노멀 벡터와 같다. 따라서 hitLayerMask.transform.up 가 된다.

그리고 이 평면이 지나는 점은 오브젝트의 두께, 여기에서는 yHeight를 알면 구할 수 있다.

(오브젝트의 크기는 미리 알고 있어야 한다.)

이 평면의 한 점은 hitLayerMask.point + hitLayerMask.collider.transform.up * yHeight 가 된다.

 

여기서는 간단히 큐브를 이용하였으므로 yHeight는 localScale의 y값이 된다.

    float yHeight;
    
    void Start()
    {
        yHeight = this.transform.localScale.y;
    }

 

이제 오브젝트를 옮길 위치 objectHitPosition이 어디로 이동해야 하는지 알아야 한다.

 

현재 알 수 있는 정보는

 

objectHitPosition이 지나는 평면의 노멀벡터 - hitLayerMask.transform.up

objectHitPosition이 지나는 평면의 점 - hitLayerMask.point + hitLayerMask.collider.transform.up * yHeight

Raycast의 시작점 (A) - Camera.main.transform.position

Raycast의 끝점 (B) - hitLayerMask.point

 

구해야하는 점 - objectHitPosition.transform.position

 

이 문제는 결국 평면과 직선의 접점 좌표 구하기 문제와 같다.

AB가 이어졌을 때 검은색 평면을 지나는 점을 찾기 때문이다.

    Vector3 getContactPoint(Vector3 normal, Vector3 planeDot, Vector3 A, Vector3 B)
    {
        Vector3 nAB = (B - A).normalized;

        return A + nAB * Vector3.Dot(normal, planeDot - A) / Vector3.Dot(normal, nAB);
    }

 

따라서 OnMouseDrag는 아래와 같다.

    void OnMouseDrag()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(ray.origin, ray.direction * 100.0f, Color.red);

        int layer = 1 << LayerMask.NameToLayer("Plane");
        if(Physics.Raycast(ray, out hitLayerMask, Mathf.Infinity, layer))
        {
            Vector3 normal = hitLayerMask.transform.up;
            Vector3 planeDot = hitLayerMask.point + hitLayerMask.collider.transform.up * yHeight;
            Vector3 A = Camera.main.transform.position;
            Vector3 B = hitLayerMask.point;

            objectHitPosition.transform.position = getContactPoint(normal, planeDot, A, B);
        }
    }

 

여기까지 적용하면 아래와 같은 결과가 나온다.

오브젝트의 방향이 바뀌지 않았기 때문에 문제가 발생하였다. 

 

this.transform.rotation = Quaternion.LookRotation(hitLayerMask.collider.transform.forward);
코드를 추가하여 transform의 방향을 hitLayerMask의 forward와 일치시켜주면 된다.

    if(Physics.Raycast(ray, out hitLayerMask, Mathf.Infinity, layer))
    { 
        Vector3 normal = hitLayerMask.transform.up;
        Vector3 planeDot = hitLayerMask.point + hitLayerMask.collider.transform.up * yHeight;
        Vector3 A = Camera.main.transform.position;
        Vector3 B = hitLayerMask.point;

        this.transform.rotation = Quaternion.LookRotation(hitLayerMask.collider.transform.forward);
        objectHitPosition.transform.position = getContactPoint(normal, planeDot, A, B);
    }

 

OnDrawGizmos를 추가하여 오브젝트의 방향을 표시해두면 정상적으로 회전되는 것을 알 수 있다.

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.blue;
        Gizmos.DrawRay(this.transform.position, this.transform.localRotation * Vector3.up * 3.0f);
    }

 

위의 코드를 모두 적용하면 처음에 본 결과를 얻을 수 있다.

 

최종 코드는 아래와 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(BoxCollider))]
public class MoveOnThePlane : MonoBehaviour
{
    RaycastHit hit, hitLayerMask;
    GameObject objectHitPosition;
    float yHeight;

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.blue;
        Gizmos.DrawRay(this.transform.position, this.transform.localRotation * Vector3.up * 3.0f);
    }

    Vector3 getContactPoint(Vector3 normal, Vector3 planeDot, Vector3 A, Vector3 B)
    {
        Vector3 nAB = (B - A).normalized;

        return A + nAB * Vector3.Dot(normal, planeDot - A) / Vector3.Dot(normal, nAB);
    }

    void OnMouseDown()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if(Physics.Raycast(ray, out hit))
        {
            objectHitPosition = new GameObject("Empty");
            objectHitPosition.transform.position = hit.point;
            this.transform.SetParent(objectHitPosition.transform);
        }
    }

    void OnMouseUp()
    {
        this.transform.parent = null;
        Destroy(objectHitPosition);
    }

    void OnMouseDrag()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(ray.origin, ray.direction * 100.0f, Color.red);

        int layer = 1 << LayerMask.NameToLayer("Plane");
        if(Physics.Raycast(ray, out hitLayerMask, Mathf.Infinity, layer))
        { 
            Vector3 normal = hitLayerMask.transform.up;
            Vector3 planeDot = hitLayerMask.point + hitLayerMask.collider.transform.up * yHeight;
            Vector3 A = Camera.main.transform.position;
            Vector3 B = hitLayerMask.point;

            this.transform.rotation 
              = Quaternion.LookRotation(hitLayerMask.collider.transform.forward);
            objectHitPosition.transform.position = getContactPoint(normal, planeDot, A, B);
        }
    }

    void Start()
    {
        yHeight = this.transform.localScale.y;
    }
}

 

Unity Plus:

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Unity Pro:

 

Unity Pro

The complete solutions for professionals to create and operate.

unity.com

 

Unity 프리미엄 학습:

 

Unity Learn

Advance your Unity skills with live sessions and over 750 hours of on-demand learning content designed for creators at every skill level.

unity.com

반응형

댓글