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가지다
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키로 초기화 후 점수 초기화
OnDestroy() 에서 점수구현을 하면, 씬 전환 시 게임 오브젝트들이 파괴 되면서 점수가 올라가는 현상발생. 포탄으로 파괴 될 경우에만 점수 올라가는 식으로 변경
PracticeTakeHit 컴포넌트를 수정했다
private void Die()
{
if(gameObject.tag == "Enemy")
{
PracticeSingleton.Instance.GetScore(1);
}
Instantiate(explosionPrefab, transform.position, transform.rotation);
Destroy(gameObject);
}
이렇게 하면 포탄에 의해 죽었을 때만 점수가 올라간다
점수가 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();
}
}
}
