본문 바로가기
개발/Unity

유니티 - BoundBox로 블럭이 움직일 위치 미리 예상하기 (Runtime Collider, 런타임 콜라이더)

by 피로물든딸기 2022. 6. 12.
반응형

Unity 전체 링크

 

드래그로 블럭을 한 칸 움직였지만, 마우스를 놓았을 때 오른쪽으로 움직일지, 왼쪽으로 움직일지 예상할 수가 없다.

 

아래와 같이 BoundBox를 이용하여 블럭이 어디로 이동하는지 미리 계산하고 보여주자.

 

BoundBox는 아래의 무료 에셋을 이용하였다.

Runtime에 볼 수 있는 Collider의 느낌이 나는 괜찮은 에셋이다.

https://assetstore.unity.com/packages/tools/utilities/boundboxes-10962

 

BoundBoxes | 유틸리티 도구 | Unity Asset Store

Use the BoundBoxes from virtualPlayground on your next project. Find this utility tool & more on the Unity Asset Store.

assetstore.unity.com


BoundBox는 일반 큐브이고 콜라이더와 Mesh Renderer를 제거하였다.

 

에셋에 있는 BoundBox.cs에서 namespace를 제거하였고, 아래의 코드를 추가하여 싱글턴으로 만들었다.

    static BoundBox instance = null;
    public static BoundBox Instance
    {
        get
        {
            if (null == instance) instance = FindObjectOfType<BoundBox>();
            return instance;
        }
    }

    void Awake()
    {
        if (null == instance) instance = this;
    }

 

그리고 setActive를 할 수 있도록 함수를 추가하였다.

    public void boundSetActive(bool set)
    {
        this.gameObject.SetActive(set);
    }

 

이제 Block의 OnMouseDown에서 BoundBox를 켜주고, 현재의 위치를 기억해둔다.

    void OnMouseDown()
    {
		/* 코드 추가 */
        BoundBox.Instance.boundSetActive(true);
        BoundBox.Instance.transform.position = this.transform.position;

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

 

OnMouseDrag에서 반올림을 이용하여 다음의 위치를 미리 계산해둔다.

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

        int layer = 1 << LayerMask.NameToLayer(HIDDEN_PLANE);
        if (Physics.Raycast(ray, out hitLayerMask, Mathf.Infinity, layer))
        {
            ...
            
            Vector3 boundTemp = BoundBox.Instance.transform.position;

            boundTemp.x = Mathf.Round(this.transform.position.x);
            BoundBox.Instance.transform.position = boundTemp;
        }
    }

 

OnMouseUp가 moveBlock을 호출한다.

moveBlock이 종료될 때, BoundBox를 꺼주면 된다.

    void OnMouseUp()
    {
        ...
        
        moveBlock(tempPos);
    }
    
    ...
    
    private IEnumerator moveBlockCoroutine(Vector3 targetPosition)
    {
        ...

        transform.position = targetPosition;

        BoundBox.Instance.boundSetActive(false);
    }

 

Block.cs의 최종 코드는 아래와 같다.

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

public class Block : MonoBehaviour
{
    const string HIDDEN_PLANE = "HiddenPlane";

    public float blockMoveTime = .3f;

    RaycastHit hit, hitLayerMask;
    GameObject hitPos;

    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()
    {
        BoundBox.Instance.boundSetActive(true);
        BoundBox.Instance.transform.position = this.transform.position;

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

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

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

            Vector3 temp = getContactPoint(normal, planeDot, A, B);
            Vector3 setPos = hitPos.transform.position;

            setPos.x = temp.x;
            hitPos.transform.position = setPos;

            Vector3 boundTemp = BoundBox.Instance.transform.position;

            boundTemp.x = Mathf.Round(this.transform.position.x);
            BoundBox.Instance.transform.position = boundTemp;
        }
    }

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

        Vector3 tempPos = this.transform.position;
        tempPos.x = Mathf.Round(tempPos.x);

        moveBlock(tempPos);
    }

    public void moveBlock(Vector3 targetPosition)
    {
        StartCoroutine(moveBlockCoroutine(targetPosition));
    }

    private IEnumerator moveBlockCoroutine(Vector3 targetPosition)
    {
        float elapsedTime = 0.0f;
        Vector3 currentPosition = transform.position;

        while (elapsedTime < blockMoveTime)
        {
            elapsedTime += Time.deltaTime;

            transform.position = Vector3.Lerp(currentPosition, targetPosition, elapsedTime / blockMoveTime);

            yield return null;
        }

        transform.position = targetPosition;

        BoundBox.Instance.boundSetActive(false);
    }
}

