내일배움캠프 2일차 TIL - 협업의 장점

백흰범·2024년 4월 16일
0

오늘 한 일

  • 결과에 점수 표시하기
  • 최단 시간을 플레이 중 화면에서 띄우기

+ 추가로

  • 다른 조원 분들의 코드 검사 및 수정하기
  • 그 외에 새로 알게된 요소



결과에 점수 표시하기

점수에 대한 고민

남은 시간만큼 시간 점수 배율과 곱해서 점수를 주면 될 것 같고
매칭 점수는 맞춘카드 맞춘 보너스 - 매칭 횟수 매칭 페널티로 계산해주면 될 것 같다.
처음에는 그냥 1000점 - 매칭 횟수 * 매칭 페널티로 생각했지만,
아무것도 안하고 가만히 있으면 1000점이기 때문에 게임의 뱡향성에 옳지 않다고 생각했다.


코드 짜기

필드에 이러한 오브젝트와 변수를 선언해주자.

**public Text timeLeftTxt;**        // 남은 시간 텍스트
**public Text scoreTxt;**        // 점수 텍스트
**public int cardMaxCount = 0;**    // 최대 카드 갯수 (맞춘 수도 점수에 반영할 것이다.)
**int timeBonus = 100;**        // 시간 점수로 'time * timeBonus'로 계산할 예정
**int matchBonus = 100; **   // 카드를 맞추면 얻는 점수로 'matchBonus * 맞춘 수'로 계획
**int matchPenalty = 10;**     // 매칭한 횟수만큼 점수를 깎는다. 'matchCount * matchPenalty'

Board.cs에서 총 카드 갯수를 할당 받기

(Board.cs Start 함수 최하단)
**GameManager.Instance.cardMaxCount = arr.Length;**

게임 오버에 반영해줄 점수 계산 함수를 만들자.

     void ScoreCalculate()
    {
        int timeScore = (int)time * timeBonus;
        int matchScore = (matchBonus * (cardMaxCount - cardCount)) - (matchCount * matchPenalty);
    // (매치보너스 * (총 카드 갯수 - 남은 카드 갯수)) - (매칭 시도 횟수 * 매칭 페널티)
        if (matchScore < 0 || timeScore < 0) { matchScore = 0; timeScore; } // 음수 처리
        int score = timeScore + matchScore;
        scoreTxt.text = score.ToString();
    }

게임 오버 함수를 건드려주자

    void GameOver()
    {
        Time.timeScale = 0.0f;
        **matchNum.text = "매칭 시도 횟수 : " + matchCount.ToString();
        timeLeftTxt.text = "남은 시간 : " + time.ToString("N2");
        ScoreCalculate();**
        endPanel.SetActive(true);
    }

UI를 만들어주자

TimeLeftTxt (Duplicate MatchTxt)

  • position -> 0, -175, 0
    • 초기 텍스트 - 남은 시간 0.00

ScoreTxt (Duplicate MatchTxt)

  • position -> 0, -260, 0
    Width 1000, Height 200
    • Font Size - 80
    • 초기 텍스트 - 0

// 이제 적절히 컴포넌트에 오브젝트만 넣어주면 완성이다.


코드 정리

(GameManager.cs)
    선언 필드

public Text timeLeftTxt;
public Text scoreTxt;

public int cardMaxCount = 0;    
int timeBonus = 100;
int matchBonus = 100;
int matchPenalty = 10;

void Update()
{
    time -= Time.deltaTime;
    timeTxt.text = time.ToString("N2");
    if (time <= 0) 
    {
        time = 0;    // 시간이 깎여서 음수가 되는 상황을 대비
        GameOver();
    }
}

void GameOver()
{
    Time.timeScale = 0.0f;
    matchNum.text = "매칭 시도 횟수 : " + matchCount.ToString();
    timeLeftTxt.text = "남은 시간 : " + time.ToString("N2");
    ScoreCalculate();
    endPanel.SetActive(true);
}

void ScoreCalculate()
{
    int timeScore = (int)time * timeBonus;
    int matchScore = (matchBonus * (cardMaxCount - cardCount)) - (matchCount * matchPenalty);
    if (matchScore < 0 || timeScore < 0) { matchScore = 0; timeScore = 0; } // 음수 처리
    int score = timeScore + matchScore;	// 점수 계산 함수 호출
    scoreTxt.text = score.ToString();   // 점수 텍스트에 반영
}

(Board.cs)
GameManager.Instance.cardMaxCount = arr.Length;

결과물




최고 시간을 플레이 중에 표시하기

무엇을 기준으로 삼아서 표시할까?

