2D와 UI를 저번 주에 배웠고, UI를 활용해서 이번 주에는 포트리스 형식의 게임을 만들기로 했다.
UI를 통해 각도를 조절하고, 발사하는 등 UI를 적절한 위치에 배치하고, Unity Event에 맞추어 다음 코드를 실행하는 작업을 혼자서 생각하며 완성해나갔다.
목, 금요일에는 조기 수료에 관한 내용을 듣고, 애니메이션, 파티클에 대한 내용을 배웠다.
새롭게 배운 내용들 뿐 아니라 이제는 알고리즘 역시 열심히 공부해야겠다는 생각이 들었고, 우리 조 내에서도 알고리즘 스터디를 시작했다.
이번 학습 일지에서 역시 월, 화, 수 열심히 '포트리스 형식의 게임'을 만들면서, 스터디하면서 느낀 것들과 목, 금 새로운 내용들을 배우면서 느꼈던 것들을 적어보려고 한다.
Starters 7주차 주간 과제 - '포트리스 게임 만들기'
캐릭터 선택, 클리어 UI 만들기
선택창과 클리어 UI를 만드는 데에 가장 생각해야 했던 부분은 이른바 반응형으로 구현하는 것이었다.
반응형 UI란, 모바일이든 PC든 그리고, 어떠한 사이즈의 디스플레이든 같은 레이아웃을 보여주도록 하는 것이다.
이를 위해서 Rect Transform 컴포넌트를 사용했다.
anchor를 0에서 1로 맞추면 화면 전체에 항상 꽉 차게 보이게 되는데, 나는 상하좌우로 마진을 100씩 두어서 게임 화면 위로 창이 뜨는 것 같이 만들었다.
물론 캔버스 창의 크기는 화면에 꽉 차게 하여 틈 사이로 보이는 게임 UI를 조작할 수는 없도록 했다.
캐릭터를 선택하는 버튼은 토글 버튼을 이용하여 만들었다.
토글 버튼 세 개를 라디오 버튼 그룹으로 묶어서 하나만 선택할 수 있도록 하였고, value가 바뀔 때마다 캐릭터 인덱스를 전달해서 본 게임의 캐릭터 스프라이트를 해당 캐릭터의 스프라이터로 바꾸었다.
또한, 약간의 애니메이션을 통해서 캐릭터를 클릭했을 때에 만세 하는 모션을 UI에 추가하기도 했다.
메인 UI 그리고, 총알 발사 기능 만들기
메인 UI는 (추가로 더 넣은 것 빼고) 딱 세개 넣었다.
slider(힘 조절), Radial Slider(UI Extensions / 각도 조절), Text Mesh Pro ('스페이스바를 눌러주세요' 문구 표시)
스페이스바를 누르고 뗄 때마다 count를 1씩 더했고, 해당 값에 따라 몇 번 스페이스바를 눌렀는지 확인했다.
그리고, 스페이스바를 한 번 눌렀을 때에 Radial Slider의 filled amount를 더하거나 빼면서 각도를 정하도록 하고, 두 번째 눌렀을 때에 Slider의 value를 바꿔서 파워를 조절하도록 만들었다. (두 번째 스페이스바를 뗄 때에는 count를 0으로 초기화시켰다.)
그리고, 두 번째 스페이스바를 떼었을 때에 총알에 있는 flying 메서드를 실행하도록 하였다.
이 과정에서 이 모든 일련의 메서드들을 자주 사용하기 때문에 하나의 게임 매니저 클래스로 관리할 필요가 있었고, 그러기 위해 싱글톤 클래스를 만들었는데 이것이 가장 어려운 작업 중 하나였다.
나는 제너릭 클래스로 싱글톤 클래스를 하나 만들었고, 게임 매니저에서 이를 상속받아 새로운 하나의 싱글톤 클래스화 시켰다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T instance
{
get
{
if (_instance == null)
{
GameObject obj;
obj = GameObject.Find(typeof(T).Name);
if (obj == null)
{
obj = new GameObject(typeof(T).Name);
_instance = obj.AddComponent<T>();
}
else
{
_instance = obj.GetComponent<T>();
}
}
return _instance;
}
}
void Awake()
{
DontDestroyOnLoad(gameObject);
}
void Update()
{
}
}
public class GameManager : Singleton<GameManager>
{
// Input값 받기, 몬스터 HP, 총알 각도, 힘 등... 게임 전반에 필요한 변수, 메서드 저장
public void InputSpaceDown()
{
if (keyCount == 0)
{
if (!fullValue)
{
anglerSlider.fillAmount += 1f * Time.deltaTime;
if (anglerSlider.fillAmount == 1f)
fullValue = true;
}
else
{
anglerSlider.fillAmount -= 1f * Time.deltaTime;
if (anglerSlider.fillAmount == 0f)
fullValue = false;
}
}
else if (keyCount == 1)
{
if (!fullValue)
{
powerSlider.value += 1f * Time.deltaTime;
if (powerSlider.value == 1f)
fullValue = true;
}
else
{
powerSlider.value -= 1f * Time.deltaTime;
if (powerSlider.value == 0f)
fullValue = false;
}
}
}
public void InputSpaceUp()
{
keyCount++;
fullValue = false;
if (keyCount == 1)
{
angle = (int)(anglerSlider.fillAmount * 180f);
}
else if (keyCount == 2)
{
power = powerSlider.value;
ShootingBullet();
bulletCount++;
}
}
public void ShootingBullet()
{
chr.Shooting(angle, power * 20f);
angle = 0;
power = 0;
anglerSlider.fillAmount = 0f;
powerSlider.value = 0f;
keyCount = 0;
}
}
다른 기능 추가하기 - '바람 / 적 몬스터 움직이기 등'
포트리스, 웜즈 등에 보면 반드시 있는 공통적인 요소가 하나 있다. (사실 하나는 아니지만, 꼭 반드시 있는 요소이다.)
바로 '바람'이라는 요소이다.
나 역시 바람이라는 요소를 추가하고 싶어졌다.
나는 15초를 간격으로 바람의 세기, 방향을 바꿨다.
Time.time을 deltaTime으로 계속 더했고, 초기 시간과 빼서 15초가 되는 순간마다 랜덤으로 세기와 방향에 해당하는 변수의 값을 바꾸었다.
void Update()
{
curTime += Time.deltaTime;
if ((curTime - startTime) >= tarTime)
{
int dir = Random.Range(-90, 90);
float pow = Random.Range(0, 0.1f);
windDirV3 = new Vector3(0, 0, dir);
windDir = new Vector2(0, dir);
windSpeed = pow;
startTime = curTime;
}
}
바람을 적용하는 것은 더 쉬웠다.
바람의 방향 Vector 값에 세기를 곱한 값을 총알의 velocity에 더하면 해당 방향으로 해당 세기만큼의 힘을 적용할 수 있다.
사격을 할 때에 가만히 있는 표적을 맞추는 것보다는 움직이는 표적을 맞추었을 때에 더 성취감을 느낀다고 느꼈는데, 내 게임에서도 몬스터가 가만있기보다는 움직이면 좋겠다는 느낌이 들었다.
먼저 좀비를 관리할 수 있도록 좀비 리스트를 만들었고, 오브젝트 풀링을 통해서 적정 마릿수의 좀비만 재활용하기로 했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyManager : Singleton<EnemyManager>
{
[System.NonSerialized] public List<GameObject> Enemies;
public GameObject enemyPrefab;
public GameObject GameClear;
public int EnemyMaxCount = 8;
public int EnemyKillCount = 0;
public int EnemyCurCount;
public int targetKillCount = 10;
private float startTime;
private float curTime;
public float tarTime;
public float tarMin = 8f;
public float tarMax = 12f;
void Awake()
{
Init();
}
public void Init()
{
startTime = 0;
curTime = tarTime - 2f;
tarTime = Random.Range(tarMin, tarMax);
Enemies = new List<GameObject>();
EnemyCurCount = 0;
GameClear = GameObject.Find("Player").GetComponent<Character>().GameClear;
enemyPrefab = GameObject.Find("Player").GetComponent<Character>().enemyPrefab;
}
public void InitEnemies()
{
for (int i = 0; i < EnemyMaxCount; i++)
{
GameObject enem = Instantiate(enemyPrefab, transform.position, Quaternion.identity);
Enemies.Add(enem);
enem.name = "Enemy";
enem.transform.SetParent(GameObject.Find("Enemies").transform);
enem.SetActive(false);
}
}
void RespawnEnemy()
{
GameObject curEnemy = Enemies[0];
curEnemy.SetActive(true);
Enemies.RemoveAt(0);
EnemyCurCount++;
}
void Update()
{
if (EnemyKillCount >= targetKillCount)
{
GameClear.SetActive(true);
return;
}
if (Enemies.Count == 0)
{
InitEnemies();
}
curTime += Time.deltaTime;
if ((curTime - startTime) >= tarTime)
{
if (EnemyCurCount >= EnemyMaxCount)
InitEnemies();
RespawnEnemy();
startTime = curTime = 0;
tarTime = Random.Range(tarMin, tarMax);
}
}
}
리스트 안에 enemyMaxCount 만큼의 좀비를 담았고, 적정 시간이 흐를 때마다 RespawnEnemy 함수를 호출해서 리스트의 좀비를 setActive(True)로 가져올 수 있었다.
다만, 이미 모든 좀비를 가져와서 사용한 경우 maxCount만큼 더 좀비를 만들어서 리스트에 넣은 후에 리스폰시켰다.
Starters 7주차 스터디 - '알고리즘 스터디'
저번 주부터 새롭게 알고리즘 스터디를 계획했었다.
하지만 이번 주부터 본격적으로 수요일마다 문제를 정해주고 일주일 동안 풀어오는 식으로 진행하기로 하였다.
이번 주는 처음이니만큼 (알고리즘이 아예 처음인 사람이 많기도 했고) 백준에서 사칙연산부터 반복문에 해당하는 문제 4개 + 원하는 문제 몇 개씩만 풀어오기로 했다.
앞으로 8주 차부터는 전 주에 푼 문제를 서로 어떻게 풀었는지 코드 리뷰를 하고 정리하는 시간을 가질 계획이다.
블로그 회고록으로도 짧게나마 이 내용도 적어보도록 하겠다.
Starters 7주차 수업 - '애니메이션 / 포스트 프로세싱 / 파티클'
애니메이션 - 'FSM'
유한 상태 기계라고 불리는 FSM은 어떤 상태에 맞추어 자동으로 어떤 행동이나 애니메이션을 호출할 수 있도록 할 때에 많이 쓰이는 기법이라고 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using FSM;
enum RState
{
Walk = 0,
Attack,
Run
}
public class CsEnemy : MonoBehaviour
{
StateMachine stateMachine;
public GameObject character;
RState rState;
Animator anim;
void Awake()
{
// 애니메이터 스테이트 등을 동적으로 생성할 수는 없음
// 애니메이터에서 미리 생성해야함
anim = GetComponent<Animator>();
stateMachine = new StateMachine();
stateMachine.AddState(RState.Attack.ToString(), onLogic: (state) =>
{
transform.position = Vector3.MoveTowards(transform.position, character.transform.position, 0.2f * Time.deltaTime);
anim.SetInteger("State", (int)RState.Attack);
Debug.Log("Attack");
});
stateMachine.AddState(RState.Run.ToString(), onLogic: (state) =>
{
transform.position = Vector3.MoveTowards(transform.position, character.transform.position, 2f * Time.deltaTime);
anim.SetInteger("State", (int)RState.Run);
Debug.Log("Run");
});
stateMachine.AddState(RState.Walk.ToString(), onLogic: (state) =>
{
transform.position = Vector3.MoveTowards(transform.position, character.transform.position, 1f * Time.deltaTime);
anim.SetInteger("State", (int)RState.Walk);
Debug.Log("Walk");
});
stateMachine.AddTransition(
RState.Walk.ToString(),
RState.Attack.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Attack);
return Vector3.Distance(character.transform.position, this.transform.position) < 1f;
}
);
stateMachine.AddTransition(
RState.Walk.ToString(),
RState.Run.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Run);
return Vector3.Distance(character.transform.position, this.transform.position) > 5f;
}
);
stateMachine.AddTransition(
RState.Attack.ToString(),
RState.Walk.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Walk);
return Vector3.Distance(character.transform.position, this.transform.position) > 3f
&& Vector3.Distance(character.transform.position, this.transform.position) < 5f;
}
);
stateMachine.AddTransition(
RState.Attack.ToString(),
RState.Run.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Run);
return Vector3.Distance(character.transform.position, this.transform.position) > 5f;
}
);
stateMachine.AddTransition(
RState.Run.ToString(),
RState.Walk.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Walk);
return Vector3.Distance(character.transform.position, this.transform.position) > 3f
&& Vector3.Distance(character.transform.position, this.transform.position) < 5f;
}
);
stateMachine.AddTransition(
RState.Run.ToString(),
RState.Attack.ToString(),
(Transition) =>
{
anim.SetInteger("State", (int)RState.Attack);
return Vector3.Distance(character.transform.position, this.transform.position) < 1f;
}
);
stateMachine.SetStartState("Walk");
stateMachine.Init();
}
void Update()
{
stateMachine.OnLogic();
}
}
위의 코드는 적들의 상태에 따라 행동을 부여하는 일종의 FSM으로, Enum을 사용하여 상태를 구분하고 그 상태 값을 통해 행동을 부여했다.
FSM 라이브러리를 사용하면, 위에서 사용한 것처럼 StateMachine을 통해 상태를 쉽게 관리하고, 변동시킬 수 있다.
addState를 통해서 상태를 넣고, addTransition을 통해서 상태 변화와 행동을 정해둘 수 있게 된다.
유니티 에디터의 애니메이터 역시 state 변수와 transition을 통해 상태를 변화시키는데, 이 역시 일종의 FSM이라고 볼 수 있다.
포스트 프로세싱
가끔 오브젝트가 어떤 구역에 들어왔을 때에 이펙트를 적용하고 싶을 때가 있다.
특히나 이 이펙트가 적용되어야 하는 것이 카메라에 보이는 모든 것이거나 거의 대부분이라면.. 우리는 오브젝트 각각에 이펙트를 넣기보다는 카메라 자체에 효과를 집어넣는 식으로 후처리 하는 편이 훨씬 나을 것이다. 바로 이것이 포스트 프로세싱이라고 했다.
유니티에서 포스트 프로세싱을 사용하기 위해서는 일단 패키지 매니저에서 import를 해야 한다.
그러고 나서는 카메라에 포스트 프로세싱 레이어 컴포넌트를 붙이고, 월드 상에는 포스트 프로세싱 볼륨이라는 범위를 설정하고, 어떤 효과를 줄지 고르면 끝이다! (프로세싱 레이어 컴포넌트에 어떤 오브젝트가 범위에 들어왔을 때에 효과를 줄지, 그 오브젝트를 등록하기는 해야 한다. (1인칭 시점인 경우 카메라를 넣어도 된다.)
파티클
파티클은 행동에 생기를 불어넣을 수 있게 이펙트를 넣어주는 것이라고 보면 쉽다.
아직까지도 어려운 기능 중 하나인데, 사실 파티클에 대한 설정이 세세하게 많기 때문이다.
처음 발생할 때를 설정하는 Emission, 파티클의 모양을 설정하는 Shape, 렌더링을 설정하는 Renderer 등등 설정할 것이 많은데, 심지어는 파티클에 trigger나 collision을 생성해서 오브젝트와 부딪혀 나오도록 할 수도 있다는 사실을 알게 되었다.
모닥불에 불티가 튀는 효과 등을 만들 때에 아마도 쓰이지 않을까 싶다.
Starters 7주차 후기
사실 수업 외에도 7주 차에 조기 수료에 관한 내용들을 듣게 되었다.
알고리즘 테스트 외에 이전까지 배웠던 개념을 가지고, 실제 기획안과 접목하여 기능을 만드는 등 현실감 있는 테스트를 할 예정으로 보였다.
지금까지도 열심히 수업을 듣고, 독학도 열심히 했지만, 테스트를 위해서 더 열심히 해야겠다는 생각이 많이 들었다.
조기 수료라는 목표 외에도 내가 얼마나 배웠고, 성장했는지를 알 수 있는 좋은 기회이기 때문에 더 그런 생각이 드는 것 같다.
여하튼, 다다음주 테스트를 위해 다음 주는 알고리즘에 대해서도 독학을 더 열심히 하고 해당 회고록에도 내용을 올려보도록 하겠다.또 그러기 위해서 앞으로의 교육도 열심히 잘 받고, 성장할 수 있도록 다시 한번 힘내야겠다! 파이팅!!
유데미 코리아 바로가기 :
Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea
유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.
www.udemykorea.com
💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.
'Starters 부트캠프 > B - log' 카테고리의 다른 글
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 9주차 학습 일지 (0) | 2022.08.21 |
---|---|
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 8주차 학습 일지 (0) | 2022.08.14 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 6주차 학습 일지 (2) | 2022.07.30 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 5주차 학습 일지 (2) | 2022.07.24 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 4주차 학습 일지 / 유데미 [C#과 Unity로 3D 게임 개발하기] 강의 리뷰 (4) | 2022.07.16 |