여는 말, 

 

이전 포스팅에서, Game Development 과목을 수강하기로 마음을 먹었으며 과제로는 Pacman 게임을 개발하는 것이 과제중 하나 였음을 언급한적이 있습니다. 오늘의 포스팅은, 내가 어떻게 문제해결을 위해 접근을 했고, 그 과정을 통해 내가 깨달은 것을 정리하는 이야기를 포스팅하고자한다. 

 

무엇보다 이번학기에서 가장 난이도를 많이 느낀 과목이기 때문에

 

 


과제 개요

3과 4 과제는 연결된 과제로, 고전 아타리 게임을 재창조하는 것입니다.
여러분은 대부분의 자산을 직접 개발해야 하며,
게임은 원작과 같은 핵심 게임 디자인을 가지고 있지만,

외관과 느낌은 다르게 만들어야 합니다.

주요 디자인 및 개발 중점

 

과제 전반부 

  1. 지정된 유니티 버전 사용.
  2. 게임은 2D로 스프라이트를 사용해야 함.
  3. 최종 게임은 세 개의 씬(메인 메뉴, 원작 레벨 재창조, 디자인 혁신)을 포함해야 함.
  4. 모든 시각 자산은 본인이 직접 제작한 것이어야 하며, 원작의 캐릭터를 복제해서는 안 됨.
  5. 스프라이트 애니메이션을 포함해야 함.
  6. 모든 주요 상호작용에 대한 오디오 포함.
  7. 모든 스크립트는 본인이 직접 작성해야 함.
  8. 유니티의 Rigidbody 물리 기능 또는 CharacterController 컴포넌트 사용 금지.
  9. Tilemap 기능 사용 가능, 그러나 Animated Tile 기능은 금지됨.
  10. 4과제는 3과제를 기반으로 하므로, 3과제에서 놓친 부분이 4과제에 영향을 미칠 수 있음.

 

평가 기준

  • Git 리포지토리 설정 및 커밋 일관성
  • 프로젝트 구조의 조직성
  • 오디오 자산의 완성도
  • 시각 자산의 완성도

 

과제 후반부 

 앞서 언급된
"주요 디자인, 개발 중점" 에서 

 "게임의 주 상호작용,UI 의 배치점수"가 들어간다.
여기서 게임의 주 상호작용은,  Ghost AI 의 움직이 사용자가 얼마나 "플레이" 가능할정도로 만들 수 있느냐를 말한다

 

대충이런것들

 


UI 개발 

 

나의 접근 방법 

 

UI Canvas 의 접근 방법은 쉬웠다, 기초적인 Empty Object 의 을 형성하고, 이름을 변경 "UI Controller" 로 만든 다음, 그리고 UI를 조절할 줄아는 Script 를 할당하는 것. 그리고 그 Script 에 만들어진 public 메쉬에, Scene 에다가 나오는 것들을 할당하는 방법이다. 

 

 

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;
using TMPro;

public class UIManager : MonoBehaviour
{
    public TextMeshProUGUI gameTimerText;
    public TextMeshProUGUI ghostTimerText;
    public TextMeshProUGUI livesText;
    public TextMeshProUGUI countdownText; // Inspector에서 연결
    public TextMeshProUGUI timeText;  // This will reference the TimeText UI element
    public TextMeshProUGUI scoreText; // This will reference the ScoreText UI element

    private float gameTimer;
    private int playerLives = 3; // Starting lives, you can change this value as needed
    private bool isGhostTimerActive = false;
    private float ghostTimerDuration = 10f; // Ghost scared duration, adjust as needed

    // Game Start timers 
    private bool isGameStarted = false;
    private AudioSource backgroundMusic;

    private void Awake()
    {
        DontDestroyOnLoad(gameObject); // Do not destroy this object when loading a new scene
    }

    // Start Game Button function
    public void StartLevel1()
    {
        Debug.Log("Start Button Clicked!");
        SceneManager.LoadScene("Level1");
        //ResetGameTimer();
        //StartCoroutine(ShowCountdown());
        UpdateLivesText();
    }
    public void StartLevel2()
    {
        Debug.Log("Level 2 Button clicked!");
        SceneManager.LoadScene("Level2");

    }


    // Quit Game Button function
    public void QuitGame()
    {

        Debug.Log("Game Quit!");
        Application.Quit();
    }

    public void Level2()
    {
        SceneManager.LoadScene("Level2"); // Return to the start scene
    }

    private void LoadHighScore()
    {
        int highScore = ScoreManager.instance.GetHighScore();
        scoreText.text = "High Score: " + highScore;
  
    }




