Coding Feature.

[Unity 2D] Portal 같은 게임 만들기 #5 포탈건 매커니즘 구현하기 2 (포탈 생성하기) 본문

Toy Project/mini-portal [Unity2D]

[Unity 2D] Portal 같은 게임 만들기 #5 포탈건 매커니즘 구현하기 2 (포탈 생성하기)

codingfeature 2024. 1. 7. 14:25

앞서 포탈건의 조준선을 시각화했다면 이제는 본격적으로 포탈건으로 포탈을 만들수 있도록 구현해보겠습니다.

 

플레이어가 어떤 벽면에 포탈건을 조준하고 쏠 때 벽면의 각도와 위치에 따라서 포탈이 그에 알맞게 생성되도록 해야 합니다.

 

우선 포탈을 만들 수 있는 벽면을 "Portalable"이라고 명명하겠습니다.

 

그리고 앞서 만들었던 GameObject인 "Ground"의 앞면에 Portalable을 붙여놓습니다.

그래서 실제로 포탈이 만들어질 때 Ground가 아니라 Ground의 바로 앞에 있는 Portalable에 생성되도록 했습니다.

 

 

그 다음 Portalable에 포탈건을 쏠 때, 포탈건으로부터 쏠 때의 위치와 각도, 그리고 Portalable의 위치와 각도를 사용해서 포탈이 Portalable의 어느 위치에서 생성될 지 구해야 합니다.

 

이는 두 벡터의 교점을 구함으로써 구했습니다.

 

 

위에서 구한 식으로 포탈이 생성될 위치 좌표를 구하고, 각도는 Portalable 의 각도와 동일하게 생성되도록 코드를 짜면 되겠네요!

 

위에서 작성한 식을 코드로 표현하면 아래와 같습니다!

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Portalable")
        {
            m_pointedGameObject = collision.gameObject;

            float beta1, beta2;
            beta1 = player.transform.position.y - (m_mouseDir.y / m_mouseDir.x) * player.transform.position.x;
            beta2 = m_pointedGameObject.transform.position.y - Mathf.Tan(m_pointedGameObject.transform.rotation.eulerAngles.z * Mathf.Deg2Rad) * m_pointedGameObject.transform.position.x;

            portal_X = (beta2 - beta1) / ((m_mouseDir.y / m_mouseDir.x) - Mathf.Tan(m_pointedGameObject.transform.rotation.eulerAngles.z * Mathf.Deg2Rad));
            portal_Y = (m_mouseDir.y / m_mouseDir.x) * portal_X + beta1;
        }
    }

 

위에서는 OnTriggerStay2D 함수를 사용했습니다.

이를 위해서 Edge Collision과 Portalable의 Box Collision에 Is Trigger 항목을 체크하고 Portalable에 RigidBody 2D 속성을 추가했습니다.

 

void DrawAimLine()
{
    m_colliderpoints = edgeCollider.points;

    m_mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); // 화면상 마우스 좌표 -> 게임상 좌표로 변환.

    m_startPos = player.transform.position;
    m_startPos.z = 0;

    m_endPos = m_mousePos;
    m_endPos.z = 0;

    m_mouseDir = (m_endPos - m_startPos);
    m_mouseDir.Normalize(); // 캐릭터 To 마우스까지 방향의 단위벡터.

    lineRenderer.SetPosition(0, m_mouseDir * aimLineStartLength + m_startPos); // 캐릭터 좌표에서 선 시작.
    lineRenderer.SetPosition(1, m_mousePos); // 마우스 좌표에서 선 끝.

    m_colliderpoints[0] = m_mouseDir * aimLineStartLength + m_startPos; // Edge Collider 선 시작.
    m_colliderpoints[1] = m_mouseDir * aimLineLength + m_startPos; // Edge Collider 선 끝.

    edgeCollider.points = m_colliderpoints; // Edge Collider Points 설정.
}

 

