오늘 학습 키워드
TopDown 클론 코딩
오늘 학습 한 내용을 나만의 언어로 정리하기
TopDown
투사체
private void OnTriggerEnter2D(Collider2D collision)
{
if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
{
}
else if (rangeWeaponHandler.target.value == (rangeWeaponHandler.target.value | (1 << collision.gameObject.layer)))
{
}
}
- Layer 비교
- Layer는 이진 값으로 되어있음
- ex. 3번 레이어가 켜져있으면 (1000), 5번도 켜져있으면 (101000)
- collision.gameObject.layer는 정수로 나올 것임.
- ex. 3번 레이어가 켜져있으면 3, 4번 레이어가 켜져있으면 4.
- 그래서 1을 왼쪽 쉬프트 연산하는거임
- Layer는 이진 값으로 되어있음
파티클 구현
- Simulation Space - World : World Position에 생성됨
- Color over Lifetime : 시간 지남에 따른 색 변화
- Size over Lifetime : 시간 지남에 따른 크기 변화
- Limit Velocity over Lifetime : 속도 제한
- 애니메이션 이벤트 연결하기
- 특정 애니메이션 구간에 실행될 함수를 지정할 수 있는데, 그러려면 스크립트가 같이 달려있어야 함
적 오브젝트 기초 구성
코루틴
- 시간 기반 / 비동기적 작업 처리를 위한 메소드. 중간에 실행을 중단하고 나중에 재개할 수 있음
- 비동기적 작업 : 프레임 단위로 나눠서 실행됨
- yield 키워드를 사용해 특정 조건/시간까지 기다렸다가 재개할 수 있음
- yield return 종류
- null : 다음 프레임까지 대기
- new WaitForSeconds(시간) : 지정된 시간(초)만큼 대기
- new WaitForSecondsRealtime(시간) : 실제 시간 기준으로 지정된 시간(초)만큼 대기
- new WaitUntil(조건) : 조건이 참이 될 때까지 대기
- new WaitWhile(조건) : 조건이 거짓이 될 때까지 대기
- break : 코루틴 실행을 즉시 종료
- 코루틴을 실행할 때에는 StartCoroutine(함수명)으로 써야함
- 코루틴은 IEnumerator 형태로 반환하게 되어있음
// 몬스터 소환에 코루틴을 사용한 예시
private IEnumerator SpawnWave(int waveCount)
{
enemySpawnComplite = false;
yield return new WaitForSeconds(timeBetweenWaves); // timebetweenWaves 초 만큼 대기함
for(int i = 0; i < waveCount; i++)
{
yield return new WaitForSeconds(timeBetweenSpawns); // timeBetweenSpawns 초 만큼 대기한 후 적 소환
SpawnRandomEnemy();
}
enemySpawnComplite = true;
}
적 소환하기
private void SpawnRandomEnemy()
{
if(enemyPrefabs.Count == 0 || spawnAreas.Count == 0)
{
Debug.LogWarning("Enemy Prefabs 또는 Spawn Areas가 설정되지 않았습니다.");
return;
}
GameObject randomPrefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Count)]; // 랜덤 적 GameObject
Rect randomArea = spawnAreas[Random.Range(0, spawnAreas.Count)]; // 스폰될 수 있는 위치 중 하나
Vector2 randomPosition = new Vector2( // 스폰될 수 있는 위치 내에서 랜덤값
Random.Range(randomArea.xMin, randomArea.xMax),
Random.Range(randomArea.yMin, randomArea.yMax));
GameObject spawnEnemy = Instantiate(randomPrefab, new Vector3(randomPosition.x, randomPosition.y), Quaternion.identity);
EnemyController enemyController = spawnEnemy.GetComponent<EnemyController>();
// 생성된 적에 달려있는 EnemyController를 가져와서
// 활성화 되어있는 적 목록에 추가
activeEnemies.Add(enemyController);
}
기즈모 그리기
- 기즈모 : 개발을 위한 아이콘.
private void OnDrawGizmosSelected()
{
if (spawnAreas == null) return;
Gizmos.color = gizmoColor;
foreach (var area in spawnAreas)
{
Vector3 center = new Vector3(area.x + area.width / 2, area.y + area.height / 2);
Vector3 size = new Vector3(area.width, area.height);
Gizmos.DrawCube(center, size);
}
}
- 대강 그림판으로 설명
스테이지 구성
- 기존의 EnemyManager에서 직접 컨트롤하던 스테이지를 GameManager가 하도록 변경함
데미지 / 피격처리
데미지 처리
- 기존 ProjectileController 에서 수정함.
private void OnTriggerEnter2D(Collider2D collision)
{
// level과 부딛힘
if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
{
DestroyProjectile(collision.ClosestPoint(transform.position) - direction * .2f, fxOnDestroy);
}
// target과 부딛힘. 데미지 처리 필요
else if (rangeWeaponHandler.target.value == (rangeWeaponHandler.target.value | (1 << collision.gameObject.layer)))
{
ResourceController resourceController = collision.GetComponent<ResourceController>();
if (resourceController != null)
{
resourceController.ChangeHealth(-rangeWeaponHandler.power);
if (rangeWeaponHandler.IsOnKnockback)
{
BaseController controller = collision.GetComponent<BaseController>();
if (controller != null)
{
controller.ApplyKnockback(transform, rangeWeaponHandler.KnockbackPower, rangeWeaponHandler.KnockbackTime);
}
}
}
DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestroy);
}
}
사망 처리
- BaseController (모든 움직이는 캐릭터들의 부모) 에서 관리
// BaseController.cs
public virtual void Death()
{
// 죽었을 때 처리
// 1. 속도 0으로
_rigidbody.velocity = Vector3.zero;
// 2. 모든 스프라이트 빨갛고 약간 투명하게
foreach(SpriteRenderer renderer in transform.GetComponentsInChildren<SpriteRenderer>())
{
Color color = renderer.color;
color.a = 0.3f;
renderer.color = color;
}
// 3. 코드 동작 안하도록 끄기. 참고 : MonoBehaviour가 상속하는게 Behaviour임.
foreach(Behaviour component in transform.GetComponentsInChildren<Behaviour>())
{
component.enabled = false;
}
// 4. 2초 이후에 삭제
Destroy(gameObject, 2f);
}
- 이걸 실제로 불러주는 부분은 ResourceController에 있음.
public bool ChangeHealth(float change)
{
if(change == 0 || timeSinceLastChange < healthChangeDelay)
{
return false;
}
timeSinceLastChange = 0;
CurrentHealth += change;
CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;
CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth;
if(change < 0)
{
animationHandler.Damage();
}
if(CurrentHealth <= 0f)
{
Death(); // 여기서 호출
}
return true;
}
private void Death()
{
baseController.Death(); // 본인이 알고있는 baseController의 Death 호출
}
스테이지 순환
- EnemyManager에서 관리
- 적 생성 : EnemyManager에서 정해진 순서대로
- 적 파괴 : 파괴 자체는 EnemyController에서 인지하고, 그걸 EnemyManager로 정보를 전달함
무기 착용 / 공격
적 무기 추가
- 기존에 있던 P_Bow_EquipWeapon 을 두 개 복제해서 사용함
- Target Layer를 Enemy가 아닌 Player로 변경함
근거리 무기 / 적 구현
근거리 무기 오브젝트 생성
- 근거리 충돌 처리
// 형태를 가지고 있는 레이캐스트 사용
RaycastHit2D hit = Physics2D.BoxCast(
transform.position + (Vector3)Controller.LookDirection * collideBoxSize.x, // 내가 있는 위치 + 내가 보고 있는 방향으로
collideBoxSize, // 미리 정해둔 충돌 기준 크기만큼
0, // 0도로
Vector2.zero, // 방향 없이
0, // 거리 0 (근거리니까)
target // 타겟만 찾기
);
- 근거리 무기의 경우 Rotate를 재설정 해주어야 함
public override void Rotate(bool isLeft)
{
// base.Rotate(isLeft);
if (isLeft)
transform.eulerAngles = new Vector3(0, 180, 0);
else
transform.eulerAngles = new Vector3(0, 0, 0);
}
사운드 처리
VFX 처리
- SoundSource 를 만들어서 Prefab화 함. 필요할 때 부르게
public class SoundSource : MonoBehaviour
{
private AudioSource _audioSource;
public void Play(AudioClip clip, float soundEffectVolume, float soundEffectPitchVariance)
{
if(_audioSource == null)
_audioSource = GetComponent<AudioSource>();
// 만약에 클립 재사용 되는 경우에 밑에 있는 비활성화 (Disable) 함수 실행 안되게 하려고 캔슬하는거임.
CancelInvoke();
_audioSource.clip = clip;
_audioSource.volume = soundEffectVolume;
_audioSource.Play();
_audioSource.pitch = 1f + Random.Range(-soundEffectPitchVariance, soundEffectPitchVariance); // 효과음 랜덤성
Invoke("Disable", clip.length + 2);
}
public void Disable()
{
_audioSource?.Stop();
Destroy(this.gameObject);
}
}
BGM 처리
- BGM 은 SoundManager에서 씀
public class SoundManager : MonoBehaviour
{
public static SoundManager instance;
[SerializeField][Range(0f, 1f)] private float soundEffectVolume;
[SerializeField][Range(0f, 1f)] private float soundEffectPitchVariance;
[SerializeField][Range(0f, 1f)] private float musicVolume;
private AudioSource musicAudioSource;
public AudioClip musicClip;
public SoundSource soundSourcePrefab; // 효과음 내주는 애
private void Awake()
{
instance = this;
musicAudioSource = GetComponent<AudioSource>();
musicAudioSource.volume = musicVolume;
musicAudioSource.loop = true;
}
private void Start()
{
ChangeBackGroundMusic(musicClip);
}
public void ChangeBackGroundMusic(AudioClip clip)
{
musicAudioSource.Stop();
musicAudioSource.clip = clip;
musicAudioSource.Play();
}
public static void PlayClip(AudioClip clip)
{
SoundSource obj = Instantiate(instance.soundSourcePrefab);
SoundSource soundSource = obj.GetComponent<SoundSource>();
soundSource.Play(clip, instance.soundEffectVolume, instance.soundEffectPitchVariance);
}
}
UI 구성
- 저번처럼 UI를 State로 관리함.
UI Anchor (UI 앵커)
- UI 앵커 : Canvas 상의 UI 요소가 화면 크기나 해상도가 변경되더라도 일정한 위치와 크기를 유지하도록 돕는 기능
UI 렌더링 순서
-
하이어라키 순서와 동일함.
-
상단부에서 하단부로 내려오는 순서대로 그려짐 ⇒ 맨 하단부에 있는게 위에 그려짐
-
순서 변경 방법
- 하이어라키에서 드래그 앤 드롭으로 바꾸기
- 스크립트에서 Transform.SetSiblingIndex 로 바꾸기
-
캔버스 추가 설정
- 캔버스 마다의 Sort Order를 변경할 수 있음. 숫자가 높은 캔버스가 위에 그려짐.
체력 감소 UI 구현
- Delegate 사용
// ResourceController.cs
private Action<float, float> OnChangeHealth;
...
public bool ChangeHealth(float change)
{
if(change == 0 || timeSinceLastChange < healthChangeDelay)
{
return false;
}
timeSinceLastChange = 0;
CurrentHealth += change;
CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;
CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth;
// 체력 바뀌면 OnChangeHealth 호출
OnChangeHealth?.Invoke(CurrentHealth, MaxHealth);
if(change < 0)
{
animationHandler.Damage();
if(damageClip != null)
SoundManager.PlayClip(damageClip);
}
if(CurrentHealth <= 0f)
{
Death();
}
return true;
}
...
public void AddHealthChangeEvent(Action<float, float> action)
{
OnChangeHealth += action;
}
public void RemoveHealthChangeEvent(Action<float, float> action)
{
OnChangeHealth -= action;
}
// GameManager.cs
private void Awake()
{
Instance = this;
player = FindObjectOfType<PlayerController>();
player.Init(this);
uiManager = FindObjectOfType<UIManager>();
enemyManager = GetComponentInChildren<EnemyManager>();
enemyManager.Init(this);
_playerResourceController = player.GetComponent<ResourceController>();
// 혹시 모르니 미리 지움
_playerResourceController.RemoveHealthChangeEvent(uiManager.ChangePlayerHP);
// 그리고 더하기
_playerResourceController.AddHealthChangeEvent(uiManager.ChangePlayerHP);
}
// UIManager.cs
public void ChangePlayerHP(float currentHP, float maxHP)
{
gameUI.UpdateHPSlider(currentHP / maxHP);
}
- 기억 되짚기 : Func 는 반환이 있는 델리게이트, Action은 반환이 없는 델리게이트!
InputSystem 활용
- 다양한 Input을 처리할 수 있게 해주는 시스템. 기존의 Legacy 버전과 새로운 버전, 두 가지 방식으로 입력 처리 가능
- 특징
- Unity 새로운 입력 처리 방식
- 다양한 입력 장치를 쉽게 통합 가능
- 키 매핑 변경이 간단함. 이벤트 기반 구조
- 구조
- Input Actions Asset
- 입력 처리를 설정
- Player Input 컴포넌트
- Input Actions를 쉽게 적용할 수있는 컴포넌트
- Unity Event 를 통해 입력 처리
- Input Actions Asset
사용방법
- Input Actions 를 추가한다.
2. Action Maps에 하나 추가
-
원하는 만큼 액션 추가
-
어떻게 값을 받아올 것인지 설정
-
예시 1. Move 라는 액션에는 Vector2 방식으로 아래와 같이 가져옴 (WASD)
-
예시 2. Look 이라는 액션에는 Vector2 방식으로 아래와 같이 가져옴 (마우스 위치)
- Input Action을 적용할 오브젝트에 컴포넌트로 Player Input을 추가함
- 그리고 Actions 에 방금 만든 Input Action을 추가함
- 참고 : Behavior 를 Send Messages 방식으로 하면, 같은 오브젝트 안에 있는 스크립트에게 메시지를 전달함. 키가 입력되면 자동으로 역호출함.
-
참고 2: Unity Events 로 하면 Button에 OnClick 이벤트 등록하는 것 마냥 할 수도 있긴 함. 여기서는 Send Message 방식으로 구현할 것
이번 TopDown에서 적용
- 원래 HandleAction에 있던 내용을 확인해 봄.
protected override void HandleAction()
{
// 이동
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical= Input.GetAxisRaw("Vertical");
movementDirection = new Vector2(horizontal, vertical).normalized;
// 이동
// 보는 방향 수정
Vector2 mousePosition = Input.mousePosition;
Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
lookDirection = (worldPos - (Vector2)transform.position);
if(lookDirection.magnitude < .9f)
{
lookDirection = Vector2.zero;
}
else
{
lookDirection = lookDirection.normalized;
}
// 보는 방향 수정
// 발사
isAttacking = Input.GetMouseButton(0);
}
- 이동은 OnMove()로, 보는 방향 수정은 OnLook() 으로, 발사는 OnFire()로 옮겨주면 됨.
- 왜냐? SendMessage로 역 호출되기 때문임.
// 변경된 PlayerController.cs
void OnMove()
{
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
movementDirection = new Vector2(horizontal, vertical).normalized;
}
void OnLook()
{
Vector2 mousePosition = Input.mousePosition;
Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
lookDirection = (worldPos - (Vector2)transform.position);
if (lookDirection.magnitude < .9f)
{
lookDirection = Vector2.zero;
}
else
{
lookDirection = lookDirection.normalized;
}
}
void OnFire()
{
isAttacking = Input.GetMouseButton(0);
}
- 하지만 이건 기존의 방식이랑 동일하니까, 매개변수를 넣음으로써 Input Action을 지대로 사용하자.
// Input System을 쓰는 방식으로 변경된 PlayerController.cs
void OnMove(InputValue inputValue)
{
movementDirection = inputValue.Get<Vector2>().normalized;
}
void OnLook(InputValue inputValue)
{
Vector2 mousePosition = inputValue.Get<Vector2>();
Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
lookDirection = (worldPos - (Vector2)transform.position);
if (lookDirection.magnitude < .9f)
{
lookDirection = Vector2.zero;
}
else
{
lookDirection = lookDirection.normalized;
}
}
void OnFire(InputValue inputValue)
{
isAttacking = inputValue.isPressed;
}
- 하지만, 발사하려는 게 아니고 UI를 누르려고 한 경우에는 예외처리를 해주어야 하기 때문에 OnFire만 아래 코드로 바꿔줌.
// PlayerController.cs > OnFire()
void OnFire(InputValue inputValue)
{
// 포인터가 이벤트 시스템을 눌렀을 경우에는 스킵함
if (EventSystem.current.IsPointerOverGameObject()) return;
isAttacking = inputValue.isPressed;
}
스탠다드반 OT
- Firebase는 WebGL과 적용이 안됨.
- 자바스크립트로 불러와서 C#으로 옮겨야 함.
- 개발자는 주어진걸 구현하는게 제일 중요하다.
- 유니티 자체는 쓸 수 있는 분야가 많으니까, 넓은 시야로 보자.
- 나라장터도 한 번 보자. 어떤 사업이 크게 뜨는지 보임
- TA쪽은 디자인 패턴을 확실하게 본다. (근데 그거 하나로 취직하긴 힘듬)
- 게임 업계가 아니더라도 관련 포지션을 해봤다면 좋음. (ex. 플러그인 폴더)
- 쉬지않고 개발하다보면 기회는 온다.
- 대표로써 딱 하나만 보고 개발자를 뽑아야 한다면, 본인의 수준을 알고 있는 개발자. ⇒ 메타인지가 잘 된 개발자.
- 유나이트나 발표회 같은 컨퍼런스 참여하면 신기술 많이 배울 수 있음.
- 구글 스토어에 출시하는건 구글이 승인해준걸로 인정됨. 이런 승인 권한이 있는 곳에 출시하면 되긴하는데 분류에 따라 또 달라짐
- 개발할때 테스트할 방법을 생각해야한다.
- 깃허브 중요함!!!
- 개발할때 자기가 어느 한군데 꽂혀서 시간보내고 있다는 사실을 최대한 빠르게 자각하는 방법은 사람마다 다르지만.. 차라리 완전히 빠지는게 나음. 계속 빠져봐야됨. 계속 뻘짓을 해봐야 더 대처능력이 늘게 되기 때문.
- IOS 빌드용 맥북이 따로 필요함
개인 질문
- Q. 상용 서버를 공부할만한 방법
- 가상 윈도우 하나 만들어서 돌려보는게 좋음.
- 포톤, 파이어베이스 단계별로 넘어가기
- 파이어베이스 먼저 하기. 60%는 업계에서 사용
- 어드레서블 ? ← 검색해보기.
- Q. 디지털 트윈쪽 회사를 알아보는것도 도움이 크게 될것으로 기대하고 있는데 괜찮을지
- 아예 신입이 들어가기는 어려움. 건축이나 공장 시스템. 딱딱한 산업분야에서 진행중임.
- Q. 게임 수학을 공부할때 좋은 책이 있을까요? 아니면 참고자료..?
- 개발쪽으로 더 하고싶으면 파이어베이스를 먼저 공부해보자!
개인프로젝트 시작
- 해야되는게 산더미임
- 일단 맵을 먼저 만들었음. 시간이 없기 때문에, 이번 강의에서 주어진 타일을 재사용 하기로 결정함.
캐릭터 이동
- 일단 지금 당장은 움직이는게 플레이어밖에 없기 때문에, PlayerController 하나만 만들기로 결정.
- 그리고 Input Manager 되게 유용한 것 같아서 사용했음.
- WASD 말고 방향키로도 움직이게 하고싶어서, 그렇게 진행했음.
- 따라서… 캐릭터 이동 및 맵 탐색은 클리어!
맵 설계 및 상호작용 영역
- 일단 아이디어는, 각 방에 들어갈 때 마다 무슨 방인지 방 이름이 게임 상단에 나오게 하는것이었음.
- 내일 하자…
잊지 말자
- Instantiate(오브젝트, 위치, 회전) 으로 만들 수도 있다!
- transform.right != Vector2.right : tranform.right는 오브젝트의 오른쪽임. 물체의 오른쪽을 설정해주면 나머지 부분 회전도 알아서 들어감
학습하며 겪었던 문제점 & 에러
문제 1
- 문제&에러에 대한 정의
분명 Input Action 설정하고 붙여줬는데 플레이어가 움직이질 않음.
- 내가 한 시도
- 일단 로그를 찍어서, OnMove()가 호출되는지 확인함 ⇒ 호출되기는 함. 그럼 함수에서 문제인듯
- 내부 변수인 MovementDirection을 수정만 하지 대입을 안하는 상태였음.
- Movement 함수 작성함
-
해결 방법 Movement 함수 작성함
-
이 문제&에러를 다시 만나게 되었다면?
이해하고 베껴오자..
문제 2
- 문제&에러에 대한 정의
플레이어가 움직이다보면 이따구로 이상하게 뒤집어짐
- 내가 한 시도
Rotate Z 를 Freeze 해봤음.
- 해결 방법
그게 정답임!
- 새롭게 알게 된 점
콜라이더에 부딛치다보면 회전할 수 있으니 주의.
- 이 문제&에러를 다시 만나게 되었다면?
설정을 제대로 했나 확인해보자..
내일 학습 할 것은 무엇인지
- 개인프로젝트 필수과제 끝내기..