이전에 했던 것들의 연장선이다. 이번에는 투사체를 구현하는 것을 해보도록 하자
1. 코드 수정
먼저 CharacterController에서 움직임과 에임 등을 다루고 있는데 여기서 Attack 이벤트도 바꿔줘야 한다.
이유는 ScriptableObject로 커스텀했던 캐릭터 정보를 이제는 Attack에도 적용시켜줘야 하기 때문이다.
* TopDownCharacterController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TopDownCharacterController : MonoBehaviour
{
// event 외부에서는 호출하지 못하게 막는다.
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
// Attack 이벤트를 적용하려면 Attack정보를 가져와야하기 때문에 <AttackSO> 를 받음
public event Action<AttackSO> OnAttackEvent; // 수정됨
protected CharacterStatsHandler Stats { get; private set; }
private float _timeSinceLastAttack = float.MaxValue;
protected bool IsAttacking { get; set; }
// Stats 가 null인 이슈가 있었음
protected virtual void Awake()
{
Stats = GetComponent<CharacterStatsHandler>();
}
protected virtual void Update()
{
HandleAttackDelay();
}
private void HandleAttackDelay()
{
if(Stats.CurrentStats.attackSo == null)
{
// Attack 정보가 없기 때문에 공격 x
return;
}
if(_timeSinceLastAttack <= Stats.CurrentStats.attackSo.delay)
{
//Debug.Log(">>" + Stats.CurrentStats.attackSo.delay);
_timeSinceLastAttack += Time.deltaTime;
}
else if (IsAttacking && _timeSinceLastAttack > Stats.CurrentStats.attackSo.delay)
{
_timeSinceLastAttack = 0;
CallAttackEvent(Stats.CurrentStats.attackSo);
}
}
public void CallMoveEvent(Vector2 direction)
{
// null이 아닐 때만 동작
OnMoveEvent?.Invoke(direction);
}
public void CallLookEvent(Vector2 direction)
{
OnLookEvent?.Invoke(direction);
}
public void CallAttackEvent(AttackSO attackSo)
{
OnAttackEvent?.Invoke(attackSo);
}
}
/// 이동 처리 연습
OnAttackEvent 에서 AttacSO를 받으면서 하위 CallBack 함수들에도 다 AttackSO를 넣어주도록 하자.
그리고 OnAttackEvent 를 구독한 OnShoot 메서드가 들어있는 TopDownShooting 클래스도 마찬가지로 수정해주고 추가해주도록 하자.
2. 부채꼴 모양 로직
* TopDownShooting.cs
private void OnShoot(AttackSO attackSo)
{
// 캐릭터가 탄을 부채꼴 모양으로 쏠 수 있도록 준비하는것이다.
RangedAttackData rangedAttackData = attackSo as RangedAttackData;
float projectilesAngleSpace = rangedAttackData.multipleProjectilesAngle;
int numberOfProjectilesPerShot = rangedAttackData.numberofProjectilesPerShot;
// 탄이 부채꼴모양으로 배치되기 위한 최소한의 각도
float minAngle = -(numberOfProjectilesPerShot / 2f) * projectilesAngleSpace + 0.5f * rangedAttackData.multipleProjectilesAngle;
Debug.Log("rangedAttackData: " + rangedAttackData);
Debug.Log("projectilesAngleSpace: " + projectilesAngleSpace);
Debug.Log("numberOfProjectilesPerShot: " + numberOfProjectilesPerShot);
Debug.Log("minAngle: " + minAngle);
Debug.Log("rangedAttackData.multipleProjectilesAngle: " + rangedAttackData.multipleProjectilesAngle);
//총알을 여러개 생성
for(int i = 0; i < numberOfProjectilesPerShot; i++)
{
float angle = minAngle + projectilesAngleSpace * i;
float randomSpread = Random.Range(-rangedAttackData.spread, rangedAttackData.spread); // 랜덤한 퍼짐을 부여하여 탄 각도 변동 부여
angle += randomSpread;
CreateProjectile(rangedAttackData, angle);
}
}
탄을 생성하여 탄을 부채꼴모양으로 쏠 수 있게끔 해주는 역할이다.
그리고 CreateProjectiled에 계산된 값을 전달해줘야하는데, 그렇다면 생성해야할것은 탄 매니저인 ProjectileManger이다.
3. ProjectileManager
ProjectileManger 는 탄을 관리해주는 역할을 하는 싱글톤 매니저이다. 간단한 싱글톤으로 생성하여 접근할 수 있게끔 한 후, 총알을 Instantiate로 생성해준다.
ProjectileManger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ProjectileManager : MonoBehaviour
{
// 탄을 관리해주는 역할
// 싱글톤 매니저를 사용할 예정
[SerializeField] private ParticleSystem _impactParticleSystem;
[SerializeField] private GameObject testObj;
public static ProjectileManager instance;
private void Awake()
{
instance = this;
}
void Start()
{
}
// 2. 생성하는 부분 총알 생성
public void ShootBullet(Vector2 startPosition, Vector2 direction, RangedAttackData attackData )
{
GameObject obj = Instantiate(testObj);
obj.transform.position = startPosition;
// 발사
RangedAttackController attackController = obj.GetComponent<RangedAttackController>(); // 초기화 진행
attackController.InitializeAttack(direction, attackData, this);
obj.SetActive(true);
}
}
Awake에서 instance를 생성하여 싱글톤을 해준 후 아까 필요했던 CreateProjectile() 메서드에서 총알을 생성해주도록 하는 역할이 ShootBullet() 메서드이다.
그러면 아까 ShootBullet을 가져올 TopDownShooting.cs에 다시 돌아가 CreateProjectile에 값을 대입해준다.
* TopDownShooting.cs
// 1. 총알좀 만들어달라고 projectileManager에게 요청
private void CreateProjectile(RangedAttackData rangedAttackData, float angle)
{
_projectileManager.ShootBullet(
projectileSpownPosition.position, // 발사위치
RotateVector2(_aimDriection,angle), // 회전 각
rangedAttackData // 발사, 공격 정보
);
}
private static Vector2 RotateVector2(Vector2 v, float degree)
{
return Quaternion.Euler(0, 0, degree) * v; // 이 Vector 를 이 각도로 회전시켜라는 식
}
Quaternion 값을 그대로 return한다면 개수가 맞지 않아 return이 되지않는다. 이를 받아온 vector값을 곱해준다면. 계산식이 맞게 된다.
그러면 발사위치(오브젝트 활의 자식에 넣어둠), 회전 각도와 공격정보를 담아 Manager에게 총알을 만들어주게끔 요청할 수 있게 된다.
이후에 ProjectileManger 에 ShootBullet에서 값들을 정해주면된다.
4. Arrow 설정
Box Collider를 추가하고 크기에 맞게 설정하고, 활이 날아가기 때문에 Rigidbody 2D를 넣어준다. Rigidbody 2D에서는 중력값만 빼주도록 하자.
Trail Renderer
trailRenderer를 넣어주면 뒤에 색이 따라오면서 어느정도의 시간이 지나면 사라지는 효과를 줄 수 있다.
기본 color가 분홍색이 되어있는데 applyActiveColorSpace를 꺼주고 마음대로 커스텀하면된다.
5. RangedAttackController cs 생성
이제 Arrow에 적용시켜줄 스크립트를 작성하면 된다.
RangedAttackController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RangedAttackController : MonoBehaviour
{
[SerializeField] private LayerMask levelCollisionLayer;
private RangedAttackData _attackData;
private float _currentDuration;
private Vector2 _direction;
private bool _isReady;
private Rigidbody2D _rigidbody;
private SpriteRenderer _spriteRenderer;
private TrailRenderer _trailRenderer;
private ProjectileManager _projectileManager;
public bool fxOnDestory = true;
private void Awake()
{
_spriteRenderer = GetComponentInChildren<SpriteRenderer>(); // 나를포함 내 자식까지 검사
_rigidbody = GetComponent<Rigidbody2D>();
_trailRenderer = GetComponent<TrailRenderer>();
}
private void Update()
{
if(!_isReady)
{
return;
}
_currentDuration = Time.deltaTime;
// 사용시간이 끝나면 Destroy
if (_currentDuration > _attackData.duration)
{
DestroyProjefctile(transform.position, false);
}
_rigidbody.velocity = _direction * _attackData.speed;
}
private void OnTriggerEnter2D(Collider2D collision)
{
// 내가 찾던 Layer와 충돌했을 때
if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
{
// ClosestPoint(transform.position) 부딪힌 물체와 가장 가까운 위치
// collision.ClosestPoint(transform.position) - _direction * .2f 벽에서 부딪힌곳에서 조금 안쪽으로
DestroyProjefctile(collision.ClosestPoint(transform.position) - _direction * .2f, fxOnDestory);
}
}
// InitializeAttack 에서 초기화 진행
public void InitializeAttack(Vector2 direction, RangedAttackData attackData, ProjectileManager projectileManager) // 인스턴스로 찾을 수 있지만, 상호 참조를 할 수 있게끔 설계
{
_projectileManager = projectileManager;
_attackData = attackData;
_direction = direction;
UpdateProjectileSprite();
// 이후에 재사용
_trailRenderer.Clear();
_currentDuration = 0;
_spriteRenderer.color = attackData.projectileColor;
transform.right = direction;
_isReady = true;
}
// 초기화
private void UpdateProjectileSprite()
{
transform.localScale = Vector3.one * _attackData.size; // Data에 따라 사이즈가 달라짐, 캐릭터마다 사이즈가 다르기 때문
}
// 삭제될 때
private void DestroyProjefctile(Vector3 position, bool createFx)
{
if(createFx)
{
}
// 재사용
gameObject.SetActive(false);
}
}
Awake에서는 캐싱처리를 위해 값을 받아주고, Update에서는 호출되지 않는다면 활이 날아가지 않으며 사용시간이 끝나면 활을 DestroyProjefctile()로 없애준다.
OnTriggerEnter2D는 이전에 포스트에서 한번 언급을 했지만, 여기서는 Layer를 사용해서 Tag와 비슷한 Layer를 설정해두고, 이 값을 가지고 있는 오브젝트들이있다면 활이 사라지게끔 해주는 역할이다.
InitializeAttack() 메서드는 앞에 ProjectileManager에서 받아올 값을 담아준다.
UpdateProjectileSprite() 메서드에서는 스프라이트의 크기가 이후에 변경될 수 있기 때문에 이에 맞춰서 크기를 조절해주는 역할을 한다.
'Unity' 카테고리의 다른 글
Dictionary 정리 및 활용 (0) | 2023.12.13 |
---|---|
LayMask와 비트연산자 처리 (1) | 2023.12.11 |
[프로그래머스] 숫자 문자열과 영단어 C# (1) | 2023.12.08 |
[Unity] 캐릭터 스탯 만들기 (1) | 2023.12.07 |
[유니티] 벽돌깨기 - 벽돌(몬스터) 풀링 (2) | 2023.12.07 |