-
내일배움캠프 18일차 TIL - 2D게임 기초 개발 입문 1TIL/Unity 2024. 5. 9. 20:48
[학습목표]
Top-Down 게임을 제작하면서 유니티 2D 게임 개발 기초를 학습한다. 강의를 시청하고 입력과 캐릭터 이동, Input System, 충돌 처리와 타일맵, 쿼터니언과 조준 시스템, 공격시스템, 스텟 시스템 등에 대해 학습하고 구현한다.
[학습내용]
유니티 공부가 본격적으로 시작된 거 같다. 이번주에 강의를 완강하는 것을 목표로 열심히 들었다. 오늘부터는 Top-Down 뷰로 구성된 슈팅게임을 구현한다.
개요
갈수록 프로젝트가 복잡해지기 때문에 이런 도식화가 필요해지는 걸 느끼는 거 같다. 대충 둘러보면 입력과 관련된 클래스, 공격과 관련된 클래스, 애니메이션과 관련된 클래스, 에너미 시스템과 관련된 클래스 등이 보인다. 오브젝트 풀도 배울 수 있을 것으로 보인다.
유니티 스크립트
- 유니티 스크립트는 MonoBehaviour를 상속받은 클래스를 통해 작동한다. 필요한 변수, 함수 등을 정의하고 구현한다.
필요한 경우 Start(), Update()등을 오버라이딩 할 수 있다. - 실행 순서가 정해진 메소드들이 있다. 주로 게임 오브젝트의 생성, 초기화, 업데이트, 파괴 등을 수행한다.
중요한 기능
- PPU(Pixel Per Unit): 스프라이트의 픽셀 수와 그것이 차지하는 공간의 관계를 설명한다. PPU 값이 클수록 스프라이트는 작아진다. 성능에 관계있으므로 적당한 값이 중요하다.
- SerializeField: private로 선언된 변수를 인스펙터에서 직접 수정 가능한 필드로 표시해준다.
캐릭터 이동 기본 코드
using UnityEngine; public class InputManagerMovement : MonoBehaviour { Rigidbody2D rigidbody; [SerializeField] private float speed; void Start() { rigidbody = GetComponent<Rigidbody2D>(); } void Update() { float vertical = Input.GetAxis("Vertical"); float horizontal = Input.GetAxis("Horizontal"); Vector2 direction = new Vector2(horizontal, vertical); direction = direction.normalized; // normalized는 벡터의 길이를 1로 만들어주는 정규화 rigidbody.velocity = direction * speed; } }
Input System
위의 기초적인 InputManager로는 한계가 있다. 다양한 플랫폼에 대응하거나, 키를 리바인딩 하는 등의 기능이 모자란 부분이 있고, 한 클래스에 모든 게 구현되어 있어, 단일 책임 원칙(SRP)과 거리가 먼 구조이다.
새로운 Input System의 핵심 개념
- Input Action: 입력 행동을 정의한다. 예) 점프, 공격
- Input Action Asset: 여러 입력 행동을 그룹화하고, 이를 통해 재사용이 가능해진다.
- Player Input Component: 자동으로 입력을 처리하고 해당 게임 오브젝트에 메시지를 보내는 컴포넌트
Input System을 사용하려면 Package를 추가해야 한다. 프로젝트의 경량화를 위해 빼놓은 것 같다.
Input System의 다양한 옵션
- Action Type: 입력을 어떻게 받는가
- Value: 일반적인 상태에 사용. 다양한 컨트롤에 대응
- Button: 눌렀을 때 발생하는 액션에 사용. Control Type이 Button으로 고정
- Pass-Through: 명확화(Disambiguation)를 거치지 않은 value.
- Control Type: 입력 데이터가 어떤 식으로 들어오는가
- Axis(float)
- Button(0 or 1)
- Vector2
Input Action 생성, 수정하기
우측 상단에서 Add Control Scheme을 통해 Control Scheme에 키보드와 마우스를 추가해줘야 한다.
이후 Action Map을 추가해주고, Action Type과 Control Type을 설정해 줬다.
Move, Look: Value/Vector2
Fire: Value/Any
그리고 Move에 +버튼을 눌러 바인딩으로 Add Up/Down/Left/Right Composite를 설정해줬다.
플레이어 이동 관련 스크립트
using System; using UnityEngine; public class TopDownController : MonoBehaviour { public event Action<Vector2> OnMoveEvent; public event Action<Vector2> OnLookEvent; public void CallMoveEvent(Vector2 direction) { OnMoveEvent?.Invoke(direction); } public void CallLookEvent(Vector2 direction) { OnLookEvent?.Invoke(direction); } }
TopDownController.cs
using UnityEngine; using UnityEngine.InputSystem; public class PlayerInputController : TopDownCharacterController { private Camera _camera; private void Awake() { _camera = Camera.main; } public void OnMove(InputValue value) { // Debug.Log("OnMove" + value.ToString()); Vector2 moveInput = value.Get<Vector2>().normalized; CallMoveEvent(moveInput); } public void OnLook(InputValue value) { // Debug.Log("OnLook" + value.ToString()); Vector2 newAim = value.Get<Vector2>(); Vector2 worldPos = _camera.ScreenToWorldPoint(newAim); newAim = (worldPos - (Vector2)transform.position).normalized; if (newAim.magnitude >= .9f) // Vector 값을 실수로 변환 { CallLookEvent(newAim); } } public void OnFire(InputValue value) { Debug.Log("OnFire" + value.ToString()); } }
PlayerInputController.cs
using UnityEngine; public class TopDownMovement : MonoBehaviour { private TopDownController movementController; private Rigidbody2D movementRigidbody; private Vector2 movementDirection = Vector2.zero; private void Awake() { movementController = GetComponent<TopDownController>(); movementRigidbody = GetComponent<Rigidbody2D>(); } private void Start() { movementController.OnMoveEvent += Move; } private void FixedUpdate() { ApplyMovement(movementDirection); } private void Move(Vector2 direction) { movementDirection = direction; } private void ApplyMovement(Vector2 direction) { direction = direction * 5; movementRigidbody.velocity = direction; } }
TopDownMovement.cs
이 세 스크립트는 다음과 같은 순서로 진행된다.
- Player 게임 오브젝트에 추가한 PlayerInput 컴포넌트가 PlayerInputController의 OnMove() 호출
이 때 OnMove()라는 정해진 메소드 이름을 사용 - OnMove()가 TopDownController의 CallMoveEvent()를 호출
- CallMoveEvent()가 OnMoveEvent에 등록된 이벤트를 탐색
- TopDownMovement로부터 등록되어 있는 Move() 실행
- 움직일 방향 설정 완료
- FixedUpdate() 때 ApplyMovement()를 호출해 실제 이동 실행
타일맵
2D 게임에서 자주 사용되는 타일 형태 맵을 제작할 수 있도록 Tilemap 시스템이 있다.
구성 요소
- Tilemap GameObject: 특정 타일의 배치를 관리한다. Tilemap Grid의 자식으로 위치한다.
- Grid GameObject: 타일맵이 위치할 기본 격자
- Tilemap Renderer: 모양 그리는 렌더러
- Tilemap Collider 2D: 물리적인 경계를 통해 상호작용 가능하게 한다.
- Tile Assets: 개별타일의 모양과 동작을 정의한다. 여러 타일을 묶어 Tileset이라고 한다.
타일맵 그리기
Inspector 창에서 Create - 2D Object - Tilemap - Rectangular
기본적으로 Grid 밑에 생성
Window - 2D - Tile Palette로 타일 파레트 창을 띄울 수 있다.
Create New Palette를 누르고 스프라이트를 드래그하여 파레트 안으로 넣는다.
조준 시스템
조준 시스템을 구현하기 위해서는 먼저 쿼터니언과 삼각함수(주로 아크탄젠트)를 알아야 한다.
쿼터니언
- 4차원 복소수를 이용한 회전 표현 방법. (x, y, z, w)의 형태로 표현
- 기존 오일러각(Euler Angle) 방식은 짐벌락(Gimber Lock) 문제 발생 가능성 존재
- 짐벌락이란 과한 회전 등에 의해 여러 축이 서로 겹쳐졌을 때 컴퓨터가 처리하지 못하는 문제
- 유니티는 Vector3를 통한 오일러각으로 표현되지만 시중의 많은 소프트웨어는 쿼터니언 사용
- 값은 직접 건드리지 않는 것이 좋음
- 주요 메소드
- Quaternion.Euler(0f, 0f, 90f): 오일러각을 쿼터니언으로 변경
- Quaternion.LookLotation: 앞과 위를 특정한 방향으로 하는 회전 쿼터니언 생성
- Quaternion.Slerp: 쿼터니언과 다른 쿼터니언 사이의 내분점, 예) Q1과 Q2 사이 30% 지점은?
삼각함수와 아크탄젠트
삼각함수는 알려진 한 각도를 끼고 있는 두 변의 비율을 알기 위해 사용하는 함수다. 탄젠트는 그 중에서도 밑변과 높이를 이용하기 때문에 이를 좌표로 사용할 수 있다.
반대로 역삼각함수는 비율을 통해 각도를 구하는 함수이다. 아크탄젠트는 그렇다면 비율이 좌표이기 때문에 좌표를 통해 각도를 구할 수 있다.
또한 좌표의 x, y값의 부호를 통해 360도를 다 구분해 낼 수 있다.
Mathf.Atan2(y, x)와 같은 방식으로 사용하며, y와 x의 위치에 유의해야 한다.
라디안의 형태로 값을 반환하며, Mathf.Rad2Deg를 곱해주어 디그리 형태로 바꿔줘서 사용한다.
using UnityEngine; public class TopDownAimRotation : MonoBehaviour { [SerializeField] private SpriteRenderer armRenderer; [SerializeField] private Transform armPivot; [SerializeField] private SpriteRenderer characterRenderer; private TopDownController _controller; private void Awake() { _controller = GetComponent<TopDownController>(); } void Start() { _controller.OnLookEvent += OnAim; } public void OnAim(Vector2 newAimDirection) { // OnLook RotateArm(newAimDirection); } private void RotateArm(Vector2 direction) { float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; // Rad2Deg는 Radian을 Degree로 바꿔줌 characterRenderer.flipX = Mathf.Abs(rotZ) > 90f; armPivot.rotation = Quaternion.Euler(0, 0, rotZ); } }
TopDownAimLotation.cs
공격 시스템
마우스 클릭을 통해 실제 탄알을 발사하는 기능을 만든다. 아직 날아가진 않고 생성까지만 구현한다.
public void OnFire(InputValue value) { IsAttacking = value.isPressed; }
PlayerInputController.cs 수정
public event Action OnAttackEvent; private float timeSinceLastAttack = float.MaxValue; protected bool IsAttacking { get; set; } protected virtual void Update() { HandleAttackDelay(); } private void HandleAttackDelay() { if(timeSinceLastAttack <= 0.2f) { timeSinceLastAttack += Time.deltaTime; } if(IsAttacking && _timeSinceLastAttack > 0.2f) { timeSinceLastAttack = 0; CallAttackEvent(); } }
TopDownController.cs 코드 추가
using UnityEngine; public class TopDownShooting : MonoBehaviour { private TopDownController controller; [SerializeField] private Transform projectileSpawnPosition; private Vector2 aimDirection = Vector2.right; public GameObject testPrefab; private void Awake() { controller = GetComponent<TopDownController>(); } void Start() { controller.OnAttackEvent += OnShoot; controller.OnLookEvent += OnAim; } private void OnAim(Vector2 newAimDirection) { aimDirection = newAimDirection; } private void OnShoot() { CreateProjectile(); } private void CreateProjectile() { Instantiate(testPrefab, projectileSpawnPosition.position, Quaternion.identity); } }
TopDownShooting.cs
탄알(Arrow)은 프리펩으로 만들고 Instantiate 하는 방식을 사용한다.
보통 프리펩을 만들 땐 하위 게임 오브젝트에 그 기능을 구현하는 방식이 선호된다고 한다.
스크립터블 오브젝트(Scriptable Object, SO)
- 유니티에서 데이터를 저장하고 관리하는 유연한 데이터 컨테이너
- 재사용 가능한 데이터/설정을 저장
- 에디터와 통합되어 인스펙터 창에서 수정, 관리 가능
먼저 AttackSO를 만들고, 그걸 상속받아 원거리 공격용 RangedAttackSO를 만든다.
using UnityEngine; [CreateAssetMenu(fileName = "DefaultAttackSO", menuName = "TopDownController/Attacks/Default", order = 0)] public class AttackSO : ScriptableObject { [Header("Attack Info")] public float size; public float delay; public float power; public float speed; public LayerMask target; [Header("Knock Back Info")] public bool isOnKnockback; public float knockbackPower; public float knockbackTime; }
AttackSo.cs
using UnityEngine; [CreateAssetMenu(fileName = "RangedAttackSO", menuName = "TopDownController/Attacks/Ranged", order = 1)] public class RangedAttackSO : AttackSO { [Header("Ranged Attack Data")] public string bulletNameTag; public float duration; public float spread; public int numberofProjectilesPerShot; public float multipleProjectilesAngel; public Color projectileColor; }
RangedAttackSO.cs
다음으로, 캐릭터 스탯을 저장할 CharacterStat을 만든다.
using System; using UnityEngine; public enum StatsChangeType { Add, // 0 Multiple, // 1 Override, // 2 } [Serializable] public class CharacterStat { public StatsChangeType statsChangeType; [Range(1, 100)] public int maxHealth; [Range(1f, 20f)] public float speed; public AttackSO attackSO; }
CharacterStat.cs
using UnityEngine; using System.Collections.Generic; public class CharacterStatHandler : MonoBehaviour { [SerializeField] private CharacterStat baseStats; public CharacterStat CurrentStat { get; private set; } public List<CharacterStat> statsModifiers = new List<CharacterStat>(); private void Awake() { UpdateCharacterStat(); } private void UpdateCharacterStat() { AttackSO attackSO = null; if (baseStats.attackSO != null) { attackSO = Instantiate(baseStats.attackSO); } CurrentStat = new CharacterStat { attackSO = attackSO }; CurrentStat.statsChangeType = baseStats.statsChangeType; CurrentStat.maxHealth = baseStats.maxHealth; CurrentStat.speed = baseStats.speed; } }
CharacterStatHandler.cs
지금은 핸들러에서 스탯 보정 작업을 구현하지 않아, 기본 스탯값으로 초기화하는 기능만 존재한다.
나중에 구현하게 되면 역할이 생길 것이다.
SO 데이터 생성
Create - TopDownController - Attacks - Ranged로 생성
[결과물]
[회고]
어제는 쉬어가는 느낌이 강했는데, 오늘은 많이 빡빡했다. 내일도 오늘과 비슷한 양을 학습해야 이번 주 안에 강의를 끝낼 수 있을 거 같다. 그래도 배운 게 많아서 뿌듯하긴 하다. 머릿속에 다 들어온지는 모르겠지만.
'TIL > Unity' 카테고리의 다른 글
내일배움캠프 20일차 TIL - 스파르타타운 게임 제작 (0) 2024.05.13 내일배움캠프 19일차 TIL - 2D게임 기초 개발 입문 2 (0) 2024.05.10 내일배움캠프 17일차 TIL - Pong 게임 구현 (0) 2024.05.08 내일배움캠프 5일차 TIL - 프로젝트 발표, KPT 회고 (1) 2024.04.19 내일배움캠프 4일차 TIL - GitHub 특강, 프로젝트 완성 (1) 2024.04.18 - 유니티 스크립트는 MonoBehaviour를 상속받은 클래스를 통해 작동한다. 필요한 변수, 함수 등을 정의하고 구현한다.