0. 오늘 계획
저번까지 심리스 맵처럼 구현을 완료하여 이번에는 샘플로 씬들을 컨셉별로 만들어보려 했는데, 제가 작업하려고 맥북을 들고 카페로 왔는데 마우스 없이 트랙패드로 하려니 정말 너무 힘들어서 다음으로 미루기로 했습니다.
월드맵으로는 사막, 초록숲, 보라숲(음산한 느낌?), 붉은숲(단풍 느낌?), 설원 정도 생각하고 있었고, 샘플용 던전 하나 정도 생각해두었습니다.
그래서 오늘은 게임 구조를 한번 정리해볼까 합니다.
1. 플레이어 코드 분리
현재는 플레이어 캐릭터 하나뿐이지만 이후에는 몬스터, NPC 등등 플레이 불가능한 개체들도 추가될 것이라 미리 코드를 분리해보려 합니다.
조작에 관한 부분은 PlayerEntityController로 넘기고, 나머지는 EntityController에 그대로 둘 생각입니다.
protected Animator m_Animator;
protected UnityEngine.AI.NavMeshAgent m_NavMeshAgent;
protected bool m_IsWalking = false;
protected Vector2 m_MoveDir;
우선은 개체별로 공용으로 쓸법한 맴버들은 private에서 protected로 수정해주었습니다.
개인적으로 정말 이 클래스에서만 쓰는 게 아니면 대부분 protected로 선언해줍니다.
protected virtual void Start()
{
m_Animator = GetComponent<Animator>();
m_NavMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
}
개체별로 재정의 할 수도 있겠다 싶은 함수나 프로퍼티는 virtual로 선언해줍니다.
물론 재정의 필요할 때 virtual로 바꿔줘도 되지만, 제 경험상 외부에서 접근 가능한 public 함수, 프로퍼티나 MonoBehaviour함수는 자주 재정의됩니다.
public class PlayerEntityController : EntityController
{
protected override void Start()
{
base.Start();
InitPlayerInput(GetComponent<UnityEngine.InputSystem.PlayerInput>());
}
#region Input System
protected UnityEngine.InputSystem.PlayerInput m_PlayerInput;
protected UnityEngine.InputSystem.InputAction m_InputAction_Move;
public void InitPlayerInput(UnityEngine.InputSystem.PlayerInput playerInput)
{
if (m_PlayerInput == playerInput)
return;
//이전 액션에 콜백 제거
if (m_InputAction_Move != null)
{
m_InputAction_Move.started -= OnInput_Move;
m_InputAction_Move.canceled -= OnInput_Move;
m_InputAction_Move.performed -= OnInput_Move;
m_InputAction_Move = null;
}
m_PlayerInput = playerInput;
if (m_PlayerInput != null)
{
m_InputAction_Move = m_PlayerInput.currentActionMap.FindAction("Move");
}
//신규 액션에 콜백 등록
if (m_InputAction_Move != null)
{
m_InputAction_Move.started += OnInput_Move;
m_InputAction_Move.canceled += OnInput_Move;
m_InputAction_Move.performed += OnInput_Move;
}
}
void OnInput_Move(UnityEngine.InputSystem.InputAction.CallbackContext context)
{
if (context.valueType == null)
{
m_MoveDir = Vector2.zero;
}
else
{
m_MoveDir = context.ReadValue<Vector2>();
}
}
#endregion
}
EntityController에서 조작에 관한 부분만 우선 PlayerEntityController로 가져왔습니다.
캐릭터 게임오브젝트에 달려있는 EntityController를 PlayerEntityController로 변경해주었습니다.
2. 플레이 데이터 관리
게임 플레이 데이터를 담고 있는 클래스가 필요하여 싱글턴으로 구현해볼 겁니다.
public class GamePlayData : SingletonTemplate<GamePlayData>
{
}
이전에 만든 싱글턴 템플릿으로 만들었습니다.
MonoBehaviour를 이용하지 않는 이유는 이름 그대로 데이터를 들고 있는 역할을 할 것이기 때문입니다.
public class GamePlayData : SingletonTemplate<GamePlayData>
{
#region Player Entity Controller
PlayerEntityController m_PlayerCtrler;
public static PlayerEntityController PlayerCtrler => Instance.m_PlayerCtrler;
public static void RegistPlayerEntityController(PlayerEntityController playerCtrler)
{
Instance.m_PlayerCtrler = playerCtrler;
}
#endregion
}
우선은 PlayerEntityController를 등록해주도록 구현했습니다.
외부에서 접근하는 코드들을 모두 static으로 한건 그냥 코드 길이 줄이려고 한 겁니다.
protected override void OnEnable()
{
GamePlayData.RegistPlayerEntityController(this);
}
PlayerEntityController에 등록하는 부분입니다.
Vector3 PlayerPosition
{
get
{
if (GamePlayData.PlayerCtrler == null) return Vector3.zero;
return GamePlayData.PlayerCtrler.transform.position;
}
}
이전에 SceneChanger에서 PlayerPosition프로퍼티를 대충 껍데기만 만들어뒀는데, 이제야 속을 채울 수 있게 되었습니다.
3. 캐릭터 동적 생성
현재 캐릭터는 SampleScene에 들어가 있는 상태인데, 다른 씬을 불러오거나 캐릭터가 바뀌게 될 수 있어 미리 작업해두려 합니다.
우선은 프리팹에 붙은 컴포넌트들을 그대로 프리팹과 함께 저장했습니다.
IEnumerator Start()
{
var playerPrefabRequest = Resources.LoadAsync<GameObject>("Prefabs/Character/ModularCharacterPBR_Male");
yield return playerPrefabRequest;
if (playerPrefabRequest.asset != null)
{
var playerObj = GameObject.Instantiate((GameObject)playerPrefabRequest.asset, Vector3.zero, Quaternion.identity);
DontDestroyOnLoad(playerObj);
}
SceneChanger.Instance.ChangeScene("SampleScene");
}
Intro에서 해당 프리팹을 불러오고 Instantiate하는 코드를 추가했습니다.
DontDestoryOnLoad를 추가 한 이유는 씬이 변경될 때 같이 제거되어버리기 때문입니다.
이 부분은 이후에 다시 Intro로 돌아오는 경우가 발생하는 경우 Player를 제거해버리는 코드를 추가하면 문제없을 것 같네요.
"Stop" can only be called on an active agent that has been placed on a NavMesh. UnityEngine.StackTraceUtility:ExtractStackTrace () EntityController:Update () (at Assets/Scripts/Entity/EntityController.cs:53)
아앗... 에러가 났네요.
NavMesh가 없는 곳에서 NavAgent를 움직이려다 보니 발생한 오류 같습니다.
if (m_NavMeshAgent.isOnNavMesh == true)
m_NavMeshAgent.isStopped = true;
간단하게 isOnNavMesh로 NavMesh위에 있을 때만 동작하도록 수정했습니다.
SampleScene을 로드하고 캐릭터를 움직이는 것까지 문제없었으나, 캐릭터를 움직여보니 카메라가 따라가질 않습니다.
CinemachineVirtualCamera에 Follow와 LookAt을 캐릭터의 Transform으로 잡아두었는데, SampleScene에 캐릭터가 사라졌으니 해당 값이 Null인 상태 일 겁니다.
[RequireComponent(typeof(CinemachineVirtualCameraBase))]
public class CinemachinePlayerCamera : MonoBehaviour
{
CinemachineVirtualCameraBase m_VCam;
public CinemachineVirtualCameraBase VirtualCamera
{
get
{
if (m_VCam == null)
{
m_VCam = GetComponent<CinemachineVirtualCameraBase>();
}
return m_VCam;
}
}
PlayerEntityController m_PrevPlayerCtrler = null;
private void OnEnable()
{
Refresh();
}
private void Start()
{
Refresh();
}
private void Update()
{
if (m_PrevPlayerCtrler != GamePlayData.PlayerCtrler)
{
Refresh();
}
}
private void LateUpdate()
{
if (m_PrevPlayerCtrler != GamePlayData.PlayerCtrler)
{
Refresh();
}
}
public void Refresh()
{
if (GamePlayData.PlayerCtrler == null)
{
VirtualCamera.Follow = null;
VirtualCamera.LookAt = null;
}
else
{
VirtualCamera.Follow = GamePlayData.PlayerCtrler.transform;
VirtualCamera.LookAt = GamePlayData.PlayerCtrler.transform;
}
m_PrevPlayerCtrler = GamePlayData.PlayerCtrler;
}
}
간단하게 CinemachineVirtualCamera가 플레이어로 세팅될 수 있도록 컴포넌트를 추가했습니다.
플레이어가 중간에 바뀔 수 있어 Update, LateUpdate에서는 이전 프레임의 플레이어와 비교하는 코드를 추가했습니다.
잘 세팅되었습니다.
4. 메인카메라 수정
어차피 CinemachineVirtualCamera로 대부분 세팅될 텐데 메인카메라가 씬마다 세팅되어야 할 이유가 있을까 싶어서 지워주기로 했습니다.
그리고 메인카메라가 중복으로 존재하면 메인카메라를 빼앗아가는 문제도 있고 AudioListener컴포넌트가 자동 생성되어 "There are 2 audio listeners in the scene."로그에 시달리게 됩니다.
우선은 SampleScene에 있던 메인카메라를 삭제하고, Intro에 메인카메라에 AudioListener, CinemachineBrain 컴포넌트를 세팅해두었습니다.
if (Camera.main != null)
{
DontDestroyOnLoad(Camera.main);
}
간단하게 씬 전환 때 제거되지 않도록 DontDestroyOnLoad로 바꿔주었습니다.
5. 앞으로 할 일
이번에는 간단하게 코드 정리 위주로 진행했고 다음에 해야 할 일들을 정리해봤습니다.
- 맵 관련 기능 구현
- 테스트용 샘플 맵 추가하기
- 워프 기능
- 위치에 따른 맵 설정값 변경하는 기능
- UI
- 이동 관련 디테일 (점프, 낙하 등등)
- 공격, 스킬 기능
- NPC 배치 및 구현
- 저장 기능
순서는 바뀔 수 있을 것 같지만, 이 정도면 어느 정도 뼈대는 잡히지 않을까 싶어요.
'개인프로젝트 일지' 카테고리의 다른 글
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 9일차 (0) | 2022.09.25 |
---|---|
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 8일차 (0) | 2022.09.25 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 6일차 (0) | 2022.09.12 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 5일차 (2) | 2022.09.10 |
유니티로 "젤다의 전설: 꿈꾸는 섬" 모작 4일차 (0) | 2022.09.08 |