여는 말,
이전 포스팅에서, Game Development 과목을 수강하기로 마음을 먹었으며 과제로는 Pacman 게임을 개발하는 것이 과제중 하나 였음을 언급한적이 있습니다. 오늘의 포스팅은, 내가 어떻게 문제해결을 위해 접근을 했고, 그 과정을 통해 내가 깨달은 것을 정리하는 이야기를 포스팅하고자한다.
무엇보다 이번학기에서 가장 난이도를 많이 느낀 과목이기 때문에
과제 개요
3과 4 과제는 연결된 과제로, 고전 아타리 게임을 재창조하는 것입니다.
여러분은 대부분의 자산을 직접 개발해야 하며,
게임은 원작과 같은 핵심 게임 디자인을 가지고 있지만,
외관과 느낌은 다르게 만들어야 합니다.
주요 디자인 및 개발 중점
과제 전반부
- 지정된 유니티 버전 사용.
- 게임은 2D로 스프라이트를 사용해야 함.
- 최종 게임은 세 개의 씬(메인 메뉴, 원작 레벨 재창조, 디자인 혁신)을 포함해야 함.
- 모든 시각 자산은 본인이 직접 제작한 것이어야 하며, 원작의 캐릭터를 복제해서는 안 됨.
- 스프라이트 애니메이션을 포함해야 함.
- 모든 주요 상호작용에 대한 오디오 포함.
- 모든 스크립트는 본인이 직접 작성해야 함.
- 유니티의 Rigidbody 물리 기능 또는 CharacterController 컴포넌트 사용 금지.
- Tilemap 기능 사용 가능, 그러나 Animated Tile 기능은 금지됨.
- 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 위를 움직이게 하면 모든 것이 끝난다.
하지만..
이는 곧, 팩맨에 입장에서, 내가 보지 못하는 "벽" 이 팩맨앞에 전개가 되어있다는 것을 의미했다. 이 보이지 않는 벽의 문제를 해결하지 못하는 한, 다음 단계로 나아갈 수 없는 것을 의미했다.
즉, 내가 의도치 않은 레벨 디자인
이런게 발생하면, 아무리 내가 맵을 잘 짜고 고스트에게 얼마나 좋은 추적 좋은 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을 배울거야(유니티가 언리얼의 선수과목이었기 때문)
유니티 좋아.