Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository (github.com)

 

GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

Tales Saga Chronicle Blast의 Script만이 포함된 Repository - GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

github.com

 

이전 글에서 언급했던 수정사항들을 일부 적용했다. 내용을 요약하면 아래와 같다.

 

[AudioManager]
변수명, 접근자 수정
코루틴 사용을 좀 더 깔끔하게 수정
내부에서 불필요하게 Getter Setter 사용하고 있던 부분 수정
인스펙터에서 BGM FadeOut 시간 정할 수 있도록 수정

볼륨이 항상 1초에 걸쳐서 FadeOut 되도록 수정

[BulletController -> MidoriBullet] (이름 변경)
미도리 총알에 붙는 스크립트라 이름 변경

[ButtonManager] (삭제됨)
GameManager, StoryManager, OptionWindow로 기능 분리

[DialogManager]
\n -> \r\n 수정
+csv로 관리할 예정임

[EnemyController -> ShootingGameEnemy] (이름 변경)
좀 더 직관적인 이름으로 변경
일부 변수 이름 수정

[FontLocalizer] (신규)
TMP_Text와 함께 사용함으로써 텍스트의 로컬라이징을 담당하는 스크립트
언어가 바뀔 때 텍스트의 위치를 조정할 수 있음
추후 테이블로 뺄 예정이나 일단 임시로 한국어/일본어 텍스트를 저장&변경 하고 있음.

+RequireComponent 추가해야 할 듯

[GameManager]
변수명 수정
옵션에 해당하는 기능 분리(OptionWindow로 이동)
Find 사용하던 것들 일부 삭제, 인스펙터에서 추가하도록 수정
버튼 OnClick 스크립트로 추가하도록 수정
로컬라이징 부분을 LocalizeManager와 FontLocalizer로 분리
스토리 선택 창을 SelectStageWindow로 분리함.

[JoystickController]
기존에 안 올렸었던 것 같은데 화면 터치로 조이스틱 조작할 수 있도록 해주는 스크립트

[LazerController -> LaserController] (이름 변경)
Lazer -> Laser 전체적인 오타 수정

[LocalizeManager] (신규)
로컬라이징을 담당하는 Manager로 언어가 변경되면 여기서 처리되며, 언어별 폰트를 관리함.
기본적으로는 UI가 켜질 때 로컬라이징이 처리되기 때문에 켜진 FontLocalizer를 저장
언어를 변경할 때 등록된 FontLocalizer들을 로컬라이징 해준다.

[MidoriPlaneController -> MidoriPlane]
불필요한 Controller를 이름에서 제외

[OptionWindow] (추가됨)
옵션 창을 관리함. 옵션에 해당하는 기능들은 전부 여기서 관리
Button의 OnClick이나 Slider의 OnValueChanged도 코드로 관리
이제 언어를 변경할 때 게임을 종료하지 않고, 실시간으로 변경할 수 있도록 수정하였음.

[SelectStageWindow]
스테이지를 선택하는 창을 분리하였음
세팅 방식이 깔끔해지긴 했으나 storyStartText 등은 여전히 개선이 필요함.

[SliderController] (삭제됨)
옵션에 사용되던 것이라 OptionWindow로 기능 분리

[Singleton]
게임 종료할 때 파괴된 것 생성하느라 오류가 떠서 그 부분 삭제함.

[StarColorController]
변수 이름 수정
float 수정된 것 끊겨서 다시 지정해줘야 함 -_-;;

[StoryManager]
사용하지 않던 변수 삭제
버튼 OnClick 스크립트로 추가하도록 수정
Find 부분 상당 부분 삭제. Episode와 Story의 용어에 혼동이 있어서 Story로 통일함.

 

 

 

얼마간 상당한 대공사를 했는데, 결과가 나쁘지 않은 것 같다.

 

가장 큰 변경은 로컬라이징으로, 시작할 때 한 번에 처리하지 않고 실시간으로 처리할 수 있도록 LocalizeManager와 FontLocalizer를 두어서 게임 종료 없이 언어 변경을 처리할 수 있도록 만든 점이다.

 

다만 아직 테이블로 처리하는 부분을 구현하지 않아 일부 언어별 텍스트를 인스펙터에 넣어 놓은 상태이고, 스토리 대사도 테이블 처리가 아직이라 좀 더티하게 처리되는 부분이 아쉽다고 할 수 있겠다. 이외에도 여전히 스크립트 내에 하드 코딩으로 박아놓은 내용들이 많은데, csv 테이블로 처리할 수 있도록 계획하고 있다.

 

그리고 GameManager에서 로컬라이징, 스토리 선택 창, 옵션 창 등 다양한 것들을 맡고 있었는데 이것들을 분리해주어서 훨씬 깔끔해졌다.

 

전체적인 코드도 버튼이나 Slider에 스크립트 내에서 이벤트를 추가해줌으로써 관리가 훨씬 편해졌고, Find를 상당 부분 줄여 가독성도 좋아졌고 성능도 꽤나 개선됐다. 몇몇 처리를 할 때 내부 클래스들을 추가해서 향후 뭔가 하나씩 추가될 때마다 하나씩 코드를 작성할 필요 없이 유연하게 대응 가능하도록 짠 부분도 있다.

 

 

여전히 개선해야 할 부분들이 많은데, 아래와 같다.

 

1. Dialog와 UI의 텍스트 등을 csv 테이블로 만들어서 처리하기

2. ObjectPooling을 맡아주는 스크립트 만들기

3. 스토리 연출을 일일이 코드로 작성하지 않고 툴로 만들어 json파일로 저장하고, 이걸 읽어서 처리할 수 있는 기능 만들기

4. Addressables를 활용하면 좋을 듯

5. ShootingGameManager의 기능들도 분리해야 할 듯

 

이외에도 아직 구현이 안 된 대화 로그? 그런 것들도 추가해보면 좋을 것 같다.

 

현재 Dialog를 구글 스프레드 시트에 작성하고 csv로 만들기는 했는데, csv를 읽어오는 처리를 어떻게 할지가 약간 고민이다. 그냥 어거지로 읽어와서 대충 누더기로 파싱하고 처리하는 건 충분히 가능하겠지만, 깔끔하게 처리하고 싶어서... 좀 더 고민이 필요할 것 같다.

Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository (github.com)

 

GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

Tales Saga Chronicle Blast의 Script만이 포함된 Repository - GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

github.com

 

벌써 TSCB를 마지막으로 건드린지도 5달 가까이 지났다.

이전에 라이더 글을 쓰면서 코드를 봤는데, 막연하게 굉장히 쓰레기같은 코드를 짜놨을 거라 생각했는데, 의외로 괜찮은 부분도 있었고 구조적으로 문제가 있는 부분도 꽤나 있었다.

 

이번 기회에 작성했던 코드들을 보면서 어떤 문제가 있었고, 어떤 방식으로 대체하면 좋을지를 적어보고자 한다. 그리고... 가능하다면 그 개선 작업도 차근차근 할 수 있으면 좋겠다. ㅋㅋ.

 

루트 폴더부터 알파벳 순으로 정리할 예정이다.

 

Script 폴더

[AudioManager.cs]

FadeOutMusic()

볼륨을 0.01f씩 빼는 게 아니라 (볼륨 / 100)을 변수로 둬서 항상 1초에 걸쳐서 페이드 아웃되도록 수정하면 좋을 것 같다.

 

어차피 SetBGMVolume, GetVolume 1번씩만 할 거면 굳이 따로 volume 변수 만들 필요 없을 것 같다.

 

