전략 패턴 ( Strategy Pattern )

더보기

목적을 이루기 위해 정해진 공통의 행동이 있지만 그 안에서 공통의 행동전략들을 만들어 접근하는 패턴

예시 1)

A가 공항을 가기야된다라는 목적
1.차를 타고 간다. -> 이동한다.
2.비행기를 타고 간다. -> 이동한다.
3.걸어간다. -> 이동한다.
이동이라는 행동이 있지만 각 전략(동작)들이 다 다르다.

예시 2)

Player가 게임 아이템과 충돌했을때 사용되려는 목적
1.회복 아이템을 획득 했다. -> 아이템의 획득 및 사용
2.공격 아이템을 획득 했다. -> 아이템의 획득 및 사용
이런식으로 사용이 가능해진다.

그럼 이때 행동전략의 회복아이템,공격 아이템의 동작방식이 다를 경우 각 class의 로직을 따로 작성을 해줄수도 있다.

Interface를 이용하여 전략의 틀(규칙)을 정한뒤  
각 Item들은 Interface를 상속하여 사용하고 (Item의 부모클래스에만 해도 사용 가능)
그 아이템을 먹은 Player class에서 해당 인터페이스를 변수를 호출하여 사용하는것이다.

즉, 이 위의 행동들을 간단하게 요약하면 [일반적으로 같은 작업을 수행하는 다양한 방법을 선택] 하는 것이다.
 

예시 코드 

예시 코드는 플레이어가 월드에 뿌려진 아이템을 Ray 로 검출시 UI에 아이템 설명이 출력되고

상호작용 키를 누르면 아이템이 플레이어 클래스의 저장되고 해당 아이템은 사라진다다. 즉 이 두 목적은 모든 아이템에서 동일하게 일어나기에 목적으로 설정하여 전략패턴으로 사용이 가능해지는 것이다.

 

IInteractable.cs & itemObject.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

//아이템은 보통 여러 종류가 있고 기능도 다르게 동작하는 것들도 있을것이다.
//그럴때마다 클래스를 새로 만들거나 if문으로 계속 연결하면서 만들면 작업 효율 및 가독성이 떨어지고 결합성은 올라가 스파게티 코드가 되어버린다.
//그런점을 방지하기 위해 Interface를 해서 Item에서 공통적으로 사용하는 기능들을 미리 선언하여 묶어 사용하기로 한다.

public interface IInteractable //상호작용이 가능한 오브젝트 인터페이스
{
    public string GetinteractPrompt(); // 화면에 띄워줄 Prompt 함수들을 작성
    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); //상호작용하면 월드에 있는 아이템은 기능을 다했으므로 사라져야한다.

    }
}

Player 객체가 아이템에 다가가면(Ray로 검사) GetinteractPrompt() 메서드로  UI에 아이템 이름 및 설명을 출력하고 

 OnInteract() 메서드로 상호작용 키를 누르면 각 아이템의 해당 기능(행동 전략)을 동작하는 기능이다. 

 

InterAction.cs ( 플레이어 Class 내에 참조도어 사용중 )

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;

//상호작용 기능을 하는 Class
//카메라에서 Ray를 쏴서 충돌되는 오브젝트들의 IInteractalbe 컴포넌트가 있는지 로 체크하여 사용할 예정

public class Interaction : MonoBehaviour
{
    public float checkRate = 0.05f; //Ray를 다시 쏘게하는 체크 주기(최신화)
    private float lastCheckTime; //마지막에 Ray를 쏜 시간 저장
    public float maxCheckDistance; // Ray의 측정거리
    public LayerMask layerMask; //Raycast시 사용되는 레이어마스크(어떤 레이어가 달려있는 게임오브젝트 체크기준)

    //캐싱하는 자료가 이 두 변수에 담겨져있음
    public GameObject curInteractGameObject; // 상호작용 성공시 해당 아이템의 게임오브젝트를 저장할 변수 
    private IInteractable curInteractable; // 해당 게임오브젝트를 캐싱할 Interface 변수

    //인강 수업 진행중에는 Inspector에서 넣어서 사용하지만 나중에는 스크립트로 불러오게 하는것을 생각해봐야함
    public TextMeshProUGUI promptText; // 상호작용시 뜨는 prompt, //UI와 기능 분리시 어떡해 할지 리팩토링 추천
    
