-
내일배움캠프 19일차 TIL - 2D게임 기초 개발 입문 2TIL/Unity 2024. 5. 10. 20:00
[학습목표]
Top-Down 게임을 제작하면서 유니티 2D 게임 개발 기초를 학습한다. 강의를 시청하고 레이어와 투사체, 오브젝트 풀, 애니메이션, 에너미와 피격 기능을 학습하고 구현한다.
[학습내용]
오늘까지 해서 유니티 기초 개발 입문 강의를 모두 시청했다. 내일부터는 개인 과제 구현에 시간을 쏟을 계획이다.
비트 연산자와 레이어 마스크
- 유니티에서 각 게임 오브젝트는 특정 레이어에 속할 수 있다.
- 레이어는 비트 필드로 표현되어 각 비트가 다른 레이어를 나타낸다.
- 비트 연산자에는 AND(&), OR(|), XOR(^), NOT(~), 4가지 종류가 있다.
- 1 << n은 1을 n번째 비트 위치로 시프트한다는 의미이다.
n번째 레이어를 나타내는 비트마스크를 생성하는 데 사용되기도 한다.
충돌 검사, 레이캐스팅 제어, 카메라 렌더링 설정 등이 가능하다.
투사체 구현
우선 Enemy, Level, Player 레이어를 생성하고 게임 오브젝트에 적용시켜 줬다. 모든 자식 오브젝트에도 똑같이 적용시킬 것이냐는 문구가 뜨면 적용을 골랐다.
그 다음 ProjectileController 스크립트를 작성해줬다.
using UnityEngine; public class ProjectileController : MonoBehaviour { [SerializeField] private LayerMask levelCollisionLayer; private RangedAttackSO attackData; private float currentDuration; private Vector2 direction; private bool isReady; private Rigidbody2D rigidbody; private SpriteRenderer spriteRenderer; private TrailRenderer trailRenderer; public bool fxOnDestory = true; private void Awake() { spriteRenderer = GetComponentInChildren<SpriteRenderer>(); rigidbody = GetComponent<Rigidbody2D>(); trailRenderer = GetComponent<TrailRenderer>(); } private void Update() { if (!isReady) { return; } currentDuration += Time.deltaTime; if (currentDuration > attackData.duration) { DestroyProjectile(transform.position, false); } rigidbody.velocity = direction * attackData.speed; } private void OnTriggerEnter2D(Collider2D collision) { if (IsLayerMatched(levelCollisionLayer.value, collision.gameObject.layer)) { Vector2 destroyPosition = collision.ClosestPoint(transform.position) - direction * .2f; DestroyProjectile(destroyPosition, fxOnDestory); } else if (IsLayerMatched(attackData.target.value, collision.gameObject.layer)) { DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestory); } } private bool IsLayerMatched(int layerMask, int objectLayer) { return layerMask == (layerMask | (1 << objectLayer)); } public void InitializeAttack(Vector2 direction, RangedAttackSO attackData) { this.attackData = attackData; this.direction = direction; UpdateProjectileSprite(); trailRenderer.Clear(); currentDuration = 0; spriteRenderer.color = attackData.projectileColor; transform.right = this.direction; isReady = true; } private void UpdateProjectileSprite() { transform.localScale = Vector3.one * attackData.size; } private void DestroyProjectile(Vector3 position, bool createFx) { if (createFx) { // TODO : ParticleSystem에 대해서 배우고, 무기 NameTag로 해당하는 FX가져오기 } gameObject.SetActive(false); } }
ProjectileController.cs
그리고 TopDownController와 TopDownShooting 스크립트도 수정해줬는데, 아래 코드가 있었다.
private void OnShoot(AttackSO attackSO) { RangedAttackSO RangedAttackSO = attackSO as RangedAttackSO; float projectilesAngleSpace = RangedAttackSO.multipleProjectilesAngel; int numberOfProjectilesPerShot = RangedAttackSO.numberofProjectilesPerShot; // float minAngle = -(numberOfProjectilesPerShot / 2f) * projectilesAngleSpace + 0.5f * RangedAttackSO.multipleProjectilesAngel; float minAngle = -(numberOfProjectilesPerShot - 1) / 2f * projectileAngleSpace; for (int i = 0; i < numberOfProjectilesPerShot; i++) { float angle = minAngle + projectilesAngleSpace * i; float randomSpread = Random.Range(-RangedAttackSO.spread, RangedAttackSO.spread); angle += randomSpread; CreateProjectile(RangedAttackSO, angle); } }
TopDownShooting.cs 코드 수정
minAngle에 값을 할당하는 코드가 있었는데, 뭔가 최적화가 되어있지 않은 코드 같았다. 더 생각해보다가 저렇게 줄일 수 있다는 사실을 깨달았다. 그래서 줄여서 작성해보았다.
다음은 발사체인 Arrow를 수정해줬다. BoxCollider2D와 Rigidbody2D 컴포넌트를 붙여줬고, TrailRenderer라는 컴포넌트도 붙여줬다.
처음 보는 컴포넌트였는데, 쉽게 말하면 별똥별 꼬리같은 이펙트를 남기는 컴포넌트인 거 같다. 물체가 움직일 때 잔상을 어떻게 남길지 설정해주는 컴포넌트다.
오브젝트 풀
- 오브젝트 풀은 게임 개발에 널리 쓰이는 테크닉이다.
- 많은 객체를 생성해야 할 때 미리 생성해두었다가 필요할 때 가져다 쓴 후 반납하는 방식이다.
- 가비지 컬렉터의 부담을 줄여준다.
- 적절한 풀 크기를 사용하면 큰 성능개선을 가져올 수 있지만, 반대의 경우를 조심해야 한다.
using System.Collections.Generic; using UnityEngine; public class ObjectPool : MonoBehaviour { [System.Serializable] public class Pool { public string tag; public GameObject prefab; public int size; } public List<Pool> Pools; public Dictionary<string, Queue<GameObject>> PoolDictionary; private void Awake() { PoolDictionary = new Dictionary<string, Queue<GameObject>>(); foreach (var pool in Pools) { Queue<GameObject> objectPool = new Queue<GameObject>(); for (int i = 0; i < pool.size; i++) { GameObject obj = Instantiate(pool.prefab); obj.SetActive(false); objectPool.Enqueue(obj); } PoolDictionary.Add(pool.tag, objectPool); } } public GameObject SpawnFromPool(string tag) { if (!PoolDictionary.ContainsKey(tag)) return null; GameObject obj = PoolDictionary[tag].Dequeue(); PoolDictionary[tag].Enqueue(obj); obj.SetActive(true); return obj; } }
ObjectPool.cs
여기서 Dictionary를 사용하는 이유는 Queue를 사용할 경우 O(n)의 시간복잡도를 가지지만, Dictionary를 사용할 경우 O(1)의 시간복잡도를 가지기 때문이다.
애니메이션
애니메이션과 애니메이터, 둘이 이름이 비슷해서 자주 헷갈린다.
애니메이션(animation)
- 게임 오브젝트에 애니메이션을 추가하는 데에 사용
- 애니메이션 클립을 재생할 수 있음
- 애니메이션 컴포넌트는 간단한 애니메이션에 적합하고 스크립트를 통해 제어 가능
- 애니메이션 윈도우를 통해 생성/편집 가능
애니메이터(Animator)
- 애니메이션의 상태를 제어하고 전환을 관리하는 데에 사용
- 애니메이션 컨트롤러를 사용하여 애니메이션의 복잡한 상태 기계를 구현
- 여러 애니메이션 클립을 조절하거나, 전환을 제어하거나, 복잡한 애니메이션 시퀀스를 구현하는 데에 적합
- Mecanim 애니메이션 시스템의 일부로서, 애니메이션 블렌딩, 트리, 상태머신 등의 기능 제
using UnityEngine; public class AnimationController : MonoBehaviour { protected Animator animator; protected TopDownController controller; protected virtual void Awake() { animator = GetComponentInChildren<Animator>(); controller = GetComponent<TopDownController>(); } }
AnimationController.cs
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem.XR; public class CharacterAnimationController : AnimationController { private static readonly int IsWalking = Animator.StringToHash("IsWalking"); private static readonly int IsHit = Animator.StringToHash("IsHit"); private static readonly int Attack = Animator.StringToHash("Attack"); private readonly float magnituteThreshold = 0.5f; protected override void Awake() { base.Awake(); } void Start() { controller.OnAttackEvent += Attacking; controller.OnMoveEvent += Move; } private void Move(Vector2 obj) { animator.SetBool(IsWalking, obj.magnitude > magnituteThreshold); } private void Attacking(AttackSO obj) { animator.SetTrigger(Attack); } private void Hit() { animator.SetBool(IsHit, true); } private void InvincibilityEnd() { animator.SetBool(IsHit, false); } }
CharacterAnimationController.cs
적 구현
적을 구현하는 작업은 플레이어를 구현하는 것과 비슷한 느낌이기에 새롭거나 중요한 개념을 위주로 정리한다.
싱글톤 패턴
- 소프트웨어 디자인 패턴 중 하나로, 특정 클래스의 인스턴스가 하나만 존재하도록 보장하고, 이를 전역적인 접근이 가능하도록 해주는 패턴
- 전역 변수를 사용하지 않고도 객체간 데이터를 공유하거나, 전역적인 상태를 관리하거나, 특정 서비스를 애플리케이션 전체에서 사용할 수 있게 함
- 잘못 사용하면 코드의 결합도를 높이고 테스트와 유지보수가 어려워짐
FindGameObjectWithTag
- 지정된 태그와 일치하는 첫 번째 활성 게임 오브젝트를 반환한다.
- 자원을 많이 잡아먹는 연산이므로 Update() 등에서 프레임마다 호출하면 성능에 심각한 영향을 미칠 수 있음
- Start()나 Awake()에서 한 번만 사용하고, 참조를 저장해서 재사용하는 것이 권장됨
Physics[2D].Raycast
- 특정 방향으로 일직선으로 검사하여 콜라이더와 교차하는지 감지하는 기술
- 시작점, 방향, 최대 검사 거리, (선택적) 레이어 마스크 등을 매개변수로 받음
- hit 정보를 반환하며, 충돌한 객체, 충돌 지점, 충돌 지점의 정규화 벡터 등의 정보가 담김
- 남발하는 경우 상당한 성능을 잡아먹으므로 호출 시점과 대상을 최적화하는 것이 중요
- 디버깅을 위해 Debug.DrawRay()를 사용해 시각적으로 확인할 수 있음
이 외에 넉백이나 데미지 피격 등은 기존의 코드들을 수정하는 느낌이 강하므로 그것들을 정리하기 보단, 발생했던 문제들을 적어본다.
! 문제 발생 1
유니티를 시작했는데, Player 게임 오브젝트에 달아놓은 스크립트 컴포넌트들을 찾지 못해 에러가 자꾸 발생한다.
can't add script component because the script class cannot be found... 라는 메시지가 뜨며, 클래스 이름을 확인하라거나 오류가 있는지 검사해보라는 문구가 뜬다. 둘 다 확인해보았으나 문제가 없었다.
해결
유니티를 저장하고 재실행해봤는데도 되지 않자, 구글에 검색해보았다. 어떤 글에서 위의 방법이 통하지 않으면 패키지를 reimport 해보라는 말이 있었다. 그래서 인스펙터 창에서 우클릭 - reimport all을 눌러보자 정상적으로 돌아왔다. 유독 이번 버전에 버그가 많은 거 같다.
! 문제 발생 2
강의에서 가르쳐주는 대로 스크립트를 작성했는데, 플레이어에게서 화살이 나가지 않는 버그가 생겼다. 빨간 줄도 없어서 어디에서 문제가 발생하는지 찾기 어려웠다.
해결
코드 흐름을 쫓아가며 중간중간에 일일이 Debug.Log()를 삽입하여 어디까지 정상적으로 실행되는지 확인했다. 그런데 if문 하나를 통과하지 못하는 걸 알게 되었다.
알고 보니 if(rangedAttack == null) return; 이 조건문의 ==가 !=로 잘못 적혀있었던 것이다. 자동완성으로 작성한 구문이기에 저런 부분을 놓친 거 같다. 자동완성을 사용할 때에도 꼭 눈으로 읽으며 확인해야겠단 생각이 든다.
[결과물]
[회고]
오늘은 어제에 비해 막히는 부분은 많이 없었다. 다행인 것 같다. 개인 과제는 8강까지만 들어도 가능하다고 한다. 주말에 힘을 써봐야겠다.
'TIL > Unity' 카테고리의 다른 글
내일배움캠프 21일차 TIL - 스파르타타운 제작 마무리 (0) 2024.05.14 내일배움캠프 20일차 TIL - 스파르타타운 게임 제작 (0) 2024.05.13 내일배움캠프 18일차 TIL - 2D게임 기초 개발 입문 1 (0) 2024.05.09 내일배움캠프 17일차 TIL - Pong 게임 구현 (0) 2024.05.08 내일배움캠프 5일차 TIL - 프로젝트 발표, KPT 회고 (1) 2024.04.19