-
내일배움캠프 29일차 TIL - 3D 게임 기초 개발 2TIL/Unity 2024. 5. 27. 19:48
[한 줄 요약]
유니티 숙련 주차 개인 강의를 학습하고, 3D 게임 개발 기초 단계 중 데미지 처리, 조명, 아이템 구현 등을 실습했다.
[학습 내용]
유니티 숙련 주차 강의
상태바 UI 구현
UI 캔버스 해상도 변경
캔버스 생성 후 UI ScaleMode를 Scale With Screen Size로 변경
이렇게 하면 플레이 환경에서 해상도가 바뀌더라도 개발 때 의도한 모습으로 구현된다.
체력바 UI 생성 및 구현
- 기본적인 UI를 만들어준다.
- 패키지 매니저에서 2D Sprite를 설치한다.
- 2D Sprite의 Square를 하나 만든 뒤 체력바에 추가한다.
- Image Type을 Filled로, Fill Method를 Horizontal로 변경해준다.
정렬 그룹 추가
- 빈 오브젝트 Conditions를 추가
- 컴포넌트로 Vertical Layout Group을 붙여준다.
- 앵커를 바꾸거나 위치를 적당하게 조절해준다.
- 체력바 오브젝트를 Conditions 아래로 넣는다.
배고픔, 스태미나 추가
- 체력바와 동일하게 아이콘을 바꾸고 색 변경을 해준다.
- 스크립트를 생성하고 Conditions 오브젝트에 UICondition 스크립트, 각 상태바에 Condition 스크립트를 붙여준다.
- 코드를 작성하고 인스펙터 창에서 각 변수들 값을 넣어준다.
- PlayerCondition 스크립트도 작성하고 Player 오브젝트에 붙여준다.
TryGetComponent
- Unity에서 사용하는 메서드로, 게임 오브젝트의 컴포넌트를 가져오는 기능을 제공한다.
- 특정 컴포넌트가 게임 오브젝트에 연결되어 있는지 확인하고, 연결되어 있다면 해당 컴포넌트를 가져올 수 있다.
- bool TryGetComponent<T>(out T Component) where T : Component;
- 예외를 발생시키지 않고 컴포넌트를 가져와서 사용할 수 있다.
카메라 절두체
- 피라미드 같은 모양의 윗부분을 밑면에 병렬로 잘라낸 입체형상인 절두체를 따라 카메라 렌더링에 적용한 것을 말한다.
- 시야범위(FOV)와 Near Clipping Plane, Far Clipping Plane 등의 값을 조절해 절두체의 모양을 바꾸거나, 렌더링할 범위를 지정해줄 수 있다.
Coroutine
- 작업을 다수의 프레임에 분산하는 메서드다.
- Unity에서 코루틴은 실행을 일시정지하고 제어를 Unity에 반환하지만, 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드다.
- 스레드가 아니고, 코루틴의 동기작업은 여전히 메인 스레드에서 실행된다.
플레이어 데미지 처리
캠프파이어 생성
- 준비된 캠프파이어 프리펩을 하이어라키 창에 추가한다.
- 캠프파이어 오브젝트에 Sphere Collider 컴포넌트 추가
- 피격 시 화면을 붉게 할 UI 이미지 오브젝트 추가
- Image 컴포넌트의 색을 붉게 조정해주고 일단 컴포넌트를 비활성화 해둔다. (오브젝트 비할성화 X)
- 이미지 오브젝트 이름을 DamageIndicator로 바꾸고 DamageIndicator 스크립트 추가
- 기존 PlayerCondition 스크립트에 IDamagable 인터페이스 추가
- CampFire 스크립트를 작성하고 캠프파이어 오브젝트에 추가
- DamageIndicator 스크립트 작성 후 데미지 인디케이터 오브젝트에 추가
조명 (Lighting)
Light Source
게임 또는 3D 렌더링에 광원을 추가하는 데 사용된다. 특정 위치 또는 방향에서 발생하는 빛을 나타낸다.
유형
- 점 광원(Point Light) : 모든 방향으로 균등하게 빛을 발산하는 광원 (ex : 가로등)
- 방향성 광원(Directional Light) : 무한히 멀리 위치하여 한 방향으로만 빛을 발산하는 광원
- 스포트라이트(Spotlight) : 씬의 한 점에 위치햐여 원뿔모양으로 빛을 발산하는 광원
- 면 광원(Area Light) : 표면 영역 전체에 걸쳐 균등하게 모든 방향으로 빛을 방출하며, 사각형의 한쪽 면에서만 빛을 발산하는 광원
속성
- 위치, 방향, 강도(Intensity), 색상(Color), 범위(Range), 각도(Angle) 등
- 그림자 : 라이트와 객체 사이의 관계에 따라 그림자는 라이트가 부딪히는 오브젝트 뒤에 생성
- Lightning Intensity Multiplier : 실제 환경의 빛을 조절한다.
- Reflecting Inensity Multiplier : 실제 오브젝트에 반사되는 정도를 조절한다.
성능
조명은 성능에 큰 영향을 미친다. 특히 그림자가 포함된 경우 렌더링 성능에 부정적인 영향이 있을 수 있기 때문에 최적화에 신경 써야 한다.
AnimationCurve
유니티에서 애니메이션의 키프레임을 사용하여 값을 보관하는데 사용되는 클래스이다. 시간에 따라 값을 부드럽게 변화시키는 커브를 정의하고 이를 기반으로 애니메이션을 만들 수 있다.
구성 요소
- 키프레임(Keyframe) : 시간에 따른 값을 정의하는 점을 의미한다. 시간(t)과 해당 시간에 대응하는 값(value)로 이루어진다.
- 보간 방식(Interpolation Mode) : 인접한 키프레임 사이의 값을 보간하는 방법을 지정한다. 기본적으로는 Cubic Bezier 보간 방식이 사용되며, 이외에도 선형, 스텝 등의 보간 방식이 있다.
주요 메서드 및 멤버 변수
- void AddKey(float time, float value); // 새로운 키프레임 추가
- float Evaluate(float time); // 특정 시간에 해당하는 값을 보간하여 반환
- keys : 키프레임의 배열을 가져온다. 이를 통해 키프레임을 추가, 삭제, 수정할 수 있다.
낮과 밤 구현
Sun Source
프로젝트에는 Directional Light로 구현한 태양이 떠있다. Rotation을 돌리면 태양의 위치가 바뀌고, 이를 통해 낮과 밤을 구현할 수 있다.
단순히 Directional Light만으로 태양을 구현한 것이 아니라, 이전에 Lighting 렌더링 창에서 Sun Source를 Directional Light로 설정해줬기 때문이다.
낮과 밤 구현
- 빈 오브젝트 DayAndNight 추가 후 Directional Light를 자식 오브젝트로 추가
- Directional Light를 복제하여 각각 Sun, Moon으로 이름을 바꾸고, Moon에 푸른 빛 추가 후 비활성화
- DayNightCycle 스크립트 추가 후 DayAndNight 오브젝트에 추가
- DayNightCycle 스크립트 컴포넌트 값 수정
인터페이스
특징
- 추상화 : 인터페이스는 추상적 개념으로, 실제로 구현된 메서드가 없고 메서드의 시그니처만을 가진다. 그래서 인스턴스화가 될 수 없으며 구현체가 필요하다.
- 메서드 시그니처 : 구현클래스가 반드시 구현해야 하는 메서드들의 시그니처를 정의한다. 메서드 이름, 매개변수, 반환형이 포함된다.
- 다중상속 가능 : 한 클래스가 여러 인터페이스를 동시에 구현할 수 있다.
- 강제적 구현 : 클래스가 인터페이스를 구현할 때, 인터페이스 안의 모든 메서드를 구현해야 한다.
- 인터페이스 간 확장 : 인터페이스는 다른 인터페이스를 확장(extends)하여 더 큰 범위의 공통 동작을 정의할 수 있다.
사용 이유
- 코드의 결합도를 낮추고 응집도를 올리기 위해 사용한다.
- 협업의 관점
- 개발 기간을 단축 : 인터페이스라는 틀만 작성하면 구현클래스에서 코드 작성 및 개발 가능
- 표준화 가능 : 여러 개발자가 작업해도 정형화된 작업 가능
- 독립적 프로그래밍 : 선언은 인터페이스에서, 구현은 클래스에서
아이템 구현
아이템 데이터
- 레이어에 Interactable 레이어 추가
- Crosshair 이미지 UI 추가
- 빈 오브젝트로 Item_Wood 만들고 자식 오브젝트로 프리펩 Log 가져온 뒤에 언팩
- Item_Wood 오브젝트에 Box Collider와 Rigidbody 컴포넌트 추가
- ItemObject 스크립트 생성하고 Item_Wood 오브젝트에 추가
- 3 ~ 5의 과정을 반복하여 아이템 4개 추가
- 5개의 아이템 오브젝트 모두 프리펩화
- ScriptableObject를 상속받는 ItemData 스크립트 생성 및 작성
- ItemData SO로 여러 아이템 데이터 생성 후 값 입력
- ItemObject 스크립트 작성
- ItemObject 스크립트 아이템 컴포넌트에 추가 후 값 설정
- 아이템 오브젝트를 프리펩에 오버라이드
아이템 상호작용
- Interaction 스크립트 작성
- Player 오브젝트에 Interaction 스크립트 컴포넌트 추가
- Player Input의 Interaction 이벤트에 OnInteractInput 메서드 추가
- 아이템들의 레이어를 모두 Interactable로 변경
- PromptText 텍스트 UI 생성
- Window - TextMeshPro - Font Asset Creater에서 폰트 추가
- Player 오브젝트의 Interaction 스크립트 컴포넌트 변수에 PromptText 텍스트 UI 추가
Font Asset Creator의 Character Sequence
순서대로 사용할 문자 코드의 범위를 적어주면 된다.
- 32-126 : 영어 알파벳
- 44032-55203 : 한글
- 12593-12643 : 한글 (자음 or 모음 단독)
- 8200-9900 : 특수 문자
코드
더보기UICondition.cs
using UnityEngine; public class UICondition : MonoBehaviour { public Condition health; public Condition hunger; public Condition Stamina; private void Start() { CharacterManager.Instance.Player.condition.uiCondition = this; } }
Condition.csusing UnityEngine; using UnityEngine.UI; public class Condition : MonoBehaviour { public float curValue; public float startValue; public float maxValue; public float passiveValue; public Image uiBar; private void Start() { curValue = startValue; } private void Update() { uiBar.fillAmount = GetPercentage(); } private float GetPercentage() { return curValue / maxValue; } public void Add(float value) { curValue = Mathf.Min(curValue + value, maxValue); } public void Subtract(float value) { curValue = Mathf.Max(curValue - value, 0f); } }
PlayerCondition.cs
using System; using UnityEngine; public interface IDamagable { void TakePhysicalDamage(int damage); } public class PlayerCondition : MonoBehaviour, IDamagable { public UICondition uiCondition; Condition health { get { return uiCondition.health; } } Condition hunger { get { return uiCondition.hunger; } } Condition stamina { get { return uiCondition.Stamina; } } public float noHungerHealthDecay; public event Action onTakeDamage; private void Update() { hunger.Subtract(hunger.passiveValue * Time.deltaTime); stamina.Add(stamina.passiveValue * Time.deltaTime); if (hunger.curValue <= 0f) { health.Subtract(noHungerHealthDecay * Time.deltaTime); } if (health.curValue <= 0f) { Die(); } } public void Heal(float amount) { health.Add(amount); } public void Eat(float amount) { hunger.Add(amount); } public void Die() { Debug.Log("사망"); } public void TakePhysicalDamage(int damage) { health.Subtract(damage); onTakeDamage?.Invoke(); } }
Player.cs (수정)
using System; using UnityEngine; public class Player : MonoBehaviour { public PlayerController Controller; public PlayerCondition condition; public ItemData itemData; public Action addItem; private void Awake() { CharacterManager.Instance.Player = this; Controller = GetComponent<PlayerController>(); condition = GetComponent<PlayerCondition>(); } }
DamageIndicator.cs
using System.Collections; using UnityEngine; using UnityEngine.UI; public class DamageIndicator : MonoBehaviour { public Image image; public float flashSpeed; private Coroutine coroutine; private void Start() { CharacterManager.Instance.Player.condition.onTakeDamage += Flash; } public void Flash() { // 코루틴이 이미 존재할 경우 기존의 코루틴을 종료 if (coroutine != null) { StopCoroutine(coroutine); } image.enabled = true; image.color = new Color(1f, 100f / 255f, 100f / 255f); coroutine = StartCoroutine(FadeAway()); } private IEnumerator FadeAway() { float startAlpha = 0.3f; float a = startAlpha; while (a > 0) { a -= (startAlpha / flashSpeed) * Time.deltaTime; image.color = new Color(1f, 100f / 255f, 100f / 255f, a); yield return null; } image.enabled = false; } }
CampFire.cs
using System.Collections.Generic; using UnityEngine; public class CampFire : MonoBehaviour { public int damage; public float damageRate; List<IDamagable> things = new List<IDamagable>(); private void Start() { InvokeRepeating("DealDamage", 0, damageRate); } void DealDamage() { for (int i = 0; i < things.Count; i++) { things[i].TakePhysicalDamage(damage); } } private void OnTriggerEnter(Collider other) { if (other.TryGetComponent(out IDamagable damagable)) { things.Add(damagable); } } private void OnTriggerExit(Collider other) { if(other.TryGetComponent(out IDamagable damagable)) { things.Remove(damagable); } } }
DayNightCycle.cs
using UnityEngine; public class DayNightCycle : MonoBehaviour { [Range(0f, 1f)] public float time; public float fullDayLength; public float startTime = 0.4f; private float timeRate; public Vector3 noon; [Header("Sun")] public Light sun; public Gradient sunColor; public AnimationCurve sunIntensity; [Header("Moon")] public Light moon; public Gradient moonColor; public AnimationCurve moonIntensity; [Header("Other Lighting")] public AnimationCurve lightingIntensityMultiplier; public AnimationCurve reflectionIntensityMultiplier; private void Start() { timeRate = 1.0f / fullDayLength; time = startTime; } private void Update() { time = (time + timeRate * Time.deltaTime) % 1.0f; UpdateLighting(sun, sunColor, sunIntensity); UpdateLighting(moon, moonColor, moonIntensity); RenderSettings.ambientIntensity = lightingIntensityMultiplier.Evaluate(time); RenderSettings.reflectionIntensity = reflectionIntensityMultiplier.Evaluate(time); } void UpdateLighting(Light lightSource, Gradient gradient, AnimationCurve intensityCurve) { lightSource.transform.eulerAngles = (time - (lightSource == sun ? 0.25f : 0.75f)) * noon * 4f; lightSource.color = gradient.Evaluate(time); lightSource.intensity = intensityCurve.Evaluate(time); // 해나 달이 지면 비활성화 GameObject go = lightSource.gameObject; if (lightSource.intensity == 0 && go.activeInHierarchy) { go.SetActive(false); } else if (lightSource.intensity > 0 && !go.activeInHierarchy) { go.SetActive(true); } } }
ItemData.cs
using System; using UnityEngine; public enum ItemType { Equipable, Consumable, Resource } public enum ConsumableType { Health, Hunger } [Serializable] public class ItemDataConsumable { public ConsumableType type; public float value; } [CreateAssetMenu(fileName = "Item", menuName = "New Item")] public class ItemData : ScriptableObject { [Header("Info")] public string displayName; public string description; public ItemType type; public Sprite Icon; public GameObject dropPrefab; [Header("Stacking")] public bool canStack; public int maxStackAmount; [Header("Consumable")] public ItemDataConsumable[] consumables; }
ItemObject.cs
using UnityEngine; public interface IInteractable { public string GetInteractPrompt(); public void OnInteract(); } public class ItemObject : MonoBehaviour, IInteractable { public ItemData data; public string GetInteractPrompt() { string str = $"{data.displayName}\n{data.description}"; return str; } public void OnInteract() { CharacterManager.Instance.Player.itemData = data; CharacterManager.Instance.Player.addItem?.Invoke(); Destroy(gameObject); } }
Interaction.cs
using TMPro; using UnityEngine; using UnityEngine.InputSystem; public class Interaction : MonoBehaviour { public float checkRate = 0.05f; private float lastCheckTime; public float maxCheckDistance; public LayerMask layerMask; public GameObject curInteractGameObject; private IInteractable curInteractable; public TextMeshProUGUI promptText; private Camera camera; private void Start() { camera = Camera.main; } private void Update() { if (Time.time - lastCheckTime > checkRate) { lastCheckTime = Time.time; Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2)); RaycastHit hit; if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask)) { if (hit.collider.gameObject != curInteractGameObject) { curInteractGameObject = hit.collider.gameObject; curInteractable = hit.collider.GetComponent<IInteractable>(); SetPromptText(); } } else { curInteractGameObject = null; curInteractable = null; promptText.gameObject.SetActive(false); } } } private void SetPromptText() { promptText.gameObject.SetActive(true); promptText.text = curInteractable.GetInteractPrompt(); } public void OnInteractInput(InputAction.CallbackContext context) { if(context.phase == InputActionPhase.Started && curInteractable != null) { curInteractable.OnInteract(); curInteractGameObject = null; curInteractable = null; promptText.gameObject.SetActive(false); } } }
[결과물]
[회고]
오늘은 아이템과 관련된 것들을 구현했다. 많은 것들을 한 거 같은데 생각보다 결과물이 엄청나지는 않았다. 그래도 스크립터블 오브젝트나 레이캐스트를 활용해서 이것들을 구현해보는 과정이 좋은 경험이었던 것 같다. 아이템 외에도 조명을 다루거나, 코루틴, 레이아웃 그룹 등을 활용해볼 수 있어서 좋았다. 이걸 잊어버리지 않게 계속 복습해야겠다.
'TIL > Unity' 카테고리의 다른 글
내일배움캠프 31일차 TIL - 3D 게임 기초 개발 - 개인 과제 1 (0) 2024.05.29 내일배움캠프 30일차 TIL - 3D 게임 기초 개발 3 (0) 2024.05.28 내일배움캠프 28일차 TIL - 3D 게임 기초 개발 1 (0) 2024.05.24 내일배움캠프 27일차 TIL - 유니티 입문 팀 프로젝트 6 (0) 2024.05.23 내일배움캠프 26일차 TIL - 유니티 입문 팀 프로젝트 5 (0) 2024.05.22