
오늘 학습 키워드
유니티 숙련 팀 프로젝트, 스탠다드 반 강의
오늘 학습 한 내용을 나만의 언어로 정리하기
팀플
스폰 위치에서 너무 멀어지면 돌아가게 하고 싶어요
- 그룹을 만들기 전에, 이 몬스터가 그룹에서 너무 멀리 떨어지면 원 위치로 돌아가도록 만들려고 했음.
- 그래서 AIState.Returning을 추가함
// Monster.cs
void Update()
{
// 플레이어와 몬스터간의 거리 계산
playerDistance = Vector3.Distance(CharacterManager.Instance.Player.transform.position, transform.position);
spawnDistance = Vector3.Distance(spawnPosition, transform.position);
if (!isReturning && spawnDistance > MaxDistFromSpawn)
{
SetState(AIState.Returning);
isReturning = true;
}
Debug.Log($"{aiState}");
// State가 멈춤 상태가 아니면 Moving 애니메이션 재생
if(animator != null) animator.SetBool("Moving", aiState != AIState.Idle);
switch (aiState)
{
case AIState.Idle:
case AIState.Wandering:
PassiveUpdate();
break;
case AIState.Returning:
// Debug.Log("너무 멀어짐. 돌아감.");
ReturningUpdate();
break;
case AIState.Attacking:
AttackingUpdate();
break;
}
}
void ReturningUpdate()
{
if (isReturning && agent.remainingDistance < 0.1f)
{
SetState(AIState.Wandering);
isReturning = false;
}
else if (isReturning && spawnDistance > MaxDistFromSpawn)
{
Debug.Log("돌아가기");
agent.isStopped = true;
// 스폰 위치 바라보기
Vector3 lookDirection = (spawnPosition - transform.position).normalized;
lookDirection.y = 0;
if (lookDirection != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(lookDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 5f);
}
agent.isStopped = false;
agent.SetDestination(spawnPosition);
}
}
이제 그룹 단위로 몬스터를 소환해요
- 이번에 GPT의 도움을 많이 받음
- MonsterManager가 사각형의 두 꼭짓점을 입력받으면, 그 두 점으로 생성할 수 있는 큰 사각형 내에서 랜덤으로 위치를 뽑아서 스폰하도록 함
- 몬스터 그룹을 결정짓는 키는 Vector3 위치임
public class MonsterManager : MonoSingleton<MonsterManager>
{
[Header("Monster Group Prefabs")]
[SerializeField] private List<GameObject> monsterGroupPrefabs; // 여러 개의 프리팹 중 랜덤 선택
[Header("Spawn Settings")]
[SerializeField] private Vector3 cornerA; // 평지의 꼭짓점 A [SerializeField] private Vector3 cornerB; // 평지의 꼭짓점 B [SerializeField] private int groupCount = 5; // 몬스터 그룹 갯수
[SerializeField] private float minDistance = 5f; // 그룹 간 최소 간격
[SerializeField] private int maxRetryCount = 30; // NavMesh 유효 위치 찾기 재시도 횟수
[SerializeField] private float respawnDelay = 60f;
private Dictionary<Vector3, GameObject> activeMonsterGroups = new Dictionary<Vector3, GameObject>();
private Dictionary<Vector3, ObjectPool<GameObject>> groupPools = new Dictionary<Vector3, ObjectPool<GameObject>>();
private List<Vector3> spawnPositions = new List<Vector3>();
private void Awake()
{
spawnPositions = GenerateRandomPositions(cornerA, cornerB, groupCount, minDistance);
// 포지션 하나마다 몬스터 그룹 설정
foreach (var pos in spawnPositions)
{
GameObject prefab = monsterGroupPrefabs[Random.Range(0, monsterGroupPrefabs.Count)];
var pool = new ObjectPool<GameObject>(
createFunc: () =>
{
GameObject obj = Instantiate(prefab, pos, Quaternion.Euler(0f, Random.Range(0f, 360f), 0f));
obj.SetActive(false);
// 각 몬스터에 manager/그룹 키 알려주기
foreach (var m in obj.GetComponentsInChildren<Monster>())
{
m.Init(this, pos);
m.onDeath += OnMonsterDeath;
}
return obj;
},
actionOnRelease: (obj) => { obj.SetActive(false); },
actionOnDestroy: (obj) => { Destroy(obj); },
collectionCheck: true,
defaultCapacity: 1, // 어차피 풀 관리 할 게 하나밖에 없음. 그룹단위라서
maxSize: 5
);
groupPools[pos] = pool;
}
// 초기 스폰
foreach (var pos in spawnPositions)
{
Debug.Log($"{pos}에서 생성됨");
SpawnGroupAt(pos);
}
}
/// <summary>
/// 사각형의 네 꼭짓점 중 두 꼭짓점(a, b)를 받아서 count 만큼의 Vector3을 생성함. 이 때 각 Vector3은 minDist 이상의 거리 차이를 가지게 함.
/// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <param name="count"></param> /// <param name="minDist"></param> /// <returns></returns> private List<Vector3> GenerateRandomPositions(Vector3 a, Vector3 b, int count, float minDist)
{
List<Vector3> positions = new List<Vector3>();
float minX = Mathf.Min(a.x, b.x);
float maxX = Mathf.Max(a.x, b.x);
float minZ = Mathf.Min(a.z, b.z);
float maxZ = Mathf.Max(a.z, b.z);
int safetyLimit = 500;
int placed = 0;
while (placed < count && safetyLimit > 0)
{
safetyLimit--;
Vector3 randomPos = new Vector3(
Random.Range(minX, maxX),
a.y,
Random.Range(minZ, maxZ)
);
if (NavMesh.SamplePosition(randomPos, out NavMeshHit hit, 2f, NavMesh.AllAreas))
{
bool valid = true;
foreach (var p in positions)
{
if (Vector3.Distance(p, hit.position) < minDist)
{
valid = false;
break;
}
}
if (valid)
{
positions.Add(hit.position);
placed++;
}
}
}
return positions;
}
public void SpawnGroupAt(Vector3 pos)
{
if (!groupPools.ContainsKey(pos)) return;
// Get 할때 GameObject SetActive True 해놔서 Get만 해도 됨
var group = groupPools[pos].Get();
group.SetActive(true);
if(!activeMonsterGroups.ContainsKey(pos))
activeMonsterGroups.Add(pos, group);
// group이 active 되어도 안에 있는 애들이 inactive라 다시 세팅 해줘야함
foreach (var m in group.GetComponentsInChildren<Monster>(true))
{
m.gameObject.SetActive(true);
}
}
public void OnMonsterDeath(Vector3 groupKey)
{
if (!groupPools.ContainsKey(groupKey)) return;
GameObject group = activeMonsterGroups[groupKey];
bool allDead = false;
// 활성화 된 애들이 한 명도 없으면 다 죽은거
int length = group.GetComponentsInChildren<Monster>(false).Length;
if (length == 0)
allDead = true;
if (allDead)
{
// 풀에 돌려주기
groupPools[groupKey].Release(group);
StartCoroutine(RespawnAfterDelay(groupKey));
}
}
private IEnumerator RespawnAfterDelay(Vector3 groupKey)
{
yield return new WaitForSeconds(respawnDelay);
Debug.Log("몬스터 재생성");
SpawnGroupAt(groupKey);
}
}스탠다드 반 강의 (주제 : 비동기, 코루틴, Task)
프로세스와 스레드
- 프로세스 : 현재 실행되어 작업중인 컴퓨터 프로그램
- 스레드 : 프로세스 내에서 실행되는 단위.
- 하나의 프로세스는 여러 스레드를 가질 수 있음
동기와 비동기
- 동기 : 코드를 순차적으로 실행하는 방식
- 한 작업이 시작되면 그 작업이 끝날 때까지 다음 작업을 시작할 수 없음
- 비동기 : 작업이 완료될 때까지 기다리지 않고 다음 작업을 시작할 수 있음
코루틴
- 작업을 다수 프레임에 분산할 수 있음.
- 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 이어서 계속할 수 있는 메소드
- 장점
- 비동기 작업 처리
- 간편한 시간 지연 구현
- 코드 가독성 향상
- 단점
- GameObject에 의존적임. 오브젝트가 비활성화 되거나 삭제되면 코루틴도 종료됨
- 여러 개 사용 시 관리가 어려움. 중첩 시 메모리 사용량 크게 증가.
Yield Return의 종류
- null : update 끝날 때 까지
- new WaitForEndOfFrame() : 렌더링 끝난 프레임이 종료될 때 까지
- new WaitForSeconds() : 시간이 지날 때까지
- new WaitForSecondsRealtime() : 시간이 지날 때까지 (Time.timeScale 영향 안 받음)
- new WaitUntil() : 괄호 안의 조건이 True일 때까지
- new WaitWhile() : 괄호 안의 조건이 False일 때까지
- break : 코루틴 종료 (yield break로 씀)
코루틴은 멀티스레드가 아니다!!!!!!!!!!
- 유니티 API는 여러 이슈를 고려해 단일 스레드를 사용함
- 메인 스레드 내에서 비동기처럼 작동하는 것.
Task
-
C#에서 지원하는 기능
-
멀티스레드 가능 (유니티에서 쓰면 단일스레드 방식으로 씀)
-
async와 await를 통해 반환값을 받을 수 있음
-
주로 외부 플러그인이나 서버와 통신할 때 Task 형식의 비동기로 처리함
-
단점
- Unity 생명주기와 다르게 작동하기 때문에 효율이 안좋음.
- C#을 포함한 외부 플러그인들은 멀티스레드를 활용하는 메소드가 많음.
- 그래서 메인스레드로 유도하는 기능(MainThreadDispather)이 필요함.
-
예시 코드
void Start()
{
TaskTest();
}
public async void TaskTest()
{
Debug.Log("실행");
await Task.Yield(); // yield return null과 동일
Debug.Log("종료");
}await의 종류
- Task.Yield() : yield return null과 동일
- Task.Delay() : 시간 기다리기. 밀리세컨드 기준임
반환값을 받으려면?
void Start()
{
var operation = TaskTest();
// operation.Result 쓰면 됨.
// 근데 바로 Debug.Log로 찍는다고 해도 안나옴.
// 왜냐? 3초 기다리게 해놨으니까.
}
public async Task<string> TaskTest()
{
await Task.Delay(3000);
return "결과";
}- 값이 나올 때까지 기다려봅시다 ⇒ 기다리는 애도 비동기로 만들어 주어야 함
public async void TaskTest()
{
var operation = Plus(3, 5);
// 또는, var operation = await Plus(3, 5);
while(!operation.IsCompleted)
{
Debug.Log("기다리는 중");
await Task.Yield();
}
}
public async Task<int> Plus(int a, int b)
{
Debug.Log("3초 이후에 실행");
await Task.Delay(3000);
return a+b;
}- 스레드를 확인할 수 있음
Debug.Log(Thread.currentThread.ManagedThreadId)- 아래의 방법대로 하면 멀티스레드로 가능함. 다만 이러면 Task 안에서 유니티 API 사용 불가
Task.Run(() => {});
Task task = new Task(함수이름)대체 기술
- UniTask : Task와 사용 방식은 거의 동일함. 최적화에 유리. 다만 외부 플러그인과 호환성이 좋지 않고 멀티스레드 사용으로는 부적합함
- Awaitable : Unity 2023 이상부터 사용 가능. Unity 6 에서는 코루틴과 Task를 대체할 수 있을 정도로 기능이 크게 개선되었다고 함.
실사용 예시
적이 플레이어를 쫓아오도록 만들기
- 이렇게 update문에 안넣고 따로 관리하면 최적화 굿
public GameObject target;
IEnumerator Update()
{
while(true)
{
transform.LookAt(target.transform.position);
Vector3 direction = (target.transform.position - transform.position).normalized;
tranform.position += direction;
yield return new WaitForSeconds(0.1f);
}
}코루틴을 멈추기
// 참고 : 함수 명으로 멈추는게 가능은 하지만, 당연히 권장되는 사항은 아님.
IEnumerator co;
co = StartCoroutine(co);
StopCoroutine(co);
// 이렇게 관리하면 나중에 다시 시작시키기에도 좋음StopAllCoroutine() 쓰지마세요!!!!!
- 모든 코루틴이 다 종료되는거기 때문에 위험함!!!!
씬 이동도 비동기로 가능
var operation = SceneManager.LoadSceneAsync("다른씬");
// 다만, 여기서 SceneManager.LoadSceneAsync의 반환은 Task는 아니고 AsyncOperation임.
// 그래서 await은 안됨.
while(!operation.isDone)
{
Debug.Log("로딩 창 띄우기");
await Task.Yield();
}
Debug.Log("완료")데이터를 비동기로 불러옵시다.
var operation = Resources.LoadAsync<GameObject>("object");
while(!operation.isDone)
{
Debug.Log("로딩중");
Debug.Log(operation.progress); // 로드 진행상황 받아올 수 있으니 좋음.
await Task.Yield();
}질문
- OnSceneLoaded vs SceneManager.LoadSceneAsync
- 퍼포먼스 상의 차이는 없음
- 다만 LoadSceneAsync는 코드의 흐름이 한 눈에 보인다는 점이 좋고
- 그리고 OnSceneLoaded는 델리게이트라서 코드 추적이 어려움.
- 튜터님은 후자를 선호하심