오늘 학습 키워드
리팩토링 + 도전 과제 시도
오늘 학습 한 내용을 나만의 언어로 정리하기
리팩토링
NPC, Interactive?
- 생각해보니까 모든 NPC를 따로 클래스를 만들 필요가 있나 싶어서. 싹 다 지우고 메시지를 public으로 돌려서 넣음
- 그리고 interactive 랑 npc랑 둘 다 결국 상호작용한다는건 같으니까, 그냥 BaseInteractive 로 합쳐버리기로 함
- 결과
public abstract class BaseInteractive : MonoBehaviour
{
[SerializeField] private string name;
public string Name { get => name; set => name = value; }
public virtual void Interact()
{
Debug.Log($"{Name} 상호작용 시작");
return;
}
}
public class BaseNPC : BaseInteractive
{
[SerializeField] private GameObject messagePivot;
public GameObject MessagePivot { get => messagePivot; set => messagePivot = value; }
protected TextMeshProUGUI message;
[SerializeField] protected string npcMessage;
public string NpcMessage { get => npcMessage; }
protected virtual void Start()
{
if (MessagePivot == null)
{
Debug.LogError($"{Name}'s Message Pivot is Null");
}
message = MessagePivot.GetComponentInChildren<TextMeshProUGUI>();
message.text = NpcMessage;
MessagePivot.SetActive(false);
return;
}
public override void Interact()
{
base.Interact();
ShowMessage();
}
public void ShowMessage()
{
CancelInvoke();
Debug.Log($"{Name} NPC와 상호작용");
MessagePivot.SetActive(true);
Invoke("EndMessage", 2f);
return;
}
public void EndMessage()
{
MessagePivot.SetActive(false);
return;
}
}
메타버스는 게임매니저가 없는데요?
- 지금 사실상 PlayerController가 역할을 죄다 가지고있음.
- 예를 들어, Flappy Plane 게임을 하다가 나왔을 때 플레이어의 마지막 위치를 기억했다가 돌아오는 건 플레이어 자체에서 하는 것보다 게임매니저가 하는게 맞는듯.
- 그리고 애니메이션 깜빡하고 안넣었음..
// 애니메이터 파라미터를 이름으로 하드코딩 하지 말고, 아래 방법을 쓰자
private static readonly int IsMoving = Animator.StringToHash("IsMove");
- 수정 전 :
// 함수 내용은 지웠음. 그냥 할일이 많았다는 것만 표현하려고..
public class PlayerController : MonoBehaviour
{
...
private void Awake()
{
}
// Update is called once per frame
void Update()
{
}
private void FixedUpdate()
{
}
private void OnEnable()
{
// 옮길 예정
}
private void OnDisable()
{
// 옮길 예정
}
public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 옮길 예정
}
private void Rotate(Vector2 direction)
{
}
private void Movement(Vector2 direction)
{
}
void OnMove(InputValue inputValue)
{
}
void OnLook(InputValue inputValue)
{
}
void OnInteraction(InputValue inputValue)
{ }
}
- 수정 후
// PlayerController.cs
public class PlayerController : MonoBehaviour
{
...
private void Awake()
{
}
// Update is called once per frame
void Update()
{
}
private void FixedUpdate()
{
}
private void Rotate(Vector2 direction)
{
}
private void Movement(Vector2 direction)
{
}
void OnMove(InputValue inputValue)
{
}
void OnLook(InputValue inputValue)
{
}
void OnInteraction(InputValue inputValue)
{
}
}
// GameManager.cs
public class GameManager : MonoBehaviour
{
...
private void Awake()
{
}
private void OnEnable()
{
}
private void OnDisable()
{
}
public void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
}
}
도전 과제 진행
추가 미니게임
- 아이디어 : 코드 슈터. 왼쪽 마우스 클릭을 하면 코드가 키보드에서 나감. 그걸로 NPC 맞추기
고민
- GameManager가 미니게임마다 똑같이 생겼는데 어떻게 하지
- 해결 방안 : Global.GameManager를 만들어서 걔를 상속하도록 함
총알 구현까지는 쉽게 진행함
NPC 랜덤 생성기
- 아이디어 : Rect Transform으로 NPC가 생성될 구역을 지정함
- 문제 : 그러면 그 안에서 랜덤하게 생성하게 하려면 어떻게 해야할까? 최소X/Y와 최대X/Y를 어떻게 구해야 할까?
- 검증 : 직접 Rect Transform 내부에 Empty GameObject를 생성한 후, 위치를 돌려가면서 확인 해 보았다.
- 결과 : 결과는 다음과 같았다. (앵커는 오타. 피벗임)
-
추가 문제 : 그럼 피벗의 위치에 따라서 달라질까?
-
추측 : 만약 피벗이 좌측 하단이라면, min x = 0, max x = width, min y = 0, max y = height
-
검증 : 아까와 같은 방식으로 Rect Transform 내부에 Empty GameObject를 생성해보기로 함. (피벗 위치를 바꾸고)
-
결과 : 아니었다. 결과는 피벗이 0.5 / 0.5 일 때와 같았다
-
결론 : Rect Transform 밑에 있는 GameObject의 Local Position 범위는 Rect Transform의 피벗 위치와 상관 없이 X : (-width/2, width/2) Y : (-height/2, height/2) 이다.
-
그리하여 만들어진 SpawnManager
public class SpawnManager : MonoBehaviour
{
[Header("Game Settings")]
[SerializeField] private float spawnDelay;
public float SpawnDelay { get { return spawnDelay; } }
[SerializeField] public Sprite[] npcSprites;
public Sprite[] NPCSprites { get { return npcSprites; } }
[SerializeField] private GameObject npcPrefab;
public GameObject NPCPrefab { get { return npcPrefab; } }
GameManager gameManager;
RectTransform rectTransform;
float XRange;
float YRange;
private void Awake()
{
gameManager = FindObjectOfType<GameManager>();
rectTransform = GetComponent<RectTransform>();
if (NPCSprites.Length == 0)
Debug.LogError("No Sprites");
}
private void Start()
{
XRange = rectTransform.sizeDelta.x / 2;
YRange = rectTransform.sizeDelta.y / 2;
}
public void GameStart()
{
InvokeRepeating("SpawnEnemy", 0.2f, SpawnDelay);
}
public void Pause()
{
CancelInvoke();
}
public void SpawnEnemy()
{
int spriteNum = Random.Range(0, npcSprites.Length);
Sprite sprite = npcSprites[spriteNum];
Vector3 randomPos = new Vector3(
Random.Range(-XRange, XRange),
Random.Range(-YRange, YRange)
);
GameObject go = Instantiate(NPCPrefab, randomPos, Quaternion.identity);
Enemy enemy = go.GetComponent<Enemy>();
enemy.Init(gameManager);
Debug.Log($"Sprite Number : {spriteNum}");
enemy.SetSprite(sprite);
}
}
중간 리팩토링
- 막 짜다보니 각 UI가 gameManager를 직접적으로 호출하거나 불러오는 경우가 있었는데, 무조건 UIManager를 통해서 값을 전달하도록 변경함.
학습하며 겪었던 문제점 & 에러
문제 1
- 문제&에러에 대한 정의
Global.GameManager 안에는 이런 코드가 있었다.
protected string bestScoreKey;
...
public virtual void GameOver()
{
Debug.Log("Game Over!");
int bestScore = PlayerPrefs.GetInt(bestScoreKey, 0);
if (currentScore > bestScore)
{
bestScore = currentScore;
PlayerPrefs.SetInt(bestScoreKey, bestScore);
}
Debug.Log($"key : {bestScoreKey} | score : {bestScore}");
}
Global.GameManager를 상속받는 다른 미니게임들의 GameManager는 이렇게 구성했다.
private void Awake()
{
instance = this;
bestScoreKey = "FlappyBestScore";
uiManager = FindObjectOfType<UIManager>();
}
...
public override void GameOver()
{
base.GameOver();
Debug.Log($"key : {bestScoreKey} | score : {bestScore}");
uiManager.SetGameOver(currentScore, bestScore);
}
목적은 Global.GameManager에 bestScoreKey를 미니게임.GameManager에서 각각 설정해 주고, GameOver시에는 base.GameOver()를 실행한 뒤에 각각 연결된 uiManager에 SetGameOver를 호출하려는 생각이었다.
분명 Global.GameManager의 디버그 로그에서는 제대로 bestScore가 불러와졌음에도 불구하고
그런데, 미니게임.GameManager의 디버그 로그를 보았을 때 bestScoreKey가 비어있었다.
- 내가 한 시도
- FlappyPlane.GameManager에서, 아래의 코드를 변수 부분에 추가 하였으나 실패하였다.
protected new string bestScoreKey = "FlappyBestScore";
- FlappyPlane.GameManager에서, void GameOver() 부분을 아래와 같이 변경하였으나 실패하였다.
public override void GameOver()
{
Debug.Log($"key : {bestScoreKey} | score : {bestScore}");
uiManager.SetGameOver(currentScore, base.bestScore);
}
- 결국, Global.GameManager의 GameOver를 abstract로 만들고, 각각의 미니게임.GameManager에서 아래의 코드로 변경하였다.
public override void GameOver()
{
Debug.Log("Game Over!");
int bestScore = PlayerPrefs.GetInt(bestScoreKey, 0);
if (currentScore > bestScore)
{
bestScore = currentScore;
PlayerPrefs.SetInt(bestScoreKey, bestScore);
}
Debug.Log($"key : {bestScoreKey} | score : {bestScore}");
uiManager.SetGameOver(currentScore, bestScore);
}
- 다만 이렇게 하면 코드가 중복됨…
- 월요일에 튜터님들께 여쭤볼 예정.
문제 2
- 문제&에러에 대한 정의
Player에 Pause 키 입력을 구현하고,
Pause를 누르면 일시정지 되도록 한뒤에 다시 눌렀을 경우에는 일시정지가 풀리도록 작업했음
Pause를 눌렀을 때 플레이어를 SetActive(false)를 해서 숨기게 했음
근데 그러다보니 Player Input 자체도 SetActive(false)가 되어서 돌아가지지가 않았음
- 내가 한 시도
그러면 일시정지는 게임매니저가 가지고 있게 하자! 는 아이디어로 부딛혀봄
- 해결 방법
일시정지를 게임 매니저가 가지고 있게 했더니 성공함
- 새롭게 알게 된 점
SetActive(false)를 해버리면 그 안에 있는 스크립트도 전부 작동을 멈추니 주의할 것.
- 이 문제&에러를 다시 만나게 되었다면?
캐릭터 조작 외의 다른 유틸적인 부분들은 조작을 따로 빼자
문제 3
- 문제&에러에 대한 정의
CodeShooter를 만드는 중에 생긴 일.
Bullet 은 BoxCollider2D와 RigidBody2D를 가지고 있었음.
Enemy는 BoxCollider2D만 가지고 있었음.
Bullet이 Enemy를 맞으면 Enemy의 Die를 호출하고 자기 자신을 파괴하도록 하였음.
OnCollisionEnter2D를 사용함.
근데, Debug.Log를 사용했는데도 애초에 충돌했다는 문구가 안뜸.
- 내가 한 시도
충돌 처리를 Bullet이 아니고 Enemy에서 하게 해봤음
- 해결 방법
성공함!
- 새롭게 알게 된 점
충돌 처리를 어디서 하느냐에 따라 좀 다른듯.. 근데 왜 안됐는지 모르겠음. 이것도 월요일에 튜터님 찾아 뵐 예정
내일 학습 할 것은 무엇인지
로컬 리더보드 만들고, 커스텀 캐릭터 만들고… 탑승물까지 구현하기