몇가지 무기를 추가하고,
무기 관련 코드에 대해서 전체적인 리펙토링을 진행하였다.
구조체는 스택 메모리 영역에 할당한다.
그리고 값 복사를 사용하기에,
무거운 데이터를 포함하면 성능이 저하된다.
함수 인자로 자주 넘기면 불필요한 복사가 발생한다.
Boxing 발생 가능성이 있다.
아래는 기존 ProjectileContrller 스크립트이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//발사체 충돌과 파괴 처리
public class ProjectileController : MonoBehaviour
{
[SerializeField] private ProjectileData data;
private Vector2 direction;
private float elapsedTime;
private Rigidbody2D rb;
private SpriteRenderer sr;
public float TotalAtk;
/// <summary>
/// 발사체를 초기화합니다.
/// </summary>
/// <param name="data">ScriptableObject로 정의된 발사체 데이터</param>
/// <param name="direction">발사 방향 (정규화된 벡터)</param>
public void Initialize(ProjectileData data, Vector2 direction, float totalatk)
{
this.data = data;
this.direction = direction.normalized;
this.elapsedTime = 0f;
rb = GetComponent<Rigidbody2D>();
sr = GetComponentInChildren<SpriteRenderer>();
//final_Attack += 무기공격력 + 부모의 공격력
TotalAtk = totalatk;
// 발사체 색상 설정
if (sr != null)
sr.color = data.Color;
// 회전을 통해 방향 맞추기
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
}
public float GetAttackPower()
{
return TotalAtk + data.attackPower;
}
private void Update()
{
if (data == null) return;
elapsedTime += Time.deltaTime;
// 수명 경과 시 파괴, 수명은 화살 데이터에서
if (elapsedTime >= data.lifetime)
{
DestroyProjectile(transform.position);
return;
}
// 이동
rb.velocity = direction * data.moveSpeed;
}
private void OnTriggerEnter2D(Collider2D other)
{
int layer = other.gameObject.layer;
// 레벨(벽 등) 충돌 체크
if (((1 << layer) & data.layer.value) != 0)
{
DestroyProjectile(other.ClosestPoint(transform.position));
return;
}
//// 대상(플레이어/몬스터) 충돌 체크
//if (((1 << layer) & data.targetLayerMask.value) != 0)
//{
// if (other.TryGetComponent<IDamageable>(out var dmg))
// dmg.TakeDamage(data.attackPower);
// DestroyProjectile(other.ClosestPoint(transform.position));
//}
}
//임시 코드, 맞았을 경우 이펙트 효과
public void DestroyProjectile(Vector3 hitPosition)
{
// 충돌 이펙트 생성
if (data.impactEffect != null)
Instantiate(data.impactEffect, hitPosition, Quaternion.identity);
Destroy(gameObject);
}
}
위 코드로 해골을 발사할 때, 문제가 생겼다.