    private IEnumerator UpdateGhostTimer()
    {
        float timer = ghostTimerDuration;
        while (timer > 0)
        {
            ghostTimerText.text = timer.ToString("F1"); // Display with one decimal place
            timer -= Time.deltaTime;
            yield return null;
        }

        ghostTimerText.text = ""; // Hide timer text when finished
        isGhostTimerActive = false;
        // Reset Ghosts to normal state after the timer ends
        // (You might want to call a function here to handle this)
    }

    // Update Lives Text
    public void UpdateLivesText()
    {
        livesText.text = $"Lives: {playerLives}";
    }

    // Method to decrement lives
    public void LoseLife()
    {
        playerLives--;
        UpdateLivesText();

        if (playerLives <= 0)
        {
            GameOver();
        }
    }

    // Game Over function
    private void GameOver()
    {
        StopAllCoroutines(); // Stop timers
        Debug.Log("Game Over!");
        // Save the score and time here if needed
        SceneManager.LoadScene("StartScene"); // Return to the start scene
    }

    // Reset game timer
    //private void ResetGameTimer()
    //{
    //    gameTimer = 0f;
    //    gameTimerText.text = FormatTime(gameTimer);
    //}
}

 

그 다음 UI Controller 에 Animation 을 담당하는 Script 도 할당해주자, 왜냐하면 개발자(채점자)로 하여금, 이 비어있는 Object 는 UI 만 건드는 것임을 이미 오브젝트의 변수명을 통해 알려주고 있기 때문에, 또 다른 Script 를 생성하는 OOP(객체 지향 프로그래밍) 의 아이디어를 이용 "Animated Border on UI Scene"이라고 붙여 주었다

 

이로써, 어려운 단계는 넘어갔다.

 

 


Game Map 개발 

이는 방법론에 관해서 고민을 많이 했었다, 어떻게 하면 Pacman 이 정해진 "경로"를 따라 움직이면서, 그 경로 위에 "점수" 를 나타내는 Object 가 나타나고, 어떤 "점수 " Object 는 특정한 "효과"를 부여한다. 것도 고민에 넣어야 했었다. 그리고
Map 을 디자인하는 것은 두가지 방법론이 존재했는데,

 

하나는 "손으로 직접 도트를 찍는 것" 이고,

하나는 Pacman 의 이동경로를 "Code" 를 통해 길을 가게 해주는 것이 었다.

 

더 높은 점수를 받기 위해서는, 후자의 선택을 해야했지만. Algorithm 의 선행지식이 없던 나에게는, "손으로 직접 도트를 찍는 " 것을 사용했다. 어느정도 돌아가는 길이고, 지루하고 힘든 일이지만. 그래도 문제가 "해결"이된다면, 그것이 "해결이니까"

 : 그리고 이 방법은 교수님이 추천하는 방법이었다 

 

" 문제의 해결방법은 알겠는데, 어떻게 해야할지는 모를때.

하나, 하나 배치해도 상관없습니다.

User 입장에서 게임이 어떻게 작동이 되는지 확인하는 것 을 가지고 '채점'하는 거니까요"

문제는 해결했다,

 

이로써, 게임의 Node 도 생성이 되었고, 각각 Object 위에 경로도 생성되었다. 이제 남은것은 Pacman 이 정해진 Node 위를 움직이게 하면 모든 것이 끝난다.

 

하지만.. 

 

 

User Input 은 되는데 Object 가 무한히 출동 판정이 난다

 

이는 곧, 팩맨에 입장에서, 내가 보지 못하는 "벽" 이 팩맨앞에 전개가 되어있다는 것을 의미했다. 이 보이지 않는 벽의 문제를 해결하지 못하는 한, 다음 단계로 나아갈 수 없는 것을 의미했다.

 

즉, 내가 의도치 않은 레벨 디자인 

 

출처 :: 나무위키, 보이지 않는 벽

 

이런게 발생하면, 아무리 내가 맵을 잘 짜고 고스트에게 얼마나 좋은 추적 좋은 Algorithm 을 넣었어도, 아무런 의미가 없게 되어버린다.

 

이 문제를 해결하기 위해 2일 내내 머리를 싸매면서 유니티 엔진을 괴롭혔다.

이하 내가 시도한 방법론을 묘사한다 

 

1 . Gizmo 를 할당, 보이지 않는 벽을 개발자 시야에서 육안으로 확인 할 수 있게 한다.

2.  새로운 Scene 에서 Pacman 을 넣어본다, 그리고 Node 를 다시한번 설정한다 

만약, 여기서 아무런 문제가없다면, 내 Scence 에서 다른 것과 충돌하고 있음을 의미한다

3. Prefab 생성 Script 를 확인한다.

