이번 과제의 핵심은 인터페이스(Interface)를 활용하여 객체 지향적인 구조로 '일반 모드'와 '스나이퍼 모드'를 분리하고 제어하는 것이다.
ICombatMode 인터페이스를 통해 Play() 메서드를 공통화함.CombatNormal: 일반적인 연사 전투 모드 담당.CombatSniper: 레이캐스트 기반의 강력한 스나이퍼 모드 담당.Update 함수에서 전반적인 시스템(쿨타임, 체력, 입력 탐지)을 관리하며, 모드 전환 시 인터페이스 변수인 _combatMode에 실제 객체를 할당한다.
public interface ICombatMode
{
void Play();
}
public class CombatPlayer : MonoBehaviour
{
private ICombatMode _combatMode;
private bool _isSniper = false;
[SerializeField] private CombatNormal _normalMode;
[SerializeField] private CombatSniper _sniperMode;
void Update()
{
if (_life <= 0) return;
// 스나이퍼/노말 모드 전환
if (Input.GetKeyDown(KeyCode.LeftShift))
{
_isSniper = !_isSniper;
_combatMode = _isSniper ? (ICombatMode)_sniperMode : _normalMode;
_director.CheckSniperMode();
}
// 쿨타임 및 발사 로직
if (_fireCooldown > 0) _fireCooldown -= Time.deltaTime;
if (_fireCooldown <= 0 && Input.GetAxis("Fire1") > 0f)
{
if (_combatMode != null)
{
_combatMode.Play();
_fireCooldown = _isSniper ? _sniperInterval : _fireInterval;
}
}
}
}
using UnityEngine;
public class CombatNormal : MonoBehaviour, ICombatMode
{
[SerializeField] private Transform _firePosition; // 생성 위치, 회전
public void Play()
{
// 총알 생성
GameObject bullet = PoolManager.instance.Get(PoolKey.BULLET);
bullet.transform.position = _firePosition.position;
bullet.transform.rotation = _firePosition.rotation;
bullet.GetComponent<BulletController>().Fire();
// 총알 발사 이펙트
GameObject muzzleFlash = PoolManager.instance.Get(PoolKey.MUZZLEFLASH);
muzzleFlash.GetComponent<PooledParticle>().RestartAt(_firePosition);
}
}
using System.Collections;
using UnityEngine;
public class CombatSniper: MonoBehaviour, ICombatMode
{
// Sniper Mode
[SerializeField] private LayerMask _layermask;
[SerializeField] private int _maxDistance = 100;
[SerializeField] private Camera _sniperCamera;
[SerializeField] private float _raserTime = 0.3f;
[SerializeField] private float _sniperDamage = 30f;
[SerializeField] private Transform _firePosition;
[SerializeField] private Director _director;
private LineRenderer _line;
private void Awake()
{
_sniperCamera = Camera.main;
_line = GetComponent<LineRenderer>();
_line.startWidth = 0.01f;
_line.endWidth = 0.01f;
}
IEnumerator SniperRaser()
{
// 레이저 생성
_line.enabled = true;
Ray ray = _sniperCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, _maxDistance, _layermask))
{
// 레이저 위치 설정
if (_line != null)
{
_line.SetPosition(0, _firePosition.position);
_line.SetPosition(1, hit.point);
}
// 몬스터 데미지
if (hit.collider.TryGetComponent(out MonsterAI enemy))
{
enemy.TakeDamage(_sniperDamage);
}
// 총알 충돌 이펙트
if (hit.collider.CompareTag("Enemy"))
{
GameObject explosion = PoolManager.instance.Get(PoolKey.EXPLOSION);
explosion.GetComponent<PooledParticle>().RestartAt(hit.transform);
}
// 총알 충동 + 총알 구멍 이펙트
else if (hit.collider.CompareTag("Wall"))
{
GameObject explosion = PoolManager.instance.Get(PoolKey.EXPLOSION);
explosion.GetComponent<PooledParticle>().RestartAt(hit.transform);
// 총알 구멍
GameObject bulletHole = PoolManager.instance.Get(PoolKey.BULLETHOLE);
bulletHole.transform.position = hit.point;
bulletHole.transform.forward = hit.normal;
bulletHole.transform.position += hit.normal * 0.01f;
}
}
// 총기 반동
_director.StartCoroutine(_director.SniperNoise());
// 레이저 회수
yield return new WaitForSeconds(_raserTime);
_line.enabled = false;
}
public void Play()
{
StartCoroutine(SniperRaser());
}
}
[문제 상황]
처음에는 CombatNormal과 CombatSniper 각각에 Update() 함수를 두고, CombatPlayer에서 이 컴포넌트들을 끄고 켜는 방식으로 제어하려 했다. 하지만 두 클래스가 MonoBehaviour를 상속받고 활성화된 상태이다 보니, 현재 모드가 아님에도 불구하고 Update()가 계속 돌아가며 참조되지 않은 변수들로 인해 NullReferenceException이 프레임 단위로 발생하는 문제가 생겼다.
[해결 방법]
Update 처리는 중앙 제어 장치인 CombatPlayer에서만 수행하도록 변경했다.CombatPlayer는 현재 어떤 모드인지 상세히 알 필요 없이, 오직 _combatMode.Play()만 호출하면 되도록 구조를 개선했다. 이로 인해 불필요한 Null 에러를 완벽히 차단하고 코드 가독성을 높였다.