_isBGMChanged 말고 FadeOutMusic을 저장해뒀다가 PlayBGM할 때 코루틴을 중단하는 것이 좀 더 깔끔할 것 같음.

 

매개변수에 p_ 없애기

 

Sound를 SerializeField에 때려박는 건 별로 안 좋은 것 같다. 테이블 만들어서 관리하면 좋을 듯.

 

 

[ButtonManager]

굳이 버튼 매니저까진 필요 없을 듯 한데... 그냥 관련 스크립트에 추가해서 쓰는 것이 직관적일 듯.

 

현재는 유니티 에디터에서 OnClick을 설정해주는데, 어떤 버튼이 어디서 사용되는지 추적이 힘들고, 함수 변경할 때마다 참조가 끊어지니 재설정을 해줘야 한다. 코드에서 추가하도록 변경하자.

 

[DialogManager]

테이블로 관리하자.

 

[GameManager]

Find는 인스펙터에서 매번 등록하기가 번거롭고 종종 참조가 끊어지는 경우가 있어서 사용했었는데, 구조 변경에 너무 취약하니 빼고 인스펙터에 등록하자.

 

변수 이름들 수정하기.

 

\n부분 \r\n으로 변경하자.

 

로컬라이징도 테이블로 하자. (키값으로 언어 설정에 맞는 텍스트 받아오도록)

 

PlayerPrefs.HasKey("Stage1Cleared")

이게 1~4까지 있는데, 현재 몇 스테이지까지 진행했는지만 가지고 있으면 될 것 같다

 

FadeInImage, FadeOutImage가 GameManager에 있을 이유가 없을 것 같다. 이외에도 필요 없는 것 있으면 분리하기.

 

필요없는 것들 수정하면 코드가 상당히 짧아질 것 같음. GameManager보다는 좀 더 적절한 이름이 있을 것 같다. IntroManager... 싱글톤도 필요 없을 수도. 그러면 IntroController쯤 되려나?

 

[SliderController]

Awake에만 초기화가 있어도 될 것 같은데...

아마 Awake 이전에 다른 함수들이 호출되었던 것 같다. OnEnable을 하면 해결되려나?

 

슬라이더도 인스펙터에 추가하고 이벤트 달아줄 수 있으면 좋을 것 같다.

 

[StoryManager]

음... 굉장히 끔찍한 코드다. 일단 DoStoryAction()만 다르게 처리되어도 상당히 짧아질 수 있을 것 같다.

 

한 씬에 때려박는 것보다 씬 전환이 나을 것 같다.

 

선택지 관련도 뭔가 테이블로 처리할 수 있으면 좋겠다. 선택지 뿐만 아니라 스토리 전체적으로 가능하면 좋을 것 같은데 약간 어려울 것 같다. 기똥찬 방법을 고민해봐야 할 듯. 몇 가지 행동들을 정의해놓고 에디터에서 진행을 구성할 수 있도록 툴을 만들고, 이걸 json으로 바꿔서 번호를 매긴 다음에 각 번호에 해당하는 스토리를 진행할 때 이걸 다시 파싱해서 스토리 액션을 취하는 방식?

 

원래도 이런 걸 하고 싶었는데 그땐 모르는 게 너무 많아서 못했었다. 지금이라면 해볼 수 있을 것 같다.

 

전체적인 구조 개선이 필요할 것 같은데 굉장히 힘든 작업이 될 것 같다. 천천히 해보면 좋은 경험이 될 듯.

 

Script/Game1 폴더

 

[BulletController]

아군의 공격만 해당되는 것 같아서 MidoriBullet이라고 이름을 바꾸면 좋을 듯

 

[EnemyController]

StopAllCoroutines() 말고 명확하게 코루틴을 저장해놓고 정지해주는 방법이 좋을 것 같다.

 

BossController는 분리하면 좋을 것 같은데... EnemyController를 상속받아서 사용하게끔 하면 될 듯?

 

보스 스킬은 숫자 4 이런식말고 최소한 const로 선언해서 하든가 하는 게 좋을 듯

 

[LazerController]

함수가 좀 긴 것 같아서 분리 가능하면 분리하면 좋을 것 같다.

 

총알 사라지는 처리는 나쁘지 않은데 레이저는 살짝 짤리는 문제가 있어서 아예 왼쪽 오른쪽 공간을 덮어버리면 어떨지 확인해보기

-> 아마도 스토리에서 바로 보여주는 식이라 미도리 일러스트 순서때문에 그랬던 것 같은데... 잠깐 부모를 바꾼다거나 하는 식으로 하면 어떨까 싶음

 

[MidoriPlaneController]

굳이 Controller라는 이름을 붙여야만 했을까? 떼도 괜찮을 듯.

EnemyController도 걍 ShootingGameEnemy라고 하면 될 것 같다.

 

[ShootingGameManager]

ㅋㅋ 기능 분리가 좀 필요할 것 같다. 슈팅 게임 만들면서 사용할 기능들을 전부 하나에 때려박아서... 오브젝트 풀링이나 LookRotation2D 등은 밖으로 꺼내서 활용해도 될 것 같음.

 

CreateNewMidoriHalo() 등은 거의 비슷한 코드들이라 하나로 묶어서 처리하도록 처리하면 좋을 것 같다.

 

여기는 기능은 유지하되 전체적으로 코드 정리가 필요할 것 같다.

 

[StarColorController]

별들 하나하나마다 반짝거리는 시퀀스를 추가해줘서 성능상 좀 부담이 있을까 싶기도 한데 폰으로 했을 때도 딱히 렉걸리는 건 없었던 것 같아서 이대로 유지해도 괜찮을 것 같음.

 

단, DOTween.Sequence의 Loop가 GameObject의 active 상태가 false가 되면 멈춘다는 확인이 필요할 것 같다.

 

 

이외에도 Canvas scaler로 화면 비율도 조절해줘야 할 듯

옵션은 따로 OptionManager 만드는 편이 깔끔할 것 같다.

 

 

생각보다 고칠 부분이 엄청 많지는 않을 것 같다.

 

테이블 추가해서 연동하고 사용

스토리 관련 툴 만들고 따로 처리하는 과정

필요하다면 Addressables 사용하는 것

 

 

정도가 큼직한 변경이 될 것 같다.

 

일단은 쉽게 처리할 수 있는 것들부터 차근차근 수정해야겠다.

오늘은 3D RPG 개발하던 부분을 이어서 자동 전투를 구현해보았다.

 

이전 글 마지막 부분에 언급을 해놓긴 했는데, DirectX 책으로 공부를 하다가 핸들을 돌려서 다시 돌아왔다. 벡터까지는 유니티에서 자주 사용하는 데다가, 유니티 내에서 내가 사용한 함수들이 직접 어떻게 작동하는지 알아볼 수 있었고, Vector는 구조체라 가비지가 생성되지 않는 것까지도 알 수 있었다. 여기서 더 나아가서 가비지 컬렉터에 대해서 더 깊게 알 수 있게 되었다.

그런데, 행렬의 경우나 렌더링 파이프라인 같은 경우는 벡터와는 많이 달랐다. 먼저 내가 다뤄본 경험이 전무하기도 하고, 실제로 유니티 내에서는 이런 작업들을 알아서 처리해주기 때문에 어떻게 접근해야 할지도 상당히 어려웠다. 때문에 내용을 이해하는 것도 상당히 벅찼고, 활용에 대한 부분도 가닥을 잡기가 힘들었다. 이것을 왜 공부해야 되는지 모르는 상태였고, 목적성을 잃다 보니 공부가 진전이 되지도 않았다. 물론 알아두면 도움은 되겠지만, 지금 당장 의미를 모르는 상태에서 배우는 것이 머리 속에 자리잡긴 어려울 것 같으며, 당장은 다뤄보지 않은 기능이나 기술들이 많기 때문에 이런 부분을 경험해보는 것이 당장은 실무에서 더 도움이 될 것 같다고 생각했다. 아직까지 게임 개발에 있어서 깊은 부분을 경험해보지 못했는데, 경험이 쌓이고 연차가 쌓이면서 점차 이런 공부에 대한 필요성을 느끼게 되면 더 의미있고 깊이있는 공부가 가능하지 않을까 기대해본다.

 

 

