38일차) [Unity] Raycast 스나이퍼 모드 구현 및 기믹 추가

엄기태·2026년 2월 24일

Unity

목록 보기
12/13

1. 구현 목표

  • 과제 1: Raycast – 스나이퍼 모드 구현
  • Raycast를 활용한 스나이퍼 모드 전환 및 로직 구현
  • LineRenderer를 이용한 시각적 조준선(레이저) 처리
  • 과제 2: 신박한 기능 추가
  • 기존 기능 개선 및 클릭 상호작용 효과 3종 추가

2. 구현 결과

📋 주요 소스코드

PlayerCombat.cs (스나이퍼 모드 핵심)

public class PlayerCombat : MonoBehaviour
{
    // 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;
    private LineRenderer _line;

    private void Awake()
    {
        _sniperCamera = Camera.main;
        _line = GetComponent<LineRenderer>();
        _line.startWidth = 0.01f;
        _line.endWidth = 0.01f;
    }

    void Update()
    {
        // ... (생략)
        if (Input.GetAxis("Fire1") > 0f)
        {
            if (!_director.SniperMode)
            {
                // 일반 모드: 총알 생성
                GameObject bullet = PoolManager.instance.Get(PoolKey.BULLET);
                // ... 위치 설정 및 발사
            }
            
            // 스나이퍼 모드: Raycast 발사
            if (_director.SniperMode)
            {
                StartCoroutine(SniperRaser());
            }
            // ... (생략)
        }
    }

    IEnumerator SniperRaser()
    {
        _line.enabled = true;
        Ray ray = _sniperCamera.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, _maxDistance, _layermask))
        {
            _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 bulletHole = PoolManager.instance.Get(PoolKey.BULLETHOLE);
                bulletHole.transform.position = hit.point;
                bulletHole.transform.forward = hit.normal;
                bulletHole.transform.position += hit.normal * 0.01f;
            }
        }
        yield return new WaitForSeconds(_raserTime);
        _line.enabled = false;
    }
}

SplitEffect.cs (분열 및 재질 유지)

public void Play(GameObject target)
{
    for (int i = 0; i < _splitCount; i++)
    {
        GameObject piece = GameObject.CreatePrimitive(PrimitiveType.Cube);
        piece.transform.position = target.transform.position;
        piece.transform.localScale = target.transform.localScale * _splitScale;
        
        // 기존 Material 설정으로 어색함 해소
        Renderer renderer = piece.GetComponent<Renderer>();
        renderer.material = _material;

        Rigidbody rb = piece.AddComponent<Rigidbody>();
        rb.AddForce(Random.onUnitSphere, ForceMode.Impulse);
        Destroy(piece, _splitLifeTime);
    }
    Destroy(gameObject);
}

Explosion.cs

using UnityEngine;

public class ExplosionEffect : MonoBehaviour, IClickEffect
{
	[SerializeField] private ParticleSystem _particle;

	private void Explosion(GameObject target)
	{
		// 폭발 이펙트 생성
		ParticleSystem explosion = Instantiate(_particle);
		// 이펙트 위치 설정
		explosion.transform.position = target.transform.position;
		explosion.transform.rotation = target.transform.rotation;
		// 타겟 삭제
		Destroy(target);
	}
	public void Play(GameObject target)
	{
		Explosion(target);
	}
}

ChangeColorEffect.cs

using UnityEngine;

public enum Colors
{
	RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE
}
public class ChangeColorEffect : MonoBehaviour, IClickEffect
{
	[SerializeField] private Material[] _materials;
	[SerializeField] private int _index = 0;

	public void Play(GameObject target)
	{
		ChangeColor(target);
	}

	private void ChangeColor(GameObject target)
	{
		// 색 전환
		MeshRenderer renderer = target.GetComponent<MeshRenderer>();
		renderer.material = _materials[_index];
		// 인덱스 전환
		_index++;
		if (_index >= _materials.Length)
		{
			_index = 0;
		}
	}
}

TelleportEffect.cs

using UnityEngine;
using UnityEngine.SceneManagement;
public class TelleportEffect : MonoBehaviour, IClickEffect
{
	public void Play(GameObject target)
	{
		Telleport(target);
	}

	private void Telleport(GameObject target)
	{
		SceneManager.LoadScene("Lobby");
	}

}


3. 구현 과정

  1. 스나이퍼 모드 로직: * LineRenderer를 Awake에서 캐싱하고 기본 너비를 설정하여 초기화했다.
  • Input.GetAxis("Fire1")을 통해 스나이퍼 모드일 때는 총알을 생성하지 않고 Raycast만 쏘도록 예외 처리를 했다.
  • RaycastHit 정보를 이용해 레이저의 시작점(총구)과 끝점(충돌 지점)을 실시간으로 그려준 뒤, 코루틴으로 일정 시간 후 회수했다.
  1. 기능 확장: * Split: 분열 시 기존 오브젝트의 Material을 그대로 적용하도록 개선했다.
  • Explosion: 파티클 시스템을 이용해 클릭 시 오브젝트가 폭발하는 시각 효과를 추가했다.
  • Color Change: Enum과 배열을 활용해 클릭할 때마다 순차적으로 색상이 변하는 기믹을 구현했다.
  • Teleport: SceneManager를 사용하여 클릭 시 로비 씬으로 즉시 이동하는 기능을 추가했다.

4. 문제 해결 방법 (Troubleshooting)

  • 문제점: LineRenderer 코드는 완벽했으나 실제 플레이 시 선이 보이지 않거나 에러가 발생함.
  • 원인: 스크립트가 붙은 Player 오브젝트에 LineRenderer 컴포넌트 자체가 추가되어 있지 않았음.
  • 해결: 유니티 에디터에서 컴포넌트를 추가하고, 재질(Material)이 없어 마젠타색으로 나오던 현상을 Sprites-Default 재질 할당으로 해결했다.

5. 학습 내용 정리

📘 Interface 활용 방안

  • 공통 규격화: IDamageable 인터페이스를 통해 적, 벽, 장애물 등 클래스가 달라도 동일한 방식으로 데미지를 줄 수 있다.
  • 상호작용 시스템: IInteractable을 사용하여 문 열기, 아이템 줍기 등 다양한 이벤트를 하나의 레이캐스트 로직으로 통합 관리할 수 있다.
  • 유연성: 클래스는 단일 상속만 가능하지만, 인터페이스는 다중 상속이 가능하여 기능을 조립하듯 사용할 수 있다.

📘 Raycast 개념

  • 레이저를 쏘아 부딪힌 물체의 정보(거리, 좌표, 법선 벡터 등)를 얻는 기능이다.
  • 특정 LayerMask를 지정하여 원하는 물체만 감지하거나 무시할 수 있어 성능 최적화에 용이하다.

6. 회고 및 향후 계획

  • 아이디어: 오늘 배운 Raycast와 Interface를 조합하면, 타워 디펜스 게임에서 타워를 특정 위치에 설치하거나 적을 선택해 스킬을 사용하는 기능을 쉽게 만들 수 있을 것 같다.

profile
코딩 학습 공간

0개의 댓글