
오전에는 기획분들이 사용하기 위한 플레이어 프리팹을 제작하는 것을 목표로 했고, 오후에는 무기 프리팹을 캐릭터 애니메이션에 연동시켜 공격 모션과 파지 모션을 구현했다. 내일 아바타 마스크를 활용한 모션 확장을 진행하려했는데, 목요일까지 튜토리얼과 레벨 1을 구현하는 기획안이 나와 일정을 맞추기 위해 우선 순위를 바꿔야할 것 같다.
오늘은 공유한 코드는 문 열기 상호작용 기능으로 신경 쓴 것은 3가지이다.
1. 문의 회전방향 및 종류 고려(오른쪽으로 열리는지, 왼쪽으로 열리는지)
2. 문이 열리는 것을 코루틴으로 하여 문이 실제로 열리는 듯한 느낌을 주도록 고려
3. 플레이어가 문에 맞는 열쇠를 가지고 있어야 열 수 있도록 구현
Key열쇠는 Scriptable Object로 구현했다. 상호작용에 사용되는 실질적인 필드는 KeyID지만, 인벤토리에 들어가는 기타 아이템이기에 Icon과 Description을 갖도록 구성했다.
public class Key : ScriptableObject
{
public string KeyId;
public Sprite Icon;
public string Description;
}
PlayerInteract플레이어의 상호작용 기능을 담당하는 메서드이다. 상태 패턴으로 확장한다면 PlayerInteractHandler의 형식으로 행동 로직 담당 메서드가 참조하도록 할 수 있다.
public class PlayerInteract : MonoBehaviour
{
// 상호작용 가능 거리
[SerializeField] private float _interactDistance;
// 상호작용 키 설정
[SerializeField] private KeyCode _interactKey;
// 상호작용 여부를 나타내는 플래그
private bool _isInteracted;
// 상호작용 가능 거리에서 상호작용 키를 눌렀을 때 호출되는 메서드
public void Interact(List<Key> playerKeys)
{
// 상호작용이 가능하면(상호작용 중이 아니면)
if (!_isInteracted)
{
// 현재 위치에서 전방으로 Ray 발사
Vector3 startPos = transform.position;
Vector3 direction = transform.forward;
// Ray에 닿은 오브젝트가 있는 경우
if (Physics.Raycast(startPos, direction, out RaycastHit hit, _interactDistance))
{
// 해당 오브젝트의 Layer가 Door에 해당하면
if (hit.collider.gameObject.layer == 7)
{
// Door를 받아서 Toggle 메서드 호출
Door door = hit.collider.GetComponent<Door>();
door.Toggle(playerKeys);
}
}
}
}
}
Door우선 DoorType을 구분하기 위해 enum 설정을 하였다. Slide를 넣기는 했지만 실제 구현은 하지 않아서 추후에 관련 로직이 추가된다면 글을 수정할 예정이다. 위에서 말한 고려사항이 모두 Door 스크립트에 있어서 이를 참고해서 보면 좋을 것 같다.
public enum DoorType
{
RotateRight, RotateLeft, Slide
}
public class Door : MonoBehaviour
{
// 문의 종류
[SerializeField] private DoorType _doorType;
// 문이 열리는 각도(90도)
[SerializeField] private float _openAngle;
// 문이 다 열리는데 걸리는 시간
[SerializeField] private float _duration;
// 문이 열려있을 때 회전 값 캐싱
private Quaternion _openedRotation;
// 문이 닫혀있을 때 회전 값 캐싱
private Quaternion _closedRotation;
// 문에 맞는 열쇠
private Key _key;
// 문이 열려있는지를 캐싱하는 플래그
private bool _isOpen;
// 문이 회전하고 있는 중인지 캐싱하는 플래그
private bool _isOnRotated;
// 내부 필드 초기화는 Awake에서
private void Awake()
{
Init();
}
// 실제 외부(PlayerInteract)에서 호출되는 상호작용 메서드
public void Toggle(List<Key> playerKeys)
{
// 열리고 있는 중이면 중복 회전 방지를 위해 스킵
if (_isOnRotated) return;
// 닫혀 있다면
if (!_isOpen)
{
// 문을 여는 메서드 호출
TryOpen(playerKeys);
}
// 열려 있다면
else
{
// 문을 닫는 메서드 호출
Close();
}
}
// 문을 여는 메서드
private void TryOpen(List<Key> playerKeys)
{
// 플레이어가 가지고 있는 키 중에 문에 맞는 열쇠가 있다면
if (playerKeys.Any(k=>k.KeyId == _key.KeyId))
{
// 상태를 회전하고 있는 중으로 바꾸고
_isOnRotated = true;
// 문의 종류에 따라 메서드 호출 결정
switch (_doorType)
{
case DoorType.RotateRight:
RotateDoor();
break;
case DoorType.RotateLeft:
RotateDoor();
break;
case DoorType.Slide:
SlideDoor();
break;
}
}
}
// 문을 닫는 메서드
private void Close()
{
// 상태를 회전하고 있는 중으로 바꾸고
_isOnRotated = true;
// 문의 종류에 따라 메서드 호출 결정
switch (_doorType)
{
case DoorType.RotateRight:
RotateDoor();
break;
case DoorType.RotateLeft:
RotateDoor();
break;
case DoorType.Slide:
SlideDoor();
break;
}
}
// 기본 타입의 문 상호작용 메서드
private void RotateDoor()
{
// 열려있는 경우 닫고, 닫혀있는 경우 열도록 구성
Quaternion rotation = _isOpen ? _closedRotation : _openedRotation;
// 문 회전 코루틴 호출
StartCoroutine(RotateRoutine(rotation));
}
private void SlideDoor()
{
// 슬라이드 타입 문의 상호작용 메서드
}
// 문을 서서히 회전시키는 코루틴
private IEnumerator RotateRoutine(Quaternion rotation)
{
// 현재 회전 값 캐싱
Quaternion startRotation = transform.rotation;
// timer 설정
float timer = 0f;
// 열리는데 걸리는 시간 동안
while (timer < _duration)
{
// 프레임마다 문을 회전
transform.rotation = Quaternion.Slerp(startRotation, rotation, timer / _duration);
yield return null;
}
// Slerp 보정
transform.rotation = rotation;
// 현재 문의 상태 isOpen에 캐싱
_isOpen = (rotation == _openedRotation);
// 회전하지 않는 중으로 변경
_isOnRotated = false;
}
// 문의 종류에 따른 회전값 초기화
private void Init()
{
// 닫혀있는 상태의 회전 값은 기본(현재) 회전값으로 가정
_closedRotation = transform.rotation;
// 열려있는 상태의 회전 값은 경첩의 위치로 결정
_openedRotation = _closedRotation *
Quaternion.Euler(0, _openAngle * (_doorType == DoorType.RotateRight ? -1 : 1), 0);
}
}