
차지샷 구현 중의 화면
플레이어가 원거리 공격 시,
특정 시간 동안 공격 키를 누르고 있다가 발사하면 더 크고 강력한 투사체가 날아가는 차지 샷 기능을 구현하고 싶었다.
기본 공격과 입력 키가 같다. (Input System에서 OnAttack)
키 입력 - 시작 (if : 원거리무기이면서, 차지샷 기능 해금 되어있으면) 차징 시작
키 입력 - 중 ( 일반 사격 등 )
키 입력 - 종료 (if : 원거리 무기이면서, 차지샷 기능이 해금 되어있으면 + 요구 차지 시간 끝나면, 차지 샷 발사)
차지 샷 탄환 스프라이트 + 애니메이션도 기본 탄환과는 다르게 + 충돌 판정도 크게
플레이어 차징 시 주변에 에너지가 모이는 애니메이션 재생
( 키를 누르고 있으면서 0.1초 지나면 재생해야함, 일반 공격에도 차징 애니메이션이 작동하기 문제 때문에 )
차지 샷 투사체는 맵, 몬스터를 관통하되 몬스터 피격+피격 이펙트는 작동해야 함
기본 투사체보다 빠른 스피드 + 다단 히트
차징이 완료되면 플레이어가 밝게 빛나는 효과(깜빡이는) 주기
내일 아침은 맥모닝을 먹을 것
(New Input System의 Invoke Unity Event 사용)
입력 과정에서 차지샷이 발동하는 조건을 만들고 싶었기에,
플레이어 입력을 구현한 클래스의 OnAttack 메서드에서 차징에 진입하고 끝나는 조건을 구성해보았다.
실제 차지샷 발사는 플레이어 어택(공격) 메서드에서 구현 할 예정.

_isWeaponChange 이라는 변수는 true일 때 근거리 공격이고 false 일 때 원거리 공격이다. (현재 무기를 스왑하는 기능이 있다, 공격 키가 같기 때문에)lockAction[Paction.ChargeShot] 은 이런 차지샷이나 여러가지 기능을 해금해주는 Enum인데, 예를 들면 특정 아이템을 장착하거나 획득하여 해금해주려고 한다.
차징 관련 코드만 따로 보자면..
// 키 입력이 시작되었을 때
if (context.started && !_isWeaponChange && lockAction[Paction.ChargeShot])
{
_playerAttacks.StartCharging(); // 차징 시작 로직
}
첫 번째 if 조건문은, 키 입력 시작되면서 원거리 무기이고, 차징 스킬이 해금되었을 때 플레이어 어택 클래스의 StartCharging(차징 시작) 메서드로 들어갈수 있도록 해주었다.
두 번째 조건문에 context.perform은 생략 ( 차징과 관계 없는 다른 공격 기능들 )
else if (context.canceled && !_isWeaponChange && lockAction[Paction.ChargeShot])
{
_playerAttacks.ReleaseCharge(); // 차징 종료 로직
}
다음 세 번째 else if 조건문에서,
키 입력이 끝나고, 원거리 무기이면서, 차징 샷이 해금되었을 때 차징 종료 메서드로 넘어갈 수 있게 해주었다.
본격적으로 차지샷을 로직을 구현하는 건 플레이어 공격 클래스이다.
일단 구현 단계별로 정리 + 주석으로 설명해보았다.
// ! 애니메이션, 다른 클래스 등의 컴포넌트 참조 코드는 생략하였습니다. !
[SerializeField] private GameObject ChargedBulletPref; // 차지샷 탄환 프리팹
[SerializeField] private float ChargeShotPower = 20f; // 차지샷 파워 (날아가는 속도 느낌)
[SerializeField] private float maxChargeTime = 1.5f; // 차지 최대 충전 시간, 충전이 완료 되야 차지샷 탄환으로 발사 됨.
private float chargeTime = 0f; // 차징 타임 추적 변수
private bool isCharging = false; // 차징 여부
private bool isFullCharge = false; // 차징 완료 여부
[Header("Charging Effect")]
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] public Material chargingMaterial; // 차징 시 플레이어의 머테리얼을 변경해주고
[SerializeField] public Color color1; // 컬러 변경을 통해
[SerializeField] public Color color2; // 깜빡임을 만들어주려고 함.

