종스크롤 2D 슈팅 게임 만들기(4)
이제 보스의 무기들을 구현하는 Boss_Weapon 스크립트를 만든다. IWeapon 인터페이스를 상속받고 3가지의 패턴이 있다.

그리고 EnemySpawnManager에서 10번 일반 몬스터를 소환했다면 보스 몬스터를 소환한다. 보스 몬스터를 생성할 때 보스의 weapon을 Boss_Weapon에서 구현한 것으로 지정하고 포스를 초기화한다. 그리고 보스가 죽었을 때 OnBossDied 이벤트를 이용해서 다음 레벨로 가는 NextLevel 함수를 호출한다. 그리고 이번에는 테이블을 이용한 몬스터의 데이터 세팅을 한다. 먼저 엑셀로 Data Table을 작성한다.

그리고 excel importer라는 에셋을 다운받아서 Asset 폴더에 넣는다. 그리고 해당 엑셀 파일을 Asset 폴더에 집어넣고 우클릭을 해서 Create->ExcelAssetScript를 눌러 스크립트 파일을 하나 생성한다. 그리고 ShootingGame_Entity라는 스크립트를 하나 생성해서 엑셀 파일에서 읽어올 Data의 이름과 형식을 정해준다.

그리고 아까 생성한 엑셀 파일과 같은 이름의 스크립트에서 List의 타입을 ShootingGame_Entity로 바꿔준다.
public List<ShootingGame_Entity> MonsterTable;
그리고 엑셀 파일을 Reimport하면 동일한 이름의 Scriptable Object가 생성된다. Scriptable Object는 대량의 데이터를 저장할 수 있는 유니티에서 제공하는 데이터 컨테이너이다.

그리고 이 몬스터 데이터는 EnemySpawnManager에서 가져온다. 위에서 보았듯이 Data는 List로 저장이 되는데 List는 데이터를 찾을 때 순차적으로 접근해서 데이터를 찾기 때문에 비효율적이다. 그래서 Map(유니티에서는 Dictionary)으로 매핑해서 key 값으로 접근해서 데이터를 찾아올 수 있도록 한다.
[SerializeField] private ShootingGame gameDatas;
private Dictionary<int, ShootingGame_Entity> monsterDatas =
new Dictionary<int, ShootingGame_Entity>();
private void Awake()
{
for (int i = 0; i < gameDatas.MonsterTable.Count; i++)
{
monsterDatas.Add(gameDatas.MonsterTable[i].MonsterID, gameDatas.MonsterTable[i]);
}
}
그래서 아까 만들어뒀던 Scriptable Object를 EnemySpawnManager에 집어넣을 수 있게 되었다. 그리고 Enemy 스크립트로 가서 Data로 몬스터의 HP를 세팅할 수 있다.
public void InitMonsterData(ShootingGame_Entity newData)
{
maxHP = newData.MonsterHP;
curHP = maxHP;
}
그리고 EnemySpawnManager에서 초기화할 수 있다.
if (obj.TryGetComponent<Enemy>(out Enemy enemy))
{
int tableID = 10000 + (10 * (spawnLevel / 3 + 1)) + (1 * (spawnLevel % 3 + 1));
if (monsterDatas.TryGetValue(tableID, out ShootingGame_Entity data))
{
enemy.InitMonsterData(data);
Debug.Log(data.MonsterHP);
}
enemy.SetEnable(true);
}
지금까지는 게임을 하는 BattleScene을 다루었고 게임 시작 화면, 로비 화면, 로딩 화면에 대해서 다뤄본다. 먼저 Scene을 만든다. IntroScene(게임 시작), LobbyScene(로비), LoadingScene(로딩)으로 씬을 만든다. 그리고 로딩에 대해서 잠깐 알아보자. 메모리에 프로그램에서 쓸 데이터들을 불러오게 되는데 메모리보다 큰 용량의 데이터들을 불러올 수 없다. 그래서 RAM을 비우고 다시 필요한 데이터를 가져오게 되는데 이것을 로딩이라고 한다.

