본문 바로가기

디자인패턴

디자인 패턴 <Component Pattern>

이번 포스팅에서는 디자인패턴들 중 하나인 컴포넌트 패턴(Component Pattern)에 대해 이야기 해보려한다.

 

먼저 다음과 같은 상황이 있다고 가정해보자.

아이템, 몬스터, 캐논 세 클래스가 전부 탐지하는 속성과 기능을 가지고 있음

 

아이템, 몬스터, 캐논이란 세 종류의 클래스가 있고 그 클래스들은 전부 다른 오브젝트를 탐지하는데 필요한 속성과 기능을 가지고 있다고 가정한다. 

 

그런데, 세 클래스가 전부 같은 속성과 기능을 가지고 있으므로 당연히 같은 코드가 클래스내에 중복으로 작성될 것이다. 이를 해결하기 위한 방법이 어떤것이 있을까?

 

가장 먼저, 상속을 이용한 방법이있다. 아래 사진을 보자

 

탐지라는 클래스를 만들어 아이템, 몬스터, 캐논이 이를 상속한 형태

 

다음과 같이 작성한다면 탐지 클래스에 있는 탐지속성과 기능은 아이템, 몬스터, 캐논에 작성하지 않더라도 상속될 것이다.

하지만 이렇게 코드를 구성하게 되면 당연히 바람직하지 않다.

 

왜냐하면, 상속을 한다는 것은 IS-A 관계가 성립한다는 말인데 그렇게 되면 아이템, 몬스터, 캐논이 '탐지' 라는 클래스가 되어버린다. 다시말해, 아이템, 몬스터, 캐논은 전부 '탐지' 이다.  뭔가 이상하지 않은가?

 

만약 이런식으로 코드를 구성하면 아래와 같은 상황에서 더욱 난감해진다.

 

플레이어 클래스가 추가된 상황

 

플레이어 클래스가 추가된 모습이다. 그런데 클래스 내에 속성과 기능을 보면 플레이어 클래스던 몬스터 클래스던 둘 다 공통으로 '맞고 패는데 필요한 속성, 기능' 을 가지고있다. 

 

그렇다면 이또한 중복된 코드이기에 '캐릭터'클래스를 새로 만들어 플레이어와 몬스터 클래스가 캐릭터 클래스를 상속하도록 해야할 것이다.

 

플레이어, 몬스터 클래스가 캐릭터 클래스를 상속하는 모습

 

 

이러면 몬스터라는 클래스는 결국 '탐지'라는 클래스와 '캐릭터'라는 클래스를 다중으로 상속하고있기에 유지보수에 매우 불리한 구조가 된다. 이러한 문제때문에 상속을 무조건 코드를 복사하여 저장하는 것으로 이해하면 위험한 것이다.

 

그렇다면 어떻게 해결하는것이 바람직할까?

 

그것은 바로, '탐지'라는 클래스와 '맞고패기' 클래스를 컴포넌트로 만들어 필요한 클래스가 HAS-A 관계로 가지고있을 수 있도록 하는 것이다. 아래 사진을 보자.

 

탐지, 맞고패기 클래스를 컴포넌트로 만듦

 

이렇게 만들게되면 탐지컴포넌트, 맞고패는 컴포넌트를 필요한 클래스에 따라서 독립적으로 가지기만하면 된다.

 

 

이렇게 플레이어, 몬스터 클래스는 맞고 패는 컴포넌트를, 아이템, 몬스터, 캐논 클래스는 탐지 컴포넌트를 HAS-A 관계로 가지고 필요할 때 호출하여 사용하면 된다.

 

