오늘은 unRailed의 캐릭터의 상호작용을 만들어볼예정이다.
일단 캐릭터 이동부분은 간단하게 만들었다.
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
public float speed = 6.0f;
public float jumpSpeed = 8.0f;
public float gravity = 20.0f;
private Vector3 moveDirection = Vector3.zero;
private CharacterController controller;
void Start()
{
controller = GetComponent<CharacterController>();
}
void Update()
{
if (controller.isGrounded)
{
// 플레이어가 땅에 있을 때 이동 방향 설정
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
moveDirection = new Vector3(moveHorizontal, 0.0f, moveVertical);
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= speed;
}
// 중력 적용
moveDirection.y -= gravity * Time.deltaTime;
// 이동 적용
controller.Move(moveDirection * Time.deltaTime);
}
}
이렇게 설정하고 나서

캐릭터 컨트롤러라는 컴포넌트를 이용해서 움직인다.
스크립트
using UnityEngine;
public class PlayerInteraction : MonoBehaviour
{
public float interactionRange = 2.0f; // 상호작용 범위 (전방)
public float closeRange = 1.0f; // 가까운 범위 탐지 구 반지름
public Transform holdPoint; // 들기 위한 지점(Transform)
public LayerMask interactableLayer; // 상호작용 가능한 레이어 마스크
public Material highlightMaterial; // 하이라이트할 때 사용할 재질
private IInteractable heldObject = null; // 현재 들고 있는 객체
private IHighlightable highlightedObject = null; // 현재 하이라이트된 객체
void Update()
{
HighlightInteractable(); // 상호작용 가능한 객체 하이라이트 처리
HandleInput(); // 입력 처리 (Space 키)
}
private void HandleInput()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (heldObject == null)
{
TryPickup(); // 들기 시도
}
else
{
TryPlaceOrDrop(); // 놓기 또는 드롭 시도
}
}
}
private void TryPickup()
{
Collider[] hitColliders = Physics.OverlapSphere(transform.position, closeRange, interactableLayer); // 발밑의 가까운 범위에 있는 Collider 배열 가져오기
foreach (Collider hitCollider in hitColliders)
{
IInteractable interactable = hitCollider.GetComponent<IInteractable>(); // 상호작용 가능한 객체 가져오기
if (interactable != null && IsFacing(hitCollider.gameObject)) // 방향이 맞고 상호작용 가능한 객체인 경우
{
heldObject = interactable; // 객체를 들고
heldObject.Pickup(holdPoint); // 들기 처리
return;
}
}
Debug.Log("No interactable object in range."); // 범위 내에 상호작용 가능한 객체가 없는 경우 로그 출력
}
private bool IsWithinView(GameObject target)
{
Vector3 directionToTarget = (target.transform.position - transform.position).normalized; // 캐릭터에서 타겟까지의 방향 벡터
Vector3 forward = transform.forward; // 캐릭터의 전방 벡터
// y축을 무시한 방향 벡터 계산
forward.y = 0;
directionToTarget.y = 0;
// 벡터를 정규화하여 다시 계산
forward.Normalize();
directionToTarget.Normalize();
float angle = Vector3.Angle(forward, directionToTarget); // 전방과 타겟 사이의 각도 계산
return angle <= 30f; // 180도의 절반인 90도 이내인지 확인하여 반환
}
private void HighlightInteractable()
{
if (heldObject != null) return; // 들고 있는 객체가 있으면 반환
Collider[] hitColliders = Physics.OverlapSphere(transform.position, closeRange, interactableLayer); // 발밑의 가까운 범위에 있는 Collider 배열 가져오기
foreach (Collider hitCollider in hitColliders)
{
IHighlightable highlightable = hitCollider.GetComponent<IHighlightable>(); // 하이라이트 가능한 객체 가져오기
if (highlightable != null && IsWithinView(hitCollider.gameObject)) // 객체가 존재하고 캐릭터가 보고 있는 방향에 있는 경우
{
if (highlightedObject != null && highlightedObject != highlightable)
{
highlightedObject.ResetHighlight(); // 기존에 하이라이트된 객체가 있으면 리셋
}
highlightedObject = highlightable; // 현재 하이라이트된 객체 설정
highlightedObject.Highlight(); // 하이라이트 처리
return;
}
}
if (highlightedObject != null)
{
highlightedObject.ResetHighlight(); // 하이라이트된 객체가 있으면 리셋
highlightedObject = null;
}
}
private bool IsFacing(GameObject target)
{
Vector3 directionToTarget = (target.transform.position - transform.position).normalized; // 캐릭터에서 타겟까지의 방향 벡터
return Vector3.Dot(transform.forward, directionToTarget) > 0.5f; // 캐릭터의 전방 벡터와 타겟까지의 방향 벡터의 내적값이 0.5보다 큰지 반환
}
private void TryPlaceOrDrop()
{
if (heldObject.TryPlace())
{
heldObject = null; // 놓기 성공하면 들고 있는 객체 비우기
}
else
{
heldObject.Drop(); // 놓기 실패하면 객체 드롭
heldObject = null;
}
}
private void OnDrawGizmos()
{
// 가까운 범위 탐지 구를 그립니다.
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position, closeRange);
// 전방 상호작용 범위를 그립니다.
Gizmos.color = Color.red;
Vector3 forward = transform.forward * interactionRange;
Gizmos.DrawLine(transform.position, transform.position + forward);
// 상호작용 각도를 그립니다.
Gizmos.color = Color.green;
// y축을 0으로 설정하여 평면에서만 각도 계산
Vector3 forwardFlat = new Vector3(transform.forward.x, 0, transform.forward.z).normalized;
// 30도 각도로 변경
float angle = 30f;
Vector3 rightBoundary = Quaternion.Euler(0, angle, 0) * forwardFlat * interactionRange;
Vector3 leftBoundary = Quaternion.Euler(0, -angle, 0) * forwardFlat * interactionRange;
Gizmos.DrawLine(transform.position, transform.position + rightBoundary);
Gizmos.DrawLine(transform.position, transform.position + leftBoundary);
// 상호작용 각도 안의 객체를 시각적으로 표시
Collider[] hitColliders = Physics.OverlapSphere(transform.position, closeRange, interactableLayer);
foreach (Collider hitCollider in hitColliders)
{
Vector3 directionToTarget = (hitCollider.transform.position - transform.position).normalized;
directionToTarget.y = 0; // y축을 무시한 방향 벡터
float targetAngle = Vector3.Angle(forwardFlat, directionToTarget);
if (targetAngle <= angle) // 설정된 각도 내에 있는지 확인
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, hitCollider.transform.position);
}
}
}
}
레이캐스트를 이용해 앞의 물체를 감지하고 스페이스바를 이용해 지정해둔 트랜스폼으로 오브젝트가 들어오게 된다.
using UnityEngine;
public class Pickupable : MonoBehaviour, IInteractable
{
private Transform originalParent;
public void Pickup(Transform holdPoint)
{
originalParent = transform.parent;
transform.parent = holdPoint;
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
}
public void Drop()
{
transform.parent = originalParent;
}
}
Pickup 메서드:
Pickup(Transform holdPoint): 객체를 집을 때 호출됩니다. 인자로 받은 holdPoint에 해당 객체를 부모로 설정하여 집은 위치로 이동시킵니다. 이 때 객체의 로컬 위치와 회전을 초기화하여 정확히 holdPoint 위치에 배치됩니다.
Drop 메서드:
Drop(): 객체를 놓을 때 호출됩니다. originalParent를 통해 이전에 객체가 속했던 부모를 찾아 그 아래로 다시 배치합니다.
using UnityEngine;
public class Highlightable : MonoBehaviour, IHighlightable
{
private Renderer RenDerer;
private Material originalMaterial;
public Material highlightMaterial;
void Start()
{
RenDerer = GetComponent<Renderer>();
originalMaterial = RenDerer.material;
}
public void Highlight()
{
RenDerer.material = highlightMaterial;
}
public void ResetHighlight()
{
RenDerer.material = originalMaterial;
}
}
Start 메서드:
Start(): 객체가 활성화될 때 호출되며, Renderer 컴포넌트를 가져와 originalMaterial에 원래의 재질을 저장합니다.
Highlight 메서드:
Highlight(): 객체를 강조 표시할 때 호출됩니다. highlightMaterial을 객체의 Renderer에 적용하여 객체를 강조 표시합니다.
ResetHighlight 메서드:
ResetHighlight(): 객체의 강조 표시를 원래 상태로 되돌릴 때 호출됩니다. originalMaterial을 객체의 Renderer에 적용하여 객체의 재질을 원래의 상태로 복구합니다.
이렇게 설정하고 밑의 두개의 스크립트는 상호작용할 물건에 넣의면되고 PlayerInteraction은 플레이어에 넣게 되면 상호작용이 가능하다.