    private Camera camera; //Ray를 카메리 기준으로 발사할것이기에 카메라를 멤버변수로 선얺

    // Start is called before the first frame update
    void Start()
    {
        camera = Camera.main;
    }

    // Update is called once per frame
    void Update()
    {

        if(Time.time - lastCheckTime > checkRate)
        {
            lastCheckTime = Time.time;

            //new Vector3(Screen.width / 2, Screen.height / 2) : 화면의 정 중앙에서 Ray를 쏘게 하기위해 /2 연산을 함
            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)
    {
        /*curInteractable != null : 인터액션 상호작용을 하려면 현재 상호작용이 가능한 
         타겟(IInteractable 상속클래스)가 있어야되기때문에 조건문에 추가*/
        if (context.phase == InputActionPhase.Started && curInteractable != null)
        {
            curInteractable.OnInteract();
            curInteractGameObject = null;
            curInteractable = null;
            promptText.gameObject.SetActive(false);
        }
    }
}

프로토타입 패턴 ( ProtyType Pattern  )  

더보기

Prototype은 실제 산업에서는 대량생산전 각종 테스트를 수행하는데 사용된다.
하지만 코드영역 에서는 Prototype은 자기 자신을 복사본으로 찎어내는 틀에 가깝습니다.

유니티에는 기본적으로 Prototype 디자인패턴이 적용되어있는데 
Prefab에 적용되어 있습니다. 우리가 각종 오브젝트들과 컴포넌트를 합쳐 하나의 프리팹으로 만들고
그 프리팹을 호출하여 월드에 배치하는 것이 프로토타입패턴입니다.

Prefab 자체가 프로토 타입이다.

컴포지트 패턴 ( Composite Pattern  )

더보기

Composite 패턴은 단일 객체와 복합 객체를 동일하게 다룰 수 있게 해주는 디자인 패턴입니다.

이 패턴은 객체들을 트리 구조로 구성하여, 부분-전체 계층 구조를 표현할 수 있습니다.

복합 객체(Composite)와 단일 객체(Leaf)를 같은 인터페이스로 다루게 되므로, 클라이언트는 객체의 구성 요소가 단일 객체인지, 복합 객체인지 신경 쓸 필요 없이 동일한 방식으로 조작할 수 있습니다. 그렇기에 각종 컴포지트에서 편하게 동일한 메서드를 호출하여 사용 할 수 있게 하는것이 장점입니다. 

Unity에서 GameObject의 Inspector에 들어가는 다양한 컴포넌트들이 Composite 패턴입니다. GameObject는 여러 컴포넌트를 가질 수 있으며, 각각을 독립적으로 관리하면서도 GameObject 하나에 모두 붙일 수 있습니다GameObject는 복합 객체로, 각 컴포넌트는 단일 객체로 작동하며, 둘 모두 동일한 방식으로 조작됩니다.

컴포지트 패턴에서 각종 구성요소 설명

 

컴포지트 패턴의 간단한 UML
Unity에서의 컴포넌트 시스템은 컴포지트 디자인패턴의 일종이다.

 

 

퍼사드 패턴 ( Facade Pattern 

더보기

나의 객체에서 동작해야되는 하위 동작들을 전부 참조하여 사용하는 패턴을 퍼사드 패턴이라고한다.

예시1)

플레이어 객체는 

조작(Controller),

모델 출력(SpriteRenderer,MeshRenderer),

스테이터스(StatusHandler),

상호작용(Interaction)

등등 많은 하위 동작들이 필요하다.


플레이어 Class내에 조작,모델출력,스테이터스,상호작용 등등을 참조해서 사용하고 조작 같은경우 이동,공격,점프,회피 등등 여러 행동들이 있는데 이것은 또 추가의 파서드 패턴이 되어 관리하게된다.

즉, 하나의 객체에 여러 하위 동작들을 담아 사용하는것을 퍼사드 패턴이라고 한다. 

 

Player.cs

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

public class Player : MonoBehaviour
{
    public PlayerController controller;
    public PlayerCondition condition;
    public Equipment equip;

    public ItemData itemData; //상호작용하는 아이템 데이터를 저장하는 변수 (타겟 오브젝트)
    public Action additem; //event를 선언하여 아이템을 추가하는 함수

    public Transform dropPosition; //인벤토리에서 아이템을 버리는 위치 변수 

