TIL(2024,06,21)팀프로젝트 자유주제 (unRailed)만들어보기

김보근·2024년 6월 21일

Unity

목록 보기
22/113
post-thumbnail

오늘은 나무 와 돌 및 도구 상호작용 리팩토리를 진행해볼 예정이다.

Axe 스크립트

using UnityEngine;

public class Axe : MonoBehaviour, IInteractable
{
    private Transform originalParent;    // 원래의 부모 Transform을 저장하는 변수

    public float detectionRange = 2.0f;  // 충돌 검출 범위
    public LayerMask interactableLayer;  // 상호작용 가능한 레이어 마스크
    private Rigidbody rb;               // Rigidbody 컴포넌트 변수

    void Start()
    {
        rb = GetComponent<Rigidbody>(); // Rigidbody 컴포넌트 가져오기
        if (rb == null)
        {
            rb = gameObject.AddComponent<Rigidbody>(); // Rigidbody가 없으면 추가
        }
        rb.isKinematic = false; // 운동학적 상태 설정 (움직임 여부)
        rb.collisionDetectionMode = CollisionDetectionMode.Continuous; // 충돌 감지 모드 설정
    }

    void Update()
    {
        CheckForCollision(); // 충돌 검출 함수 호출
    }

    void CheckForCollision()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, detectionRange, interactableLayer); // 주어진 위치에서 주어진 반경 내의 충돌체 검색
        foreach (Collider hitCollider in hitColliders)
        {
            if (hitCollider.CompareTag("wood")) // 충돌체가 "wood" 태그를 가지고 있는지 확인
            {
                Tree tree = hitCollider.GetComponent<Tree>(); // 충돌체에서 Tree 컴포넌트 가져오기
                if (tree != null)
                {
                    tree.Hit(); // Tree의 Hit 메서드 호출
                    Debug.Log("Hit the tree with an axe."); // 디버그 로그 출력
                }
            }
        }
    }

    public void Pickup(Transform holdPoint)
    {
        originalParent = transform.parent; // 현재 부모 Transform 저장
        transform.parent = holdPoint; // 새로운 부모 Transform으로 설정
        transform.localPosition = Vector3.zero; // 로컬 위치를 원점으로 설정
        transform.localRotation = Quaternion.identity; // 로컬 회전을 기본 값으로 설정
        rb.isKinematic = true; // 운동학적 상태를 Kinematic으로 설정 (움직임 없음)
    }

    public void Drop()
    {
        transform.parent = originalParent; // 원래의 부모 Transform으로 되돌리기
        rb.isKinematic = false; // 운동학적 상태를 비-Kinematic으로 설정 (움직임 가능)
    }

    public bool TryPlace()
    {
        // 특정한 도끼의 배치 로직 구현 (만약 필요하다면)
        return false; // 기본적으로 false 반환
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("wood")) // 충돌체가 "wood" 태그를 가지고 있는지 확인
        {
            Tree tree = other.GetComponent<Tree>(); // 충돌체에서 Tree 컴포넌트 가져오기
            if (tree != null)
            {
                tree.Hit(); // Tree의 Hit 메서드 호출
                Debug.Log("Hit the tree with an axe."); // 디버그 로그 출력
            }
        }
    }
}

Start(): 게임 오브젝트가 활성화될 때 호출되며, Rigidbody를 가져오거나 없으면 추가하고, 충돌 감지 모드를 설정합니다.
Update(): 매 프레임마다 CheckForCollision() 메서드를 호출하여 주변에 있는 상호작용 가능한 객체를 검출합니다.
CheckForCollision(): 주어진 범위 내에서 "wood" 태그를 가진 객체를 검출하여 해당 객체의 Tree 컴포넌트의 Hit() 메서드를 호출하고, 디버그 로그를 출력합니다.
Pickup(): 도끼를 집을 때 호출되며, 원래의 부모를 저장하고 새로운 부모로 설정하여 위치와 회전을 초기화하며, Kinematic 상태를 설정하여 움직이지 않게 합니다.
Drop(): 도끼를 떨어뜨릴 때 호출되며, 원래의 부모로 되돌리고 Kinematic 상태를 해제하여 움직일 수 있게 합니다.
TryPlace(): 특정한 배치 로직을 구현할 수 있는 메서드이며, 여기서는 기본적으로 false를 반환하여 구체적인 로직이 구현되지 않았음을 나타냅니다.
OnTriggerEnter(Collider other): 충돌체가 "wood" 태그를 가진 경우에 호출되며, 해당 충돌체의 Tree 컴포넌트의 Hit() 메서드를 호출하고 디버그 로그를 출력합니다.