무엇을 표시할까? (최단 시간?, 남는 시간?)
최단 시간으로 갈까 아니면 많이 남는 시간으로 갈까?
// 남은 시간이 높냐에 따라서 따지기로 했다. (제일 직관적이라고 생각함.)
그리고 추가적인 계산을 할 필요도 없어서 좋다.


코드 짜기

다시 만나는 반가운 코드

	PlayerPrefs의 메서드들
    // .SetFloat("담아낼 변수", 할당할 데이터), .GetFloat("가져올 내용이 담긴 변수")
    // .HasKey("지정한 변수") <- 있다면 true 없다면 false를 반환한다.

일단 필요한 변수부터 작성하자

public GameObject bestTimePanel;	// 비활성화 전용
public Text bestTimeTxt;		  	// 최고 시간 표시용

게임이 끝나고 시간을 할당해줄 함수를 만들자

	// (기본형)
void BestTime()
{
    if (PlayerPrefs.HasKey("bestTime"))    // 최고 시간이 있나요?
    {
        float bestTime = PlayerPrefs.GetFloat("bestTime"); // 최고 시간을 가지고 온다
        if (time > bestTime)        //남은 시간이 최고 시간 보다 많나요?
        {
            PlayerPrefs.SetFloat("bestTime", time);    // 갱신해준다.
        }
    }
    else
    {
        PlayerPrefs.SetFloat("bestTime", time);        // 갱신해준다.
    }
}

	// (압축형)
void BestTime()
{
    if (PlayerPrefs.HasKey("bestTime"))
    {
        float bestTime = PlayerPrefs.GetFloat("bestTime");
        if (bestTime > time) { return; } // 최고 시간이 있는데, 최고 시간이 더 높을 경우에 함수에서 나온다.
    }
    PlayerPrefs.SetFloat("bestTime", time);
}

최고 시간이 있다면 보여주고 없으면 가린다.

void ShowBestTime()
{
    if (PlayerPrefs.HasKey("bestTime"))
    {
        bestTimeTxt.text = PlayerPrefs.GetFloat("bestTime").ToString("N2");
    }
    else    // 없으면 숨기기로 했다.
    {
        bestTimePanel.SetActive(false);
    }
}

UI 만들기

BestTimePanel(Legacy - Text)

  • Pos x : -65 y: 560 z : 0
    W : 300 H : 80

    • Text
      최고시간 :

    • Font
      ~ 배민조아
      ~ size : 50
      ~ color : 255 255 0

BestTimetxt (Duplicate BestTimePanel)

[ BestTimePanel의 하위 관계 ]

  • Pos x : 180 y : 0 z : 0
    W : 200 H : 75

    • Text
      초기 텍스트 : 00.00


// 항상 오브젝트 집어넣는 걸 잊지 말자.

만들어놓은 함수를 호출하자

void Start()
{
    Time.timeScale = 1.0f;
    audioSource = GetComponent<audioSource>();
    ShowBestTime();
}

(Matched 함수 내부)
 if (cardCount == 0) // 더 이상 카드가 없다면
 {
     BestTime();     // 게임을 해냈으니 최단 시간 함수 호출
     GameOver();  
 }

스테이지별 최고 시간 나누기

stage를 구별해줄 변수 선언

string stageBestTime = “bestTime”;
void Start ()
{
    **stageBestTime = level + stageBestTime;**     // 나중에 PlayerPrefs를 구별할 때 사용
    (ShowBestTime() 이전에)
}

그리고 PlayerPrefs를 사용하는 모든 괄호 안에
stageBestTime 변수를 넣어주면 된다!


코드 정리

// (GameManager.cs)

    // 최단 시간 표시 오브젝트
    public GameObject bestTimePanel;
    public Text bestTimeTxt;

    // stage별 최고 시간을 구별하기 위한 변수
    string stageBestTime = "bestTime";

    void Start()
    {
        Time.timeScale = 1.0f;
        audioSource = GetComponent<AudioSource>();
        stageBestTime = level + stageBestTime;     // 스테이지에 따른 변수 변경
        ShowBestTime();	   // 최단 시간 보여주기 
    }

	// Matched 함수 내 일부분...
            if (cardCount == 0) // 더 이상 카드가 없다면
            {
                BestTime(); 	// 게임을 해냈으니 최단 시간 할당하는 함수 호출
                GameOver()
            }

    void BestTime()
    {
        if (PlayerPrefs.HasKey(stageBestTime))
        {
            float bestTime = PlayerPrefs.GetFloat(stageBestTime);
            if (bestTime > time) { return; }
        }
        PlayerPrefs.SetFloat(stageBestTime, time);
    }

    void ShowBestTime()
    {
        if (PlayerPrefs.HasKey(stageBestTime))
        {
            bestTimeTxt.text = PlayerPrefs.GetFloat(stageBestTime).ToString("N2");
        }
        else
        {
            bestTimePanel.SetActive(false);
        }
    }