해골이 뒤집어 져서 발사된다.
화살은 뒤집어져도 똑같아서 몰랐다.
이를 수정하려면 어떻게 해야할까?
일단 위 문제는 투사체 프리팹의 구조에서, SpriteRenderer 오브젝트가 Pivot 아래에 있어서,
Pivot에 회전에 대해서 변화를 주어야 한다.
일단 발사체 생성되는 ProjectileContrller에서 Initialize() 에서
Pivot을 건드려서 수정하였다.
아래는 수정과 리펙토링을 한 코드이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//발사체 충돌과 파괴 처리
public class ProjectileController : MonoBehaviour
{
[SerializeField] private ProjectileData data;
[SerializeField] private Transform pivot;
private Vector2 direction;
private float elapsedTime;
private Rigidbody2D rb;
private SpriteRenderer sr;
public float totalAtk;
/// <summary>
/// 발사체를 초기화합니다.
/// </summary>
/// <param name="data">ScriptableObject로 정의된 발사체 데이터</param>
/// <param name="direction">발사 방향 (정규화된 벡터)</param>
public void Initialize(ProjectileData data, Vector2 direction, float totalatk)
{
this.data = data;
this.direction = direction.normalized;
this.elapsedTime = 0f;
rb = GetComponent<Rigidbody2D>();
sr = GetComponentInChildren<SpriteRenderer>();
totalAtk = totalatk;
Debug.Log(totalAtk);
Debug.Log(totalAtk + data.attackPower);
ApplyVisualSettings();
}
private void ApplyVisualSettings()
{
if (sr != null)
{
sr.color = data.Color;
}
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
if (pivot != null)
{
pivot.localRotation = direction.x < 0
? Quaternion.Euler(180f, 0f, 0f)
: Quaternion.identity;
}
}
public float GetAttackPower()
{
//final_Attack += 무기공격력 + 부모의 공격력
return totalAtk + data.attackPower;
}
private void Update()
{
if (data == null) return;
elapsedTime += Time.deltaTime;
// 수명 경과 시 파괴, 수명은 화살 데이터에서
if (elapsedTime >= data.lifetime)
{
DestroyProjectile(transform.position);
return;
}
// 이동
rb.velocity = direction * data.moveSpeed;
}
private void OnTriggerEnter2D(Collider2D other)
{
int layer = other.gameObject.layer;
// 레벨(벽 등) 충돌 체크
if (((1 << layer) & data.layer.value) != 0)
{
DestroyProjectile(other.ClosestPoint(transform.position));
return;
}
//// 대상(플레이어/몬스터) 충돌 체크
//// 플레이어/몬스터 측에서 처리
//if (((1 << layer) & data.targetLayerMask.value) != 0)
//{
// if (other.TryGetComponent<IDamageable>(out var dmg))
// dmg.TakeDamage(data.attackPower);
// DestroyProjectile(other.ClosestPoint(transform.position));
//}
}
//임시 코드, 맞았을 경우 이펙트 효과
public void DestroyProjectile(Vector3 hitPosition)
{
// 충돌 이펙트 생성
if (data.impactEffect != null)
Instantiate(data.impactEffect, hitPosition, Quaternion.identity);
Destroy(gameObject);
}
}
기능을 메소드로 빼내서 호출하는 방식으로 가독성을 높였다.
투사체 회전 세팅에 대한 로직은 ApplyVisualSettings() 에서 처리한다.
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0f, 0f, angle);
회전을 통한 방향을 정렬하는 코드로
direction 벡터의 y/x의 비율을 Atan2로 계산하여 라디안 단위로 각도를 구하고
Mathf.Rad2Deg 를 통해 몇 도인지 변환한다.
그런 다음에 Quaternion.Euler 로 Z축 회전만 설정하여
발사체 오브젝트 전체가 정확히 날아갈 방향을 향하도록 회전시킨다.
이 코드만 있으면 위 해골처럼 뒤집어진 모습으로 발사하게 된다.
아래 코드로 뒤집어준다.
if (pivot != null)
{
pivot.localRotation = direction.x < 0
? Quaternion.Euler(180f, 0f, 0f)
: Quaternion.identity;
}
pivot 은 스프라이트를 감싸는 오브젝트로
발사 방향이 왼쪽일 때, x축 180도 회전을 하여 좌우로 뒤집니다.
그 외에는 Quaternion.identity(회전 없음)을 유지해,
오른쪽 방향 발사 시엔 뒤집히지 않게 한다.
추가적으로 근거리 무기 스크립트인 MeleeWeapon.cs와
원거리 무기 스크립트인 RangeWeapon.cs 코드를 리펙토링하였다.
BaseWeapon을 상속받는 클래스들이기에
난잡하게 써둔 코드를 모둔 리펙토링 하였다.
public class MeleeWeapon : BaseWeapon
{
[Header("���� ���� ���� ������")]
[SerializeField] private Vector2 hitboxSize = Vector2.one;
[SerializeField] private Vector2 hitboxOffset = Vector2.zero;
private Vector3 _originalScale;
public Transform Target;
public StatController Owner;
//public GameObject Owner_Moster;
private float lastAttackTime
protected override void Start()
{
base.Start();
lastAttackTime = -Mathf.Infinity;
animator = GetComponentInChildren<Animator>();
_originalScale = transform.localScale;
Owner = GetComponentInParent<StatController>();
//Owner_Moster = GetComponentInParent<GameObject>();
//Target = Owner_Moster.GetComponent<Monster_Melee>().target.transform;
// �� AnimatorController �Ҵ� Ȯ��
if (animator == null)
Debug.LogError("Animator�� ã�� �� �����ϴ�.");
else if (animator.runtimeAnimatorController == null)
Debug.LogError("AnimatorController�� �Ҵ���� �ʾҽ��ϴ�.");
//Target = Owner_Moster.GetComponent<Monster_Melee>().target.transform;
}
void Update()
{
FlipTowardsTarget();
CheckAndAttack();
}
private void FlipTowardsTarget()
{
Vector2 dir = (Target.position - transform.position).normalized;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
bool shouldFlip = angle > 90f || angle < -90f;
float appliedAngle = shouldFlip ? angle + 180f : angle;
transform.localEulerAngles = new Vector3(0f, 0f, appliedAngle);
Vector3 scale = _originalScale;
scale.x = shouldFlip
? -Mathf.Abs(_originalScale.x)
: Mathf.Abs(_originalScale.x);
transform.localScale = scale;
}
private void CheckAndAttack()
{
if (Target == null)
return;
float dist = Vector2.Distance(transform.position, Target.position);
if (dist <= data.attackRange)
{
Attack(Target.position);
}
}
public float GetAttackPower()
{
float Total = data.attackPower + Owner.Atk;
return Total;
}
public override void Attack(Vector3 v)
{
float cooldown = 1f / Speed;
if (Time.time < lastAttackTime + cooldown)
lastAttackTime = Time.time;
base.Attack(v);
Debug.Log("��������");
Vector2 dir = ((Vector2)v - (Vector2)transform.position).normalized;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
Vector2 center = (Vector2)transform.position + hitboxOffset;
Quaternion rotation = Quaternion.Euler(0, 0, angle);
RaycastHit2D[] hits = Physics2D.BoxCastAll(
center,
hitboxSize * WeaponSize,
angle,
Vector2.zero,
0f,
data.layer
);
//foreach (var hit in hits)
//{
// //if (hit.collider != null && hit.collider.TryGetComponent<IDamageable>(out var dmg))
// //{
// // dmg.TakeDamage(AtkPower);
// //}
//}
// (����) ������ ����Ʈ�� ���� ��� ����
}
}
위 코드는 MeleeWeapon스크립트이고 아래 코드로 리펙토링을 진행하였다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
public class MeleeWeapon : BaseWeapon
{
[Header("근접 공격 범위 오프셋")]
[SerializeField] private Vector2 hitboxSize = Vector2.one;
[SerializeField] private Vector2 hitboxOffset = Vector2.zero;
private Vector3 _originalScale;
public Transform Target;
public StatController Owner;
//public GameObject Owner_Moster;
private float lastAttackTime;
protected override void Awake()
{
base.Awake();
animator = GetComponentInChildren<Animator>();
Owner = GetComponentInParent<StatController>();
//Owner_Moster = GetComponentInParent<GameObject>();
}
protected override void Start()
{
base.Start();
lastAttackTime = -Mathf.Infinity;
_originalScale = transform.localScale;
//Target = Owner_Moster.GetComponent<Monster_Melee>().target.transform;
}
void Update()
{
FlipTowardsTarget();
CheckAndAttack();
}
private void FlipTowardsTarget()
{
Vector2 dir = (Target.position - transform.position).normalized;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
bool shouldFlip = angle > 90f || angle < -90f;
float appliedAngle = shouldFlip ? angle + 180f : angle;
transform.localEulerAngles = new Vector3(0f, 0f, appliedAngle);
Vector3 scale = _originalScale;
scale.x = shouldFlip
? -Mathf.Abs(_originalScale.x)
: Mathf.Abs(_originalScale.x);
transform.localScale = scale;
}
private void CheckAndAttack()
{
if (Target == null)
return;
float dist = Vector2.Distance(transform.position, Target.position);
if (dist <= data.attackRange)
{
Attack(Target.position);
}
}
public float GetAttackPower()
{
float Total = data.attackPower + Owner.Atk;
return Total;
}
public override void Attack(Vector3 v)
{
if (!AttackCoolTime())
return;
base.Attack(v);
Debug.Log("근접공격");
// 공격 방향에 따라 히트박스 회전
Vector2 dir = ((Vector2)v - (Vector2)transform.position).normalized;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
Vector2 center = (Vector2)transform.position + hitboxOffset;
Quaternion rotation = Quaternion.Euler(0, 0, angle);
}
private bool AttackCoolTime()
{
float cooldown = 1f / Speed;
if (Time.time < lastAttackTime + cooldown)
return false;
lastAttackTime = Time.time;
return true;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (data == null) return;
// 회전 및 스케일 적용된 상태에서 Offset, Size에 따라 박스 표시
Vector3 center = transform.position + (Vector3)hitboxOffset;
Quaternion rot = transform.rotation;
Vector3 size = new Vector3(hitboxSize.x, hitboxSize.y, 1f);
Gizmos.color = new Color(0f, 1f, 0f, 0.5f); // 초록 반투명
Matrix4x4 old = Gizmos.matrix;
Gizmos.matrix = Matrix4x4.TRS(center, rot, Vector3.one);
Gizmos.DrawWireCube(Vector3.zero, size);
Gizmos.matrix = old;
}
#endif
}
아래는 RangeWeapon 스크립트이다.
Attack()메소드 내부에 로직을 각 기능별로 분리하여 메소드로 만들어서 간단하게 호출하는 방식으로 리펙토링하였다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
public class RangeWeapon : BaseWeapon
{
[SerializeField] private Transform projectileSpawnPoint;
public ProjectileData ProjectileData => data.projectileData;
public float ProjSpeed => ProjectileData.moveSpeed;
public float Duration => ProjectileData.lifetime;
public Color Color => data.projectileData.Color; //화살 색
//public GameObject InpactEffect => ProjectileData.impactEffect;
public int MultiShotCount => data.multiShotCount + data.dungeon_ShotCount; // 한 번에 쏘는 화살 수
public float MultiShotAngle => data.multiShotAngle; // 화살 퍼짐 각도
private float lastAttackTime;
public StatController owner;
public float totalatk_OwnerAndWeapon => data.attackPower + owner.Atk + data.dungeon_AddPower;
private Vector3 originalScale;
protected override void Awake()
{
owner = GetComponentInParent<StatController>();
}
protected override void Start()
{
lastAttackTime = -Mathf.Infinity; //첫 공격이 즉시 가능하도록
originalScale = transform.localScale;
}
public override void Attack(Vector3 targetPosition) //위치를 파라미터로 받아와서 공격
{
if (!AttackCoolTime())
return;
Debug.Log(totalatk_OwnerAndWeapon);
base.Attack(targetPosition);
//타겟을 향해서 회전
FaceTarget(targetPosition);
//투사체 생성 요청 -> projectileManager
SpawnProjectiles(targetPosition);
}
private bool AttackCoolTime()
{
float cooldown = 1f / Speed;
if (Time.time < lastAttackTime + cooldown)
return false;
lastAttackTime = Time.time;
return true;
}
private void FaceTarget(Vector3 targetPosition)
{
Vector2 toTarget = ((Vector2)targetPosition - (Vector2)transform.position).normalized;
float angle = Mathf.Atan2(toTarget.y, toTarget.x) * Mathf.Rad2Deg;
bool shouldFlip = angle > 90f || angle < -90f;
float appliedAngle = shouldFlip ? angle + 180f : angle;
transform.localEulerAngles = new Vector3(0f, 0f, appliedAngle);
Vector3 scale = originalScale;
scale.x = shouldFlip ? -Mathf.Abs(originalScale.x) : Mathf.Abs(originalScale.x);
transform.localScale = scale;
}
private void SpawnProjectiles(Vector3 targetPosition)
{
Vector2 baseDirection = ((Vector2)targetPosition - (Vector2)transform.position).normalized;
Vector2 spawnPos = projectileSpawnPoint.position;
float baseAngle = Mathf.Atan2(baseDirection.y, baseDirection.x) * Mathf.Rad2Deg;
for (int i = 0; i < MultiShotCount; i++)
{
float offset = -(MultiShotCount - 1) * 0.5f * MultiShotAngle + i * MultiShotAngle;
float shotAngle = baseAngle + offset;
Vector2 dir = Quaternion.Euler(0f, 0f, shotAngle) * Vector2.right;
ProjectileManager.Instance.SpawnProjectile(
ProjectileData,
spawnPos,
dir,
totalatk_OwnerAndWeapon
);
}
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (data == null) return;
// 무기 위치 기준으로 attackRange 원형 표시
Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f); // 주황 반투명
Gizmos.DrawWireSphere(transform.position, data.attackRange);
}
#endif
}