아무튼, 자동 전투를 구현한 내용을 정리해보겠다. 먼저 자동 전투를 구현하기 위해 UI 작업을 하면서 가장 고민했던 부분이 자동 전투를 위해 적을 선택하는 구조를 어떻게 구현해야 할까? 였다. 필드에 따라 적들의 목록이 달라지기 때문에 적들의 데이터를 받아와서 목록을 수정하고 적들한테 번호는 어떻게 매겨야 하는지... 고민을 많이 했는데, 생각해보니 플레이어가 속해있는 필드에 따라서 다른 창을 띄워주면 될 것 같았다. 만약 필드 이동에 따라 씬이 변경된다면 더 간단한 작업일 것 같다. 때문에 구조 관련해서는 특별한 작업을 하지는 않았다. 새로운 필드나 몬스터가 추가될 때마다 미리 만들어 둔 UI를 띄우면 될 것으로 생각된다.

 

두 번째로 고민했던 부분은 선택한 적들에 대한 정보를 어떤 형태로 저장할 것인가? 였다. 처음에는 적의 번호를 int 형태로 List에 저장하도록 하였는데, 아무래도 List는 가비지가 생성되기도 하고, 적을 선택하고 끌 때 On/Off 되며 한 필드 내에서 적의 수가 20명이 넘지 않기 때문에 길이가 20인 bool 배열을 사용하기로 하였다. 또, PlayerCharacerController에서 작업을 다루기에는 양이 많은 것 같아 AutoBattleManager라는 스크립트를 따로 추가하여 관련 기능들을 처리하도록 하였다.

 

AutoBattleManager에서 작성한 코드는 다음과 같다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace SingletonPattern
{
    public class AutoBattleManager : Singleton<AutoBattleManager>
    {
        public GameObject enemyArea;
        public GameObject enemyList;

        private bool[] _enemyAreaArray = new bool[20];

        private GameObject _nearestArea;
        private GameObject _nearestEnemy;

        private bool _isOnAutoBattle = false;
        private bool _isCompletedMove = false;
        private bool _isCompletedSetPath = false;

        public bool IsOnAutoBattle => _isOnAutoBattle;
        public bool IsCompleteMove { get { return _isCompletedMove; } set { _isCompletedMove = value; } }
        public bool IsCompletedSetPath { get { return _isCompletedSetPath; } set { _isCompletedSetPath = value; } }


        public void AddEnemyAreaArray(int enemyNum)
        {
            _enemyAreaArray[enemyNum] = !_enemyAreaArray[enemyNum];
            enemyList.transform.GetChild(enemyNum).GetChild(4).GetChild(0).gameObject.SetActive(!enemyList.transform.GetChild(enemyNum).GetChild(4).GetChild(0).gameObject.activeSelf);
        }

        public void StartAutoBattle()
        {
            for (int i = 0; i < _enemyAreaArray.Length; ++i)
            {
                if (_enemyAreaArray[i] == true) break;

                if (i + 1 == _enemyAreaArray.Length) return;
            }

            _isOnAutoBattle = true;
        }

        public void StopAutoBattle()
        {
            _isOnAutoBattle = false;
            _isCompletedMove = false;
            _isCompletedSetPath = false;

            PlayerCharacterController.Instance.ResetAgentPath();
        }

        public Vector3 GetNearestAreaPosition()
        {
            _nearestArea = null;
            float minDistance = 0;

            for (int i = 0; i < _enemyAreaArray.Length; ++i)
            {
                if (_enemyAreaArray[i] == false) continue;

                float distance = Vector3.Distance(PlayerCharacterController.Instance.transform.position, enemyArea.transform.GetChild(i).position);
                if (distance < minDistance || _nearestArea == null)
                {
                    minDistance = distance;
                    _nearestArea = enemyArea.transform.GetChild(i).gameObject;
                }
            }

            return _nearestArea.transform.position;
        }

        public GameObject GetNearestEnemy()
        {
            _nearestEnemy = null;
            float minDistance = 0;

            for (int i = 0; i < _enemyAreaArray.Length; ++i)
            {
                if (_enemyAreaArray[i] == false) continue;

                for (int j = 0; j < enemyArea.transform.GetChild(i).childCount; ++j)
                {
                    if (enemyArea.transform.GetChild(i).GetChild(j).GetComponent<EnemyController_Melee>().IsAlive == false) continue;

                    float distance = Vector3.Distance(PlayerCharacterController.Instance.transform.position, enemyArea.transform.GetChild(i).GetChild(j).position);

                    if (distance < minDistance || _nearestEnemy == null)
                    {
                        minDistance = distance;
                        _nearestEnemy = enemyArea.transform.GetChild(i).GetChild(j).gameObject;
                    }
                }
            }

            return _nearestEnemy;
        }
    }
}

AddEnemyAreaArray()에서는 버튼을 누를 때마다 UIManager를 통해 호출되는 함수인데, 적 선택을 On/Off해주고 UI에서 체크 표시를 보여주고, 사라지게 하는 기능을 담당한다.

 

StartAutoBattle() 에서는 체크된 적이 하나라도 있다면 자동 전투를 시작하고, 그렇지 않으면 return하도록 하였다.

 

StopAutoBattle() 에서는 각종 변수들을 초기화해주었다.

 

GetNearestAreaPosition()는 가장 가까운 EnemyArea의 Position을 return 해주도록 하였다. 처음에는 GameObject를 반환하도록 생각했으나, Vector가 구조체이고 상당히 가볍다는 것을 알았기 때문에 Vector를 return 해주었다.

GetNearestEnemy()는 가장 가까운 적의 GameObject를 return 해준다. 적을 찾는 과정에서 처음에는 기본으로 0번에 있는 적을 넣고 순회하며 비교를 했는데, 0번에 있는 적이 가장 가깝고 죽었을 경우 죽은 적이 사라질 때까지 때리는 이슈가 있어서 기본을 null로 하고, null인 경우에도 적을 추가하도록 수정하여 문제를 해결했다.

 

위 두 개의 함수는 PlayerCharacterController의 Update에서 호출된다. 자동 전투에 NavMeshAgent를 활용하여 이동할 것이기 때문에 Update 문에서 자동 전투 관련 기능들 수행하도록 해주었다.

 

