어떤 장르의 게임이든 잘 사용되는 추격 기능에 대한 코드를 보겠습니다. 여기선 Vector3, 정규화, Time.deltaTime 3가지를 사용하여 만들 수 있는데 여기서 벡터는 수학적 개념이 들어간 기능입니다. 여기서 벌써 벽이 느껴지는데 먼저 Vector 연산에 대해 다뤄보겠습니다.
<벡터 연산>
위가 벡터의 기본 연산 방법 예시입니다. 덧셈, 곱셈, 나눗셈은 거리에 대한 것뿐이지만, 뺄셈은 다릅니다.
그림에 적혀있듯이 A - B = B에서 A로의 방향과 거리를 뜻합니다. 즉, x축 3에서 1이 되려면 -2, y축에서 1에서 3이 되려면 2가 필요합니다. 그래서 (-2, 2)가 됩니다. 이는 유니티에서 오브젝트 간의 추격 기능에 중요하게 다룹니다. 네 개의 연산 외에도 내각과 크로스하는 점을 다루는 내적, 외적이라는 개념이 존재하는데, 이는 유니티 기능 중 광원을 다룰 때 글을 쓰겠습니다. 그렇다면 유니티에서 벡터 뺄셈을 오브젝트 추격에 어떻게 사용되는지 알아보겠습니다.
Chaser라는 오브젝트가 Target 오브젝트를 추적하는 코드를 구현하려합니다. 여기서 Chaser의 Position값, Target의 Position값을 다뤄야 합니다. 밑에서 변수를 선언합니다.
//Chaser.cs Script
using UnityEngine; //namespace (using 기능 관련 코드)
//UnityEngine.MonoBehaviour이 풀네임이기 때문에
//UnityEngine 네임스페이스 코드 써야한다.
public class Chaser : MonoBehaviour //UnityEngine.MonoBehaviour
{
//public: Inspector창에 표시되어 유니티 내에서 수정 가능
//protected, private: Inspector창에 표시되지 않아 수정 불가 (코드로만 가능)
//[SerializeField] 직렬화는 private, protected를 사용하더라도 Inspector창에 표시
//[HideInInspector] public으로 만들어도 Inspector창에 표시되지 않음. (잘 안씀)
[SerializeField] private GameObject targetGo = null; //게임 오브젝트 모든 객체 넣을 수 있음.
public Transform targetTr = null;
[SerializeField] private float moveSpeed = 10f; //C#언어에서는 .5f를 넣어도 0.5f와 같이 처리해줌.
}
- public 접근 지정자: Inspector창에서 타입에 맞는 변수를 마음껏 지정할 수 있습니다.
- protected, private 접근 지정자: Inspector창에서 변수를 지정할 수 없습니다. 스크립트에서만 가능합니다.
- [SerializeField]: 직렬화로 Inspector창에서 접근 지정자 private로 선언한 변수를 다루기 위해 사용합니다.
여기서 Transform, Target(Script), GameObject 타입을 유니티 내에서 지정할 수 있습니다. 지금은 null로 초기화해서 None으로 표시되어있지만 Hierarchy창에 있는 Target오브젝트를 마우스로 끌어서 지정해 줄 수 있습니다. 이제 여기서 각 변수에서 사용할 기능을 Awake()에서 정의해 줍니다.
private void Awake()
{
targetTr = targetGo.GetComponent<Transform>(); //해당 오브젝트의 Component(객체)를 이용하기 위한 코드
targetTr = targetGo.transform; //모든 게임 오브젝트는 transform값을 가지고있기 때문에 기본적으로 가져올 수 있음.
targetGo = targetTr.gameObject; //Transform 값만 알고있어도 그 게임 오브젝트를 사용할 수 있음.
}
targetTr은 Target의 위치를 위한 변수였습니다. 즉, Target 게임 오브젝트 변수인 targetGo의 Component 중 Transform 기능을 사용하겠다는 GetComponent<Transform>();를 사용해 줍니다.
- targetTr = targetGo.transform;
- targetGo = targetTr.gameObject; 두 개는 주석으로 설명 생략하겠습니다.
- 둘은 이번 기능 자체로만 보면 생략하셔도 됩니다. // 주석처리
이제 2개의 오브젝트의 Vector3를 빼기 하여 추격하는 기능 코드를 보겠습니다.
private void Update() //가변 프레임 (유니티에선 프레임을 고정하는 것이 어렵다) 최대 60프레임 가능
{
ChaseToTarget(); //Update의 성능을 제대로 쓰기 위해선 메서드를 따로 생성해 호출하는 것이 효율적
}
private void ChaseToTarget()
{
//Vector는 Struct구조체, transform은 class클래스
//구조체는 복사본, targetPos, myPos명을 바꿔도 값이 안바뀜.
Vector3 targetPos = targetTr.position; //타겟 위치
Vector3 myPos = transform.position; //Chaser(나)의 위치
Vector3 dir = targetPos - myPos; //나의 위치에서 타겟 위치의 방향 / 길이가 존재하기 때문에 정규화가 필요함.
dir.Normalize(); //정규화, 이 코드는 원본 dir 자체를 정규화하는 것
Vector3 dirToNormal = dir.normalized; //위와 같음. 2가지 방법이 있음. 이 코드는 원본은 안바뀌고 원본의 정규화가 된 벡터만 가지는 경우
//transform.position = transform.position + dir; // 내 위치에서 방향만큼 가기만 하면 됨. 이러면 순간이동
transform.position = transform.position + (dir * moveSpeed * Time.deltaTime); //속도가 빠른 것을 제어하기 위해 방향벡터에 속도 곱) 0.01666..프레임이 나와야함.
}
- ChaseToTarget() 함수의 설명
- Vector3 targetPos = targetTr의 포지션값
- Vector3 myPos = (this.)의 포지션값
- Vector3 dir = targetPos - myPos; : 여기서 벡터 빼기 연산이 사용됩니다. 즉, Chaser의 포지션에서 Target 오브젝트 간의 거리와 방향을 나타내는 벡터값
<정규화>(Normalize)
정규화는 벡터의 특정 길이를 1로 만들어줍니다. 벡터 빼기 연산대로 만약 정규화를 하지 않고 추적 기능 코드인
transform.position = transform.position + (dir * moveSpeed);
를 사용하여 프로젝트 재생하면 Chaser 오브젝트가 Target 오브젝트로 순간이동하게 됩니다. 우린 쫓아가는 기능을 원하는 것이기 때문에 오브젝트 간의 거리값을 그대로 쓰지 않고 방향만이 필요합니다. 이제 정규화의 기능을 간단히 알려드리겠습니다.
이런 방식입니다. 이제 Time.deltaTime을 곱하는 이유를 알려드리겠습니다.
<Time.deltaTime>(동기화 기능)
Time.deltaTime의 기능을 이해하기 위해선 프레임(Frame)이란 개념을 알아야 할 필요가 있습니다. 프레임은 쉽게 말해 영상물을 제작할 때 연속된 사진(그림) 한 컷을 의미합니다.
위의 그림을 빨간색 원이 검은색 원이 되는 영상으로 제작한다고 생각해 봅시다. 원들 하나하나를 1 Frame으로 표현하고 완성된 영상물은 8 Frame을 가진 결과물이 되는 겁니다. 현대에 영상을 접하시는 분들에게 FPS라는 말은 익숙할 겁니다. Frame Per Second로 초당 표현할 수 있는 프레임을 나타내는 말입니다. 즉, 얼마나 영상을 부드럽게 표현할 수 있는지를 나타내는 수치입니다. 기술 발전으로 게임, 영상물에서 표현할 수 있는 FPS가 증가하는 추세입니다. 그러나, 제작 당시는 모르겠지만, 이를 표현할 장치가 따라가지 못하면 제작된 FPS를 뽑아낼 수 없습니다. 예를 들어, 4 FPS만을 표현할 수 있는 컴퓨터가 있다 가정하고 위의 그림을 표현하면 이러한 형태가 됩니다.
즉, 온라인, 멀티 장르의 게임에서 이러한 상황이 발생하면 컴퓨터 성능이 따라가지 못하는 유저들은 결과가 달라질 수 있다는 것입니다. 여기서 Time.deltaTime을 이용하면 프레임은 같지만 결과도 같아지기 때문에 성능적 동기화를 이끌어낼 수 있습니다.
이제 이해가 됐을 거라 판단하고 위의 코드 중 이걸 해석하겠습니다.
transform.position = transform.position + (dir * moveSpeed * Time.deltaTime);
여기서 dir은 Normalize()로 정규화시켜줬기 대문에 타깃으로의 방향과 1 길이만을 가지고 있습니다.
이번 글에서 다룬 모든 코드를 주석 제거하고 종합해 보겠습니다.
//Chaser.cs Script
using UnityEngine;
public class Chaser : MonoBehaviour
{
[SerializeField] private GameObject targetGo = null;
public Transform targetTr = null;
[SerializeField] private float moveSpeed = 10f;
}
private void Awake()
{
targetTr = targetGo.GetComponent<Transform>();
targetTr = targetGo.transform;
targetGo = targetTr.gameObject;
}
private void Update()
{
ChaseToTarget();
}
private void ChaseToTarget()
{
Vector3 targetPos = targetTr.position;
Vector3 myPos = transform.position;
Vector3 dir = targetPos - myPos;
dir.Normalize();
Vector3 dirToNormal = dir.normalized;
transform.position = transform.position + (dir * moveSpeed * Time.deltaTime);
}
여기서 ChaseToTarget() 메서드 안에 들어간 코드들을 Update() 함수에 넣으셔도 프로그램에는 문제가 없습니다.
그러나, Update()는 프로그램이 재생될 때마다 계속 체크하는 함수기 때문에 최대한 간략하게 쓰는 것이 좋습니다. 프로그램 짱구 극장판에 나온 이 문어아저씨가 예시입니다. 기능별로 메서드를 따로 만들고 그 메서드를 Update() 함수에 넣는 형태가 가장 좋다고 보시면 됩니다.
다음 글은 위에서 배웠던 기능들을 응용하여 랜덤한 위치에 타겟을 시간마다 이동시키고 그걸 따라가는 형태를 만들어보겠습니다.
끝
'유니티(Unity) 프로그래밍' 카테고리의 다른 글
유니티3D 아날로그 시계 (0) | 2024.09.19 |
---|---|
Unity3D 추격2 (1) | 2024.09.18 |
Unity3D에 유용한 기능들 (1) (2) | 2024.09.04 |
Unity3D에서의 상속 (1) | 2024.09.04 |
유니티(Unity) Component제어 (1) | 2024.09.02 |