게임 개발/디자인 패턴

[디자인패턴] 6. 유니티에서 옵저버패턴

Heesuk Lee 2021. 8. 16. 17:31

 

 

스타크래프트가 안 떠오를수가 없다.

오늘 유니티에서 적용해볼 패턴은 '옵저버 패턴'입니다.

 

옵저버 패턴의 정의는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테

연락이가고 자동으로 갱신되는 방식으로 일대다 의존성을 가집니다.

 

위의 정의를 유의하면서 게임에서 전투를 통한 체력 표시를 구현해봅시다!

 

 

구현하기에 앞서 오늘 패턴의 또다른 중요한 원칙 '느슨한 결합'에 대해서 먼저 정리해봅시다.

 

옵저버 패턴에서는 주제(Subject)와 옵저버(Observer)가 느슨하게 결합되어있는 객체 디자인 원칙을 제공합니다. 

 

특징

1. 주제가 옵저버에 대해서 아는것은 옵저버가 특정 인터페이스를 구현한다는 것뿐입니다.

2. 옵저버는 언제든지 새로 추가할수있습니다.

3. 새로운 형식의 옵저버를 추가하려고 할때도 주제를 전혀 변경할 필요가 없습니다.

4. 주제와 옵저버는 서로 독립적으로 재사용할수있습니다.

5. 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지 않습니다.

 

인간관계마저 관통하는 원칙인걸까?

 

느슨한 결합은 변경사항이 생겨도 무난하게 처리할 수 있는 유연한 객체지향 시스템을 구축이 가능합니다.

객체사이의 상호의존성을 최소화하는 디자인이기 때문입니다.

 

그러면 우선 앞으로 유용하게 사용될 Subject와 Observer의 인터페이스를 먼저 생성해봅시다.

 


// Subject 인터페이스
public interface Subject
{
	// Observer 등록
    void RegisterObserver(Observer _observer);
    // Observer 해제
    void RemoveObserver(Observer _observer);
    // 모든 Observer 업데이트
    void NotifyObservers();
}

// Observer 인터페이스
public interface Observer
{
	// 정보 갱신 및 초기화
    void ObserverUpdate(float _myHp, float _enemyHp);
}

 

Subject와 Observer 인터페이스를 구현했으니 게임에서 체력을 관리하는 주제, Subject를 만들어 봅시다. 

 

 

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

// 체력을 관리해주는 주제 Subject
public class Hp_Subject : MonoBehaviour, Subject
{
	// 등록된 Observer들을 관리할 리스트
    private List<Observer> observers = new List<Observer>();
	
    private float myHp = 0f;
    private float enermyHp = 0f;

    public void RegisterObserver(Observer _observer)
    {
    	// Observer 등록
        this.observers.Add(_observer);
    }
    public void RemoveObserver(Observer _observer)
    {
    	// Observer 해제
        this.observers.Remove(_observer);
    }
    
    public void NotifyObservers()
    {
    	// 모든 Observer 정보 업데이트
        for (int i = 0; i < this.observers.Count; i++)
        {
        	// 각 Observer들이 보여줘야할 정보를 전부 매개변수로 가집니다.
            // 매개변수에 모든 정보를 가지지않게끔 개선할 수도 있습니다.
            // 중요한건 모든 옵저버가 갱신된다는 정의에서 벗어나지 않으면 됩니다.
            this.observers[i].ObserverUpdate(this.myHp, this.enermyHp);
        }
    }

    public void Changed(float _myHp, float _enemyHp)
    {
    	// 정보가 변경되면 호출되어
        this.myHp = _myHp;
        this.enermyHp = _enemyHp;
		// 업데이트된 정보로 갱신해줍니다.
        this.NotifyObservers();
    }
}

 

Subject가 완성이 되었으니 내 체력과 적 체력을 Observer를 상속받아 Subject에 등록 할 수 있도록 만들어봅시다.

 

 

// 내 hp ui
using UnityEngine.UI;
using UnityEngine;

public class MyHp_Observer : MonoBehaviour, Observer
{
    [SerializeField]
    private Image hpBar = null;

	// 옵저버는 멤버변수로 Subject를 가집니다.
    private Hp_Subject subject = null;
	
    public void Init(Hp_Subject _subject)
    {
    	// Subject를 초기화해줍니다.
        this.subject = _subject;
    }

    public void ObserverUpdate(float _myHp, float _enemyHp)
    {
    	// 새로 받은 정보를 갱신해줍니다.
        this.hpBar.fillAmount = _myHp;
    }
}

// 적 hp ui
using UnityEngine.UI;
using UnityEngine;

public class EnemyHp_Observer : MonoBehaviour, Observer
{
    [SerializeField]
    private Image hpBar = null;

