Unity - 인터페이스, 어댑터, 싱글톤 구현

땡구의 개발일지·2025년 4월 21일

Unity마스터

목록 보기
12/78
  • 인터페이스 구현
    탱크의 상호작용에 반응하는 게임오브젝트들을 구성하자
    탱크는 포탄을 쏴서 부딪힌 게임오브젝트가 데미지를 받을 수 있는 있다면 데미지를 가한다.
    몬스터 : 데미지를 받을 수 있으며 피격시 포탄의 공격력만큼 체력을 차감한다
  • 피격 인터페이스
public interface IInterfaceDamage
{
    public void TakeDamage(GameObject gameObject, int damage);
}
  • 피격 컴포넌트
public class PracticeTakeHit : MonoBehaviour, IInterfaceDamage
{
    [SerializeField] int HP;
    [SerializeField] int damage;
    [SerializeField] GameObject explosionPrefab;
    public int Damage { get { return damage; } }
    private void OnCollisionEnter(Collision collision)
    {
        IInterfaceDamage attacker = collision.gameObject.GetComponent<IInterfaceDamage>();
        PracticeTakeHit attackerHit = collision.gameObject.GetComponent<PracticeTakeHit>();
        if (attacker != null)
        {
            if(attackerHit != null)
            {
                TakeDamage(collision.gameObject, attackerHit.Damage);
            }
            else
            {
                TakeDamage(collision.gameObject, 0);
            }
        }
    }
    public void TakeDamage(GameObject gameObject, int damage)
    {
        if(damage != 0)
        {
            Debug.Log($"{gameObject.name}의 공격! {damage} 피해");
            HP -= damage;
        }
        if(HP<=0)
        {
            Die();
        }
    }
    private void Die()
    {
        Instantiate(explosionPrefab, transform.position, transform.rotation);
        Destroy(gameObject);
    }
}
  • 이 컴포넌트를 몬스터와 플레이어에 각각 추가한다

    • 플레이어
    • 몬스터 프리팹
  • 서로 인터페이스를 상속 받았기 때문에 인식하고 충돌 시 내용 OnCollisionEnter()을 수행한다

  • 탱크의 데미지를 0으로 해서 탱크와 몬스터가 부딪혔을 때, 몬스터는 데미지를 받지 않게 한다

  • 포탄 컴포넌트에 인터페이스를 상속해서 해당 기능을 구현한다. 인터페이스를 상속했으니, 몬스터와 포탄은 서로 충돌할 수 있다

  • 포탄 컴포넌트

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Bullet : MonoBehaviour, IInterfaceDamage
{
    [Header("Components")]
    [SerializeField] Rigidbody rigid;

    [Header("Properties")]
    [SerializeField] GameObject explosionPrefab;

    private void Awake()
    {
        rigid ??= GetComponent<Rigidbody>();

    }
    public void TakeDamage(GameObject gameObject,int damage)
    {

    }
    private void OnCollisionEnter(Collision collision)
    {
        IInterfaceDamage attacker = collision.gameObject.GetComponent<IInterfaceDamage>();
        if (attacker != null)
        {
            attacker.TakeDamage(gameObject, 1);
        }
        DestroyBullet();
    }
    void Update()
    {
        if (rigid.velocity.magnitude > 3)
        {
            transform.forward = rigid.velocity;
        }
    }
    void DestroyBullet()
    {
        Instantiate(explosionPrefab, transform.position,transform.rotation);
        Destroy(gameObject);
    }
}
  • 포탄도 동일하게 인터페이스 상속을 받는 것으로 구현했다
  • 어댑터 구현
    스위치 : 데미지를 받지 않지만 포탄을 맞으면 문을 여닫을 수 있도록 행동을 수행한다. 단, 스위치는 인터페이스를 포함하지 않으며, 스위치의 코드에서는 여닫기에 대한 기능만을 수행한다. 이를 어댑터를 통해서 포탄의 피격에 여닫기 기능을 수행하도록 구성한다
  • 스위치 컴포넌트
public class PracticeSwitch : MonoBehaviour
{
    [SerializeField] GameObject door;
    private bool IsDoorOpened => door.activeSelf == false;


    public void SwitchDoor()
    {
        if(IsDoorOpened)
        {
            Close();
        }
        else
        {
            Open();
        }
    }
    private void Open()
    {
        door.SetActive(false);
    }
    private void Close()
    {
        door.SetActive(true);
    }
}
  • 게임 오브젝트가 비활성화 되어있으면 문이 열린것, 활성화 되어 있으면 닫힌 것이라 인식하는 식의 개폐스위치

  • 포탄의 피격에 스위치를 활성화 하기 위해서는 어댑터가 필요하다

  • 스위치 어댑터

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 이벤트를 위한 네임스페이스 추가
using UnityEngine.Events;