Tree 스크립트

using UnityEngine;

public class Tree : MonoBehaviour
{
    public int hitPoints = 3;           // 나무의 체력
    private int currentHitPoints;       // 현재 체력
    public Material hitMaterial;        // 충돌 시 표시할 재질
    private Renderer renderer;          // Renderer 컴포넌트
    private Material originalMaterial;  // 초기 재질

    void Start()
    {
        currentHitPoints = hitPoints;   // 현재 체력을 초기 체력으로 설정
        renderer = GetComponent<Renderer>();  // Renderer 컴포넌트 가져오기
        originalMaterial = renderer.material; // 초기 재질 저장

        // Rigidbody 컴포넌트 추가 및 설정
        Rigidbody rb = GetComponent<Rigidbody>();
        if (rb == null)
        {
            rb = gameObject.AddComponent<Rigidbody>(); // Rigidbody가 없으면 추가
            rb.isKinematic = true; // 물리적인 움직임이 필요하지 않으면 Kinematic 설정
        }
    }

    public void Hit()
    {
        currentHitPoints--; // 체력 감소
        renderer.material = hitMaterial; // 충돌 시 표시할 재질로 변경
        if (currentHitPoints <= 0)
        {
            Destroy(gameObject); // 체력이 0 이하이면 오브젝트 파괴
        }
    }
}

hitPoints: 나무의 초기 체력을 설정하는 변수입니다.

currentHitPoints: 현재 나무의 체력을 나타내는 변수입니다.

hitMaterial: 충돌 시 표시할 재질을 설정하는 변수입니다.

renderer: 나무의 Renderer 컴포넌트를 저장하는 변수입니다.

originalMaterial: 나무의 초기 재질을 저장하는 변수입니다.

Start() 메서드: currentHitPoints를 hitPoints로 초기화합니다.
Renderer 컴포넌트를 가져와 originalMaterial에 초기 재질을 저장합니다.
Rigidbody 컴포넌트를 추가하고, 만약 Rigidbody가 없으면 Kinematic 모드로 설정하여 물리적인 움직임이 필요하지 않게 합니다.
Hit() 메서드:

나무가 공격받았을 때 호출되며, currentHitPoints를 감소시킵니다.
충돌 시 표시할 재질인 hitMaterial로 renderer의 재질을 변경합니다.
currentHitPoints가 0 이하가 되면 Destroy(gameObject)를 호출하여 나무를 파괴합니다.

PlayerInteraction

using UnityEngine;

public class PlayerInteraction : MonoBehaviour
{
    public float interactionRange = 2.0f;
    public float closeRange = 1.0f;
    public Transform holdPoint;
    public LayerMask interactableLayer;
    public Material highlightMaterial;

    private IInteractable heldObject = null;
    private IHighlightable highlightedObject = null;

    void Update()
    {
        HighlightInteractable();
        HandleInput();
    }

    private void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (heldObject == null)
            {
                TryPickup();
            }
            else
            {
                TryPlaceOrDrop();
            }
        }
    }

    private void TryPickup()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position, closeRange, interactableLayer);
        foreach (Collider hitCollider in hitColliders)
        {
            IInteractable interactable = hitCollider.GetComponent<IInteractable>();
            if (interactable != null && IsFacing(hitCollider.gameObject))
            {
                heldObject = interactable;
                heldObject.Pickup(holdPoint);

                IHighlightable highlightable = hitCollider.GetComponent<IHighlightable>();
                if (highlightable != null)
                {
                    highlightable.ResetHighlight();
                }

                highlightedObject = null;
                return;
            }
        }
        Debug.Log("No interactable object in range.");
    }

    private bool IsWithinView(GameObject target)
    {
        Vector3 directionToTarget = (target.transform.position - transform.position).normalized;
        Vector3 forward = transform.forward;

        forward.y = 0;
        directionToTarget.y = 0;

        forward.Normalize();
        directionToTarget.Normalize();

        float angle = Vector3.Angle(forward, directionToTarget);
        return angle <= 30f;
    }

    private void HighlightInteractable()
    {
        if (heldObject != null) return;

        Collider[] hitColliders = Physics.OverlapSphere(transform.position, closeRange, interactableLayer);
        foreach (Collider hitCollider in hitColliders)
        {
            IHighlightable highlightable = hitCollider.GetComponent<IHighlightable>();
            if (highlightable != null && IsWithinView(hitCollider.gameObject))
            {
                if (highlightedObject != null && highlightedObject != highlightable)
                {
                    highlightedObject.ResetHighlight();
                }
                highlightedObject = highlightable;
                highlightedObject.Highlight();
                return;
            }
        }

        if (highlightedObject != null)
        {
            highlightedObject.ResetHighlight();
            highlightedObject = null;
        }
    }

    private bool IsFacing(GameObject target)
    {
        Vector3 directionToTarget = (target.transform.position - transform.position).normalized;
        return Vector3.Dot(transform.forward, directionToTarget) > 0.5f;
    }

    private void TryPlaceOrDrop()
    {
        if (heldObject.TryPlace())
        {
            heldObject = null;
        }
        else
        {
            heldObject.Drop();
            heldObject = null;
        }
    }
}

