AWS TechCamp를 수강하게 된 것은 X(구 Twitter)를 통해 추천이 있었기에, 좋은 기회다 싶어 지원을 했었다
진행은 한국어로 친절히 진행되었고, 설령 라이브 방송을 놓치게 될지라도, 온라인 Material을 통해서, 보이는 슬라이드를 천천히 따라 하면 AWS를 이용하고 세팅하는 과정을 쉽게 배울 수 있었다. 호주에서 Cloud Computing as a Service 과목을 높은 점수로 이수했기 때문에, 잘 듣지는 않았지만, 본래 정말 모르는 사람에게도 손쉽게 설명해 주시는 친절함 속에서 부담감 없이 이수가 가능했던 것 같다.
이전의 방학 포트폴리오를 만들어야겠다 하는 포스팅의 연장선이다. 이거 저거 다 해보면서, 내가 잘 할 줄 아는 것을 찾고, 그 부분을 발전시키는 중이다. IT 의 세상의 끝은 서로 만나게 되어 있지만, 그 시작점을 어디로 삼느냐에 따라 본인의 장점을 살려 사회에 기여 할 수 있다는 점이다.
배경
물론, 무료 포트폴리오이고, 누구나 다 접근할 수 있는 것은 나에게 있어 경쟁력을 심어 줄지는 의문이 든다. 하지만, 시작하지 않으면 안될 것이다
이전 포스팅에서, 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");
}
}
코드잇을 구독한 이유는 해외 대학교 (UTS) 수업의 속도를 따라감과 동시에 한국과 해외의 코딩 사용 기법의 차이점을 알고 싶어서였다. 그리고 그 생각의 기반이 틀리지는 않았음을 알 수가 있는 좋은 계기가 되었는데, 이왕 코드잇 후기를 적으면 만원 할인해 주는 이벤트가 있었고 (6월 14일 마감됨/ 근데 지금은 6월 15일) 이왕 내가 겪은 것들을 기록을 남기지 않는다면 좀 낭비가 아닐까 하는 마음이 있어서였다.
워낙 태생이 관종인데 이 험난한 사회에 깎이고 깎여 적응해 나아가다 보니 "선택적"관종이 된 것인지라, 이 글을 코드잇 관계자가 만약에 보고 있다면 마음에 상처를 입지 않으셨으면 좋겠다. 성인 남성/여성/기타 등등 이시니까 누군가 당신에게 비판적인 소리를 한다면 "개소리하네"라고 넘기는 마음을 갖고 읽어주시길 바람.
아무튼, 코드 있을 구독한 이유는 서론에서 언급이 잠깐 되어있듯이, 서구권의 프로그래밍 교육과 한국의 프로그래밍 교육은 어떤 차이가 있나 궁금했기 때문이라.
코드잇의 강의의 장점은, 마치 약점공략하는 방법을 알려주는 듯한 느낌이 강했다. 그러니까 이게 무슨 말이냐면, 평상시 나오는 패턴들을 알려주고 언제 막기를 해야 할지 언제 구르기를 해야할지 그리고 그 구르기가 뭔지 빠르게 넘어가는 방식의 교육이라면, 해외 유학의 교육은 그런 거 없고 이론을 알려줄 테니 실용적인 방법은 알아서 네가 인터넷에서 찾아서 써보렴~. 이런 느낌이 강했다.
이게, 장단점이 있겠지만은, 대학에서 이론을 배우고 코드잇에서 활용을 활용하는 방식으로 접근했다. 어찌 보면 상황에 적응하고 상황을 적잖이 활용한 편,
그런데, 이 방법에는 구멍이 하나 있었는데, 아무래도 대학교의 교과과정은 영어(번역 없음)이고 코드잇(한국어)으로 진행되었기 때문에 그 1:1 대응을 찾는데 적잖이 애를 먹었다. 그 해결방법으로 찾은 대체제는, codeacademy.com에서 동시에 진행했다.
아니 잠깐만요, 그러니까 codeit을 하면서 동시에 codeacademy를 하셨다고요? 대체 어떤 개발자로 살아가시려고
물론 프로그래밍이라는 것 자체가 서구권애들이 사용하는 "컴퓨터와 소통하는 방법" 이니까 한국어로 '아무리" 번역을 해도 그 근간이 영어이기 때문에, 프로그래머들은 영어로 소통하니까 그렇게 까지 상관이 없겠다마는...
게임 개발에도 관심이 없는 것은 아닌데, 일단 생존의 문제가 달린 개발의 실력을 빨리 늘려야 한다 라는 관점으로 풀스택의 역량을 키우고... (풀스택 : 구글링 잘함)
내가 생각하는 프로그래밍은, 그러니까 내가 생각하는 코딩이라는 개념은 컴퓨터와 우리(인간)를 연결하는 중간의 과정이라. "프로그래머"라는 말은 곧 컴퓨터와 사람 혹은 컴퓨터와 컴퓨터 사이를 통번역 해주는 사람이라고 생각하고 있다. 물론 이 정의는 내가 정의하는 나만의 정의이기 때문에 앞으로 뭐가 더 추가될지 기대하는 바가 크지만 말이다.
아니 개인적인 감상 말고 코드잇 강의 평가 해주세요
아무튼, 강의하시는 분들이 하나같이 조곤조곤 말씀해 주셔서, 나중 가서는 이들과 내적친밀감이 생길 수밖에 없는 구조이긴 했다. 매번 말씀하시는 말투를 보면 너무나도 친절해 원숭이와 같은 프로그래밍 지능을 지닌 내가 이들에게 어찌 안 좋은 평가를 할 수가 있겠는가. 그래도 하라니까 해드림