public class PracticeAdapter : MonoBehaviour, IInterfaceDamage
{
    // 인터페이스 함수의 매개변수에 맞춘다
    public UnityEvent<GameObject, int> OnDamaged;
    public void TakeDamage(GameObject attacker , int damage)
    {
        // if(OnDamaged != null) {OnDamaged.Invoke()} 의 간략화
        OnDamaged?.Invoke(attacker,damage);
    }
}


  • 싱글톤 구현
    게임에는 GameManager 싱글톤 게임오브젝트가 있다
    싱글톤 게임오브젝트는 최대점수를 가지고 있으며, 게임 시작시 0점 부터 시작한다.
    몬스터가 탱크 방향으로 쫓아오며 탱크는 몬스터를 잡는 경우 스코어를 1점 올린다.
    탱크가 몬스터에 닿는 경우 게임오버가 되며 'R' 키로 재시작 할 수 있다 (재시작은 씬을 다시 로딩하는 것으로 할 수 있다)
    재시작시에도 최대점수는 손실되지 않으며 게임 중 가장 높은 점수를 보관하고 있는다.

  • 싱글톤 조건은 2가지다

  1. 하나의 인스턴스를 보장한다
  2. 전역적인 접근이 가능하다
public class PracticeSingleton : MonoBehaviour
{
    private static PracticeSingleton instance;
    public static PracticeSingleton Instance 
    {
        get 
        { 
            if(instance == null)
            {
                // 하나도 안 만든 경우를 위한 보험. 처음으로 호출될 시, 만들어진다.Lazy Initialization
                GameObject go = new GameObject("PracitceSingleton");
                instance = go.AddComponent<PracticeSingleton>();
            }
            return instance;
        } 
    }
    // 현재점수, 최고점수
    public int score;
    public int maxScore;
    
    // 생성 시 초기화
    public void Awake()
    {
        CreateInstance();
    }
    
    // 인스턴스가 한 개만 존재하게 한다
    public PracticeSingleton CreateInstance()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
        // 인스턴스 한개로 제한
            Destroy(gameObject);
        }
            return instance;
    }
}
  • DontDestroyOnLoad() : 씬 전환 시에도 싱글톤이 파괴되지 않게 한다

  • 안 만든 경우 무조건 만들게 하고, 한개 이상 만들 시에는 한 개를 제외한 나머지를 삭제한다

  • static으로 전역에서 접근 가능하다

  • 씬체인저

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 씬 메니저를 추가해야 된다
using UnityEngine.SceneManagement;

public class PracticeSceneManager : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            // 1. 씬 번호를 적어서 전환
            //SceneManager.LoadScene(0);
            // 2. 씬 이름을 적어서 전환
            SceneManager.LoadScene("250421_Practice");
        }
    }
}

  • 씬에 빈 오브젝트를 생성 후 컴포넌트를 추가한다

  • R키를 누르면 초기화 된다

  • 점수 구현

  • Monster 프리팹 만을 위한 컴포넌트다. OnDestroy()일 때, 점수를 추가한다

  • Monster 프리팹에 컴포넌트를 추가한다

public class PracticeGetScore : MonoBehaviour
{
    private void OnDestroy()
    {
    	PracticeSingleton.Instance.GetScore(1);
    }
}

  • 몬스터를 잡자, score가 오르기 위해 PracticeSigleton의 인스턴스가 생성됨과 동시에 score가 갱신됐다

  • 최대점수 구현

  • 씬 전환시에도 최대점수가 유지되는지 확인한다. 씬 전환시, 현재 스코어는 0점이 된다

  • 씬 전환 컴포넌트의 내용 수정

public class PracticeSceneManager : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            // 게임 스코어 초기화
            PracticeSingleton.Instance.score = 0;
            // 1. 씬 번호를 적어서 전환
            //SceneManager.LoadScene(0);
            // 2. 씬 이름을 적어서 전환
            SceneManager.LoadScene("250421_Practice");

        }
    }
}
  • 싱글톤에도 함수를 추가한다
public void GetScore(int score)
{
        this.score += score;
    if(this.score> maxScore)
    {
        maxScore = this.score;
    }
}

  • R키로 초기화 전 점수 4점

  • R키로 초기화 후 점수 초기화

  • 문제발생

  • OnDestroy() 에서 점수구현을 하면, 씬 전환 시 게임 오브젝트들이 파괴 되면서 점수가 올라가는 현상발생. 포탄으로 파괴 될 경우에만 점수 올라가는 식으로 변경

  • PracticeTakeHit 컴포넌트를 수정했다

private void Die()
{
    if(gameObject.tag == "Enemy")
    {
        PracticeSingleton.Instance.GetScore(1);
    }
    Instantiate(explosionPrefab, transform.position, transform.rotation);
    Destroy(gameObject);
}
  • 이렇게 하면 포탄에 의해 죽었을 때만 점수가 올라간다

  • 문제발생2

  • 점수가 2점씩 올라가는 문제 발생. Die() 함수가 두 번 실행된다는 건데, 이유를 찾았다. PracitceTakeHit 컴포넌트에서 TakeDamage 함수if를 두번 실행해서 그런 것이다. 내부로 감싸준다

public void TakeDamage(GameObject gameObject, int damage)
{
    if(damage != 0)
    {
        Debug.Log($"{gameObject.name}의 공격! {damage} 피해");
        hp -= damage;
        if (hp <= 0)
        {
            Die();
        }
    }

}

  • 이제 제대로 된다
profile
개발 박살내자

0개의 댓글