에셋스토어의 BoundBox.cs는 아래와 같이 변경하였다. (불필요한 내용 삭제)

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class BoundBox : MonoBehaviour
{
    static BoundBox instance = null;
    public static BoundBox Instance
    {
        get
        {
            if (null == instance) instance = FindObjectOfType<BoundBox>();
            return instance;
        }
    }

    void Awake()
    {
        if (null == instance) instance = this;
    }

    public enum BoundSource
    {
        meshes,
        boxCollider,
    }

    public BoundSource boundSource = BoundSource.meshes;

    //public bool setupOnAwake = false;
    public Material lineMaterial;

    [HideInInspector]
    public Color wireColor = new Color(0f, 1f, 0.4f, 0.74f);
    [HideInInspector]
    public bool line_renderer = false;
    [HideInInspector]
    public Object linePrefab;
    [HideInInspector]
    [Range(0.005f, 0.25f)] public float lineWidth = 0.03f;
    [HideInInspector]
    public Color lineColor = Color.red;
    [HideInInspector]
    public int numCapVertices = 0;

    protected Bounds bound;
    protected Vector3 boundOffset;
    [HideInInspector]
    public Bounds colliderBound;
    [HideInInspector]
    public Vector3 colliderBoundOffset;
    [HideInInspector]
    public Bounds meshBound;
    [HideInInspector]
    public Vector3 meshBoundOffset;

    protected Vector3[] corners = new Vector3[0];

    protected Vector3[,] lines = new Vector3[0, 0];

    private Quaternion quat;

    // private DimBoxes.DrawLines cameralines;

    protected LineRenderer[] lineList;

    //private MeshFilter[] meshes;

    //private Material lineMaterial;

    private Vector3 topFrontLeft;
    private Vector3 topFrontRight;
    private Vector3 topBackLeft;
    private Vector3 topBackRight;
    private Vector3 bottomFrontLeft;
    private Vector3 bottomFrontRight;
    private Vector3 bottomBackLeft;
    private Vector3 bottomBackRight;

    [HideInInspector]
    public Vector3 startingScale;
    private Vector3 previousScale;
    private Vector3 startingBoundSize;
    private Vector3 startingBoundCenterLocal;
    private Vector3 previousPosition;
    private Quaternion previousRotation;

    public void boundSetActive(bool set)
    {
        this.gameObject.SetActive(set);
    }

    void Reset()
    {
        calculateBounds();
        Start();
    }

    void Start()
    {
        /*cameralines = FindObjectOfType(typeof(DimBoxes.DrawLines)) as DimBoxes.DrawLines;

        if (!cameralines)
        {
            Debug.LogError("DimBoxes: no camera with DimBoxes.DrawLines in the scene", gameObject);
            return;
        }*/

        previousPosition = transform.position;
        previousRotation = transform.rotation;
        startingBoundSize = bound.size;
        startingScale = transform.localScale;
        previousScale = startingScale;
        startingBoundCenterLocal = transform.InverseTransformPoint(bound.center);
        init();
    }

    public void init()
    {
        SetPoints();
        SetLines();
        SetLineRenderers();
    }

    void LateUpdate()
    {
        if (transform.localScale != previousScale)
        {
            SetPoints();
        }
        if (transform.position != previousPosition || transform.rotation != previousRotation || transform.localScale != previousScale)
        {
            SetLines();
            previousRotation = transform.rotation;
            previousPosition = transform.position;
            previousScale = transform.localScale;
        }
    }

    void calculateBounds()
    {
        quat = transform.rotation;//object axis AABB
        Vector3 locScale = transform.localScale;

        meshBound = new Bounds();
        MeshFilter[] meshes = GetComponentsInChildren<MeshFilter>();
        transform.rotation = Quaternion.Euler(0f, 0f, 0f);
        transform.localScale = Vector3.one;

        for (int i = 0; i < meshes.Length; i++)
        {
            Mesh ms = meshes[i].sharedMesh;
            int vc = ms.vertexCount;
            for (int j = 0; j < vc; j++)
            {
                if (i == 0 && j == 0)
                {
                    meshBound = new Bounds(meshes[i].transform.TransformPoint(ms.vertices[j]), Vector3.zero);
                }
                else
                {
                    meshBound.Encapsulate(meshes[i].transform.TransformPoint(ms.vertices[j]));
                }
            }
        }
        transform.rotation = quat;
        transform.localScale = locScale;
        meshBoundOffset = meshBound.center - transform.position;
    }

    void SetPoints()
    {

        if (boundSource == BoundSource.boxCollider)
        {
            BoxCollider bc = GetComponent<BoxCollider>();
            if (!bc)
            {
                Debug.LogError("no BoxCollider - add BoxCollider to " + gameObject.name + " gameObject");
                return;

            }
            bound = new Bounds(bc.center, bc.size);
            boundOffset = bc.center;
        }

        else
        {
            bound = meshBound;
            boundOffset = meshBoundOffset;
        }
        //bound.size = new Vector3(bound.size.x * transform.localScale.x / startingScale.x, bound.size.y * transform.localScale.y / startingScale.y, bound.size.z * transform.localScale.z / startingScale.z);
        //boundOffset = new Vector3(boundOffset.x * transform.localScale.x / startingScale.x, boundOffset.y * transform.localScale.y / startingScale.y, boundOffset.z * transform.localScale.z / startingScale.z);

        topFrontRight = boundOffset + Vector3.Scale(bound.extents, new Vector3(1, 1, 1));
        topFrontLeft = boundOffset + Vector3.Scale(bound.extents, new Vector3(-1, 1, 1));
        topBackLeft = boundOffset + Vector3.Scale(bound.extents, new Vector3(-1, 1, -1));
        topBackRight = boundOffset + Vector3.Scale(bound.extents, new Vector3(1, 1, -1));
        bottomFrontRight = boundOffset + Vector3.Scale(bound.extents, new Vector3(1, -1, 1));
        bottomFrontLeft = boundOffset + Vector3.Scale(bound.extents, new Vector3(-1, -1, 1));
        bottomBackLeft = boundOffset + Vector3.Scale(bound.extents, new Vector3(-1, -1, -1));
        bottomBackRight = boundOffset + Vector3.Scale(bound.extents, new Vector3(1, -1, -1));

        corners = new Vector3[] { topFrontRight, topFrontLeft, topBackLeft, topBackRight, bottomFrontRight, bottomFrontLeft, bottomBackLeft, bottomBackRight };
    }

    protected virtual void SetLines()
    {
        if (line_renderer)
        {
            lineList = GetComponentsInChildren<LineRenderer>();
            if (lineList.Length == 0)
            {
                lineList = new LineRenderer[12];
                for (int i = 0; i < 12; i++)
                {
#if UNITY_EDITOR
                    GameObject go = PrefabUtility.InstantiatePrefab(linePrefab) as GameObject;
                    go.transform.SetParent(transform);
                    go.transform.position = Vector3.zero;
                    go.transform.rotation = Quaternion.identity;
#else
                    GameObject go = (GameObject)Instantiate(linePrefab, transform, false);
#endif
                    lineList[i] = go.GetComponent<LineRenderer>();
                }
            }
            else
            {
                for (int i = 0; i < lineList.Length; i++)
                {

#if UNITY_EDITOR
                    if (!Application.isPlaying)
                    {
                        if (PrefabUtility.GetCorrespondingObjectFromSource(lineList[i].gameObject) == linePrefab)
                        {
                            lineList[i].enabled = true;
                        }
                        else
                        {
                            lineList[i].gameObject.SetActive(false);
                            Debug.Log("BB");
                            GameObject go = PrefabUtility.InstantiatePrefab(linePrefab) as GameObject;
                            go.transform.SetParent(transform);
                            go.transform.position = Vector3.zero;
                            go.transform.rotation = Quaternion.identity;
                            lineList[i] = go.GetComponent<LineRenderer>();
                        }
                    }
#endif
                    lineList[i].enabled = true;
                }
            }

            for (int i = 0; i < 4; i++)
            {
                //width
                lineList[i].SetPositions(new Vector3[] { transform.TransformPoint(corners[2 * i]), transform.TransformPoint(corners[2 * i + 1]) });
                //height
                lineList[i + 4].SetPositions(new Vector3[] { transform.TransformPoint(corners[i]), transform.TransformPoint(corners[i + 4]) });
                //depth
                lineList[i + 8].SetPositions(new Vector3[] { transform.TransformPoint(corners[2 * i]), transform.TransformPoint(corners[2 * i + 3 - 4 * (i % 2)]) });
            }
        }
    }

    public void SetLineRenderers()
    {
        Gradient colorGradient = new Gradient();
        colorGradient.SetKeys(new GradientColorKey[] { new GradientColorKey(lineColor, 0.0f)},  new GradientAlphaKey[] { new GradientAlphaKey(lineColor.a, 0.0f) });
        foreach (LineRenderer lr in GetComponentsInChildren<LineRenderer>(true))
        {
            lr.startWidth = lineWidth;
            lr.enabled = line_renderer;
            lr.numCapVertices = numCapVertices;
            lr.colorGradient = colorGradient;
            lr.material = lineMaterial;
        }
    }
}

 

LineMaterial은 아래와 같다.

line_opaque.mat
0.00MB
LineOpaque.shader
0.00MB

 

BoundBox의 자식으로 12개의 Line Renderer가 있다.

 

여기 있는 BoundBox를 복사하는 것보다 Asset Store에서 가져온 후, 수정하는 것이 편할 수 있다.

이 글에서 사용한 BoundBox는 아래의 unitypackageimport 하면 된다.

SimpleBoundBox.unitypackage
0.01MB

 

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

반응형

댓글