39일차) [Unity] 인터페이스를 활용한 전투 모드 리팩토링

엄기태·2026년 2월 25일

Unity

목록 보기
13/13

1. 구현 목표

이번 과제의 핵심은 인터페이스(Interface)를 활용하여 객체 지향적인 구조로 '일반 모드'와 '스나이퍼 모드'를 분리하고 제어하는 것이다.

  • 공통 인터페이스 정의: ICombatMode 인터페이스를 통해 Play() 메서드를 공통화함.
  • 구현 클래스 분리:
  • CombatNormal: 일반적인 연사 전투 모드 담당.
  • CombatSniper: 레이캐스트 기반의 강력한 스나이퍼 모드 담당.

2. 구현 결과

  • 결과물 영상:

3. 주요 소스코드

📑 CombatPlayer.cs (전투 제어 메인)

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;
            }
        }
    }
}

📑 CombatNormal.cs

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);
	}
}

📑 CombatSniper.cs

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());
	}
}

4. 문제 해결 과정 (Trouble Shooting)

[문제 상황]
처음에는 CombatNormal과 CombatSniper 각각에 Update() 함수를 두고, CombatPlayer에서 이 컴포넌트들을 끄고 켜는 방식으로 제어하려 했다. 하지만 두 클래스가 MonoBehaviour를 상속받고 활성화된 상태이다 보니, 현재 모드가 아님에도 불구하고 Update()가 계속 돌아가며 참조되지 않은 변수들로 인해 NullReferenceException이 프레임 단위로 발생하는 문제가 생겼다.

[해결 방법]

  • Update의 단일화: 유니티의 매 프레임 호출을 각 모드 스크립트에서 제거했다.
  • 제어권 이전: 모든 Update 처리는 중앙 제어 장치인 CombatPlayer에서만 수행하도록 변경했다.
  • 인터페이스 활용: CombatPlayer는 현재 어떤 모드인지 상세히 알 필요 없이, 오직 _combatMode.Play()만 호출하면 되도록 구조를 개선했다. 이로 인해 불필요한 Null 에러를 완벽히 차단하고 코드 가독성을 높였다.

profile
코딩 학습 공간

0개의 댓글