private void Update()
        {
            if (!IsAlive) return;

            _inputX = Input.GetAxis("Horizontal");
            _inputZ = Input.GetAxis("Vertical");

            _previousMoveDirection = _moveDirection;
            _moveDirection = Vector3.Normalize(new Vector3(_inputX, 0, _inputZ));
            if (_joystickInputX != 0 || _joystickInputY != 0)
            {
                _moveDirection = new Vector3(_joystickInputX, 0, _joystickInputY);
            }

            // 자연스러운 애니메이션 연결을 위해 공격 중에도 이동 애니메이션 체크
            if (_moveDirection == Vector3.zero)
            {
                // 반대로 방향을 전환할 때 Idle 모션으로 전환되지 않도록 직전 프레임 상태 확인
                if (_previousMoveDirection == Vector3.zero)
                {
                    _animator.SetBool(_isMovingHash, false);
                }
            }
            else
            {
                _animator.SetBool(_isMovingHash, true);

                if (_stepSoundCooltime <= 0)
                {
                    _stepSoundCooltime = 0.3f;
                    AudioManager.Instance.PlaySFX("Step");
                }
            }

            if (IsInAttackState)
            {
                if (target == null)
                {
                    return;
                }

                Vector3 direction = Vector3.Normalize(target.transform.position - transform.position);
                Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z));
                transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 5f);

                if (_autoBattleManager.IsOnAutoBattle && _moveDirection == Vector3.zero && target.GetComponent<EnemyController_Melee>().IsAlive)
                {
                    ExecuteAttackButton(0);
                }

                return;
            }

            if (_moveDirection == Vector3.zero)
            {
                // 자동 전투 기능 수행
                if (_autoBattleManager.IsOnAutoBattle)
                {
                    if (_autoBattleManager.IsCompleteMove)
                    {
                        if (target != null)
                        {
                            _agent.stoppingDistance = 2f;
                            _agent.SetDestination(target.transform.position);

                            if (_agent.remainingDistance < _agent.stoppingDistance)
                            {
                                _animator.SetBool(_isMovingHash, false);
                                _autoBattleManager.IsCompleteMove = true;
                                ExecuteAttackButton(0);
                            }
                            else
                            {
                                MoveCharacter(_agent.velocity.normalized);
                                _animator.SetBool(_isMovingHash, true);
                            }
                        }
                        else
                        {
                            target = _autoBattleManager.GetNearestEnemy();
                            target.GetComponent<EnemyController>().OnDrawEnemyOutline();
                            _agent.SetDestination(target.transform.position);
                        }
                    }
                    else
                    {
                        if (!_autoBattleManager.IsCompletedSetPath)
                        {
                            _agent.SetDestination(_autoBattleManager.GetNearestAreaPosition());
                            _autoBattleManager.IsCompletedSetPath = true;
                        }
                        else
                        {
                            _agent.stoppingDistance = 1f;
                            MoveCharacter(_agent.velocity.normalized);
                            _animator.SetBool(_isMovingHash, true);

                            if (_agent.remainingDistance < _agent.stoppingDistance)
                            {
                                _animator.SetBool(_isMovingHash, false);
                                _autoBattleManager.IsCompleteMove = true;

                                target = null;
                                UIManager.Instance.ResetEnemyUI();
                            }
                        }                        
                    }
                }
                else
                {
                    _idleElapsedTime += Time.deltaTime;
                    CheckIdleElapsedTime();
                }
            }
            else
            {
                MoveCharacter(_moveDirection);
                ResetIdleTime();
            }

            // 특수 대기 모션의 종료 지점이 원점이 아니기 때문에 시작 지점의 rotation과 position을 맞춰줌
            if (_isPlayedSpecialIdle && _idleElapsedTime > 0)
            {
                _controller.enabled = false;
                transform.rotation = Quaternion.Slerp(transform.rotation, _idleStartRotation, Time.deltaTime * 5);
                transform.position = Vector3.Lerp(transform.position, _idleStartPosition, 0.1f);
                _controller.enabled = true;
            }

            if (_normalAttackComboTime > 0)
            {
                _normalAttackComboTime -= Time.deltaTime;

                if (_normalAttackComboTime <= 0)
                {
                    ResetNormalAttackCooltime();
                }
            }

            if (_stepSoundCooltime > 0)
            {
                _stepSoundCooltime -= Time.deltaTime;
            }
            
        }

유저가 이동을 입력하거나, 공격을 하고 있을 때는 자동 전투 기능이 실행되지 않도록 해당 기능들에 대한 처리를 마친 후에 자동 전투 관련 기능을 수행하도록 추가해주었다.

 

자동 전투 기능을 켜면, 가장 가까운 EnemyArea까지 이동을 하고, 이후에 가장 가까운 적을 찾아서 가까이 다가가고, 공격을 수행한다. 현재는 적당한 거리를 넣어놨는데, 스킬 등을 추가한다면 다음에 수행할 공격 혹은 스킬의 사거리만큼 이동하도록 수정할 필요가 있겠다. 개인적으로는 가장 가까운 적을 바로 탐색하는 편이 더 명료할 것 같은데, 현재 개발중인 게임의 기반인 루나 모바일의 기능이 이렇게 구현되어 있어서 고증을 살리는 방향을 선택했다.

 

이 부분을 다루면서 조금 어려웠던 것이, SetDestination을 설정한 이후에 바로 remainingDistance가 업데이트되지 않는다는 점이었다. SetDestination을 호출한 프레임에서는 remainingDistance가 0으로 인식되기 때문에, 다음 프레임에서 처리하도록 작업을 할 필요가 있었다. 그리고, 공격 도중에는 자동 전투 관련 기능이 작동하지 않도록 했기 때문에 연속 공격 처리를 위해 공격 상태를 처리할 때 연속 공격을 이어가도록 하였다. 이 결과 적이 죽은 이후에도 공격을 1회 더 한다는 문제가 있기는 하지만, 오히려 적이 죽자마자 다른 적을 찾아서 공격하는 편이 더 어색할 것으로 보여서 냅두었다. 만약 이 부분을 수정한다면 적이 죽을 때 수행하려고 했던 공격을 취소하는 작업을 추가해줘야 할 것 같다.

 

 

오늘 완성된 부분은 다음과 같다.

전체적으로 자동 전투 관련 기능은 잘 작동하는 것 같은데, 스켈레톤이 춤추는 문제가 아주 두드러지게 보인다. 원인이 뭘까? 리치의 경우는 괜찮은 걸로 봐서는 모델 자체의 문제인가 싶기도 하고... 조금 미뤘는데 원인을 알아봐야겠다. 또, 적들과 충돌이 있어 통과하지 못하는 부분도 개선이 필요할 것 같다.

오늘은 이전에 3D RPG게임을 apk로 빌드해서 테스트해본 것처럼 이전에 개발하던 TSCB를 apk로 빌드해보았다.

 

private void Update()
        {
            if (!_isAvailableMove) return;

            _moveSpeed = 400f * Time.deltaTime;

            float inputX = Input.GetAxis("Horizontal");
            float inputY = Input.GetAxis("Vertical");

            Vector2 moveDirection = new Vector2(inputX, inputY).normalized;

            if (_joystickInputX != 0 && _joystickInputY != 0) moveDirection = new Vector2(_joystickInputX, _joystickInputY);

            if (moveDirection.x > 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.x >= 350) moveDirection.x = 0;
            else if (moveDirection.x < 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.x < -350) moveDirection.x = 0;

            if (moveDirection.y > 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.y >= 500) moveDirection.y = 0;
            else if (moveDirection.y < 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.y <= -500) moveDirection.y = 0;

            moveDirection = moveDirection.normalized;

            _midoriPlane.GetComponent<RectTransform>().anchoredPosition += moveDirection * _moveSpeed;

            // Z키를 눌러 총알을 발사
            if (Input.GetKey(KeyCode.Z))
            {
                OnClickAttackButton();
            }
            if (Input.GetKeyDown(KeyCode.X))
            {
                OnClickSkillButton();
            }
        }

그러기위해 필요한 작업이 있었는데, 모바일에서 터치로 조작할 수 있도록 조작 기능을 추가하였다.

이를 위해서 3D RPG 개발에 사용했던 조이스틱 기능을 가져왔다. 또, 이전에 이동이 normalized 되어있지 않아 대각선으로 이동할 때 이동속도가 더 빠른 문제가 있었다. 때문에 normalized 작업을 해주었다.

 