m_colliderpoints를 보면 Edge Collider의 시작점에 aimLineStartLength만큼 곱해서 캐릭터와 어느정도 간격을 두었는데 이는 캐릭터의 Box Collider와 겹치는 것을 막기 위해서 입니다.

 

 

그리고 마우스 왼쪽 버튼을 입력했을 때 블루 포탈, 오른쪽 버튼을 입력했을 때 오렌지 포탈을 생성하도록 코드를 짰습니다.

if (Input.GetKeyDown(KeyCode.Mouse0))
{
    bluePortal.transform.position = new Vector3(portal_X, portal_Y, 0);
    bluePortal.transform.rotation = m_pointedGameObject.transform.rotation;
}

if (Input.GetKeyDown(KeyCode.Mouse1))
{
    orangePortal.transform.position = new Vector3(portal_X, portal_Y, 0);
    orangePortal.transform.rotation = m_pointedGameObject.transform.rotation;
}

 

 

위 내용을 코드 전체로 보면 다음과 같습니다.

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

public class PortalGunScript : MonoBehaviour
{
    public LineRenderer lineRenderer;
    public GameObject player;
    public EdgeCollider2D edgeCollider;
    public GameObject bluePortal;
    public GameObject orangePortal;

    public float aimLineLength;
    public float aimLineStartLength;

    Vector3 m_startPos;
    Vector3 m_endPos;
    Vector3 m_mousePos;
    Vector3 m_mouseDir;

    Vector2[] m_colliderpoints;

    GameObject m_pointedGameObject;

    float portal_X, portal_Y;
    Vector3 portal_XYZ;

    // Start is called before the first frame update
    void Start()
    {
        aimLineLength = 30.0f; // Aim Line의 길이.
        aimLineStartLength = 0.5f; // Aim Line의 시작점과 플레이어간의 간격.
        edgeCollider.enabled = true;
        lineRenderer.enabled = true;
    }

    // Update is called once per frame
    void Update()
    {
        DrawAimLine();

        if (Input.GetKeyDown(KeyCode.Mouse0))
        {
            bluePortal.transform.position = new Vector3(portal_X, portal_Y, 0);
            bluePortal.transform.rotation = m_pointedGameObject.transform.rotation;
        }

        if (Input.GetKeyDown(KeyCode.Mouse1))
        {
            orangePortal.transform.position = new Vector3(portal_X, portal_Y, 0);
            orangePortal.transform.rotation = m_pointedGameObject.transform.rotation;
        }
    }

    void DrawAimLine()
    {
        m_colliderpoints = edgeCollider.points;

        m_mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition); // 화면상 마우스 좌표 -> 게임상 좌표로 변환.

        m_startPos = player.transform.position;
        m_startPos.z = 0;

        m_endPos = m_mousePos;
        m_endPos.z = 0;

        m_mouseDir = (m_endPos - m_startPos);
        m_mouseDir.Normalize(); // 캐릭터 To 마우스까지 방향의 단위벡터.

        lineRenderer.SetPosition(0, m_mouseDir * aimLineStartLength + m_startPos); // 캐릭터 좌표에서 선 시작.
        lineRenderer.SetPosition(1, m_mousePos); // 마우스 좌표에서 선 끝.

        m_colliderpoints[0] = m_mouseDir * aimLineStartLength + m_startPos; // Edge Collider 선 시작.
        m_colliderpoints[1] = m_mouseDir * aimLineLength + m_startPos; // Edge Collider 선 끝.

        edgeCollider.points = m_colliderpoints; // Edge Collider Points 설정.
    }
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Portalable")
        {
            m_pointedGameObject = collision.gameObject;

            float beta1, beta2;
            beta1 = player.transform.position.y - (m_mouseDir.y / m_mouseDir.x) * player.transform.position.x;
            beta2 = m_pointedGameObject.transform.position.y - Mathf.Tan(m_pointedGameObject.transform.rotation.eulerAngles.z * Mathf.Deg2Rad) * m_pointedGameObject.transform.position.x;

            portal_X = (beta2 - beta1) / ((m_mouseDir.y / m_mouseDir.x) - Mathf.Tan(m_pointedGameObject.transform.rotation.eulerAngles.z * Mathf.Deg2Rad));
            portal_Y = (m_mouseDir.y / m_mouseDir.x) * portal_X + beta1;
        }
    }
}

 

 