    private void Awake()
    {
        CharacterManager.Instance.Player = this;//캐릭터 매니저에 자기 자신을 등록
        controller = GetComponent<PlayerController>();
        condition = GetComponent<PlayerCondition>();
        equip = GetComponent<Equipment>();
    }
}

 플레이어 클래스의 컨트롤러,컨디션(스테이터스관리),장착,Item 등등 하위 동작들을 참조하여  사용하고

그 행동에서도 또 다른 행동이 있을떄 또 퍼사드패턴으로 구현되어 있게 해놓았다.

 


참고자료

https://refactoring.guru/ko/design-patterns/flyweight/csharp/example

 

Q.오브젝트 풀링이란?

A. Scene에서 사용할 오브젝트(객체)들을 미리 만들어놓고서 컨테이너에 저장해놓는다.
필요할때 오브젝트 풀링에서 해당 오브젝트를 호출해서 사용한 뒤 다시 컨테이너(오브젝트 풀)에 돌려주는 설계 패턴이다.

 

Q.오브젝트 풀링을 왜 사용하는가?

A.

객체를 생성/파괴하는 메서드인 Instantiate / Destroy는 호출시에 처리비용이 매우 크다.

즉, 처리비용이 매우 크다는 말은 최적화에 안좋다는 뜻이다.

 

미리 객체를 만들어 놓으면 첫 시작할때만 생성을 하기에 실제 인게임시에는 오브젝트를 사용하려고 꺼낼때 처리비용이 크지 않다.

또한 파괴하지 않고 컨테이너(오브젝트 푸)에 돌려주기 때문에 계속 재활용이 가능한 장점도 있다.

 

Q.오브젝트 풀링을 어떡해 사용해?

A.오브젝트 풀링은 프로그래머의 재량에 따라 형태나 많이 바뀐다. 

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
	//Pool 구조체는 오브젝트 하나를 의미함
    [System.Serializable]
    public class Pool
    {
        public string tag; //오브젝트의 태그
        public GameObject prefab; //오브젝트의 프리팹
        public int size; //Pool에 담을 오브젝트의 갯수
        //TODO CODE : 오브젝트에서 사용할 필드값
    }

    public List<Pool> pools = new List<Pool>(); //사용할 오브젝트를 보관하는 컨테이너
    public Dictionary<string, Queue<GameObject>> PoolDictionary;//실제로 호출하여 사용하게 될 컨테이너

    private void Awake()
    {
        PoolDictionary = new Dictionary<string, Queue<GameObject>>();
		
        //List에 있는 오브젝트를 하나씩 호출하여 n개만큼 생성 후 오브젝트 풀(PoolDictionary)에 담음
        foreach (var pool in pools)
        {
        	//Unity 하이어라키에 오브젝트를 담을 부모 오브젝트 생성
            GameObject poolContainer = new GameObject("Pool_Container_" + pool.tag);
			
            //Queue 자료구조를 사용하여 오브젝트를 관리
            //Queue를 사용한 이유 -> 순서에 상관없는 오브젝트들 사용하기에
            Queue<GameObject> queue = new Queue<GameObject>()
            
            //생서할 오브젝트 갯수만큼 생성
            for (int i = 0; i < pool.size; i++)
            {
            	//오브젝트를 생성 -> 오브젝트를 비활성화 -> 오브젝트 풀(queue)에 담음
                GameObject obj = Instantiate(pool.prefab, poolContainer.transform);
                obj.SetActive(false);
                queue.Enqueue(obj);
            }
			
            //queue에 담은 풀을 Dictionary에 추가 -> 하나의 오브젝트 풀이 완성 
            PoolDictionary.Add(pool.tag, queue);
        }
    }
	
    //외부에서 오브젝트가 필요할때 호출되는 함수
    public GameObject SpawnFromPool(string tag)
    {
    	//오브젝트 풀의 key값을 비교하여 일치하지 않으면 탈출
        if(!PoolDictionary.ContainsKey(tag))
        {
            return null;
        }
		
        //해당 key의 오브젝트를 하나 dequeue
        GameObject obj = PoolDictionary[tag].Dequeue();
        
        //Queue에서 오브젝트 하나가 dequeue됫기에 다시 채워준다.
        PoolDictionary[tag].Enqueue(obj);
		
       	//꺼낸 오브젝트를 활성화
        obj.SetActive(true);
		
        return obj;
    }
}