공격과 스킬을 사용할 때 버튼을 눌러서 입력할 수 있도록 공격과 스킬 기능을 함수로 분리하여 키를 누르거나 버튼을 눌렀을 떄 작동하도록 하였다.

 

그리고 이번에도 화면 비율 관련 이슈가 있었는데, Canvas Scaler를 조절해주고, 배경이 비는 부분 등의 가로 크기를 늘려줘서 화면에서 위화감이 나타나지 않도록 수정했다.

이전에 사용하던 안드로이드 기기를 이용하여 테스트한 화면이다. 가로 비율이 조금 넓어서 배경을 늘려주는 작업이 필요했다. 전체적으로 기능이 동일하게 작동했는데, 슈팅게임의 조작 부분이 살짝 아쉬웠다. 조이스틱 크기가 너무 작아서 조작이 약간 불편했고, 공격의 경우 버튼을 계속 터치해야하는 불편함이 있었다. 때문에 내일 조이스틱 크기를 키우고 공격 버튼을 누르고 있으면 입력을 유지할 수 있도록 수정할 계획이다.

Github 링크(스크립트만 포함)

https://github.com/Mepkatatsu/First_3D_RPG_OnlyScript

 

GitHub - Mepkatatsu/First_3D_RPG_OnlyScript: My First 3D RPG / Script Only

My First 3D RPG / Script Only. Contribute to Mepkatatsu/First_3D_RPG_OnlyScript development by creating an account on GitHub.

github.com

*사용한 리소스의 저작권 등의 문제로 현재 개발한 부분까지의 스크립트만 따로 모아 public으로 업로드 한 Repository의 주소임

전체 기능 영상

구현한 기능

[이동] 00:00 ~ 00:07

화면의 터치한 지점에 생성되는 조이스틱 혹은 키보드 WASD로 이동

 

[카메라] 00:08 ~ 00:20

TopDown 형식의 카메라

휠을 사용한 줌 인/줌 아웃

인벤토리를 열고 닫을 때 빈 공간의 중심에 플레이어 캐릭터가 위치하도록 카메라 위치 변경

 

[특수모션] 00:17 ~ 00:20

플레이어 캐릭터가 10초간 가만히 있으면 특수 모션 재생(2종류의 춤 중에 랜덤으로 재생)

 

[공격] 00:26 ~ 00:44

공격 버튼을 눌러 공격, 1초 이내 입력시 최대 3번까지 연속 공격 실행

 

[적] 00:45 ~ 01:18

적을 클릭하거나 공격하면 타겟으로 지정되며 빨간색 테두리를 추가

정해진 지역 내에서 랜덤 주기로 랜덤한 거리만큼 이동

플레이어에게 선제 공격을 당하면 전투 상태에 돌입

전투 상태에서 플레이어가 공격 사거리 밖에 있으면 플레이어를 추격

플레이어가 지역 밖으로 나가면 원래 위치로 돌아가며 전투 상태 해제

적이 사망하면 자동으로 타겟 해제, 타겟이 변경되면 타겟 상태 유지

 

[인벤토리&아이템] 02:55 ~ 03:30

적을 처치하면 일정 확률로 아이템 획득

일정 확률로 성급 아이템 드랍

성급 아이템 감정

아이템 장착시 빈 공간이 생기면 뒤에 있는 아이템들을 1칸씩 앞으로 이동

인벤토리창 스크롤

 

[기타]

아이템 장착시 스탯 변경되며 공격력이 증가하면 적에게 주는 데미지 증가 03:34, 04:52

플레이어 사망 05:15

 

사용한 에셋

플레이어 캐릭터 에셋: Casual RPG Character - 04 Avelyn

https://www.assetsready.com/product-page/casual-rpg-character-04-avelyn

 

Casual RPG Character - 04 Avelyn | Assets ready main

Update Ver. 1.1Animations : Characters are humanoid mecanim ready. (See Mixamo motion test) Polys: 2948Verts: 2549Texture: 1024x1024-1, 256x256-1Character Icon: 256x320-1Animations(x8) :(Animation Type_Generic)Sword_F_BattleIdle_01Sword_F_Run_01Sword_F_Att

www.assetsready.com

 

몬스터1(리치) 에셋: Mini Legion Lich PBR HP Polyart

https://assetstore.unity.com/packages/3d/characters/humanoids/fantasy/mini-legion-lich-pbr-hp-polyart-91497

 

Mini Legion Lich PBR HP Polyart | 캐릭터 | Unity Asset Store

Get the Mini Legion Lich PBR HP Polyart package from Dungeon Mason and speed up your game development process. Find this & other 캐릭터 options on the Unity Asset Store.

assetstore.unity.com

 

몬스터2(스켈레톤) 에셋: Fantasy Monster - Skeleton

https://assetstore.unity.com/packages/3d/characters/humanoids/fantasy-monster-skeleton-35635

 

Fantasy Monster - Skeleton | 3D 휴머노이드 | Unity Asset Store

Elevate your workflow with the Fantasy Monster - Skeleton asset from Teamjoker. Find this & other 휴머노이드 options on the Unity Asset Store.

assetstore.unity.com

 

지형 에셋: Casual Tiny Environment - Jungle Set

https://www.assetsready.com/product-page/casual-tiny-environment-jungle-set

 

Casual Tiny Environment - Jungle Set | Assets ready main

■ Jungle 1Verts: 19971Tris: 15510Textures :Diffuse map_ 1024x1024-2, 512x512-1Light map_ 1024x1024-1■ Jungle 2Verts: 24777Tris: 13232Textures :Diffuse map_ 1024x1024-2Light map_ 1024x1024-1■ Jungle 3Verts: 21875Tris: 11403Textures :Diffuse map_ 1024x

www.assetsready.com

 

조이스틱 Sprite: Joystick Pack

https://assetstore.unity.com/packages/tools/input-management/joystick-pack-107631

 

Joystick Pack | 입출력 관리 | Unity Asset Store

Get the Joystick Pack package from Fenerax Studios and speed up your game development process. Find this & other 입출력 관리 options on the Unity Asset Store.

assetstore.unity.com

 

빌드 후 발생한 오류 수정: Log Viewer

https://assetstore.unity.com/packages/tools/integration/log-viewer-12047

 

Log Viewer | 기능 통합 | Unity Asset Store

Use the Log Viewer from dreammakersgroup on your next project. Find this integration tool & more on the Unity Asset Store.

assetstore.unity.com

 

TextMeshPro

 

+

 

소울게임즈의 루나 모바일 게임 내 UI 및 시스템 사용

https://play.google.com/store/apps/details?id=com.t3.luna&hl=ko&gl=US

 

루나 모바일(12) - Google Play 앱

로맨스 MMORPG 루나 모바일

play.google.com

 

일자별 활동

2023. 04. 04. ~ 2023. 04. 13. : 3D 개발 공부

2023. 04. 14. ~ 2023. 04. 17. : 기능, 구조 계획, 에셋 준비

2023. 04. 18. : 캐릭터 이동, 대기 모션, 카메라 구현

2023. 04. 19. : 캐릭터 공격 모션, 사망 상태 구현

2023. 04. 20. : 적 캐릭터 AI, 전투, 적 캐릭터 필드 구현

2023. 04. 21. ~  2023. 04. 23. : 아이템, 인벤토리 시스템, 스탯 시스템, UI 등 구현

2023. 04. 24. : UI 및 아이템 추가, 빌드, 최종 녹화, 코드 정리 및 구조도 작성

 

 

프로그램 구조도