결과는 다음과 같습니다.

 

 

 

 

 

위 코드를 작성하는데 있어서 여러 버그 및 수정 사항이 발생했었습니다.

하나씩 정리해보고자 합니다.

 

문제1)

사용자가 Portalable 벽면에 생긴 포탈에서 나올 때 Portalable 뒷면에 있는 Ground 벽면의 Collider 때문에 끼여 관성을 잃는 버그가 발생했었습니다.

해결)

사용자가 Portal에 나올 때 잠시동안 Is Trigger 속성을 true로 바꿔서 플레이어의 물리적 속성을 제거함으로써 Ground 벽면의 Collider에 끼이는 버그를 해결했습니다. 다만 이 문제는 추후에 다른 문제점을 발생시켰으나 이 부분은 바로 다음에 해결해보기로 하겠습니다!

if (isTouchingOrange && !m_EnteredPortal) // 오렌지 포탈에 닿은 경우 + 이미 들어간 적이 없는 경우.
{
    m_EnteredPortal = true;
    player.transform.position = bluePortal.transform.position; //  오렌지 포탈로 위치 변환.
    player.GetComponent<Rigidbody2D>().velocity = desiredDirection.normalized * m_playerVelocity.magnitude; // 포탈을 나갈때 속도의 방향 변환.

    player.GetComponent<BoxCollider2D>().isTrigger = true; // 잠시동안 플레이어 collider 트리거로 변환.
}

if (isTouchingBlue && !m_EnteredPortal) // 블루 포탈에 닿은 경우 + 이미 들어간 적이 없는 경우.
{
    m_EnteredPortal = true;
    player.transform.position = orangePortal.transform.position; //  블루 포탈로 위치 변환.
    player.GetComponent<Rigidbody2D>().velocity = desiredDirection.normalized * m_playerVelocity.magnitude; // 포탈을 나갈때 속도의 방향 변환.

    player.GetComponent<BoxCollider2D>().isTrigger = true; // 잠시동안 플레이어 collider 트리거로 변환.
}

 

 

문제2)

포탈건을 쏘고 난 뒤에 포탈에 들어가면 포탈에 들어간 각도에 맞게 관성으로 나오지 않는 버그가 발생했습니다.

해결)

이 부분은 Portal 관련 Script에서 Update 함수를 통해 지속적으로 포탈의 각도를 업데이트하지 않아서 생기는 버그였습니다. 아래처럼 지속적으로 각도를 업데이트 해주는 코드를 추가하여 해결했습니다.

void Update()
{
    m_playerVelocity = player.GetComponent<Rigidbody2D>().velocity;

    // 포탈 각도 업데이트.
    m_orangePortalRotation = orangePortal.transform.rotation.eulerAngles.z;
    m_bluePortalRotation = bluePortal.transform.rotation.eulerAngles.z;
    ...

 

 

다음에는 지금까지 구현했던 포탈, 포탈건 매커니즘에서 발생하는 버그 등을 보수해보도록 하겠습니다!

 

 

 

 

2024-01-13 수정)

지금까지 구현했던 포탈 건의 매커니즘은 Raycast2D 방식으로 수정되었습니다!

아래 글을 참고해주세요!

https://codingfeature.tistory.com/86

 

[Unity 2D] Portal 같은 게임 만들기 #11 레벨 디자인, 여러 시스템 개선, Raycast2D 사용.

우선 포탈 매커니즘, 관성 등을 사용해 풀 수 있는 레벨들을 몇 가지 더 만들어보았습니다. 그리고 포탈을 들락날락 할 때 플레이어가 스폰하는 지점을 이전까지는 거리를 임의적인 수치로 설정

codingfeature.tistory.com