Chapter 13-11. 미니 RPG : 몬스터 자동 리스폰
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 13. 미니 RPG 만들기
🚀 몬스터 자동 리스폰
서버 측에서 한다. 몬스터가 죽으면 몬스터의 전체적인 숫자를 맞춰주기 위해 랜덤한 위치에서 스폰을 한다. 이렇게 서버측에서 스폰을 한 후 클라이언트에게 알려준다.
📜 GameManager
public Action<int> OnSpawnEvent;
public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
{
GameObject go = Managers.Resource.Instantiate(path, parent);
switch(type)
{
case Define.WorldObject.Monster:
_monsters.Add(go);
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(1);
break;
case Define.WorldObject.Player:
_player = go;
break;
}
return go;
}
public void Despawn(GameObject go)
{
Define.WorldObject type = GetWorldObjectType(go);
switch(type)
{
case Define.WorldObject.Monster:
{
if (_monsters.Contains(go))
{
_monsters.Remove(go);
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(-1);
}
}
break;
case Define.WorldObject.Player:
{
if (_player == go)
_player = null;
}
break;
}
Managers.Resource.Destroy(go);
}
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(1);
//...
if (OnSpawnEvent != null)
OnSpawnEvent.Invoke(-1);
OnSpawnEvent
- 이 액션에 어떤 함수들이 등록되어 있을진 모르겟지만! 몬스터를 스폰할 때, 디스폰할 때 이 액션을 실행한다.
- 이 액션에는 파라미터만큼 몬스터를 증가시키는 함수가 등록될 것이다.
- 스폰시 👉 몬스터 1마리 증가 OnSpawnEvent.Invoke(1);
- 디스폰시 👉 몬스터 1마리 감소 OnSpawnEvent.Invoke(-1);
📜 SpawningPool
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class SpawningPool : MonoBehaviour
{
[SerializeField] int _monsterCount = 0; // 현재 몬스터 수
[SerializeField] int _keepMonsterCount = 0; // 월드 상에 유지되야 하는 몬스터 수
int _reserveCount = 0; // ReserveSpawn 코루틴 함수 당 곧 생성할 몬스터 수
[SerializeField] Vector3 _spawnPos;
[SerializeField] float _spawnRadius = 15.0f;
[SerializeField] float _spawnTime = 5.0f;
public void AddMonsterCount(int value) { _monsterCount += value; }
public void SetKeepMonsterCount(int count) { _keepMonsterCount += count; }
void Start()
{
Managers.Game.OnSpawnEvent -= AddMonsterCount;
Managers.Game.OnSpawnEvent += AddMonsterCount;
}
void Update()
{
while (_reserveCount + _monsterCount < _keepMonsterCount)
{
StartCoroutine(ReserveSpawn());
}
}
IEnumerator ReserveSpawn()
{
_reserveCount++;
yield return new WaitForSeconds(Random.Range(0, _spawnTime));
GameObject obj = Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
Vector3 randPos;
NavMeshAgent nma = obj.GetOrAddComponent<NavMeshAgent>();
while(true)
{
Vector3 randDir = Random.insideUnitSphere * Random.Range(0, _spawnRadius);
randDir.y = 0;
randPos = _spawnPos + randDir;
NavMeshPath path = new NavMeshPath();
if (nma.CalculatePath(randPos, path))
break;
}
obj.transform.position = randPos;
_reserveCount--;
}
}
- AddMonsterCount : 몬스터를 파라미터만큼 누적 증가시키기 위해
_monsterCount
업뎃- 게임 시작시 📜GameManager의
OnSpawnEvent
에 등록
- 게임 시작시 📜GameManager의
- SetKeepMonsterCount : 몬스터를 파라미터만큼 증가시키기 위해
_keepMonsterCount
업뎃 - Update
monsterCount
가keepMonsterCount
에 다다를 떄까지 몬스터 생성reserveCount
가 쓰이는 이유는 아래에.
- ReserveSpawn
- 이 코루틴 함수는 한 마리의 몬스터 스폰을 위한 함수다. 랜덤으로 0 ~
_spawnTime
시간 내의 1️⃣ 랜덤한 시간을 “대기”한 후에 2️⃣ 몬스터를 생성하고 3️⃣ 스폰된 몬스터의 위치를 랜덤하게 세팅하는 함수다._reserveCount
1 증가_reserveCount
을 증가시키는 이유- 이 코루틴이
yield return
을 한 후 WaitForSeconds로 인하여 대기하는 시간 동안 코루틴의 호출점이였던 Update의 while문이 마저 실행된다. (코루틴은 대기 시간동안 코루틴 호출점을 마저 실행한다. 그리고 대기 시간이 다 끝나면 다시 코루틴 함수 내부로 돌아온다.) _monsterCount
는 대기시간을 가진 이후에 📜GameManager의 Spawn 함수의 OnSpawnEvent.Invoke을 통해 세팅된다. 따라서 📜SpawningPool의 Update의 while 문에서는 아직 이- 따라서 대기시간을 가지는 동안 코루틴을 나와 while문을 다시 실행할 때는
_monsterCount
가 아직 1 증가가 되지 않은 상태이기 때문에 while문이 무한 루프로 돌게 된다. _monsterCount < _keepMonsterCount 이 조건이 만족하니까 ReserveSpawn 을 또 호출 하고 또 호출하고.. 대기 시간동안 ReserveSpawn 을 엄청 많이 호출하게 될 것이다. - 따라서 아직 증가되기 전인
_monsterCount
을 1 증가 후로 반영하여 while문 조건에 넣기 위해_reserveCount
를 1로 세팅하고 while문 조건을 while (_reserveCount + _monsterCount < _keepMonsterCount)로 해주는 것이다! - 이러면 증가되야 하는 수만큼만 ReserveSpawn 코루틴 함수가 호출이 될 수 있다.
- 이 코루틴이
- 랜덤한 대기 시간 가지기
- 몬스터 스폰 📜GameManager의 Spawn 호출
- 스폰한 몬스터를 랜덤한 위치로 설정 (= 랜덤한 위치에 스폰)
- 랜덤으로 뽑은 위치가 갈 수 있는 곳으로 나올 때까지 랜덤 뽑기
- 랜덤 위치 뽑기
- Random.insideUnitSphere
- (0, 0, 0) 위치를 기준으로 반지름 1 의 반경 원에서 랜덤한 Vector3 값을 리턴함
- 2D 게임이므로 랜덤
y
값은 반영 X (이렇게 안해주면 하늘이나 땅에서 스폰될 수도 있는 것이다. ㅠ)
- Random.insideUnitSphere
- 뽑은 위치가 갈 수 있는 곳인지
- nma.CalculatePath(randPos, path) 가 True 이면 랜덤뽑기 그만 하고 break.
- 랜덤 위치 뽑기
- 랜덤으로 뽑은 위치가 갈 수 있는 곳으로 나올 때까지 랜덤 뽑기
- 다시
_reserveCount
1 감소
- 이 코루틴 함수는 한 마리의 몬스터 스폰을 위한 함수다. 랜덤으로 0 ~
📜 GameScene
public class GameScene : BaseScene
{
protected override void Init()
{
//...
//Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
GameObject go = new GameObject { name = "SpawningPool" };
SpawningPool pool = go.GetOrAddComponent<SpawningPool>();
pool.SetKeepMonsterCount(5);
}
“GameScene”씬이 시작되면 “SpawningPool” 객체를 생성하고 이에 📜SpawningPool 를 바로 붙인다. 그리고 몬스터는 5 마리를 유지하도록 SetKeepMonsterCount(5) 호출.
🚀 수직으로 이동하려는 버그 고침
📜PlayerController
protected override void UpdateMoving()
{
// 몬스터가 내 사정거리보다 가까우면 공격
//...
// 이동
Vector3 dir = _destPos - transform.position;
dir.y = 0;
클릭 위치에 따라 y
좌표도 반영될 수 있다. 목표 방향인 dir
에 y
까지 포함되면 플레이어가 조금 붕 뜰 수도 있기 때문에 dir.y
은 반영하지 않도록 0 으로 세팅한다.
❤ 완강
이 글을 보시는 분이 계시다면.. 인프런에서 이 강의를 꼭 들어보시기를 추천드리고 싶습니다. 단순히 유니티 엔진 사용법만을 익히는 것에 그치지 않고 프레임워크를 구축하는 코드를 짜기 때문에 더욱 더 명강의라고 생각합니다. 😭 강추..
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기