컬러의 수치는 인스펙터에서 조정해주었음
ChargeGlow는 차징 완료 시 ~ 차지 샷 발사 전까지 빛나는 효과를 만들어주기 위해 변경 될 머테리얼이다.
private void Update()
{
if (isCharging)
{
chargeTime += Time.deltaTime;
if (chargeTime > 0.15f)
{
playerAnimations.Charging(true); // 충전 애니메이션 시작
}
}
if (isCharging && !isFullCharge && chargeTime >= maxChargeTime)
{
isFullCharge = true;
spriteRenderer.material = chargingMaterial; // 차지 머테리얼로 변경
StartCoroutine(ChargingBlinkEffect()); // 색상 변경 코루틴 시작
}
}
차징 시간을 추적
조건문을 통해 차징 시간 0.15초 후 차징이 되는 애니메이션을 재생하였음
why) 기본 공격시 차징 애니메이션이 재생 되지 않도록 어느 정도의 차징 텀을 주어 기본 공격과 차징의 시각적 표현을 구분하려는 의도.
public void StartCharging()
{
isCharging = true;
}
public void ReleaseCharge()
{
// 차징 관련 bool false로 초기화
isCharging = false;
isFullCharge = false;
// 발사 시 모든 코루틴 중지 ( 차징 효과 등 )
StopAllCoroutines();
// 머테리얼 및 컬러 복구
spriteRenderer.material = originalMaterial;
spriteRenderer.color = Color.white;
if (chargeTime >= maxChargeTime) // 차지 시간이 목표 차지 시간(1.5초) 이상이면
{
ChargeShot(); // 차지 샷 발사
}
else // 미만이면
{
Fire(); // 일반 샷 발사
}
chargeTime = 0f; // 차징 충전 시간 리셋
playerAnimations.Charging(false); // 차지 애니메이션 false
}
public void ChargeShot()
{
spriteRenderer.material = originalMaterial; // 머테리얼 복구
playerAnimations.Charging(false); // 발사 시에도 차징 애니메이션은 종료
playerAnimations.FireEffect(); // 발사 효과 애니메이션
Vector3 direction = transform.right * transform.localScale.x; // 발사 방향은 플레이어가 바라보는 방향
// 충전된 총알 프리팹을 인스턴스화하고 발사 위치와 방향을 설정
GameObject bullet = Instantiate(ChargedBulletPref, rangeAttackPosition.position + direction, Quaternion.identity);
// 총알에 리지드 바디 물리 힘을 추가하여 발사합니다.
bullet.GetComponent<Rigidbody2D>().AddForce(direction * ChargeShotPower, ForceMode2D.Impulse);
}
private IEnumerator ChargingBlinkEffect()
{
while (isCharging)
{
spriteRenderer.material.SetColor("_Color", color1);
yield return new WaitForSeconds(0.05f);
spriteRenderer.material.SetColor("_Color", color2);
yield return new WaitForSeconds(0.05f);
}
spriteRenderer.material.SetColor("_Color", Color.white);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerRangeAttackHandler : MonoBehaviour
{
[Header("Range Attack Parameters")]
[SerializeField] private GameObject RangeHitEffect; // 히트 효과 프리팹
[SerializeField] private bool isChargeShot = false; // 차지샷 프리팹은 true
PlayerStatus playerStatus; // 현재 사용x
// 다단 히트 메서드의 히트 및 히트 이펙트 등의 여러 코루틴을 관리하기 위해 사용되는 리스트
private List<Coroutine> hitCoroutines = new List<Coroutine>();
void Start()
{
Invoke("DestroyTime", 7.0f);
playerStatus = GetComponent<PlayerStatus>();
}
void DestroyTime()
{
Destroy(gameObject);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (((1 << collision.gameObject.layer) & ((1 << 8) | (1 << 9))) != 0) // 8: 그라운드 , 9: 플랫폼
{
if (!isChargeShot) // 일반 샷인 경우에만 벽이나 플랫폼에 부딪혔을 때 사라짐
{
Instantiate(RangeHitEffect, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}
else if (((1 << collision.gameObject.layer) & (1 << 7)) != 0) // 7: 몬스터
{
if (isChargeShot) // 차지 샷
{
Coroutine hitCoroutine = StartCoroutine(MultiHit(collision));
hitCoroutines.Add(hitCoroutine);
}
else // 일반 샷
{
HitEffectAndDamage(collision);
Destroy(gameObject);
}
}
}
private IEnumerator MultiHit(Collider2D collision)
{
while (collision != null && gameObject != null)
{
HitEffectAndDamage(collision);
yield return new WaitForSeconds(0.05f);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (((1 << collision.gameObject.layer) & (1 << 7)) != 0) // 7: 몬스터
{
StopAllCoroutines();
hitCoroutines.Clear();
}
}
private void HitEffectAndDamage(Collider2D collision)
{
Vector3 bulletColliderCenter = GetComponent<Collider2D>().bounds.center;
Vector3 monsterColliderCenter = collision.bounds.center;
Vector3 directionToMonster = (monsterColliderCenter - bulletColliderCenter).normalized;
Vector3 hitEffectPosition = monsterColliderCenter - directionToMonster * 0.2f;
Instantiate(RangeHitEffect, hitEffectPosition, Quaternion.identity);
// collision.SendMessage("Damaged", 1);
}
}
일단 전체 코드에서 하나하나 분석하면
[SerializeField] private GameObject RangeHitEffect; // 히트 효과 프리팹
[SerializeField] private bool isChargeShot = false; // 차지샷인 경우 true로 설정, 일반 투사체와 구분하기 위함
void Start()
{
Invoke("DestroyTime", 7.0f); // 7초 후에 자동으로 파괴되도록 설정
}
void DestroyTime()
{
Destroy(gameObject); // 게임 오브젝트를 파괴합니다.
}
private void OnTriggerEnter2D(Collider2D collision)
{
// 벽이나 플랫폼과 충돌 처리
if (((1 << collision.gameObject.layer) & ((1 << 8) | (1 << 9))) != 0)
{
if (!isChargeShot) // 일반 샷인 경우
{
Instantiate(RangeHitEffect, transform.position, Quaternion.identity);
Destroy(gameObject);
}
}
// 몬스터와의 충돌 처리
else if (((1 << collision.gameObject.layer) & (1 << 7)) != 0)
{
if (isChargeShot) // 차지 샷인 경우
{
Coroutine hitCoroutine = StartCoroutine(MultiHit(collision));
hitCoroutines.Add(hitCoroutine);
}
else // 일반 샷인 경우
{
HitEffectAndDamage(collision);
Destroy(gameObject);
}
}
}
발사체가 벽이나 몬스터와 충돌했을 때의 동작을 정의.
차지샷은 몬스터에게 연속적으로 피해(다단 히트)를 줄 수 있으며 관통이 된다(Destroy 없음), 일반 샷은 단발 피해 후 파괴된다.
또한 차지샷 적중 시 코루틴을 통해 다단 히트 메서드를 작동시키고, 다단 히트 이펙트 메서드도 작동하게 해주었다.
private IEnumerator MultiHit(Collider2D collision)
{
while (collision != null && gameObject != null)
{
HitEffectAndDamage(collision);
yield return new WaitForSeconds(0.05f);
}
}
private void HitEffectAndDamage(Collider2D collision)
{
// 충돌 지점에 히트 이펙트를 생성합니다.
Vector3 bulletColliderCenter = GetComponent<Collider2D>().bounds.center;
Vector3 monsterColliderCenter = collision.bounds.center;
Vector3 directionToMonster = (monsterColliderCenter - bulletColliderCenter).normalized;
Vector3 hitEffectPosition = monsterColliderCenter - directionToMonster * 0.2f;
Instantiate(RangeHitEffect, hitEffectPosition, Quaternion.identity);
// 몬스터에게 피해를 주는 코드(주석 처리됨)
// collision.SendMessage("Damaged", 1);
}
발사체가 몬스터에게 피해를 주고 히트 이펙트를 생성하는 메서드
실제 피해 부여 로직은 구현 예정.
히트 이펙트에 데미지 로직을 함께 넣은 이유는, 일반 탄환도 이 히트 이펙트 메서드를 공유하기 때문에 넣었는데 추후 위치가 변경 될 수 있다.
위 코드의 히트 이펙트 위치 조정에 대한 자세한 설명은 아래 주소의 주석을 참조.
https://velog.io/@wjj329/24.03.21-TIL-Unity-

키를 누르고 있을 때 플레이어 주변으로 차징 애니메이션 재생 + 차징 시작
차징이 완료 되면 캐릭터에게 번쩍이는 이펙트가 적용되어 게임 플레이어에게 시각적으로 차징이 완료 됨을 알림
키를 떼어 차징을 종료하고 차지샷을 발사 시, 발사 지점에서 폭발 이펙트 발생 및 차지샷 투사체 발사
날아가면서 테스트 몬스터를 관통하고, 0.05초마다 다단 히트 발생
결론 : 시각적인 기능 구현 파트는 언제나 재미있다.