4. RayCast 가 잘못되어, 각각 충돌을 하고 있다. (RayCast를 사용한 이유는, 각각 노드를 연결할때 사용한 방법이었다)

 


 

답은 찾지 못했다. 


 

Unity 를 잘하는 친구에게 물어보는 방법도 있었지만,

나의 자존심이 허락하지 못했다

교수님에게 물어봐도,

 

"앵 너 왜케 복잡하게 햇냐, 무튼 작동만 되면 pass 긴해 하니까 상관하지마라"


In Game UI 는 Canvas 를 Camera 에게 Overlay 로 할당하고 , 배경을 어두운 파란색으로 설정해,  Retro Game 의 느낌이 나도록 할당했다. Particle System 을 이용해 Collision method 를 모두 할당했는데, 문제는 위에서 나온 버그로 인해, 이것들이 작동하는지는 확인하지 못했다.

 

즉, 빈 깡통이라는 소리다.

개발자가 자기가 만든 게임을 확인하지 못한다니.

이 게임은 똥 쓰레기이다.

 

<<내부 Scene 에서 확인하지 못해, 개발하지 못하고 일단 작동만 확인한 코드이다>>

using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;

public class PacStudentCollision : MonoBehaviour
{
    private Animator animator;
    private AudioSource audioSource;

    public int score = 0;
    public GameObject[] lives;

    public float ghostScaredDuration = 10f;
    public GameObject collisionParticles;
    public AudioClip wallSound;
    public AudioClip powerPillSound;
    public AudioClip ghostScaredMusic;
    public AudioClip ghostNormalMusic;

    private float scaredTimer;
    private bool ghostsScared = false;
    private int currentLives;

    private AudioSource wallAudioSource;
    private AudioSource musicAudioSource;

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();

        // Initialize lives count
        currentLives = lives.Length;

        // Set up additional audio sources
        wallAudioSource = gameObject.AddComponent<AudioSource>();
        musicAudioSource = gameObject.AddComponent<AudioSource>();

        // Set looping for background music sources
        musicAudioSource.loop = true;
        musicAudioSource.clip = ghostNormalMusic;
        musicAudioSource.Play();
    }

    void Update()
    {
        if (ghostsScared)
        {
            scaredTimer -= Time.deltaTime;
            if (scaredTimer <= 0)
            {
                EndGhostScaredState();
            }
        }
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        switch (other.tag)
        {
            case "Wall":
                // Play wall collision sound
                wallAudioSource.PlayOneShot(wallSound);

                // Create collision particles
                GameObject particles = Instantiate(collisionParticles, transform.position, Quaternion.identity);
                Destroy(particles, 1.0f); // Destroy particles after 1 second
                break;

            case "Dots":
                score += 10;
                other.gameObject.SetActive(false);
                break;

            case "Bonus Cherry":
                score += 100;
                other.gameObject.SetActive(false);
                break;

            case "PowerPill":
                audioSource.PlayOneShot(powerPillSound);
                StartGhostScaredState();
                other.gameObject.SetActive(false);
                break;

            case "Ghost":
                if (!ghostsScared)
                {
                    HandlePlayerDeath();
                }
                else
                {
                    score += 300;
                }
                break;
        }
    }

    private void StartGhostScaredState()
    {
        scaredTimer = ghostScaredDuration;
        ghostsScared = true;

        // Switch to ghost scared music
        musicAudioSource.Stop();
        musicAudioSource.clip = ghostScaredMusic;
        musicAudioSource.Play();
    }

    private void EndGhostScaredState()
    {
        ghostsScared = false;

        // Switch back to normal ghost music
        musicAudioSource.Stop();
        musicAudioSource.clip = ghostNormalMusic;
        musicAudioSource.Play();
    }

    private void HandlePlayerDeath()
    {
        currentLives--;
        if (currentLives >= 0)
        {
            lives[currentLives].SetActive(false);
        }

        Instantiate(collisionParticles, transform.position, Quaternion.identity);

        if (currentLives <= 0)
        {
            GameOver();
        }
        else
        {
            RespawnPacStudent();
        }
    }

    private void RespawnPacStudent()
    {
        transform.position = new Vector3(-25, -1, 0);
    }

    private void GameOver()
    {
        Debug.Log("Game Over");
        SceneManager.LoadScene("GameOverScene");
    }
}

 


결어

 

이렇게 해도 사실 75% 를 넘었기 때문에,

점수는 받겠지만은

, 아쉬움이 많이 남는다.

 

이제 Unreal Engine을 배울거야(유니티가 언리얼의 선수과목이었기 때문)

출처 :: Google

유니티 좋아.

6년동안의 인연이, 이제야 너를 알게되는구나

+ Recent posts