RPG 장르를 만들게 된 이유
간단한 아케이드 게임과 기능 구현을 해보는 연습만 했기 때문에, 나도 하나의 게임을 만들어 보고 싶었다.
개발 기간
2019.10.10. ~ 2019.11.21. (약 6주)
영상
소스코드
https://github.com/mingyu0403/BecomeAnAdventurer
코드 구현 특징
- GameManager, GameUIManager 등 싱글톤 기법을 이용.
- 변수의 Get Set 프로퍼티를 이용한 UI 데이터 바인딩.
- 코루틴을 이용하여 최적화.
- 자주 쓰이는 오브젝트는 Object Pooling을 이용.
◎ 설정 값만큼 생성해놓고, 부족하면 더 생성함.
- 클래스들은 상속을 하여 유지보수하기 쉽게 구현.
◎ 모든 퀘스트는 Quest 추상 클래스를 상속.
◎ 모든 몬스터는 Monster 추상 클래스를 상속.
- 선형 보간 Lerp를 통한 UI Slider 값, 카메라 값 변경의 부드러움 추가.
- 몬스터
◎ Enum 타입 정의.
◎ AI 유한상태머신(FSM) 이용.
◎ 몬스터 크기에 따른 Hit Effect 크기 구현.
- 플레이어
◎ 행동을 취할 때마다 Delegate Event 발생. (함수 체인을 걸어서 확장 가능.)
◎ 퀘스트 수락 시, Delegate Event에 함수 추가.
◎ 퀘스트 완료 시, Delegate Event에 함수 제거.
◎ Player가 Camera Forward 를 기준으로 이동하기 때문에, 회전벡터 공식 사용.
- NPC
◎ Enum 타입 정의
◎ ( ${PlayerName}, 오랜만이야!! ) 처럼 string Replace를 통해 구현.
- 랜덤 버프
◎ 플레이어 스탯을 담고 있는 PlayerInfo 클래스를 기초 형태로 가지고 있음.
◎ 버프 발동 시, (Buff 클래스의 PlayerInfo)와 (Player 클래스의 PlayerInfo)의 능력치를 계산.
- UI
◎ 코루틴을 이용한 쿨타임 계산.
주요 코드
플레이어, NPC, 몬스터 의 상태 변화 Enum
// 플레이어 애니메이션 타입
public enum AnimationType
{
IDLE,
RUN,
ATTACK1,
DAMAGE,
JUMP,
DEATH,
VICTORY
}
플레이어의 행동 이벤트 감지
[HideInInspector] public List<Quest> questList = new List<Quest>();
public Action<Quest> AddQuestEvent;
public Action<Quest> RemoveQuestEvent;
public Action<Quest> CompleteQuestEvent;
public Action<Monster> MonsterKillEvent;
public Action<PlayerInfo> LevelUpEvent;
플레이어 스탯의 레벨 당 추가 값
(공격력, 공격속도, 이동속도, 크리티컬 확률, 체력, 마나, 다음 레벨에 필요한 경험치량)
// 공격 파워
private int _attackPowerStatic;
public int attackPowerStatic
{
set
{
_attackPowerStatic = value;
}
get
{
int result = _attackPowerStatic + ((level-1) * 5); // 기본 값 + 레벨당 추가 값
return result;
}
}
씬 전환 시, 씬 로드 이벤트 발생
현재 씬을 관리하는 오브젝트를 찾아서 포탈 위치를 검색한 뒤, 플레이어가 포탈 위치로 이동.
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
currentSceneName = scene.name;
// Debug.Log("씬 교체됨, 현재 씬: " + scene.name);
CurrentSceneManager currentSceneManager = GameObject.FindWithTag("SCENE_MANAGER").GetComponent<CurrentSceneManager>();
// 플레이어 위치를 포탈 위치로 옮김
GameManager.instance.playerManager.transform.position = currentSceneManager.PortalList[portalNumber].position;
// NPC들에게 퀘스트 진행 세팅
GameManager.instance.playerManager.SetQuestToAllNPC();
// 씬 전환 효과
GameUiManager.instance.ShowFadeIn();
}
플레이어의 버프 제작
(객체를 통해 원하는대로 제작 가능)
private Buff AttackPowerBuff()
{
Buff buff = new Buff();
buff.duration = UnityEngine.Random.Range(8, 16); // 8 ~ 15
int RandomUpgradeValue = UnityEngine.Random.Range(3, 11); // 3 ~ 10
buff.buffName = ChanageColorMsg(buff.duration) + "초 동안 " + ChanageColorMsg_Green("공격력") + " " + (RandomUpgradeValue * 10) + "% 증가";
buff.buffExplain = ChanageColorMsg(buff.duration) + "초 동안 " + ChanageColorMsg_Green("공격력") + "이 " + (RandomUpgradeValue * 10) + "% 만큼 증가합니다.";
buff.buffImage = GameUiManager.instance.ICON_AttackPower;
buff.changeAttackPower = true;
buff.bonusStatus.attackPowerMul = 1 + RandomUpgradeValue / 10.0f;
return buff;
}
플레이어의 퀘스트
Quest_LevelUp 클래스
- 추상클래스 Quest를 상속함.
- Override를 통한 공통 메소드 구현, 퀘스트 진행 판정 함수를 LevelUpEvent에 달아 줌.
// 퀘스트 세팅
public override void SetQuestEvent()
{
GameManager.instance.playerManager.LevelUpEvent += QuestProgress;
GameManager.instance.playerManager.AddQuest(this);
// 레벨을 넘었을 수도 있으니까 한번 판정함.
Set_StrQuestProgress();
GameUiManager.instance.questListUiController.UpdateQuestUI(this);
if (GameManager.instance.playerManager.playerInfo.level >= targetLevel)
{
base.questProgressType = Quest.QuestProgressType.SUCCESS;
GameUiManager.instance.Notice("퀘스트 \'" + base.shortDescription + "\' 달성", false);
}
}
// 퀘스트 제거
public override void RemoveQuestEvent()
{
GameManager.instance.playerManager.LevelUpEvent -= QuestProgress;
GameManager.instance.playerManager.RemoveQuest(this);
}
// 퀘스트 진행
public void QuestProgress(PlayerInfo playerInfo)
{
// 퀘스트 진행 중일 때만
if (base.questProgressType != Quest.QuestProgressType.PROGRESS)
{
return;
}
Set_StrQuestProgress();
int curr_Level = playerInfo.level;
if (curr_Level >= targetLevel)
{
base.questProgressType = Quest.QuestProgressType.SUCCESS;
GameUiManager.instance.Notice("퀘스트 \'" + base.shortDescription + "\' 달성", false);
}
GameUiManager.instance.questListUiController.UpdateQuestUI(this);
}
Quest_MonsterKill 클래스
- 추상클래스 Quest를 상속함.
- Override를 통한 공통 메소드 구현, 퀘스트 진행 판정 함수를 MonsterKillEvent에 달아 줌.
// 퀘스트 세팅
public override void SetQuestEvent()
{
GameManager.instance.playerManager.MonsterKillEvent += QuestProgress;
GameManager.instance.playerManager.AddQuest(this);
}
// 퀘스트 제거
public override void RemoveQuestEvent()
{
GameManager.instance.playerManager.MonsterKillEvent -= QuestProgress;
GameManager.instance.playerManager.RemoveQuest(this);
}
// 퀘스트 진행
public void QuestProgress(Monster monsterCtrl)
{
// 퀘스트 진행 중일 때만
if (base.questProgressType != Quest.QuestProgressType.PROGRESS)
{
return;
}
// 퀘스트 진행 판정
if (monsterCtrl.nickname == monsterName)
{
curr_Count++;
Set_StrQuestProgress();
if (curr_Count >= count)
{
base.questProgressType = Quest.QuestProgressType.SUCCESS;
GameUiManager.instance.Notice("퀘스트 \'" + base.shortDescription + "\' 달성" , false);
}
GameUiManager.instance.questListUiController.UpdateQuestUI(this);
}
}
스킬 범위 미리보기 구현
void LaunchProjectile()
{
Ray camRay = cam.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
// layer에 닿을 때만 적용
if (Physics.Raycast(camRay, out hit, 100f, layer))
{
cursor.SetActive(true);
cursor.transform.position = hit.point;
// 포물선 계산
Vector3 vo = CalculateVelocity(hit.point, shootPoint.position, 1f);
// ProjectTile을 이용한 시각화
Visualize(vo);
transform.rotation = Quaternion.LookRotation(vo);
if (Input.GetMouseButtonDown(0))
{
Rigidbody obj = Instantiate(Skill1_ShootedSphere, shootPoint.position, Quaternion.identity);
obj.velocity = vo;
SkillRealActive();
Hide();
}
}
}
// 포물선 계산
Vector3 CalculateVelocity(Vector3 target, Vector3 origin, float time)
{
Vector3 distance = target - origin;
Vector3 distanceXZ = distance;
distanceXZ.y = 0f;
float Sy = distance.y;
float Sxz = distanceXZ.magnitude;
float Vxz = Sxz / time;
float Vy = Sy / time + 0.5f * Mathf.Abs(Physics.gravity.y) * time;
Vector3 result = distanceXZ.normalized;
result *= Vxz;
result.y = Vy;
return result;
}
// ProjectTile을 이용한 시각화
void Visualize(Vector3 vo)
{
for (int i = 0; i < lineSegment; i++)
{
Vector3 pos = CalculatePosInTime(vo, i / (float)lineSegment);
lineVisual.SetPosition(i, pos);
}
}
// 포물선 중에서 초에 따른 Vector3 값 구함.
Vector3 CalculatePosInTime(Vector3 vo, float time)
{
Vector3 Vxz = vo;
Vxz.y = 0f;
Vector3 result = shootPoint.position + vo * time;
float sY = (-0.5f * Mathf.Abs(Physics.gravity.y) * (time * time)) + (vo.y * time) + shootPoint.position.y;
result.y = sY;
return result;
}
몬스터 FSM
StartCoroutine(this.CheckMonsterState()); // 0.2초마다 상태 변화해주기
StartCoroutine(this.MonsterAction()); // 상태에 따른 행동 변화
IEnumerator CheckMonsterState()
{
float GoHome = 0;
AngryPower = 0;
while (!isDie)
{
yield return new WaitForSeconds(0.2f);
float dist = (playerTr.position - monsterTr.position).sqrMagnitude;
if(dist <= attackDist * attackDist)
{
monsterState = MonsterState.ATTACK;
}
else if (dist <= traceDist * traceDist || isAngry)
{
monsterState = MonsterState.TRACE;
GoHome = 0; // 참을성 초기화
if (AngryPower > 3) // 3초동안 아무 일 없으면, 플레이어를 쫒아오지 않음.
{
isAngry = false;
}
else
{
AngryPower += 0.2f;
}
}
else
{
float d = (OringinPos - monsterTr.position).sqrMagnitude;
if (d <= 9f) // 원래 위치랑 가까우면 그냥 있음.
{
monsterState = MonsterState.IDLE;
GoHome = 0;
}
else // 멀면 카운트 세다가 3초가 되면, 홈으로 돌아감
{
if (GoHome > 3) // 3초동안 아무 일 없으면, 원래 위치로 돌아감.
{
monsterState = MonsterState.HOME;
}
else
{
monsterState = MonsterState.IDLE;
GoHome += 0.2f;
}
}
}
}
Die();
}
IEnumerator MonsterAction()
{
while (!isDie)
{
switch (monsterState)
{
case MonsterState.IDLE:
nvAgent.isStopped = true;
anim.SetBool("WALK", false);
break;
case MonsterState.HOME:
nvAgent.isStopped = false;
nvAgent.destination = OringinPos;
anim.SetBool("WALK", true);
RotateTowards(OringinPos);
break;
case MonsterState.TRACE:
if (isAttack)
{
nvAgent.isStopped = true;
} else
{
nvAgent.isStopped = false;
}
nvAgent.destination = playerTr.position;
anim.SetBool("WALK", true);
RotateTowards(playerTr.position);
break;
case MonsterState.ATTACK:
if (isAttack)
{
break;
}
anim.SetTrigger("ATTACK");
break;
}
yield return null;
}
}
느낀 점
JAVA를 배우면서 객체를 처음 배웠고, C# Winform을 배우면서 객체를 활용해 본 적이 있다.
이번에 Unity C#을 이용해 게임을 개발하면서
퀘스트, 몬스터, 버프 등 각각의 기능을 구현하면서 객체를 활용하는 것이 재밌었다.
플레이어가 가지고 있는 데이터와 UI를 바인딩하는 것도 손이 많이 갔다.
'PORTFOLIO > 개인' 카테고리의 다른 글
[개인 프로젝트] the unP (0) | 2019.06.20 |
---|---|
[개인 프로젝트] Mshooting (0) | 2019.06.20 |