유니티 버전
2020.3.18f1
들어가기 전 참고
이 포스트는 아래의 동영상을 한국어로 재구성한 자료임을 밝힌다. 어느 정도 영어 청취가 가능하다면 동영상을 봐도 좋다.
이 포스트에서는 유니티 엔진에서 3인칭 유닛(캐릭터)의 field of view(이하 시야, fov)를 구현하는 방법을 알아본다.
우리의 목표
AOS, RTS 게임을 하면 다음 스크린샷처럼 3인칭에서 3D 유닛의 시야 시스템이 존재한다.
우리는 이 포스트, 더 나아가 시리즈를 모두 공부하여 위 스크린샷처럼 유닛을 기준으로 특정 영역을 밝혀 시야 내의 오브젝트를 볼 수 있도록 하는 방법을 익히게 된다. 최종 결과물은 다음과 같이 될 것이다.
글을 따라 차근차근 익혀보자. 유니티 버전은 2020.3.18f1을 사용했다.
월드 환경 구성
당연하지만 유닛이 있을 맵을 만들어야 한다. 적당한 크기의 Plane 오브젝트를 배치하고 Cube로 장애물을 조성하자. 가시성을 위해 적절한 머터리얼을 입힐 것을 추천한다.
맵이 있으면 본인이 조종하는 유닛과 적 유닛도 필요하다. 색과 이름을 구분하여 맵에 Capsule 오브젝트를 적절히 배치한다.
위에서 아래로 보는 구도를 만들기 위해 메인 카메라의 Projection을 Orthographic으로 바꾸고 view size와 position을 적절히 조정한다.
장애물과 적을 각각 한 오브젝트로 묶어준다. 그러면 씬의 Hierachy는 다음과 같은 구조를 갖출 것이다.
여기서 Obstacles가 장애물의 집합, Character가 플레이어가 조종할 유닛, Targets가 적 유닛의 집합이다.
플레이어 이동 구현
적절히 오브젝트를 묶었으면 Character에 Rigidbody를 추가한다.
오브젝트가 외부 영향에 의해 회전하지 않도록 Freeze Rotation으로 모든 축을 얼린다. 이제 Character가 월드 좌표계로 변환된 position을 바라보면서 움직이는 스크립트 PlayerController를 작성하자.
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
float speed = 8.0f;
Rigidbody rigidbody;
Camera viewCamera;
Vector3 velocity;
float rotDegree;
void Start()
{
rigidbody = transform.GetComponent<Rigidbody>();
viewCamera = Camera.main;
}
void Update()
{
Vector3 mousePos = Input.mousePosition;
mousePos = viewCamera.ScreenToWorldPoint(mousePos);
float dz = mousePos.z - rigidbody.position.z;
float dx = mousePos.x - rigidbody.position.x;
rotDegree = -(Mathf.Rad2Deg * Mathf.Atan2(dz, dx) - 90);
velocity = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized * speed;
}
void FixedUpdate()
{
rigidbody.MoveRotation(Quaternion.Euler(0, rotDegree, 0));
rigidbody.MovePosition(rigidbody.position + velocity * Time.fixedDeltaTime);
}
}
참고로 컨트롤러에서 물체의 rotation은 영상의 구현을 따르지 않고 여기를 응용해서 구현했다.
위 코드에서 모르는 것이 있을 경우 다음 링크를 참조한다.
해당 스크립트를 Character에 추가하고 Character가 마우스 포인터를 바라보면서 잘 움직이는지 확인되었으면 본격적으로 fov를 구현하자.
플레이어 Field Of View 구현
FOV를 구현할 때 가장 중요한 것은 2가지다.
1. 시야 반경: 유닛이 어디까지 볼 수 있을지 유닛을 중심으로 원 영역을 정해야 한다.
2. 시야각: 유닛의 전방으로 뻗은 직선을 축으로 어느 각도까지 인식할지 시야각을 정해야 한다.(360도로 모든 영역을 커버하는 것 또한 가능하다.)
위 두 가지 외에도 유튜브 영상에서는 장애물 너머의 오브젝트는 인식하지 못하도록 LayerMask를 관리한다. 여기도 이를 따라 Obstacles 산하의 오브젝트에 Obstacle 마스크를 적용하고 Targets 산하의 오브젝트에 Target 마스크를 적용하자.
마스크 적용이 끝났으면 이 포스트에서 가장 중요한 FieldOfView.cs를 작성하자. 코드가 조금 길어 주석을 달았다.
FieldOfView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FieldOfView : MonoBehaviour
{
// 시야 영역의 반지름과 시야 각도
public float viewRadius;
[Range(0, 360)]
public float viewAngle;
// 마스크 2종
public LayerMask targetMask, obstacleMask;
// Target mask에 ray hit된 transform을 보관하는 리스트
public List<Transform> visibleTargets = new List<Transform>();
void Start()
{
// 0.2초 간격으로 코루틴 호출
StartCoroutine(FindTargetsWithDelay(0.2f));
}
IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisibleTargets();
}
}
void FindVisibleTargets()
{
visibleTargets.Clear();
// viewRadius를 반지름으로 한 원 영역 내 targetMask 레이어인 콜라이더를 모두 가져옴
Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
for (int i = 0; i < targetsInViewRadius.Length; i++)
{
Transform target = targetsInViewRadius[i].transform;
Vector3 dirToTarget = (target.position - transform.position).normalized;
// 플레이어와 forward와 target이 이루는 각이 설정한 각도 내라면
if (Vector3.Angle(transform.forward, dirToTarget) < viewAngle / 2)
{
float dstToTarget = Vector3.Distance(transform.position, target.transform.position);
// 타겟으로 가는 레이캐스트에 obstacleMask가 걸리지 않으면 visibleTargets에 Add
if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask))
{
visibleTargets.Add(target);
}
}
}
}
// y축 오일러 각을 3차원 방향 벡터로 변환한다.
// 원본과 구현이 살짝 다름에 주의. 결과는 같다.
public Vector3 DirFromAngle(float angleDegrees, bool angleIsGlobal)
{
if (!angleIsGlobal)
{
angleDegrees += transform.eulerAngles.y;
}
return new Vector3(Mathf.Cos((-angleDegrees + 90) * Mathf.Deg2Rad), 0, Mathf.Sin((-angleDegrees + 90) * Mathf.Deg2Rad));
}
}
이 스크립트의 DirFromAngle 메서드는 시각효과를 구현할 때 사용할 메서드이다. fov를 구현하긴 했지만 실제로 잘 동작하는지 알 수 없으니 에디터에서 시각효과로 확인할 수 있는 스크립트를 짤 것이다.
작성을 완료했으면 Character 오브젝트에 컴포넌트를 추가하고 마스크와 radius, angle을 잘 설정하자.
유니티 시각효과 구현(Editor 사용)
viewRadius, viewAngle의 범위와 raycast hit 된 오브젝트를 에디터에서 시각적으로 보기 위해 다음 CustomEditor를 구현한다. CustomEditor라는 개념이 생소할 경우 유니티 공식 문서와 여기를 먼저 볼 것을 추천한다.
FieldOfViewEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor (typeof (FieldOfView))]
public class FieldOfViewEditor : Editor
{
void OnSceneGUI()
{
FieldOfView fow = (FieldOfView)target;
Handles.color = Color.white;
Handles.DrawWireArc(fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);
Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);
Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);
Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);
Handles.color = Color.red;
foreach (Transform visible in fow.visibleTargets)
{
Handles.DrawLine(fow.transform.position, visible.transform.position);
}
}
}
문제없이 작성되었으면 에디터에서 Character를 눌렀을 때 다음과 같은 시각 효과가 나타난다.
viewRadius와 viewAngle에 적절한 값을 넣고 실행하면 잘 작동하는 것을 볼 수 있다.
여기까지 시야 내의 오브젝트를 raycast hit으로 가져오는 것까지가 에피소드 1의 내용이다. 다음 에피소드 2도 이어서 진행하도록 하겠다.
'Unity Engine > 자료 번역' 카테고리의 다른 글
유니티 Procedural Cave Gerneration 랜덤 동굴 생성 1 (1) | 2022.09.11 |
---|---|
Memory Management in Unity 유니티 메모리 관리 번역 (2) | 2022.09.10 |
유니티 Fog of War 전장의 안개 구현하기 (0) | 2022.09.09 |
유니티 Field Of View 유닛 시야 구현하기 3 (0) | 2022.03.06 |
유니티 Field Of View 유닛 시야 구현하기 2 (0) | 2022.03.04 |