🗂 프로젝트 소개
- 개발 기간 : 2022.11.28 ~ 2022.12.10
- 클라이언트 개발 환경 : Unity3D
- 이외 툴 : Git / Notion / Figma / Slack / Trello
- 백엔드 : Firebase , Photon PUN
✔ 핵심 시스템 설명
- 인게임 구조
- 전략패턴을 이용한 로그라이크식 스킬 획득 시스템 구조
- 다양한 게임 데이터 관리 방법
- 로그인 구현
씬 구조
씬의 구조는 다음과 같습니다.
Unit Spawner
유닛의 소환을 처리할 UnitSpawner는 씬에서 보시다 싶이 흰색 큐브의 좌표값이 각각의 UnitSpawner이자 생성할 좌표가 됩니다. 따라서 2 * 6 의 배열안에서 랜덤하게 몬스터의 생성이 가능합니다 unitSpawner는 어디까지나 일부분일뿐 Spawner들을 전체로 관리하는 클래스는 UnitBoard입니다.
ArrowSpawner
ArrowSpawner의 역할은 의미 그대로 화살의 생성을 서비스하는 클래스 입니다. 기획의도는 다음과 같습니다
획득한 스킬의 정보를 받아들이거나 ArrowSpawner를 상속받는 FireArrowSpawer등의 다양한 스킬의 확장성 고려하여 만들었습니다.
✔ In Game Class Diagram
아래 자료 사진은 InGame에 구현된 클래스간의 커플링된 관계를 간단하게 표현해봤습니다.
여기서 중요한 구조는 InGameStepObject와 iGB를 상속받는 Player의 구조입니다.
인게임 구조에서 제가 신경쓰고 제작한 시스템은 프레임 워크를 분리하는것에 초점을 두었습니다.
추후 다른 타입의 게임을 확장성을 고려하고 기존 코드와의 사이드 이펙트를 최소화 하기 위해서 입니다.
InGameStepObject의 역할 및 장점
InGameStepObject의 장점은 다음과 같습니다
- 불필요한 서로 다른 클래스간의 함수 Call 수 감소
- 현제 Game 상태에 따른 모든 객체의 통합관리
- 반복코딩 감소 및 디버깅 시 불필요한 분석시간 감소
이렇게 IGO를 상속받은 객체는 게임 스택의 변화와 ActionCantainer의 Invoke를 통해 제어됩니다.
마치 스튜디오의 모든 배우가 스탭에 맞춰서 칼군무를 하는것을 연상됩니다.
아래코드를 InGameStepObject가 없는 코드로 리팩토링하여 비교해 본 결과
서로 다른 클래스간의 콜수가 30에서 46으로 증가하는것을 확인했습니다.
지금의 패턴의 이름이 존재하는지는 찾아봤지만 알수없었습니다.
지금의 구조도 전략패턴으로 분리해야할지 어중간한 부분이 있는거같습니다.
public abstract class InGameStepObject : MonoBehaviour
{
protected virtual void Awake()
{
Global.inGameStepAction[ePlayStep.step_GameStart] += OnGameStart;
Global.inGameStepAction[ePlayStep.step_LevelUp] += OnLevelUp;
Global.inGameStepAction[ePlayStep.step_GameOver] += OnGameOver;
Global.inGameStepAction[ePlayStep.step_SKILLSelect] += OnSkillSelecte;
}
protected virtual void OnEnable()
{
}
protected virtual void OnDestroy()
{
Global.inGameStepAction[ePlayStep.step_GameStart] -= OnGameStart;
Global.inGameStepAction[ePlayStep.step_LevelUp] -= OnLevelUp;
Global.inGameStepAction[ePlayStep.step_GameOver] -= OnGameOver;
Global.inGameStepAction[ePlayStep.step_SKILLSelect] -= OnSkillSelecte;
}
}
InGameStepObject의 단점
- 유니티 생명주기에 대한 이해가 없으면 사용에 문제가 발생할수 있다.
- IGO과 다르게 행동해야하는 Entity는 구조에서 예외가 생길수 있다.
- 처음 구조를 봤을 시 바로 이해하기 어렵다.
BaseSkill (전략패턴)
스킬 시스템은 순수 전략패턴을 이용하여 구현했습니다.
전략패턴을 사용한 이유는 각 스킬은 자신만이 가지고 있는 고유성과, 스킬이라는 범주안에서는 서로 가지고있는
공통점을 가지고 있습니다. 그리고 이를 획득하는 행위는 레벨업 시 선택한 스킬이라는 공통점을 포함하고 있어
동적으로 행위를 자유롭게 바꿀 수 있게 해주는 전략패턴이 가장 어울린다 판단하여 이같은 방법으로 구조를 만들었습니다.
public class Projectiles2X : BaseSkill
{
public override void Get()
{
base.Get();
Debug.Log("Projectiles2X");
}
public override List<GameObject> SettingArrow(List<GameObject> arrowList)
{
foreach (var arrow in arrowList)
{
SetSizeUp(arrow);
}
return arrowList;
}
public void SetSizeUp(GameObject arrow)
{
arrow.transform.localScale = new Vector3(2, 2, 2);
}
}
스킬은 오브젝트일 필요가 없습니다.
스킬은 단순히 행위를 뜻하는 클래스 이기에 이를 굳이 객체로 표현할 필요는 없지만 그 행위를 캡슐화 하고 시각화
할 필요성은 있다느껴 스크립터블 오브젝트로 스킬들을 구현하게되었습니다. 굳이 인스펙터를 사용하지 않았던 이유는 컨테이닝을 더욱더 편리하게 사용하고싶어서 였습니다.
How to Get a skill to player?
아래는 Panel_Skill 스크립트 의 주요 함수를 가져와서 보여드립니다.
ResetSkillList는 레벨업 시 호출됩니다.
이는 기존 스킬 패널의 버튼들이 가지고있을 스킬정보들을 날리기 위함입니다. ResetSkillList함수의 주요 기능은
outputBuffer가 역할을 담당합니다.
획득할 수 있는 랜덤한 스킬 세가지를 받을수있는 변수이자, skillContainer에게 해당정보를 가져오는기 까지의 역할을 하기때문입니다.
여기서 가장 큰 핵심 시스템은
모든 스킬이 영향을 줄수 있는 객체는 이미 InGameStepObject를 상속받고있다는점입니다.
스킬을 활성화 하는 시점은 SkillSelected 스텝을 모든 오브젝트가 호출할 시점에 영향을 받게됩니다.
private void ResetSkillList()
{
numberBuffer.Clear();
outputBuffer.Clear();
for (int i = 0; i < manager.GetSkillListCount(); i++)
{
numberBuffer.Add(i);
}
while (outputBuffer.Count < nowButtonNumber)
{
int rnd = Random.Range(0, manager.GetSkillListCount());
// 값 존재여부
if (numberBuffer.Contains(rnd))
{
numberBuffer.Remove(rnd);
outputBuffer.Enqueue(rnd);
}
}
for (int i = 0; i < buttons.Length; i++)
{
BtnStatusUpdate(i);
};
}
private void BtnStatusUpdate(int indexNumber)
{
int data_int = outputBuffer.Dequeue();
var skill = manager.skillList[indexNumber];
System.Action clickEvent = () => { skill.Get(); };
buttons[indexNumber].onClick.RemoveAllListeners();
buttons[indexNumber].onClick.AddListener(() => { clickEvent?.Invoke(); });
buttons[indexNumber].onClick.AddListener(ResumeGame);
IconChange(skill, indexNumber);
}
풀링 구조
풀링의 구조는 단순합니다.
화살을 인스턴스화 하기 이전 사용이 완료된 화살을 가져오는것입니다.
만약, 재활용이 가능한 상태의 화살이 없다면
그 다음부터 새로운 화살을 인스턴스화 합니다.
/// <summary>
/// 사용할 화살 가져오기
/// </summary>
/// <returns></returns>
public GameObject getCurArrow()
{
//사용 가능한 화살 찾기
foreach (var nowArrow in curArrowList)
{
if (!nowArrow.GetComponent<Arrow>().isArrowUsingNow)
{
//위치 초기화
Create(nowArrow);
return nowArrow;
}
}
//못찾았으면 새로 제작하기.
GameObject newArrow = Instantiate(curArrowList[0]);
curArrowList.Add(newArrow);
Create(newArrow);
return newArrow;
}
로그인 기능
gpgs를 플러그인 하고싶었지만, 다양한 조건의 인해 email로그인으로 구현하게되었습니다.
해당 람다식안에서의 다른 함수의 호출이 안되는 문제가 있어
람다식에 불플러그 함수를 사용하여 업데이트문에서 원하는 함수를 호출하도록 하였습니다.
public void Create()
{
auth.CreateUserWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(t =>
{
if (t.IsCanceled)
{
Debug.Log("Cancle");
return;
}
if (t.IsFaulted)
{
}
Debug.Log("계정 생성");
this.gameObject.SetActive(false);
});
}
public void Login()
{
auth.SignInWithEmailAndPasswordAsync(email.text, password.text).ContinueWith(t =>
{
if (t.IsCanceled)
{
Debug.Log("Cancle");
return;
}
else if (t.IsFaulted)
{
Debug.Log("실패");
isFaultedAuth = true;
return;
}
isLogin = true;
PlayerPrefs.SetString("FirstLoginID", email.text);
PlayerPrefs.SetString("FirstLoginPWD", password.text);
FirebaseUser newUser = t.Result;
});
}
이외 로비 컨텐츠간 이동
Ui 스크롤뷰와 Vector.Lerp를 이용한 컨텐츠 별 이동간을 심심하지않게 연출을 구현해봤습니다.
버튼 이벤트를 누를 시 원하는 좌표를 선형보간을 이용하여 도달하게끔 제작되었습니다.
Photon Pun
아쉽게도 포톤을 이용한 pvp는 완성되지않았습니다.
추후 제작이 완료되면 반드시 올리겠습니다 .지금까지 글을 읽어주셔서 감사합니다