Update(): 매 프레임마다 상호작용 가능한 객체를 감지하고 하이라이트를 관리합니다.
HandleInput(): 플레이어 입력을 처리하여 상호작용할 객체를 들거나 놓습니다.
TryPickup(): 주변에 있는 상호작용 가능한 객체를 감지하고, 플레이어가 정면을 바라보고 있을 때만 객체를 들 수 있습니다.
HighlightInteractable(): 주변에 있는 상호작용 가능한 객체를 하이라이트 처리합니다.
IsFacing(): 플레이어가 주어진 객체를 정면으로 바라보고 있는지를 확인합니다.
TryPlaceOrDrop(): 플레이어가 들고 있는 객체를 놓거나, 놓을 수 없으면 다시 들도록 처리합니다.

InteractableObject 스크립트

using UnityEngine;

public abstract class InteractableObject : MonoBehaviour
{
    public int hitPoints;
    private int currentHitPoints;
    public Material hitMaterial;
    private Renderer renderer;
    private Material originalMaterial;
    public GameObject dropItemPrefab; // 파괴될 때 생성할 아이템의 프리팹

    protected virtual void Start()
    {
        currentHitPoints = hitPoints;
        renderer = GetComponent<Renderer>();
        originalMaterial = renderer.material;

        Rigidbody rb = GetComponent<Rigidbody>();
        if (rb == null)
        {
            rb = gameObject.AddComponent<Rigidbody>();
            rb.isKinematic = true;
        }
    }

    public void Hit()
    {
        currentHitPoints--;
        renderer.material = hitMaterial;
        if (currentHitPoints <= 0)
        {
            DropItem();
            Destroy(gameObject);
        }
    }

    private void DropItem()
    {
        if (dropItemPrefab == null)
        {
            Debug.Log("No drop item prefab assigned.");
        }
        else
        {
            Instantiate(dropItemPrefab, transform.position, Quaternion.identity);
        }
    }

    public abstract void Pickup(Transform holdPoint);
    public abstract void Drop();
    public abstract bool TryPlace();
}

목적: 게임 내에서 상호작용 가능한 객체들의 공통적인 행동을 정의합니다.
기능: Start(): 초기화 작업을 수행하고, 필요한 컴포넌트들을 추가합니다.
Hit(): 객체가 맞았을 때의 행동을 정의하고, 필요 시 아이템을 생성하도록 합니다.
DropItem(): 객체가 파괴될 때 아이템을 생성하도록 처리합니다.
추상 메서드 (Pickup(), Drop(), TryPlace()): 각 상호작용 가능한 객체들이 구체적으로 구현해야 하는 메서드들을 선언합니다.

적용해본 패턴

적용해본 패턴

스트래티지 패턴 (Strategy Pattern)
PlayerInteraction 클래스와 상호작용 가능한 객체들(Tree, Stone)에서 사용됩니다.
IInteractable 인터페이스와 IHighlightable 인터페이스를 정의하여, 각 객체들이 자신만의 방식으로 상호작용을 구현할 수 있게 합니다. 예를 들어, Tree 클래스는 IInteractable를 구현하여 나무를 들거나 나무에서 나뭇가지를 얻는 동작을 정의할 수 있습니다.
팩토리 메서드 패턴 (Factory Method Pattern)
InteractableObject 추상 클래스에서 DropItem() 메서드를 구현하는 과정에서 사용됩니다.
InteractableObject 클래스의 DropItem() 메서드는 추상 클래스에서 선언되어 있지만, 각각의 하위 클래스에서 구체적으로 어떤 아이템을 드롭할지를 결정하는 방식으로 팩토리 메서드 패턴을 일부 적용한 예입니다.

아직 패턴을 적용해본것은 익숙하지 않지만 계속 하다보면 익숙해질것이라 생각한다.

profile
게임개발자꿈나무

0개의 댓글