적 AI 구현: Finite State Machine

적 캐릭터는 StateMachine을 가지며 현재 상태로 CurrentState를 저장합니다. State는 IdleState, MoveToTargetState, MoveToWaypointState, AttackState, DeadState로 총 5종류가 있습니다. 기본적으로 IdleState로 시작하며, 10~30초간 IdleState가 유지되면 Waypoint를 생성하여 MoveToWaypointState로 전환하여 Waypoint로 이동합니다. 적에게 공격을 당하면 전투 상태에 돌입하며, 적이 사거리 내에 없을 경우 MoveToTargetState로 전환하여 적에게 이동하고, 적이 사거리 내에 있을 경우 AttackState에 돌입하여 적을 공격합니다. 각 State가 종료되면 IdleState로 전환됩니다. 적의 체력이 0이 되어 사망하면 DeadState로 넘어가고, 3초 후 GameObject를 Destroy합니다.

 

 

 

아이템 시스템

ItemDatabaseObject와 ItemObejct는 ScriptableObject를 상속하여 구현하였습니다. ItemDatabaseObject는 하나만 존재하고, 이 안에 ItemObject들이 저장되어 있습니다. ItemObject는 아이템을 생성하는 틀이며, 실제 플레이어에게 드롭되는 아이템은 ItemData 형식으로 저장됩니다. itemDatabaseObject에 등록될 때 ItemObject의 ItemData 내의 id가 중복되지 않도록 id를 부여합니다. ItemData에는 ItemStat이라는, 아이템의 능력치 각각의 정보가 저장되어 있습니다. 아이템의 등급, 성급, 강화 수치에 따라 최종 수치가 결정됩니다.

인벤토리 시스템

InventoryObject라는 ScriptableObject가 있으며, 이를 생성하여 플레이어의 장비창과 인벤토리를 구현하였습니다. InventoryObject는 ItemDatabaseObject를 가지며 아이템의 각종 비교에 사용합니다. 또한 하나의 인벤토리 슬롯의 정보를 저장한 InventorySlot을 여러 개 가진 Inventory를 가지고 있습니다. InventorySlot은 해당 슬롯에 저장된 ItemData를 저장합니다. 하나의 InventoryObject는 각각 장비창, 인벤토리 등 하나의 인벤토리 창의 정보를 가지고 있습니다.

 

InventoryUI는 InventoryObject를 가지며 인벤토리에 아이템이 추가되거나 제거될 때 UI를 갱신합니다.

이를 상속받아 구현하는 StaticInventory는 고정적인 인벤토리 창을 갖는 장비창에 등록되는 스크립트로, 등록된 GameObject들인 staticSlots를 InventoryObject의 Inventory 안에 저장된 slots의 slotUI에 순서대로 저장합니다. 마찬가지로 InventoryUI를 상속받아 구현하는 DynamicInventory는 크기가 변하는 인벤토리 창을 갖는 플레이어 인벤토리에 등록되는 스크립트로, 최초에 인벤토리들을 생성하고 아이템을 정렬하는 등의 기능을 수행합니다.

 

스탯 시스템

ScriptableObject인 StatObject를 생성하여 플레이어의 스탯을 관리하는 PlayerStat에 등록하였다. StatObject에는 현재 레벨과 경험치, 체력, 마나, attributes가 저장되어 있습니다. Attribute는 플레이어의 각 스탯들의 타입과 값을 저장합니다. 값은 ModifiableFloat의 형태로 저장되어 있는데, 기본 값인 baseValue에 아이템을 장착할 때마다 스탯별로 modifiers에 IModifier가 추가되어 최종 능력치로 modifiedValue를 반환합니다.

 

 

 

 

 

================================================================

 

 

 

 

 

더보기

3주 전에 느꼈던 것과 마찬가지로 지난 3주간 정말 많은 것들을 배웠다. 3D 개발 관련 강의를 보면서 조금 더 현업에 어울리는 사람으로 성장한 것 같다. 상태 머신과 인벤토리, 아이템 시스템을 만들면서 체계적인 프로그램의 구조와 객체 지향 프로그래밍에 어울리는 방식에 조금 익숙해진 것 같다. 지난 1주일간 개발한 개발물에 부족한 부분과 개선해야할 부분이 아직 많지만, 시간만 들인다면 충분히 해결할 수 있을 것 같다. 3주 전에 취업을 고민했었는데, 3D 개발 공부를 한 것이 아주 좋은 선택이었던 것 같다. 굉장히 힘들고 어려운 시간이었지만 어려움을 극복하면서 많이 성장한 것 같다. 아마 조급함을 느꼈으면 이렇게 성장할 수는 없었을 것이다. 나에게 주어진 시간들에 감사함을 느낀다. 취업을 하게 되면 지금보다도 훨씬 많은 것들을 배울 수 있을 것이다. 지난 2달 간처럼, 앞으로도 많은 것들을 배우고 성장해나갈 것이다.

 

전체적인 부분을 녹화하기 위해서 빌드를 했는데, 예상치 못한 문제들이 나타나서 겪고 해결한 문제들을 간단히 정리해보려고 한다.

 

그냥 쓰는 블로그 :: 빌드 후 로그 쉽게확인하기 (Log Viewer) (tistory.com)

 

빌드 후 로그 쉽게확인하기 (Log Viewer)

빌드 후에 로그를 보려고하면 불편했던 경우가 많을것이다.그때 사용하면 좋은게 바로 로그 뷰어. 에셋스토어에서 무료로 다운받을 수 있다. 다운 후 Reporter 탭에서 Create를 누르면 Hierachy뷰에 Rep

icechou.tistory.com

빌드 후에 로그를 쉽게 확인할 수 있는 Log Viewer라는 에셋을 활용하였다.

 

 

먼저 첫 번째는 Shader 관련 이슈이다.

 

Unity Build했는데 Editor에서만 된다? (tistory.com)

 

Unity Build했는데 Editor에서만 된다?

이걸 가지고 만 하루는 보낸것같다 ㅠㅠㅠUnity Editor에서는 잘 되는데 Build하면 안된다.그런데 오류도 안난다.로그도 안뜬다. 왜인지 모르겠지만 개발자모드로 빌드도 안되서(앱이 실행이 안됨;)

youn-codingnote.tistory.com

적을 선택했을 때 테두리를 그리는 부분이 작동하지 않았는데, 글에 나온 것처럼 Edit - Project Settings - Graphics - Always Included Shaders에 사용하는 쉐이더를 등록해주니 해결되었다.

 

두 번째는 OnValidate() 관련 문제였다.

 

작동이 제대로 되지 않는 부분이 있어 디버그를 띄우면서 찾아보니 OnValidate() 부분이 실행이 되지 않았다.

혹시나해서 마우스를 갖다대보니 편집기에서만 호출된다는 설명이 있었다. 때문에 해당 부분을 OnEnable() 함수로 변경해주었다.

 

apk파일로도 빌드해서 이전에 사용하던 기기로 테스트를 해보았다. 지형의 텍스쳐가 상당히 밝아진 것을 제외하면 정상적으로 작동했다.

 

해상도 대응 작업을 해볼까 싶었는데, Reference Resolution이 1280x1080이라는 근본없는 해상도로 되어있었다. 1920x1080으로 바꾸니 UI들의 위치가 영 맞지 않는 문제가 있었다. 아마 UI들의 위치를 재배치해야 될 것 같은데, 너무 시간이 오래 걸릴 것 같아서 일단 보류하기로 하였다. 다음부터는 해상도 대응을 먼저 생각하고 제대로 확인해야겠다.

