제가 잠깐 Unreal Engine 5를 사용할 때 그 엔진에서 SpringArm Camera Component를 제공하더군요.
https://docs.unrealengine.com/4.27/ko/InteractiveExperiences/UsingCameras/SpringArmComponents/
스프링 암 컴포넌트 사용법
Spring Arm 컴포넌트는 카메라 시야가 막히는 상황을 자동으로 처리하는 데 사용됩니다.
docs.unrealengine.com
SpringArm Camera는 카메라와 카메라가 비추는 플레이어 사이에 Object가 가려서 플레이어를 비추지 못할 때
카메라가 오브젝트가 없는 Position까지 움직이는 Camera 형태를 말합니다.
Unity에는 SprinArm Component를 기본적으로 제공하진 않지만
생각해보니 직접 구현해도 되겠다는 생각을 했습니다.
SpringArm은 다음을 요구합니다.
1. Object를 판별할 RayerMask와 Object를 인식하는 RayCast
2. Object를 인식했을 시 오브젝트 너머로 카메라를 움직이는 함수 (되도록 부드럽게)
먼저 1번은 unity에서 제공하는 Physics.RayCast와 RayerMask 변수를 사용하면 구현이 가능합니다.
RayCast의 효율적인 사용을 위해 먼저 Camera의 지름을 설정하는 편이 좋습니다.
왜냐하면 Camera의 지름보다 작은 오브젝트는 우리를 충분히 가리지 못하니 확대할 필요가 없으며
RayCast의 Ray radius가 작으면 작을수록 작업량이 급격하게 증가하기 때문입니다.
단 Camera에 Collider나 오브젝트를 붙이는 것이 아닌, 삼각함수를 이용하여 추상적으로
카메라의 지름을 구할 수 있습니다.
private float GetCollisionRadiusForCamera(Camera cam)
{
float halfFOV = (cam.fieldOfView / 2.0f) * Mathf.Deg2Rad; // 라디안 형태로 명시된 필드오브 뷰(FOV)를 Deg2Rad 각도로 변환
float nearClipPlaneHalfHeight = Mathf.Tan(halfFOV) * cam.nearClipPlane * CameraViewportExtentsMultipllier;
float nearClipPlaneHalfWidth = nearClipPlaneHalfHeight * cam.aspect;
float collisionRadius = new Vector2(nearClipPlaneHalfWidth, nearClipPlaneHalfHeight).magnitude;
return collisionRadius;
}
해당 함수는 카메라의 fieldOfView에 접근하여 카메라가 바라보고 있는 각도를 구합니다.
그리고 카메라의 화면 높이, 넓이를 구해줍니다.
카메라의 화면 높이와 넓이를 2차원 벡터로 선언하여 벡터의 크기(길이)를 구하면
카메라의 추상적인 지름을 구할 수 있습니다.
https://mean-dragon.tistory.com/45
카메라 화면 넓이, 높이 구하기
height = 2 * Camera.orthographicSize; width = height * Camera.aspect; orthographicSize 이 값은 오쏘그래픽(orthographic)모드일 때 카메라 크기에 절반입니다. 이 값은 카메라 수직 크기의 절반입니다. 수평 화면 크기는
mean-dragon.tistory.com
private float GetDesiredTargetLength()
{
Ray ray = new Ray(transform.position, -transform.forward);
RaycastHit hit;
if (Physics.SphereCast(ray, Mathf.Max(0.001f, CollisionRadius), out hit, TargetLength, CollisionMask))
{
return hit.distance;
}
else
{
return TargetLength;
}
}
GetDesiredTargetLength 함수는 카메라 사이의 장애물을 감지하고 장애물의 거리를 Return하는 함수입니다.
만약 감지하지 않았다면, 기본 거리인 TargetLength를 Return합니다.
2번은 Camera의 부드러운움직임에 자주 사용되는 Vector3.SmoothDamp()를 사용합니다.
https://docs.unity3d.com/kr/530/ScriptReference/Vector3.SmoothDamp.html
Vector3-SmoothDamp - Unity 스크립팅 API
Gradually changes a vector towards a desired goal over time.
docs.unity3d.com
SmoothDamp 함수는 간단히 설명하면 목표 위치까지 부드럽게 이동 시켜주는 함수입니다. 이를 활용하여 오브젝트가 감지되었을 때 그 거리만큼 카메라를 이동시킬 수 있습니다.
private void UpdateLength()
{
float targetLength = GetDesiredTargetLength();
Vector3 newSocketLocalPosition = -Vector3.forward * targetLength;
CollisionSocket.localPosition = Vector3.SmoothDamp(
CollisionSocket.localPosition, newSocketLocalPosition, ref _socketVelocity, SpeedDamp);
}
그런데 SmoothDamp 함수는 깊게 파고들면 굉장히 어려운 함수입니다. Position 보간을 위한 지수 함수의 처리과정에서 테일러 급수를 사용하여 계산 효율을 높이는 SmoothDamp 함수에 대해서는 추후에 포스팅 하겠습니다.
마지막으로 Draw Gizmo를 활용하여
스크립트가 정상적으로 작동하는지 확인하면 완성입니다. 아래는 클래스 전체 코드입니다.
public class SpringArm : MonoBehaviour
{
public float TargetLength = 3.0f;
public float SpeedDamp = 0.0f;
public Transform CollisionSocket;
public float CollisionRadius = 0.25f;
public LayerMask CollisionMask = 0;
public Camera Camera;
public float CameraViewportExtentsMultipllier = 1.0f;
private Vector3 _socketVelocity;
private void LateUpdate()
{
if (Camera != null)
{
CollisionRadius = GetCollisionRadiusForCamera(Camera);
Camera.transform.localPosition = -Vector3.forward * Camera.nearClipPlane;
}
UpdateLength();
}
private float GetCollisionRadiusForCamera(Camera cam)
{
float halfFOV = (cam.fieldOfView / 2.0f) * Mathf.Deg2Rad; // vertical FOV in radians
float nearClipPlaneHalfHeight = Mathf.Tan(halfFOV) * cam.nearClipPlane * CameraViewportExtentsMultipllier;
float nearClipPlaneHalfWidth = nearClipPlaneHalfHeight * cam.aspect;
float collisionRadius = new Vector2(nearClipPlaneHalfWidth, nearClipPlaneHalfHeight).magnitude; // Pythagoras
return collisionRadius;
}
private float GetDesiredTargetLength()
{
Ray ray = new Ray(transform.position, -transform.forward);
RaycastHit hit;
if (Physics.SphereCast(ray, Mathf.Max(0.001f, CollisionRadius), out hit, TargetLength, CollisionMask))
{
return hit.distance;
}
else
{
return TargetLength;
}
}
private void UpdateLength()
{
float targetLength = GetDesiredTargetLength();
Vector3 newSocketLocalPosition = -Vector3.forward * targetLength;
CollisionSocket.localPosition = Vector3.SmoothDamp(
CollisionSocket.localPosition, newSocketLocalPosition, ref _socketVelocity, SpeedDamp);
}
private void OnDrawGizmos()
{
if (CollisionSocket != null)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(transform.position, CollisionSocket.transform.position);
DrawGizmoSphere(CollisionSocket.transform.position, CollisionRadius);
}
}
private void DrawGizmoSphere(Vector3 pos, float radius)
{
Quaternion rot = Quaternion.Euler(-90.0f, 0.0f, 0.0f);
int alphaSteps = 8;
int betaSteps = 16;
float deltaAlpha = Mathf.PI / alphaSteps;
float deltaBeta = 2.0f * Mathf.PI / betaSteps;
for (int a = 0; a < alphaSteps; a++)
{
for (int b = 0; b < betaSteps; b++)
{
float alpha = a * deltaAlpha;
float beta = b * deltaBeta;
Vector3 p1 = pos + rot * GetSphericalPoint(alpha, beta, radius);
Vector3 p2 = pos + rot * GetSphericalPoint(alpha + deltaAlpha, beta, radius);
Vector3 p3 = pos + rot * GetSphericalPoint(alpha + deltaAlpha, beta - deltaBeta, radius);
Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p2, p3);
}
}
}
private Vector3 GetSphericalPoint(float alpha, float beta, float radius)
{
Vector3 point;
point.x = radius * Mathf.Sin(alpha) * Mathf.Cos(beta);
point.y = radius * Mathf.Sin(alpha) * Mathf.Sin(beta);
point.z = radius * Mathf.Cos(alpha);
return point;
}
public Transform GetTransform()
{
return transform;
}
}
'개발 프로젝트 > Unity - WeaponGameProject' 카테고리의 다른 글
WeaponGameProject - Weapon 클래스 설계 (1) (SphereCast를 활용하는 감지 클래스) (1) | 2023.11.27 |
---|---|
WeaponGameProject - Player 클래스 설계 (3) (bool 타입을 통한 플레이어 상태 구현) (1) | 2023.11.26 |
WeaponGameProject - Player 클래스 설계 (1) (Key Input을 클래스로 만들자) (1) | 2023.11.23 |
WeaponGameProject - 목차 (0) | 2023.11.23 |
WeaponGameProject - 개발 계획서 (0) | 2023.06.29 |