유니티 마우스 클릭 이동 방법 – 오늘은 유니티 C# 스크립트를 이용하여 마우스 클릭에 의한 플레이어 캐릭터의 이동 및 회전을 구현해 보겠습니다. 다음은 결과물의 영상입니다.
위와 같은 동작을 위해 필요한 유니티 C# 클래스는 단 하나입니다. ClickToMove.cs 라는 이름의 모노비헤이비어 기반의 클래스를 아래와 같은 방법으로 만든 뒤, 플레이어 캐릭터 역할을 하는 게임 오브젝트에 붙여 넣으면 바로 실행됩니다.
다음은 코드를 작성하는 방법에 대한 설명입니다.
ClickToMove 유니티 C# 스크립트 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
private void Update()
{
}
}
우선 유니티에서 C# 스크립트를 하나 만들고 이름을 ClickToMove 라고 변경하였습니다. 이 스크립트에서는 Start() 메소드를 사용하지 않을 것이므로 Update() 를 제외하고는 일단 지웠습니다. (그냥 놔두셔도 물론 됩니다.)
필드 선언
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private void Update()
{
}
}
제일 먼저 플레이어 캐릭터의 이동 속도 및 마우스 클릭 지점으로의 회전 속도를 지정하기 위한 변수 두 개를 선언하였습니다.
movementSpeed 는 이동 속도를, rotationSpeed 는 회전 속도를 나타냅니다. 각각 기본 값 10f 를 부여하였고, 이는 나중에 얼마든지 변경 가능합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private void Update()
{
}
}
다음으로 이동 목표 지점의 좌표를 저장할 변수 destinationPoint 를 선언하였습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
}
}
다음으로 플레이어 캐릭터의 움직임을 제어하기 위한 플래그(flag)로 부울 타입 변수인 shouldMove 를 추가하였습니다. 플레이어 캐릭터가 이동하다가 목표지점에 도달하면 멈춰야 하기 때문에 이를 제어하기 위한 것입니다.
목표 지점 위치 구하기
마우스 클릭 여부 확인
이제 마우스 클릭을 통해 이동 지점의 좌표를 구하는 부분을 만들어 보겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
}
}
}
Input.GetMouseButtonDown(0)은 마우스 왼쪽 버튼 클릭 여부를 확인하는 방법입니다. 마우스 왼쪽 버튼을 클릭하면 true를 반환하고 클릭하지 않으면 false를 반환합니다.
if 조건문은 마우스 왼쪽 버튼이 클릭되었는지 확인합니다. 클릭하면 조건문이 참이 되어서, if 블록 내부의 코드가 실행됩니다. 클릭하지 않으면 거짓이 되므로 if 블록 내부의 코드가 실행되지 않습니다.
목표 지점 위치 구하기
이제 마우스 클릭을 기반으로 실제 3차원 월드 상에서의 목표 이동 지점을 구하는 코드를 작성해 보겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
}
}
}
}
위의 코드는 3차원 월드 상에서의 마우스 클릭 지점을 구할 때, 일종의 패턴처럼 외워서 사용하시면 됩니다. 각 행의 의미는 다음과 같습니다.
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
이 명령문이 하는 일은 Ray 타입의 개체인 ray를 만드는 것입니다. 이렇게 만든 ray 는 다음 부분에서 마우스 클릭 지점을 구할 때 사용됩니다.
ray를 만들기 위해서는 Camera.main 개체의 ScreenPointToRay 메서드를 사용합니다. Camera.main 은 현재 씬(scene)에 존재하는 메인 카메라를 나타냅니다.
ScreenPointToRay 메서드는 Vector3를 매개 변수로 사용하며, 이 매개 변수는 화면상의 점(point)을 나타냅니다. 위의 코드에서는 Input.mousePosition이 매개 변수로 사용되었는데, 이 Input.mousePosition은 마우스 커서의 현재 (화면상의) 위치를 나타냅니다.
이렇게 마우스 커서의 화면상의 위치를 매개 변수로 전달하면, 이제 ScreenPointToRay 메서드는 메인 카메라의 위치에서 시작하여 화면의 포인트 방향으로 확장되는 광선(ray)을 생성합니다. 다음의 그림을 보시면 이해가 되실 겁니다.
위의 그림에서 플레이어의 눈(메인 카메라)로부터 2차원 화면상의 마우스 클릭 지점을 관통하는 광선(ray)이 쭉 나아가다가 3차원 월드상의 바닥에 충돌하고 있습니다. 위의 명령문에서 만들어 낸 Ray 개체가 가리키는 것이 그것이라고 이해하시면 됩니다.
RaycastHit 이해하기
이제 다음 코드를 보겠습니다.
RaycastHit hit;
이 코드는 Raycast 타입의 변수인 hit 를 선언하고 있습니다. RaycastHit 는 광선이 충돌하는 지점(3D 공간의 한 점)과 관련한 정보를 저장하는데 사용되는 데이터 구조(data structure)입니다.
if (Physics.Raycast(ray, out hit, 100f))
이 코드는 물리학(Physics) 클래스의 Raycast 메서드를 사용하여 레이캐스팅을 수행합니다. Raycast 메서드는 위의 코드에서 보시는 것처럼 세 가지 파라미터를 사용합니다.
- 캐스팅할 광선을 나타내는 Ray 개체. 이 경우에는 앞에서 만들어 낸 ray 가 매개 변수로 전달됩니다.
- RaycastHit. 즉, 광선이 충돌하는 지점에 대한 데이터 개체입니다. 이 경우에는 바로 위에서 구한 hit 가 매개 변수로 전달됩니다.
- 광선이 도달할 수 있는 최대 거리를 나타내는 실수(float) 값. 이 경우 100f가 매개 변수로 전달되었습니다. 더 크거나 작은 숫자를 입력하셔도 되지만 너무 짧으면 래이캐스트로 인한 충돌 자체가 일어나지 않을 수 있으니 주의하시기 바랍니다.
Physics.Raycast 메서드는 광선(ray)이 충돌체를 가진 물체와 충돌하면 true를 반환하고 그렇지 않으면 false를 반환합니다. 위의 코드에서 충돌이 감지되면 if 조건문 블록 내부의 코드가 실행되고, 그렇지 않으면 if 블록 내부의 코드가 실행되지 않을 것입니다.
따라서 위와 같은 방법으로 마우스 클릭이 이동 목표 지점에 대한 유효한 정보를 가져 오는 지 여부를 알 수 있는 것입니다.
Physics.Raycast 메서드 사용
그럼 이제부터 조건문 내부 코드를 작성하도록 하겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
shouldMove = true;
}
}
}
}
만약 Physics.Raycast 메서드가 true 를 반환한다면, hit 라는 매개 변수에는 이제 충돌 지점에 대한 여러 가지 데이터가 자동으로 저장되게 됩니다. 따라서 이 hit 를 분석해서 목표 이동 지점을 파악할 수 있습니다.
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
목표 이동 지점은 위와 같은 방식으로 hit 에 들어 있는 정보를 통해 생성해 낼 수 있습니다.
참고로 위의 코드에서는 Vector3 개체를 직접 만들었지만, 원래는 다음과 같이 hit.point 를 직접 대입하는 것이 정석입니다.
destinationPoint = hit.point;
하지만 우리의 경우에는 y 값을 hit.point의 y 좌표가 아닌, 플레이어 캐릭터 게임 오브젝트의 y 좌표인 transform.position.y 를 이용했습니다. 그 이유는 현재 플레이어 캐릭터가 지면에 닿아 있지 않고, 지면에서 약간 위에 떠 있기 때문에 이 고도를 유지하기 위해서 그런 것입니다.
만약에 여러분의 캐릭터가 위와 달리 지면에 붙어 있다면, destinationPoint = hit.point; 라고 하셔도 무방합니다.
움직임과 관련한 플래그
이제 다음 코드를 보겠습니다.
shouldMove = true;
이 코드의 의미는 명확합니다. shouldMove 플래그를 true로 설정하여 개체가 대상 지점을 향해 이동을 시작할 수 있도록 허용하는 것입니다. 이 값이 나중에 false 로 변하면, 다시 말해서 플레이어 캐릭터가 목표지점에 도달하게 되면 더 이상 이동을 허용하지 말아야 합니다. 이에 대해서는 이후의 코드에서 확인하실 수 있을 것입니다.
이제 목표 이동 지점을 구했으니 다음으로 해야 할 일은 캐릭터를 해당 지점을 향해 회전시키고, 지정된 속도에 맞춰 이동시켜야 합니다. 이를 위한 코드를 추가해 보겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
shouldMove = true;
}
}
if (shouldMove)
{
}
}
}
이제부터의 코드는 플레이어 캐릭터의 이동이 허용된 경우에만 의미가 있습니다. 따라서 위와 같이 조건문을 만든 것입니다. 이제 이 안에 회전과 관련된 코드를 먼저 작성해 넣겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
shouldMove = true;
}
}
if (shouldMove)
{
Quaternion targetRotation = Quaternion.LookRotation(destinationPoint - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
}
}
캐릭터의 회전과 관련한 이 코드 블록도 하나의 패턴으로 암기해서 사용하시는 것이 좋습니다. 처음에는 원리를 이해하기 어려워도 반복해서 사용하다 보면 자신도 모르게 의미를 이해하게 될 것입니다. 일단 각각의 코드에 대해 설명 드리겠습니다.
Quaternion targetRotation = Quaternion.LookRotation(destinationPoint - transform.position);
이 명령문은 개체가 대상 지점을 바라보기 위해 얼마만큼 회전해야 하는지를 계산합니다. Quaternion.LookRotation 매서드는 매개 변수로 정면 방향(forward direction)을 받습니다. 플레이어 캐릭터의 현재 위치에서 목표 지점을 바라보기 위한 정면 방향은 destinationPoint에서 캐릭터 개체의 현재 위치(transform.position)를 빼면 구할 수 있습니다.
이렇게 해서 구한 회전 값을 Quaternion 타입의 변수인 targetRotation 에 저장하면 됩니다.
부드러운 회전 구현
그럼 다음 코드를 보겠습니다.
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
이 코드는 바로 위에서 계산한 목표 회전 값을 향해 개체를 부드럽게 회전시키는 일을 합니다. 이를 위해서는 Quaternion 클래스의 Slerp(구면 선형 보간) 메서드를 사용합니다.
Quaternion.Slerp() 매서드는 개체의 현재 회전(transform.rotation), 목표 회전(targetRotation) 및 회전 속도(rotationSpeed)를 매개 변수로 받습니다. (이 때 Time.deltaTime 값을 회전 속도에 곱하면 게임의 프레임 속도에 관계없이 일정한 속도로 회전할 수 있습니다.)
그리고 이렇게 얻어진 매 프레임 당 캐릭터가 회전할 목표 값을 transform.rotation 에 할당하면 됩니다. 여기까지 코드를 작성하고 실행하면 캐릭터가 이동은 못해도 회전은 잘 수행하게 될 것입니다.
캐릭터 이동 구현
이제 마지막으로 이동 부분을 작성하도록 하겠습니다. 지금까지 작성한 코드에 다음과 같은 명령문을 추가합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
shouldMove = true;
}
}
if (shouldMove)
{
Quaternion targetRotation = Quaternion.LookRotation(destinationPoint - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
transform.position = Vector3.MoveTowards(transform.position, destinationPoint, movementSpeed * Time.deltaTime);
}
}
}
이 코드는 플레이어 캐릭터 개체를 앞에서 구한 목표 지점을 향해 이동시킵니다.
Vector3.MoveTowards() 메서드에 개체의 현재 위치(transform.position), 목표 지점(destinationPoint) 및 ‘이동 속도(movementSpeed) 곱하기 Time.deltaTime’의 세 가지 매개 변수를 전달하면 각 프레임당 개체가 얼마나 이동해야 원하는 속도로 목표 지점에 도달할 수 있을 지를 알 수 있습니다. 이렇게 구한 값을 다시 플레이어 캐릭터 개체의 transform.position 에 할당하면 됩니다.
목표 지점 도달 처리
이 코드를 추가하고 게임을 실행하면 마우스 클릭을 통해 캐릭터를 목표 지점으로 이동시킬 수 있습니다. 하지만 목표 지점에서 멈추지 않고 계속 움직이게 됩니다. 정지와 관련된 코드를 작성하지 않았기 때문입니다.
따라서 다음과 같은 코드를 추가함으로써 이 문제를 해결할 수 있습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClickToMove : MonoBehaviour
{
public float movementSpeed = 10f;
public float rotationSpeed = 10f;
private Vector3 destinationPoint;
private bool shouldMove = false;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
destinationPoint = new Vector3(hit.point.x, transform.position.y, hit.point.z);
shouldMove = true;
}
}
if (shouldMove)
{
Quaternion targetRotation = Quaternion.LookRotation(destinationPoint - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
transform.position = Vector3.MoveTowards(transform.position, destinationPoint, movementSpeed * Time.deltaTime);
if (transform.position == destinationPoint)
{
shouldMove = false;
}
}
}
}
이 코드는 개체의 위치가 대상 지점과 동일한지 여부를 확인합니다. 만약 그럴 경우, 이동 플래그가 false로 설정되고 플레이어 캐릭터 개체는 이동을 멈추게 됩니다.
위 코드에서 보완해야 할 부분
참고로 이 부분은 자칫 문제를 야기할 수도 있습니다. 예를 들어 게임의 프레임이 낮을 경우(너무 느릴 경우) 자칫 위의 조건문 (==)이 참인 순간을 지나쳐 버릴 수 있습니다.
이런 경우를 대비하기 위해 플레이어 캐릭터의 현재 위치와 목표 지점까지의 거리를 계산해서, 거리가 예를 들어 1미터 이상 떨어졌을 때만 이동하고, 그 보다 줄어들면 멈추게 하는 방법으로 코드를 변경할 수도 있습니다. 이 부분은 여러분이 직접 해 보시기 바랍니다.
지금까지 작성한 ClickToMove 스크립트는 어떤 게임 오브젝트에 붙여도 작동됩니다. 위의 안내에 따라 스크립트를 작성하고 확장, 변형시켜 보시기 바랍니다. 꽤 재미 있는 유니티 C# 스크립트 공부가 될 것입니다.
더 공부할 자료
유니티 C# 스크립트 작성에 대해 보다 심도 있는 공부를 원하시면 다음 링크를 체크해 보시기 바랍니다. 게임 개발을 위한 코딩 전 과정을 스탭 바이 스탭(step by step) 방식으로 자세하게 설명해 드리는 전자책입니다.
“막 유니티를 배운 주니어 게임 프로그래머로서 자신의 힘으로 첫번 째 게임을 만들고 싶지만 어디서부터 시작해야 할지 막막한 분 계신가요? 이 책을 읽어 보세요. 간단한 게임이지만 게임 구현 전체 프로세스를 자세한 설명과 함께 제공합니다. 동영상 예제까지 들어 있어요!”
더 공부할 자료 - 개발 능력 다양화를 위한 학습의 필요성
유니티 엔진의 대안으로서, 인디 개발자들에게 선풍적인 인기를 끌고 있는 엔진이 바로 고도 엔진입니다. 혹시 고도 엔진을 배워 보려고 하신다면, 다음의 온라인 강의를 체크해 보시기 바랍니다.