해당 코드는 총알이나 투사체 및 파티클 같이 순서는 상관없고 재활용을 계속 해야되는 오브젝트에 사용되는 패턴이다.


 

이 코드의 단점은 오브젝트가 하나씩 발사 될때마다 다시 Enqueue로 챙겨주고 있으며 반납을 하지 않고있다.

즉, 투사체들은 오브젝트에 닿을때 해당 조건이되면 Destroy로 파괴되고 있기에 최적화에 좋지 않은 모습을 보이고있다.

 

대처법으로는 외부에서 사용이 완료된 오브젝트를 다시 반납하는 메서드를 만들어 사용하는것이 최적화에 좋다.

 


또 다른 단점은 오브젝트가 많은 숫자를 필요로 만들때 기존 오브젝트 갯수보다 많으면 문제가 생긴다.

예시로 이런 상황이 있다. 

1. 1초에 50개 발사 

2. 오브젝트를 20개만 만들어놓음 
3. 20개를 발사하고 21개쨰를 발사할떄 미리 발사해놧던 20개 중 하나가 사라지고 21개째가 되어 재활용이 되어버리는 문제
4. 총이 나가다가 뒤로 돌아오는 현상이 발생 

이런 부자연스러운 현상이 발생하기에 

 

대처법으로는 Queue대신 List를 활용하여 List 각 하나의 원소에 접근하여 사용중인지를 체크, 만약 모든 오브젝트가 사용 중인경우 추가로 오브젝트 풀에 오브젝트를 추가 생성하는 방식이 있다. 

 


위에 코드는 예시일 뿐이며 , 다른 사람들이 사용한 오브젝트풀링의 좋은 예제가 많이 있다.

여기서는 이런 개념이 있다라는 것을 파악하고 다음 프로젝트 진행시에 사용하는것을 목표로 하자.

SRP : Single Responsibility Principle(단일 책임 원칙)

단일 책임 원칙(SRP)는 <객체는 단 하나의 책임만 가져야 한다> 는 원칙을 말한다.

 

여기서 '책임'이라는 의미는 하나의 '기능 담당'으로 보면된다.

하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되어야 한다

 

 

현재 TopDownGame 프로젝트 수업을 듣는 중에 캐릭터의 이동을 담당하는 설계를 하는 중에 로직 순서를 간단하게라도 정리 해보았다.

1번은 생성자에서 함수를 event 함수에 추가 후, 동작하지 않는다.

2번은 PlayerInputController에서 InputValue(입력값을 받음)를 받아서 Move event함수를 호출하여 부모클래스로 접근한다.

3번은 부모에있는 TopDownController의 event Action에 등록된 함수를 Invoke()로 동작시키면

TopDownMoveMent에 있는 함수를 동작시키게 한다.

 

그 이후로, 입력값을 받을때마다 2 ~ 3번을 반복하게 된다.

 

이 로직은 상위 부모 클래스로 Controller로 선언 후 각 기능(이동,공격등등)마다 Class로 설계하여 SRP 원칙을 지켰고

event 함수를 사용하여 확장성을 챙겼다고 생각한다.

 

이렇게 구성하게 되면 

'기능 변경(수정)이 일어 났을때의 파급 효과가 최대한 적게 일어난다.

즉, 커플링의 영향이 적어진다고 볼 수 있는것이다. 

 

해당 구조를 UML로 표현하면 이렇게 된다.

 

기능을 분리하였고 TopDownController를 참조하여서 그 안에 사용할 메서드를 Event로 저장합니다.

그럼 PlayerInputController는 자신의 부모클래스에 저장된 event들을 호출하여 사용이 가능해집니다.

 

Unity에서 컴포넌트로 적용한 모습

PlayerInputController 부모 클래스에 해당 기능들을 저장시키고 inputController에서 호출한다.

 

SRP(단일 책임 원칙)에 대해서는 해당 블로그 글을 참고 하였습니다.

 

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-SRP-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 SRP (단일 책임 원칙)

단일 책임 원칙 - SRP (Single Responsibility Principle) 단일 책임 원칙(SRP)는 객체는 단 하나의 책임만 가져야 한다는 원칙을 말한다. 여기서 '책임' 이라는 의미는 하나의 '기능 담당'으로 보면 된다. 즉,

inpa.tistory.com

 

+ Recent posts