다음 코드는 '탐지' 라는 속성과 기능을 하나의 컴포넌트로 작성한 C# 스크립트이다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DetectiveComponent : MonoBehaviour
{
	// 감지할 대상의 Layer
    [SerializeField] LayerMask targetLayerMask;
    
    // OverlapSphere와 Gizmo 를 그릴때 필요한 반지름
    [SerializeField] float radius;
    
    // Ray를 발사하는 최대 거리
    [SerializeField] float maxDistance;
	
    // 감지 가능 범위 검사
    [SerializeField]
    bool isRangeDetection;
    // Ray가 감지 했는지 검사
    [SerializeField]
    bool isRayDetection;
	
    public Vector3 LastDetectivePos
    {
        get;
        private set;
    }
    public bool IsDection
    {
        get
        {
            return isRayDetection && isRangeDetection;
        }
    }

	// 매개변수로 받는 layer가 존재하는지 확인하는 함수
    bool CheckInLayerMask(int layerIndex)
    {
        // 타겟의 layer에 & 연산으로 layerIndex 가 존재할때 true 반환
        return (targetLayerMask & (1 << layerIndex)) != 0;
    }

    void Update()
    {	
    	// OverlapSphere 에 닿는 모든 Collider의 배열을 반환
        Collider[] cols = Physics.OverlapSphere(transform.position, radius, targetLayerMask);
        
        // OverlapSphere 에 닿은 Collider 가 1개 이상이라면 true 반환
        isRangeDetection = (bool)(cols.Length > 0);
		
        //  OverlapSphere 에 닿은 Collider 가 1개 이상이라면 isRangeDetection == true
        if (isRangeDetection)
        {
            // Ray 맞는 대상 변수
            RaycastHit hit;
            // 방향 (구체 맞은 대상 - 나의 위치) 즉 나 -> 대상 방향
            Vector3 direction = ((cols[0].transform.position) - transform.position).normalized;
            // 항상 내 위치를 기반으로 움직이게 하기위해 (transform.position +) 를 해줌
            Debug.DrawLine(transform.position, transform.position + (direction * maxDistance), Color.blue);

            // 맞은 대상이 있다면
            if (Physics.Raycast(transform.position, direction, out hit, maxDistance))
            {
            	// 만약, CheckInLayerMask 함수의 매개변수에 들어가는 layer 가 targetLayer에 존재한다면 true 반환
                isRayDetection = CheckInLayerMask(hit.collider.gameObject.layer); 

                // isRayDetection 가 true 이면 실행
                if (isRayDetection)
                {
                    // LastDetectivePos = 맞은 대상, 즉 플레이어의 위치
                    LastDetectivePos = hit.transform.position;
                    
                    // 항상 내 위치를 기반으로 선을 그림. 
                    Debug.DrawLine(transform.position, transform.position + (direction * maxDistance), Color.red);
                }
            }
        }
    }
	
    // 구형 기즈모를 그리는 함수
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, radius);
    }
}

 

그리고 Monster 클래스는 해당 클래스를 컴포넌트로 가지게하면 된다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class Monster : MonoBehaviour
{
	// '탐지'컴포넌트를 HAS-A 관계로 가짐
    public DetectiveComponent detectiveComponent;
    
    // Velocity 로 이동하기 위한 리지드바디
    Rigidbody rb;
    
    [SerializeField] protected int hp = 100;
    int maxHp = 100;

    public int Hp
    {
        get 
        { 
            return hp; 
        }
        set
        {

            hp = value;
            if(hp > maxHp)
                hp = maxHp;

            if (hp <= 0)
                Die();
        }
    }
    
    public void Die()
    {
        Destroy(gameObject);
    }

    private void Start()
    {
    	// 리지드바디 가져오기
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
    	// 만약, 탐지컴포넌트의 IsDection 이 true 라면(탐지클래스에 설명)
        if(detectiveComponent.IsDection)
        {
        	// 탐지한 대상에게 가려는 방향설정
            Vector3 targetVec = (detectiveComponent.LastDetectivePos - transform.position).normalized;
            
            // 몬스터의 forward를 가려는 방향으로 초기화
            transform.forward = targetVec;
            
            // 쫓아가기
            rb.velocity = targetVec;
            Debug.Log("몬스터 움직임");
        }

    }
 
}

Unity 상에서 몬스터, 플레이어 큐브를 만들어 해당 컴포넌트를 부여하고 실행하면 몬스터가 플레이어를 감지했을 때 쫓아가는 모습을 확인 할 수 있다.