
DodgebacklogMapPlayer와 Tower가 위치하는 게임 플레이 빌드
GameManager게임의 진행과 종료 판정, 처리
Tower게임 시작시 총알 풀 생성, 시계방향 회전
플레이어가 시야 내에 있으면 감지 후 플레이어를 향해 발사
에셋스토어 포탑 모델링 활용
Bullet발사 시 포구의 정면 방향으로 비행
지정 거리를 이동하면 비활성화
Player유저가 조작하는 캐릭터(상하좌우)
총알(bullet)과 충돌 시 비활성화
Bullet Pool오브젝트 풀 패턴 활용
타워에 종속되지 않는 오브젝트
DodgePlayerpublic class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed;
[SerializeField] private int hp;
private Vector3 moveDir;
private void OnEnable()
{
Init();
}
private void Update()
{
Move();
}
// AxisRaw를 활용한 기본 이동 로직
private void Move()
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
moveDir = new Vector3(h, 0, v).normalized;
transform.Translate(moveDir * (moveSpeed * Time.deltaTime));
}
// 총알에 피격시 트리거 발동
private void OnTriggerEnter(Collider other)
{
if(other.gameObject.layer == LayerMask.NameToLayer("Bullet"))
{
Hit();
}
}
private void Hit()
{
hp--;
if (hp <= 0)
{
// GameManager의 이벤트를 호출하기 위한 proxy 함수 호출
GameManager.Instance.HandleGameOver();
}
}
private void Init()
{
hp = 5;
moveSpeed = 8f;
}
}
Towerpublic class TowerController : MonoBehaviour
{
[SerializeField] private Transform muzzlePos;
[SerializeField] private BulletPool bulletPool;
[SerializeField] private float rotSpeed;
[SerializeField] private float firePower;
[SerializeField] private float fireCoolTime;
[SerializeField] private LayerMask playerLayer;
[SerializeField] [Range(5f,10f)] private float detectRange;
private Transform _targetPos;
private WaitForSeconds _fireCool;
private Coroutine _fireRoutine;
private bool _isFiring;
private void Awake()
{
Init();
}
private void Update()
{
Rotate();
Detect();
}
// GameManager에서 게임 시작시 호출하는 함수
public void StartFire()
{
if (_fireRoutine == null)
{
_fireRoutine = StartCoroutine(FireRoutine());
_isFiring = true;
}
}
// GameManager에서 게임 종료시 호출하는 함수
public void StopFire()
{
if (_fireRoutine != null)
{
StopCoroutine(_fireRoutine);
_fireRoutine = null;
_isFiring = false;
}
}
private void Rotate()
{
transform.Rotate(Vector3.up, rotSpeed * Time.deltaTime);
}
// 일정 주기로 총알을 발사하기 위한 코루틴
private IEnumerator FireRoutine()
{
while (true)
{
Fire();
yield return _fireCool;
}
}
// 총알 풀에서 가져오기, 방향, 속도 로직
private void Fire()
{
Bullet instance = bulletPool.GetPool();
instance.gameObject.SetActive(true);
instance.transform.position = muzzlePos.position;
instance.transform.LookAt(transform.forward);
instance.gameObject.GetComponent<Rigidbody>().velocity = transform.forward * firePower;
}
// 일정 영역 내에 플레이어가 있으면 플레이어쪽으로 회전
private void Detect()
{
Collider[] colliders = Physics.OverlapSphere(transform.position, detectRange, playerLayer);
if (colliders.Length > 0)
{
_targetPos = colliders[0].transform;
transform.LookAt(_targetPos.position);
}
else
{
_targetPos = null;
}
}
private void Init()
{
_fireCool = new WaitForSeconds(fireCoolTime);
}
}
Bulletpublic class Bullet : MonoBehaviour
{
[SerializeField] private float liveTime;
public BulletPool _bulletPool;
private float _timer;
private bool _isReturned = false;
private void OnEnable()
{
_timer = liveTime;
_isReturned = false;
}
// 일정 시간 지나면 자동으로 파괴(회수)되는 로직
private void Update()
{
_timer -= Time.deltaTime;
if (_timer <= 0)
{
Return();
}
}
// 플레이어와 충돌 시 파괴(회수)
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.layer == LayerMask.NameToLayer("Player"))
{
Return();
}
}
// 총알의 파괴(회수) 로직
private void Return()
{
// 중복 회수를 막기 위한 플래그
if (_isReturned) return;
_isReturned = true;
// 회수당할 풀이 있는 경우 회수
if (_bulletPool != null)
{
gameObject.SetActive(false);
_bulletPool.ReturnPool(this);
}
// 없는 경우 파괴
else
{
Destroy(gameObject);
}
}
}
BulletPoolpublic class BulletPool : MonoBehaviour
{
// Queue로 구현한 오브젝트 풀
public Queue<Bullet> bullets;
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private int size;
private void Awake()
{
Init();
}
// 오브젝트 풀 기본 로직
private void Init()
{
bullets = new Queue<Bullet>();
for (int i = 0; i < size; i++)
{
Bullet instance = Instantiate(bulletPrefab);
instance.gameObject.SetActive(false);
bullets.Enqueue(instance);
}
}
// 오브젝트 풀 GetPool 로직
public Bullet GetPool()
{
// 풀에 총알이 없는 경우 신규 생성
if (bullets.Count == 0)
{
Bullet instance1 = Instantiate(bulletPrefab);
return instance1;
}
// 풀에 총알이 있는 경우 소환
else
{
Bullet instance2 = bullets.Dequeue();
instance2._bulletPool = this;
return instance2;
}
}
// 오브젝트 풀 ReturnPool 로직
public void ReturnPool(Bullet bullet)
{
// 풀에 총알이 없어서 신규 생성된 총알의 경우 파괴
if (bullet._bulletPool == null)
{
Destroy(bullet.gameObject);
}
// 풀에서 소환된 총알의 경우 반환
else
{
bullet.gameObject.SetActive(false);
bullets.Enqueue(bullet);
}
}
}
GameManagerpublic class GameManager : MonoBehaviour
{
// 매니저 싱글톤 구현
public static GameManager Instance { get; private set; }
[SerializeField] private GameObject canvas;
[SerializeField] private GameObject player;
[SerializeField] private GameObject[] towers;
[SerializeField] private Transform spawnPos;
[SerializeField] private Button retryBtn;
// 게임 오버시 실행될 이벤트
public event Action OnGameOvered;
public bool isgameOver;
// 싱글톤 기본 로직
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(Instance);
}
Instance = this;
DontDestroyOnLoad(Instance);
Init();
}
private void OnEnable()
{
OnGameOvered += GameOver;
retryBtn.onClick.AddListener(GameStart);
}
private void OnDisable()
{
OnGameOvered -= GameOver;
retryBtn.onClick.RemoveListener(GameStart);
}
// OnGameOvered 이벤트 proxy 함수
public void HandleGameOver()
{
OnGameOvered?.Invoke();
}
// 게임 시작 시 호출되는 함수
private void GameStart()
{
canvas.gameObject.SetActive(false);
player.gameObject.SetActive(true);
player.transform.position = spawnPos.position;
isgameOver = false;
foreach (var tower in towers)
{
tower.gameObject.GetComponent<TowerController>().StartFire();
}
}
// 게임 오버 시 호출되는 함수
private void GameOver()
{
player.gameObject.SetActive(false);
canvas.gameObject.SetActive(true);
isgameOver = true;
foreach (var tower in towers)
{
tower.gameObject.GetComponent<TowerController>().StopFire();
}
}
private void Init()
{
player.gameObject.SetActive(false);
canvas.gameObject.SetActive(true);
isgameOver = false;
}
}