결과물

(레벨이 다를 때마다 최고 시간이 다르게 나온다.)




다른 조원 분들의 코드 검사 및 수정하기

  • 여러가지를 검사하고 수정했지만 간략하게 필요한 것만 정리해보려고 한다.

이미 한번 뒤집은 카드 색상 변경하기

수정 전

public void Matched()
{
    SpriteRenderer firstCardBackRenderer = firstCard.transform.Find("Back").GetComponent<SpriteRenderer>();
    SpriteRenderer secondCardBackRenderer = secondCard.transform.Find("Back").GetComponent<SpriteRenderer>();
   

    if (firstCard.idx == secondCard.idx) 
    {
        // 파괴해라.
        audioSource.PlayOneShot(clip);
        firstCard.DestroyCard();
        secondCard.DestroyCard();
        cardCount -= 2;
        if(cardCount == 0)
        {
            Time.timeScale = 0.0f;
            endTxt.SetActive(true);
        }
    }
    else
    {
        // 회색 색상 설정
        Color greyColor = new Color(0.5f, 0.5f, 0.5f);
        firstCardBackRenderer.color = greyColor;
        secondCardBackRenderer.color = greyColor;

        // 닫아라.
        firstCard.CloseCard();
        secondCard.CloseCard();
    }

작동에는 문제없지만, 다른 코드들끼리 구별이 안되어 있어서 작업이 좀 더 복잡해진다면 혼란스러울 수도 있다.

수정 후

// (GameManager.cs)

	// (Matched 함수 내)
	(매칭되지 않았을 경우)
        else
        {
            firstCard.CloseCard();
            secondCard.CloseCard();
            audioSource.PlayOneShot(clip2);
            penaltyTxt.SetActive(true);
            time -= 0.5f;
            Invoke("closeTxt", 0.5f);
            Invoke("ChangeColor", 0.5f);	// 색깔 변경 함수 호출
        }
        matchCount++;
        Invoke("CleanCard", 1f);

    void ChangeColor()
    {
        SpriteRenderer firstCardBackRenderer = firstCard.transform.Find("Back").GetComponent<SpriteRenderer>();
        SpriteRenderer secondCardBackRenderer = secondCard.transform.Find("Back").GetComponent<SpriteRenderer>(); 
        Color greyColor = new Color(0.5f, 0.5f, 0.5f);
        firstCardBackRenderer.color = greyColor;
        secondCardBackRenderer.color = greyColor;
    }

색깔이 변하는 코드를 따로 함수로 감싸서 가독성 좋게 정리했다.


매칭 되었을 때 카드 이름 표시하기

! 임시로 이름을 바꿔놓았습니다.

수정 전

(Card.cs)		

public string cardName; // 추가
public Text cardText;   // 추가

    private void SetCardName()      //추가
    {
        switch (idx)
        {
            case 0:
                cardName = "이름1";
                break;
            case 1:
                cardName = "이름2";
                break;
            case 2:
                cardName = "이름3";
                break;
            case 3:
                cardName = "이름4";
                break;
            case 4:
                cardName = "이름5";
                break;
            case 5:
                cardName = "이름6";
                break;
            default:
                cardName = "르탄이";
                break;
        }

        if (cardText != null)
        {
            cardText.text = cardName;
        }
    }                           // 여기까지 새로 추가입니다.

// 위 코드의 문제점
카드에 이름 정보를 표시하긴 했지만 그 정보를 텍스트에다가 전달하는 코드가 없다.
물론 그 코드가 있더라도 원하는 대로 구현하기는 힘들 것이다.
[카드가 매칭되었는지 판단할 함수가 이 스크립트에는 없다.]

+ 왜 Card.cs에서 텍스트에 정보를 기입하기 힘들까?

  • Text 오브젝트를 가져오려면 컴포넌트에 할당해줘야하지만 Card 객체는 Prefabs인 관계로 에러를 방지하기 위해 막혀 있다.
    (씬을 로드하거나, 해당 오브젝트가 없는 씬에서 불러졌을 경우 에러가 발생하는 걸 막기 위해 기본적으로 막혀있다.)

Prefab에서 Hierarchy에 있는 객체를 할당시키는 방법

	nameObject = GameObject.Find("NameTxt");
	nameText = nameObject.GetComponent<Text>(); // 이렇게하면 Text에 접근이 가능하다.

수정 후

// (Card.cs)
public string cardName;

    private void SetCardName()      
    {
        switch (idx)
        {
            case 0:
                cardName = "이름1";
                break;
            case 1:
                cardName = "이름2";
                break;
            case 2:
                cardName = "이름3";
                break;
            case 3:
                cardName = "이름4";
                break;
            case 4:
                cardName = "이름5";
                break;
            case 5:
                cardName = "이름6";
                break;
            default:
                cardName = "르탄이";
                break;
        }
    }

public void Setting(int number)
    {
          ...
        SetCardName();
    }

// (GameManager.cs)

    public Text nameText;   // name을 보여줄 텍스트

  // Matched 함수 내
    if (firstCard.idx == secondCard.idx)	//만약에 카드가 맞다면
    {
        audioSource.PlayOneShot(clip);
        ShowName();	// 이름을 보여준다.
        firstCard.DestroyCard();
        secondCard.DestroyCard();
        cardCount -= 2;

    void ShowName()
    {

        nameText.text = secondCard.cardName;
        Invoke("EraseName", 1f);	// 이름을 보여줬다가 딜레이 이후 사라진다.
    }

    void EraseName()
    {
        nameText.text = null;
    }

이와 같이 기능에 따라 역할을 분담하여 역할 간의 혼란을 방지하고
Prefabs에 할당 불가능으로 인한 Text에 내용을 전달할 수 없는 문제를 해결했다.


! 협업은 나에게 큰 이점이 있다.


다른 사람의 도움 없이 나 혼자서 프로젝트를 진행하고 코드를 짜는 것도 물론 멋있는 일이기도 하지만, 협업을 통해서 내가 배우고 알아가는 질과 양은 혼자서 배우는 것보다 더 많은 것을 알게될 수 있다.

일단은 협업을 하게 된다면,

  • 다른 사람은 알지만 내가 모르는 코드들에 대한 정보를 알게 되고 (Coroutine 등)

다른 사람이 직면한 버그나 에러를 통해서

  • 유니티 엔진의 기능이나 각종 설정 그리고 C#의 특성을 보다 더 깊이 알 수 있다. (flow의 이해와 각종 에러에 대한 대비책 증가)

그리고 협업을 하게된다면 필수불가결적으로

  • 개발에 관한 소통능력이 무조건 오르게 되고, 그것을 받쳐주는 코드 해석 능력이 오르게된다.

타인과 공동 프로젝트에서 내가 짠 코드를 설명하기 위해 코드에 대해 더 깊이 알게되어서 협업을 하는 것 자체만으로 학습 동기와 학습 방향성을 둘 다 챙길 수 있는 좋은 공부법인 것 같다.

그리고 내가 놓쳤던 부분을 다른 사람이 채워줌으로써 코드의 안정성과 다양성이 상승하게 되어서 개인이 만들기 힘든 규모의 프로젝트라면 협업은 프로젝트에 있어서 필수적인 요소라고 생각한다.




그 외에 새로 알게된 요소

오브젝트를 Duplicate 할 때 딸려오는 C# 스크립트들을 조심해라

숨어있어서 찾기도 힘드니까 유의해야한다.
그래서 Text에는 C# 스크립트를 잘 안 넣는건가?

UI Text의 공간은 다른 오브젝트들의 클릭을 방해한다.

너무 공간을 크게 잡진 않았는지 확인해보자.

Animator에 있는 Has Exit Time은 되도록 쓰지 마라

해당 애니메이션의 끝나는 시간도 알아야하는 것과 잘못 사용하면 겹쳐서 원하는 애니메이션이 재생되지 않는다.
(비활성화와 Has Exit Time 0초는 엄연히 다르다;)

기능을 추가하기 전에 따로 복사본으로 테스트를 해라

그래야 어질러진 본 프로젝트를 다시 정리할 필요가 없다.




오늘 하루 공부를 하면서 느낀 점

내일배움캠프의 공부 일정이 아직 익숙하진 않지만, 그럼에도 많은 것을 배워서 좋은 것 같다. 익숙해지기 전까지는 페이스 조절이 안되지만 익숙해진다면 배운 내용들의 정리를 좀 더 잘할 수 있을 것 같다.

그리고 협업을 통해서 혼자서 게임을 만들어보고 싶어하는 내가 팀 프로젝트에 대해 고려해본 적 없었지만, 이번 경험에서 팀 프로젝트의 장점을 잘 알게되어 추후에 마음이 맞는 개발자끼리 모여서 개발해보고 싶은 생각이 든다.




내일 배우게될 내용

  • 깃허브 연동 및 사용해보기
profile
게임 개발 꿈나무

0개의 댓글