오늘 학습 키워드

리팩토링 + 도전 과제 시도

오늘 학습 한 내용을 나만의 언어로 정리하기

리팩토링

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가 비어있었다.

  • 내가 한 시도
  1. FlappyPlane.GameManager에서, 아래의 코드를 변수 부분에 추가 하였으나 실패하였다.
protected new string bestScoreKey = "FlappyBestScore";
  1. FlappyPlane.GameManager에서, void GameOver() 부분을 아래와 같이 변경하였으나 실패하였다.
public override void GameOver()
{
    Debug.Log($"key : {bestScoreKey} | score : {bestScore}");
    uiManager.SetGameOver(currentScore, base.bestScore);
}
  1. 결국, 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에서 하게 해봤음

  • 해결 방법

성공함!

  • 새롭게 알게 된 점

충돌 처리를 어디서 하느냐에 따라 좀 다른듯.. 근데 왜 안됐는지 모르겠음. 이것도 월요일에 튜터님 찾아 뵐 예정

내일 학습 할 것은 무엇인지

로컬 리더보드 만들고, 커스텀 캐릭터 만들고… 탑승물까지 구현하기