	// 옵저버는 멤버변수로 Subject를 가집니다.
    private Hp_Subject subject = null;
    
    public void Init(Hp_Subject _subject)
    {
    	// Subject를 초기화해줍니다.
        this.subject = _subject;
    }

    public void ObserverUpdate(float _myHp, float _enemyHp)
    {
    	// 새로 받은 정보를 갱신해줍니다.
        this.hpBar.fillAmount = _enemyHp;
    }
}

// 간단한 구조이기에 느슨한 결합에도 똑같은 코드가 작성되었지만
// 좀만 형태가 복잡해지면 느슨한 결합의 가장큰 장점인 내부적으로는 달라도
// 결과적인 역할은 동일한 코드를 볼 수 있게 됩니다.

 

재료가 될 코드들은 완성이 되었으니 눈으로 보여질 하이라키를 구성했습니다.

캔버스의 BattlePanel이 실제 동작부로 작동되고, 자식으로 MyUI, EnemyUI를 통해 옵저버 패턴을 통한 체력표시를 구현했습니다.

그리고 NextButton을 통해 매턴마다 랜덤하게 공격을 주고 받아, 내체력과 상대체력이 업데이트되는것을 지켜봅시다.

 

리소스의 부재가 아쉽습니다!

 

실제 동작부인 BattlePanel 또한 스크립트로 어떻게 돌아갈지 살펴봅시다!

 

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

// 실행부 스크립트
public class BattlePanel : MonoBehaviour
{
	// 체력관리 Subject
    [SerializeField]
    private Hp_Subject hp_Subject = null;
	
    // 나,상대 체력 Obsever
    [SerializeField]
    private MyHp_Observer myHp_Observer = null;
    [SerializeField]
    private EnemyHp_Observer enemyHp_Observer = null;
    
    // 동작을 위한 버튼
    [SerializeField]
    private Text nextButton = null;
	
    // 초기 체력 멤버변수
    private float originMyHp = 10f;
    private float originEnemyHp = 10f;

    private float currentMyHp = 0f;
    private float currentEnemyHp = 0f;

    // 게임 시작시 초기화
    private void Start()
    {
        this.myHp_Observer.Init(this.hp_Subject);
        this.enemyHp_Observer.Init(this.hp_Subject);

        this.originMyHp = 10f;
        this.originEnemyHp = 10f;

        this.currentMyHp = this.originMyHp;
        this.currentEnemyHp = this.originEnemyHp;
        
        // 옵저버의 등록
        this.hp_Subject.RegisterObserver(this.myHp_Observer);
        this.hp_Subject.RegisterObserver(this.enemyHp_Observer);
        // 옵저버들의 초기화
        this.hp_Subject.Changed(this.currentMyHp / this.originMyHp, this.currentEnemyHp / this.originEnemyHp);
    }

    // 버튼 동작시 호출
    public void Next()
    {
        if (this.currentMyHp <= 0f || this.currentEnemyHp <= 0f)
        {   
            Debug.Log("--- 전투 종료 ---");
            return;
        }
		
        // 랜덤하게 타겟 지정
        int attackIndex = Random.Range(0, 2);
        switch (attackIndex)
        {
            case 0:
                // 내가 공격
                this.currentEnemyHp -= 1f;
                Debug.Log("-내가 공격했다.");
                break;
            case 1:
            	// 적이 공격
                this.currentMyHp -= 1f;
                Debug.Log("-적이 공격했다.");
                break;
            default:
            	// 예외 처리 - 설마 호출하겠어~
                Debug.Log("-에러가 발생했다!");
                break;
        }
        
        // 결과값 업데이트
        this.hp_Subject.Changed(this.currentMyHp / this.originMyHp, this.currentEnemyHp / this.originEnemyHp);
        Debug.Log($"내 체력 : {this.currentMyHp} / 상대 체력 : {this.currentEnemyHp}");

        if(this.currentMyHp <= 0f)
        {
            this.nextButton.text = "상대 승리";
            Debug.Log("---상대 승리---");
        }
        else if(this.currentEnemyHp <= 0f)
        {
            this.nextButton.text = "나의 승리";
            Debug.Log("---나의 승리---");
        }
        else
        {
            this.nextButton.text = "다음 턴";
        }
    }
}

 

이렇게 완성된 체력관리를 옵저버패턴으로 표현한 시스템입니다.

매 턴마다 누구에게 공격을 받던 서로의 체력을 갱신해줍니다.

 

랜덤하게 공격을 주고 받아 결과또한 랜덤하게 나옵니다.

 

짤 올리려고 글쓴다는 오해를 받고있는 것 같다.

 

사실 짤 올리려고 글쓰는게 맞는것 같기도 합니다.

반응형