무기 충돌 예시
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponController : MonoBehaviour, IObservable<GameObject>
{
[Serializable]
public class WeaponTriggerZone
{
public Vector3 position;
public float radius;
}
[SerializeField] private WeaponTriggerZone[] _triggerZones;
public int AttackPower => attackPower;
[SerializeField] private int attackPower;
[SerializeField] private LayerMask targetLayerMask;
private List<IObserver<GameObject>> _observers = new List<IObserver<GameObject>>();
// ---
// 충돌 처리
private Vector3[] _previousPositions;
private HashSet<Collider> _hitColliders;
private Ray _ray = new Ray();
private RaycastHit[] _hits = new RaycastHit[10];
private bool _isAttacking = false;
private void Start()
{
_previousPositions = new Vector3[_triggerZones.Length];
_hitColliders = new HashSet<Collider>();
}
public void AttackStart()
{
_isAttacking = true;
_hitColliders.Clear();
for (int i = 0; i < _triggerZones.Length; i++)
{
_previousPositions[i] = transform.position + transform.TransformVector(_triggerZones[i].position);
}
}
public void AttackEnd()
{
_isAttacking = false;
}
private void FixedUpdate()
{
if (_isAttacking)
{
for (int i = 0; i < _triggerZones.Length; i++)
{
var worldPosition = transform.position +
transform.TransformVector(_triggerZones[i].position);
var direction = worldPosition - _previousPositions[i];
_ray.origin = _previousPositions[i];
_ray.direction = direction;
var hitCount = Physics.SphereCastNonAlloc(_ray,
_triggerZones[i].radius, _hits,
direction.magnitude, targetLayerMask,
QueryTriggerInteraction.UseGlobal);
for (int j = 0; j < hitCount; j++)
{
var hit = _hits[j];
if (!_hitColliders.Contains(hit.collider))
{
// Time.timeScale = 0f;
// StartCoroutine(ResumeTimeScale());
_hitColliders.Add(hit.collider);
Notify(hit.collider.gameObject);
}
}
_previousPositions[i] = worldPosition;
}
}
}
private IEnumerator ResumeTimeScale()
{
yield return new WaitForSecondsRealtime(10f);
Time.timeScale = 1f;
}
public void Subscribe(IObserver<GameObject> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
}
public void Unsubscribe(IObserver<GameObject> observer)
{
_observers.Remove(observer);
}
public void Notify(GameObject value)
{
foreach (var observer in _observers)
{
observer.OnNext(value);
}
}
private void OnDestroy()
{
var copyObservers = new List<IObserver<GameObject>>(_observers);
foreach (var observer in copyObservers)
{
observer.OnCompleted();
}
_observers.Clear();
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (_isAttacking)
{
for (int i = 0; i < _triggerZones.Length; i++)
{
var worldPosition = transform.position +
transform.TransformVector(_triggerZones[i].position);
var direction = worldPosition - _previousPositions[i];
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(worldPosition, _triggerZones[i].radius);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(worldPosition + direction, _triggerZones[i].radius);
}
}
else
{
foreach (var triggerZone in _triggerZones)
{
Gizmos.color = Color.green;
Gizmos.DrawSphere(triggerZone.position, triggerZone.radius);
}
}
}
#endif
}
여기서 주의해서 봐야할 점은
private void FixedUpdate()
{
if (_isAttacking)
{
for (int i = 0; i < _triggerZones.Length; i++)
{
var worldPosition = transform.position +
transform.TransformVector(_triggerZones[i].position);
var direction = worldPosition - _previousPositions[i];
_ray.origin = _previousPositions[i];
_ray.direction = direction;
var hitCount = Physics.SphereCastNonAlloc(_ray,
_triggerZones[i].radius, _hits,
direction.magnitude, targetLayerMask,
QueryTriggerInteraction.UseGlobal);
for (int j = 0; j < hitCount; j++)
{
var hit = _hits[j];
if (!_hitColliders.Contains(hit.collider))
{
// Time.timeScale = 0f;
// StartCoroutine(ResumeTimeScale());
_hitColliders.Add(hit.collider);
Notify(hit.collider.gameObject);
}
}
_previousPositions[i] = worldPosition;
}
}
}
이 부분이다, 전의 포지션 정보와, 지금의 포지션 정보를 잇는 레이를 만들어 충돌이 발생한다면 충돌이 발생했다고
맞은 지점의 collider의 GameObject에게 알린다.
transform.TransfomrVector 함수가 작동하는 방식은 이렇다.
1. _triggerZones[i].position은 자식 오브젝트 (triggerZone)의 로컬 포지션이다. 무기 오브젝트 기준으로 상대적인 오프셋을 나타낸다.
2. transform.TransformVector() 함수는transform을 기준으로 입력 벡터에 자신의(transform)의 회전과 Scale만을 사용한 결과를 반환한다.
3. 이를 바탕으로 transform의 크기나 회전 값이 바뀌어도, _triggerZones[i].position이 부모 오브젝트의 회전과 스케일에 맞춰 월드 기준에서의 방향과 크기를 갖게 된다.
만일 이런 방법을 사용하지 않는다면, triggerZone의 방향값이나 스케일은 부모 오브젝트에 따라 바뀌지 않으므로 트리거 존들이 항상 무기에 상대적으로 올바른 위치에 존재하지 않게된다.
질문 : transform.TransformVector() 에 관하여
처음에 빠르게 transform.TransformVector() 함수를 찾아봤을 때 다음과 같은 설명이 나왔다.
TransformVector()는 **로컬 좌표계(Local Space)**의 벡터를
**월드 좌표계(World Space)**로 변환하는 함수입니다.
회전과 스케일만을 적용하고 position을 무시한 방향 벡터로 변환하는 함수라고 한다.
그런데 이해가 안갔다, position을 무시한다면 모든 triggerZone의 위치들이 통일되고 다른 회전값만 지니게 되는 줄 알았다.
그래서 계속 물어봤는데 GPT와 제미나이, 클로드 모두 내 핀트를 못잡아줘서 한 삼십분 헤맸다.
결론은 TransformVector()는 부모 오브젝트의 회전과 스케일을 고려하여 월드 좌표계에서의 해당 벡터를 계산해주는 함수다. 그러니까 매개변수로 들어간 오브젝트의 벡터를 (종속되어 있는) 부모 오브젝트 기준으로 수정하여 반환하고, 이를 바탕으로 월드 포지션을 구할 수 있는 함수다.
매우 빠르거나 얇은 오브젝트가 다른 오브젝트와 충돌 판정을 제대로 거치지 않고 통과하는 현상인 터널링 현상 (Tunneling Effect ) 을 방지하기 위한 방식이다. 이 현상이 발생하는 이유는 게임 엔진은 일반적으로 매 프레임마다 오브젝트의 위치를 업데이트하고, 그 위치를 기준으로 충돌 여부를 검사한다. 만일 오브젝트의 이동 속도가 너무 빠르거나 충돌체의 크기가 작거나 얇다면
한 프레임에서 다음 프레임으로 이동하는 사이에 충돌체가 서로 "건너뛰어" 버릴 수 있다.
때문에, 이를 방지하기 위해서 다양한 방법을 사용할 수 있고 위 방법은 그 중 하나의 방법이다.
다른 방법들은 ?
1. Rigidbody의 Collsion Detection을 설정해서 할 수 있다.
2. Fixed Timestep 조절
연산 주기를 촘촘하게 바꿔 충돌 누락 방지가 가능하다.
3. Collider 크기 키우기
'시행착오' 카테고리의 다른 글
네트워크 관련 ( 포톤 퓨전 , HasAuthority ) (0) | 2025.05.04 |
---|---|
구면 좌표계로 구현하는 카메라 / Skin Width 개념 (0) | 2025.04.05 |
Unity < - > Node.js Socket.IO 데이터 교환 (0) | 2025.03.18 |
02.25 회고 + Scroll View 응용 (0) | 2025.02.26 |
Scroll View With ObjectPool (0) | 2025.02.25 |