오늘은 마지막으로 전투에 디테일을 더하기 위해 데미지 텍스트 UI를 추가했다.

 

데미지 텍스트는 Prefab으로 만들었으며, DamageText 스크립트와 Animator를 컴포넌트로 가지고 있고 이미지와 텍스트를 자식으로 가지고 있다.

 

using TMPro;
using UnityEngine;

public class DamageText : MonoBehaviour
{
    #region Variables
    private TextMeshProUGUI _textMeshPro;

    public float destroyDelayTime = 1.0f;

    #endregion Variables

    #region Properties

    public int Damage
    {
        get
        {
            if (_textMeshPro != null)
            {
                return int.Parse(_textMeshPro.text);
            }

            return 0;
        }
        set
        {
            if (_textMeshPro != null)
            {
                _textMeshPro.text = value.ToString();
            }
        }
    }

    #endregion Properties

    #region Unity Methods
    private void Awake()
    {
        _textMeshPro = transform.GetChild(1).GetComponent<TextMeshProUGUI>();
    }

    private void Start()
    {
        Destroy(gameObject, destroyDelayTime);
    }
    #endregion Unity Methods
}

데미지 텍스트는 생성되고나서 1초 후에 자동으로 파괴되도록 하였다. 프로퍼티를 통해 편하게 데미지 텍스트의 수치를 변경할 수 있도록 하였다.

 

public void ShowDamageText(GameObject target, int damage)
        {
            if (target == null) return;

            GameObject damageTextGO = Instantiate(damageTextPrefab, camera.WorldToScreenPoint(target.transform.position), Quaternion.identity, damageTextParent.transform);
            DamageText damageText = damageTextGO.GetComponent<DamageText>();

            int randNum = UnityEngine.Random.Range(30, 60);
            damageTextGO.GetComponent<RectTransform>().position = new Vector2(damageTextGO.GetComponent<RectTransform>().position.x + randNum, damageTextGO.GetComponent<RectTransform>().position.y + randNum);

            damageText.Damage = damage;
        }

UIManager에서 ShowDamageText 함수를 추가하여, 적이 피격될 때 TakeDamage에서 해당 함수를 호출하도록 하였다.

ShowDamageText가 호출되면 호출한 target의 World 좌표를 Screen 좌표로 변경하여 데미지 텍스트를 생성한다. 이후 데미지 텍스트 x, y 좌표값에 랜덤한 값을 더해 랜덤한 위치에 데미지 텍스트가 생성되도록 하고, 데미지 수치를 반영하도록 하였다.

 

애니메이션은 간단하게 잠시 표시됐다가 흐려지면서 위로 사라지는 연출로 만들었다.

 

그리고 아이템 추가가 이미지만 있으면 어렵지 않게 가능하기 때문에 완성도를 높이는 차원에서 아이템 8종을 추가해주었다. 추가된 아이템은 레벨 제한이 21이기 때문에 21레벨 아이템을 드랍하는 스켈레톤을 잡으면 드랍된다.

 

 

오늘 구현된 내용은 다음과 같다.

이번에도 무기가 좀처럼 안떠서 아이템 드롭 확률을 90%로 높였다. 기존엔 50%였고, 장비가 8종이 있었으니 평균적으로 16마리는 잡아야 무기가 나왔으니 그럴 법도 한 것 같다. 어제 녹화하지 못했던 무기 교체, 데미지 수치 변경 등까지 담은 것 같다.

 

이제 1주일 전에 계획했던 기능들을 대부분 구현한 것 같다. 이번에도 만드는 과정에서 굉장히 많은 것들을 배운 것 같다. 이제 이것들을 요약해서 총정리를 할 계획이다.

마지막 글을 쓴 이후로 무려 4일이나 경과했다.

아이템과 인벤토리 시스템 구현이 생각보다 복잡하고 어려워서 시간이 좀 많이 걸렸다.

인벤토리를 구현하기 위해서는 아이템이 구현되어있어야 하기 때문에 두 작업을 모두 마치는 데에 시간이 상당히 오래 걸린 것 같다. 이전에 다뤄본 아이템과 인벤토리 시스템을 활용했지만 구현하는 방식이 기존 방식과 상당히 동떨어져있어 구조를 뜯어고치는 등 복잡한 작업이 필요했다.

 

목요일에는 아이템의 전반적인 기능들을 구현했다.

금요일에는 루나모바일 게임 내에서 인벤토리와 아이템 등 UI를 가져왔고, Static / Dynamic 등 인벤토리 스크립트를 구현했다. (각각 플레이어 장비창, 아이템창)

토요일에는 아이템이 인벤토리에 추가되거나, 아이템의 등급과 감정하는 기능 등 아이템과 인벤토리 전반적인 기능 구현을 마쳤고, 인벤토리를 여닫을 때 카메라의 위치를 수정했다.

오늘인 일요일에는 아이템 장착, 해제와 더불어 플레이어의 스탯 시스템을 구현했다. 또한 플레이어와 적의 체력바 등 UI도 구현하였으며, 적을 클릭하거나 공격하여 적을 선택하고 테두리를 강조하는 기능도 구현했다. 아이템을 장착하거나 해제할 때 인벤토리에 빈 공간이 생기지 않도록 정렬 기능까지 구현했다. 클릭이나 공격, 피격, 장착, 해제, 움직임 등에 사용되는 효과음도 적용하였다.

 

 

이렇게 정리하고나니 굉장히 많은 것을 한 것 같다. 조금씩 정리했으면 좋았을 것 같은데, 전체적으로 기능이 부족하고, 체계적으로 작동하지 않는 것 같아서 정리를 미뤄뒀던 것 같다. 이 구조들을 정리하려고 하니 벌써부터 머리가 아파오는 것 같다. 우선은 처음부터 순서대로 정리해보겠다.

 

http://draw.io

 

Flowchart Maker & Online Diagram Software

Flowchart Maker and Online Diagram Software draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool, to design database schema, to build BPMN online, as a circuit d

app.diagrams.net

프로그램 구조도를 그리는 데에 꽤나 괜찮은 사이트가 있어 이곳에서 구조도를 작성해보았다.

 

먼저 Item 시스템의 구조도이다. ItemDatabaseObject와 ItemObejct는 ScriptableObject를 상속받아 구현하였다. ItemDatabaseObject는 하나만 존재하고, 이 안에 ItemObject들이 저장되어있다. ItemObject는 아이템을 생성하는 틀이며, 실제 플레이어에게 드롭되는 아이템은 ItemData 형식으로 저장된다. itemDatabaseObject에 등록될 때 ItemObject의 ItemData 내의 ItemObject마다 id가 중복되지 않도록 id를 부여한다. ItemData에는 ItemStat이라는, 아이템의 능력치 정보가 여럿 저장되어있다. 아이템의 등급, 성급, 강화 수치에 따라 최종 수치가 결정된다.

 

IModifier는 플레이어의 스탯 시스템을 구현하기 위한 인터페이스로, ItemStat이 상속받아 AddValue라는 함수를 구현한다. 또한, 게임 내에 강화석을 옵션석을 장착하는 슬롯이 있어 생성은 해놓았지만 아직 구현하지 않았다. ItemRank, CharacterClass, ItemType, AttributeType은 enum으로 각종 기능시 체크할 떄 사용된다.

 

다음은 Inventory 시스템의 구조도이다. InventoryObject라는 ScriptableObject가 있으며, 이를 생성하여 플레이어의 장비창과 인벤토리를 구현하였다. InventoryObject는 ItemDatabaseObject를 통해 아이템의 각종 비교에 사용한다. 또한 인벤토리 각각의 슬롯의 정보를 저장한 InventorySlot을 여러 개 가진 Inventory를 가지고 있다. InventorySlot은 해당 슬롯에 저장된 ItemData를 저장한다. 하나의 InventoryObject는 각각 장비창, 인벤토리 등 하나의 인벤토리 창의 정보를 가지고 있다.

 

