ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 내일배움캠프 29일차 TIL - 3D 게임 기초 개발 2
    TIL/Unity 2024. 5. 27. 19:48

     

    [한 줄 요약]

    유니티 숙련 주차 개인 강의를 학습하고, 3D 게임 개발 기초 단계 중 데미지 처리, 조명, 아이템 구현 등을 실습했다.


    [학습 내용]

     

    유니티 숙련 주차 강의


    상태바 UI 구현

    UI 캔버스 해상도 변경

    캔버스 생성 후 UI ScaleMode를 Scale With Screen Size로 변경

    UI Scale Mode
    Refrence Resolution의 값 변경

     

    이렇게 하면 플레이 환경에서 해상도가 바뀌더라도 개발 때 의도한 모습으로 구현된다.

     

    체력바 UI 생성 및 구현

    1. 기본적인 UI를 만들어준다.
    2. 패키지 매니저에서 2D Sprite를 설치한다.
    3. 2D Sprite의 Square를 하나 만든 뒤 체력바에 추가한다.
    4. Image Type을 Filled로, Fill Method를 Horizontal로 변경해준다.

    1. 체력바 UI 구성
    2. 2D Sprite 설치
    4. Fill Amount 변경 시 체력바가 줄어들거나 늘어난다.

     

    정렬 그룹 추가

    1. 빈 오브젝트 Conditions를 추가
    2. 컴포넌트로 Vertical Layout Group을 붙여준다.
    3. 앵커를 바꾸거나 위치를 적당하게 조절해준다.
    4. 체력바 오브젝트를 Conditions 아래로 넣는다.

    2. 정렬 그룹 생성
    오브젝트가 늘어나도
    자동 정렬된다.

     

    배고픔,  스태미나 추가

    1. 체력바와 동일하게 아이콘을 바꾸고 색 변경을 해준다.
    2. 스크립트를 생성하고 Conditions 오브젝트에 UICondition 스크립트, 각 상태바에 Condition 스크립트를 붙여준다.
    3. 코드를 작성하고 인스펙터 창에서 각 변수들 값을 넣어준다.
    4. PlayerCondition 스크립트도 작성하고 Player 오브젝트에 붙여준다.

    1. 아이콘, 색 변경
    스크립트 생성
    Health
    PlayerCondition
    Player 오브젝트

     

    TryGetComponent

    • Unity에서 사용하는 메서드로, 게임 오브젝트의 컴포넌트를 가져오는 기능을 제공한다.
    • 특정 컴포넌트가 게임 오브젝트에 연결되어 있는지 확인하고, 연결되어 있다면 해당 컴포넌트를 가져올 수 있다.
    • bool TryGetComponent<T>(out T Component) where T : Component;
    • 예외를 발생시키지 않고 컴포넌트를 가져와서 사용할 수 있다.

     

    카메라 절두체

    • 피라미드 같은 모양의 윗부분을 밑면에 병렬로 잘라낸 입체형상인 절두체를 따라 카메라 렌더링에 적용한 것을 말한다.
    • 시야범위(FOV)와 Near Clipping Plane, Far Clipping Plane 등의 값을 조절해 절두체의 모양을 바꾸거나, 렌더링할 범위를 지정해줄 수 있다.

    카메라 절두체

     

    Coroutine

    • 작업을 다수의 프레임에 분산하는 메서드다.
    • Unity에서 코루틴은 실행을 일시정지하고 제어를 Unity에 반환하지만, 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드다.
    • 스레드가 아니고, 코루틴의 동기작업은 여전히 메인 스레드에서 실행된다.

    코루틴 동작 방식

     

    플레이어 데미지 처리

    캠프파이어 생성

    1. 준비된 캠프파이어 프리펩을 하이어라키 창에 추가한다.
    2. 캠프파이어 오브젝트에 Sphere Collider 컴포넌트 추가
    3. 피격 시 화면을 붉게 할 UI 이미지 오브젝트 추가
    4. Image 컴포넌트의 색을 붉게 조정해주고 일단 컴포넌트를 비활성화 해둔다. (오브젝트 비할성화 X)
    5. 이미지 오브젝트 이름을 DamageIndicator로 바꾸고 DamageIndicator 스크립트 추가
    6. 기존 PlayerCondition 스크립트에 IDamagable 인터페이스 추가
    7. CampFire 스크립트를 작성하고 캠프파이어 오브젝트에 추가
    8. DamageIndicator 스크립트 작성 후 데미지 인디케이터 오브젝트에 추가

    1. 캠프파이어 프리펩
    2. Sphere Collider 컴포넌트 추가
    4. Image 컴포넌트 설정
    7. CampFire 스크립트 컴포넌트
    8. 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로 설정해줬기 때문이다.

    태양의 위치
    Rendering - Lighting - Sun Source

     

    낮과 밤 구현

    1. 빈 오브젝트 DayAndNight 추가 후 Directional Light를 자식 오브젝트로 추가
    2. Directional Light를 복제하여 각각 Sun, Moon으로 이름을 바꾸고, Moon에 푸른 빛 추가 후 비활성화
    3. DayNightCycle 스크립트 추가 후 DayAndNight 오브젝트에 추가
    4. DayNightCycle 스크립트 컴포넌트 값 수정

    2. Sun, Moon
    4. DayNightCycle 스크립트 컴포넌트 값 수정
    4. 그래디언트 설정 시 더블클릭하면 마커가 하나 더 생긴다.

     

     

    인터페이스

    특징

    • 추상화 : 인터페이스는 추상적 개념으로, 실제로 구현된 메서드가 없고 메서드의 시그니처만을 가진다. 그래서 인스턴스화가 될 수 없으며 구현체가 필요하다.
    • 메서드 시그니처 : 구현클래스가 반드시 구현해야 하는 메서드들의 시그니처를 정의한다. 메서드 이름, 매개변수, 반환형이 포함된다.
    • 다중상속 가능 : 한 클래스가 여러 인터페이스를 동시에 구현할 수 있다.
    • 강제적 구현 : 클래스가 인터페이스를 구현할 때, 인터페이스 안의 모든 메서드를 구현해야 한다.
    • 인터페이스 간 확장 : 인터페이스는 다른 인터페이스를 확장(extends)하여 더 큰 범위의 공통 동작을 정의할 수 있다.

     

    사용 이유

    • 코드의 결합도를 낮추고 응집도를 올리기 위해 사용한다.
    • 협업의 관점
      • 개발 기간을 단축 : 인터페이스라는 틀만 작성하면 구현클래스에서 코드 작성 및 개발 가능
      • 표준화 가능 : 여러 개발자가 작업해도 정형화된 작업 가능
      • 독립적 프로그래밍 : 선언은 인터페이스에서, 구현은 클래스에서

     

     

    아이템 구현

    아이템 데이터

    1. 레이어에 Interactable 레이어 추가
    2. Crosshair 이미지 UI 추가
    3. 빈 오브젝트로 Item_Wood 만들고 자식 오브젝트로 프리펩 Log 가져온 뒤에 언팩
    4. Item_Wood 오브젝트에 Box Collider와 Rigidbody 컴포넌트 추가
    5. ItemObject 스크립트 생성하고 Item_Wood 오브젝트에 추가
    6. 3 ~ 5의 과정을 반복하여 아이템 4개 추가
    7. 5개의 아이템 오브젝트 모두 프리펩화
    8. ScriptableObject를 상속받는 ItemData 스크립트 생성 및 작성
    9. ItemData SO로 여러 아이템 데이터 생성 후 값 입력
    10. ItemObject 스크립트 작성
    11. ItemObject 스크립트 아이템 컴포넌트에 추가 후 값 설정
    12. 아이템 오브젝트를 프리펩에 오버라이드

    1. Interactable 레이어 추가
    2. Crosshair UI
    5. Item_Wood 컴포넌트 설정
    6. 생성된 아이템
    9. 아이템 데이터 생성
    9. 데이터 값 입력
    11. ItemObject 스크립트 추가

     

    아이템 상호작용

    1. Interaction 스크립트 작성
    2. Player 오브젝트에 Interaction 스크립트 컴포넌트 추가
    3. Player Input의 Interaction 이벤트에 OnInteractInput 메서드 추가
    4. 아이템들의 레이어를 모두 Interactable로 변경
    5. PromptText 텍스트 UI 생성
    6. Window - TextMeshPro - Font Asset Creater에서 폰트 추가
    7. Player 오브젝트의 Interaction 스크립트 컴포넌트 변수에 PromptText 텍스트 UI 추가

     

    3. 이벤트 추가
    4. 레이어 변경
    6. 폰트 에셋 크리에이어 위치
    6. 폰트 에셋 크리에이터 설정
    7. PromptText 추

     

    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.cs

    using 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);
            }
        }
    }

    [결과물]

    아이템 상호작용 모습

     


    [회고]

    오늘은 아이템과 관련된 것들을 구현했다. 많은 것들을 한 거 같은데 생각보다 결과물이 엄청나지는 않았다. 그래도 스크립터블 오브젝트나 레이캐스트를 활용해서 이것들을 구현해보는 과정이 좋은 경험이었던 것 같다. 아이템 외에도 조명을 다루거나, 코루틴, 레이아웃 그룹 등을 활용해볼 수 있어서 좋았다. 이걸 잊어버리지 않게 계속 복습해야겠다.