위 사진은 IntroScene이다. 아이디를 입력하는 Input UI가 있고 게임 스타트 버튼과 유저 이름을 출력하는 Text UI가 있다. 그리고 IntroScene에 대해서 제어할 IntroSceneManager 스크립트를 생성한다. 여기서 유저의 정보를 저장한다. 그래서 enum으로 저장할 데이터를 정의한다.
public enum SAVE_Type
{
SAVE_ID,
SAVE_SceneName,
SAVE_SFX,
SAVE_BGM,
SAVE_Level,
SAVE_EXP,
SAVE_Gold,
}
그리고 Scene의 이름들도 저장한다. 이것들은 Scene이 넘어갈때 쓰인다.
public enum SCENE_Name
{
IntroScene,
LobbyScene,
BattleScene,
LoadingScene,
}
IntroScene에서는 초기화를 InitIntroScene에서 하는데 유저데이터가 있는지 없는지를 확인한다. 여기서 유저 Data는 PlayerPrefs으로 저장한다. PlayerPrefs은 유니티에서 제공하는 데이터 관리 클래스로 로컬에 데이터가 저장된다. 만약에 유저 데이터가 없다면 스타트 버튼을 눌렀을때 유저 데이터를 PlayerPrefs를 이용해 저장한다.
public void GameStartBtn()
{
if (!hasUserInfo)
{
if (inputField.text.Length >= 2)
{
Debug.Log("계정 생성 : " + inputField.text);
CreateUserData(inputField.text);
hasUserInfo = true;
}
}
if (hasUserInfo)
{
PlayerPrefs.SetString(SAVE_Type.SAVE_SceneName.ToString(), SCENE_Name.LobbyScene.ToString());
SceneManager.LoadScene(SCENE_Name.LoadingScene.ToString());
}
}
private void CreateUserData(string userID)
{
PlayerPrefs.SetString(SAVE_Type.SAVE_ID.ToString(), userID);
PlayerPrefs.SetInt(SAVE_Type.SAVE_Level.ToString(), 1);
PlayerPrefs.SetInt(SAVE_Type.SAVE_EXP.ToString(), 0);
PlayerPrefs.SetFloat(SAVE_Type.SAVE_SFX.ToString(), 1.0f);
PlayerPrefs.SetFloat(SAVE_Type.SAVE_BGM.ToString(), 1.0f);
PlayerPrefs.SetInt(SAVE_Type.SAVE_Gold.ToString(), 20000);
}
그리고 AddListener를 안쓰고 버튼의 이벤트를 다루려면 버튼의 Inspector 창의 OnClick에서 오브젝트를 지정하고 그 오브젝트 내에서 실행할 함수를 설정한다.

다음 씬으로 넘어갈때는 비동기 로딩(async loading)을 이용해서 다음 로비씬으로 넘어간다. 그래서 로딩을 진행할 때 다음 씬에 필요한 데이터들을 불러오게 했다.

위 사진은 로딩씬의 화면이다. 로딩이 진행되면 로딩 중이라는 글자 아래에 Image 바가 채워질 것이다. 로딩 씬을 제어하기 위해 LoadingSceneManager 스크립트를 작성한다.

Awake에서 로딩 바의 fillAmount를 0으로 초기화하고 StartCorountine으로 LoadAsyncScene 코루틴 함수를 실행한다. 위 함수는 AsyncOperation 클래스를 aysncScene으로 정의하고 SceneManager.LoadSceneAsync를 통해서 가고자하는 씬을 로드한다. 그전에 aysncScene의 allowSceneActivation을 false로 지정한다. allowSceneActivation은 로딩이 완료되면 바로 화면이 어떠한 간섭없이 자동으로 전환되도록 하는 것이다. allowSceneActivation이 false라면 클릭을 해서 넘어간다던지 어떠한 이벤트가 발생해야지 다음 씬으로 넘어간다. 유니티에서는 로딩이 90 퍼센트가 되면 다 된것으로 처리하고 다음화면으로 넘어가기 때문에 100 퍼센트가 되었을때 넘어가는 것 처럼 보이게 하기 위해서 일단 allowSceneActivation을 false로 하고 loadingBar가 0.99만큼 채워졌을 때 allowSceneActivation을 true로 바꿔서 다음 씬으로 넘어가게 한다. PlayerPrefs에 다음으로 넘어갈 씬을 저장하는데 그것을 이용해서 LoadScene을 한다. 그리고 timeC와 Lerp를 이용해서 loadingBar가 진행이 완료된 부분까지 가는데 자연스럽게 이동하도록 한다. 이것을 프레임마다 진행완료된 부분만큼 loadingBar를 채워나간다. 로딩을 통해서 씬을 바꾸면 가비지 컬렉터에서 가비지들이 어느정도 정리되는 것도 있다. 다음은 로비씬을 구현한다.

위 사진은 로비 씬의 화면이다. 아래 버튼들을 누르면 아래에서 팝업 창이 올라오도록 할 것이다.