InventoryUI는 InventoryObject를 가지며 인벤토리에 아이템이 추가되거나 제거될 때 UI를 갱신한다.

이를 상속받아 구현하는 StaticInventory는 고정적인 인벤토리 창을 갖는 장비창에 등록되는 스크립트로, 등록된 GameObject들인 staticSlots를 InventoryObject의 Inventory 안에 저장된 slots의 slotUI에 순서대로 저장해준다. 마찬가지로 InventoryUI를 상속받아 구현하는 DynamicInventory는 크기가 변하는 인벤토리 창을 갖는 플레이어 인벤토리에 등록되는 스크립트로, 최초에 인벤토리들을 생성하고 아이템을 정렬하는 등의 기능을 수행한다.

 

 

마지막으로 스탯 시스템의 구조도이다. 마찬가지로 ScriptableObject인 StatObject가 있다. 이를 생성하여 플레이어의 스탯을 관리하는 PlayerStat에 등록하였다. StatObject에는 현재 레벨과 경험치, 체력, 마나, attributes가 저장되어있다. Attribute는 플레이어의 각 스탯들의 타입과 값을 저장하고 있다. 값은 ModifiableFloat의 형태로 저장되어 있는데, 기본 값인 baseValue에 아이템을 장착할 때마다 스탯별로 modifiers가 추가되어 최종 능력치로 modifiedValue를 반환한다.

 

 

막상 이렇게 정리를 하니까 생각보다 그렇게 복잡하진 않은 것 같다. 구조도를 그리는 데에 2시간 정도 소요되긴 했지만.

EnemyController나 전투 시스템도 이렇게 구조도를 그리면 좋을 것 같기도 하다.

 

 

이외에는 대부분 기능을 UIManager에서 처리한다. 화면의 버튼을 누르는 기능들, 플레이어의 체력바를 업데이트하며 현재 주시 중인 적의 정보를 업데이트한다. 적의 정보를 업데이트하는 것은 적을 클릭하거나 적에게 피해를 주었을 때 작동한다.

 

그리고 처음 다뤄보는 것이 두 가지 정도 있었다. 먼저 ScrollView다. 일정 범위 내에서 스크롤을 하면 해당 범위 내에서만 데이터가 수평, 혹은 수직 방향으로 움직이며 많은 데이터를 한 곳에서 볼 수 있게끔 해준다. 사용법은 인터넷에 자세히 나와있어서 해당 부분을 참조했다. 게임에서 이전 대화 기록을 볼 때 해당 기능을 사용하면 될 것 같다.

 

Unity UGUI 스크롤뷰(ScrollView) 사용법 간단 정리 : 네이버 블로그 (naver.com)

 

Unity UGUI 스크롤뷰(ScrollView) 사용법 간단 정리

Unity에서 UI 작업시 가장 까다로우면서도 손이 많이 가는 작업으로 스크롤뷰( ScrollView ) 작업을 ...

blog.naver.com

 

두 번째로 다뤄본 것은 테두리이다. Shader를 이용하는데, 아무래도 각기 다른 에셋을 사용해서 그런지 테두리가 일정하고 균일하게 표시되진 않았으나 Shader에 대해서도 간단하게 다뤄본 것 같다. 이것도 마찬가지로 인터넷을 찾다보니 참고할만한 예제가 있어서 약간 변형하여 사용하였다.

 

유니티 - 오브젝트를 선택된 상태로 만들기 : (3) Shader Outline (tistory.com)

 

유니티 - 오브젝트를 선택된 상태로 만들기 : (3) Shader Outline

Unity 전체 링크 오브젝트를 선택된 상태로 만들기 : (1) bool 오브젝트를 선택된 상태로 만들기 : (2) 이미지로 만들기 오브젝트를 선택된 상태로 만들기 : (3) Shader Outline 오브젝트를 선택된 상태로

bloodstrawberry.tistory.com

 

using System.Collections.Generic;
using UnityEngine;

namespace SingletonPattern
{
    public class OutlineManager : Singleton<OutlineManager>
    {
        private Material _outline;

        private GameObject _target;
        private Renderer _renderer;
        private List<Material> _materialList = new();

        public void OnDrawOutline(GameObject target)
        {
            if (target == null) return;
            if (_outline == null) _outline = new Material(Shader.Find("Draw/OutlineShader"));

            DisableOutline();

            _target = target;
            _renderer = target.GetComponent<Renderer>();

            _materialList.Clear();
            _materialList.AddRange(_renderer.sharedMaterials);
            _materialList.Add(_outline);

            _renderer.materials = _materialList.ToArray();

            Debug.Log("Here");
        }

        private void DisableOutline()
        {
            if (_target == null || _renderer == null) return;

            _materialList.Clear();
            _materialList.AddRange(_renderer.sharedMaterials);
            _materialList.Remove(_outline);

            _renderer.materials = _materialList.ToArray();
        }
    }
}

나는 이런식으로 선택한 적이 바뀔 때마다 기존의 테두리를 없애주고 새로운 적에게 테두리를 생성해주었다. OnDrawOutline은 적을 클릭하거나 적을 공격할 때 호출된다.

 

이외에는 전투 시스템을 약간 수정했는데, 스탯을 가져와서 공격력을 계산하는 부분만 약간 바꾸었다.

 

 

지난 4일간에 걸쳐 구현된 부분은 다음과 같다.

UI와 사운드가 추가되니 훨씬 봐줄만한 게임이 된 것 같다. 무기가 나와야 적을 빨리 잡아서 아이템 파밍을 빨리 할텐데, 무기가 좀처럼 나오지 않아서 녹화가 약간 길어졌다. 너무 길어져서 중간에 끊었는데, 생각해보니 무기를 장착하면 데미지가 더 강해지는 것을 담지 못한 것 같다. 다음 번 녹화때는 전부 담을 수 있도록 해야겠다.

 

사실 이렇게 녹화하고보니 모든 내용을 다 정리하진 못한 것 같다. 아이템 등급은 짧게 통계를 내보고 추측하여 아이템이 드랍되도록 하였고, 성급은 표기된 확률대로 구현했다. 아이템 도감이 있어서 이미지나 등급/성급별 수치 변화도 쉽게 찾아볼 수 있었다. 아이템 스탯도 스크롤 뷰를 사용해서 구현했는데 해당 부분도 담지 못한 것 같다. 아직 구현할 것들이 조금 남아있기도 하니 내일 구현을 마무리한 후에 구현한 모든 내용들을 담을 수 있게끔 정리하고 녹화해야겠다. 또, 포트폴리오로 사용할 것이니 더 깔끔하게 잘 정리할 필요가 있을 것 같다. 지금 남은 것은 전투 부분의 UI 개선 정도이다. 데미지 텍스트, 공격 이펙트 정도만 추가하면 계획했던 기능들을 거의 구현한 것 같다. 사실 이 부분도 꼭 필요하지는 않은 것 같은데, 아무래도 전투가 밋밋해보여서 추가하는 게 좋을 것 같다. 게임에서 시각적인 부분도 꽤나 중요하기도 하고 말이다. 아이템을 더 추가해야하나 싶기도 한데, 이 부분은 일단 보류해야겠다. 내일 개발을 마무리하고 나머지 부분들을 정리하여 글을 써야겠다.

 

+ Recent posts