From 8c45104f18f021a2847523115b1b75316112c7fd Mon Sep 17 00:00:00 2001 From: username Date: Thu, 1 Jul 2021 22:18:17 +0900 Subject: [PATCH] commit --- ContinuousMovement.cs | 83 +++++++++++++++++++++++++ FadeTeleportationProvider.cs | 80 ++++++++++++++++++++++++ LocomotionController.cs | 48 +++++++++++++++ ObjectActivator.cs | 43 +++++++++++++ SceneLoad.cs | 17 ++++++ SocketTarget.cs | 50 +++++++++++++++ TwoHandGrabInteractable.cs | 108 +++++++++++++++++++++++++++++++++ XRExclusiveSocketInteractor.cs | 30 +++++++++ XROffsetGrabInteractable.cs | 44 ++++++++++++++ 9 files changed, 503 insertions(+) create mode 100644 ContinuousMovement.cs create mode 100644 FadeTeleportationProvider.cs create mode 100644 LocomotionController.cs create mode 100644 ObjectActivator.cs create mode 100644 SceneLoad.cs create mode 100644 SocketTarget.cs create mode 100644 TwoHandGrabInteractable.cs create mode 100644 XRExclusiveSocketInteractor.cs create mode 100644 XROffsetGrabInteractable.cs diff --git a/ContinuousMovement.cs b/ContinuousMovement.cs new file mode 100644 index 0000000..4d4223f --- /dev/null +++ b/ContinuousMovement.cs @@ -0,0 +1,83 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR; +using UnityEngine.XR.Interaction.Toolkit; + +public class ContinuousMovement : MonoBehaviour +{ + public float gravity = -9.81f; //중력 상수 + public float speed = 1f; + public float additionalHeight = 0.2f; //눈~정수리 위까지의 추가 키 + public XRNode inputSource; //인풋을 가져올 XRNode => Left Hand로 설정 + public LayerMask groundLayer; //히트 체크할 땅의 레이어 + + private float fallingSpeed; //떨어지는 속도 + private Vector2 inputAxis; //조이스틱 인풋값 + private CharacterController character; //이동용 컴포넌트 + private XRRig rig; //카메라 바라보는 방향 가져오기 위해 레퍼런스 가져옴 + + + // Start is called before the first frame update + void Start() + { + character = GetComponent(); + rig = GetComponent(); + } + + // Update is called once per frame + void Update() + { + //XRNode로 device가져와서 인풋값 받기 + InputDevice device = InputDevices.GetDeviceAtXRNode(inputSource); + device.TryGetFeatureValue(CommonUsages.primary2DAxis, out inputAxis); + } + + //CharacterController는 물리적으로 반응하며 움직이기 떄문에 FixedUpdate에서 처리해줘야 함 + private void FixedUpdate() //0.02 * 50 = 1 + { + CapsuleFollowHeadset(); + + //※사원수를 쓰는 이유 : https://hoodymong.tistory.com/3 + //카메라의 y 앵글값(=Yaw)을 사원수(Quaternion)으로 변환 + Quaternion headYaw = Quaternion.Euler(0f, rig.cameraGameObject.transform.eulerAngles.y, 0f); + + //Quaternion x Vector3 하면 회전 + Vector3 direction = headYaw * new Vector3(inputAxis.x, 0f, inputAxis.y); + + character.Move(direction * Time.fixedDeltaTime * speed); + + //중력 적용 : 발이 땅에 붙어있지 않을 때만 하강 속도가 증가하도록 + bool isGrounded = CheckIfGrounded(); + + if (isGrounded) + fallingSpeed = 0f; + else + fallingSpeed += gravity * Time.fixedDeltaTime; + + character.Move(Vector3.up * fallingSpeed * Time.fixedDeltaTime); + } + + //발이 땅에 붙었는지?를 리턴하는 함수 + private bool CheckIfGrounded() + { + Vector3 rayStart = transform.TransformPoint(character.center); //CharacterController의 중심점을 월드 스페이스 변환 + float rayLength = character.center.y + 0.01f; //광선 길이는 CC의 키/2 보다 살짝만 더 길게해서 발 밑을 확실히 체크하도록 해줌 + bool hasHit = Physics.SphereCast(rayStart, character.radius, Vector3.down, out RaycastHit hitInfo, rayLength, groundLayer); + + return hasHit; + } + + //캐릭터 컨트롤러의 캡슐이 내 머리 위치 따라오도록 설정 + void CapsuleFollowHeadset() + { + //카메라의 위치를 자기 자신 기준 local 좌표로 보정하여 저장 + Vector3 capsuleCenter = transform.InverseTransformPoint(rig.cameraGameObject.transform.position); + + //Capsule의 높이를 rig의 높이 + 약간의 여유값에 맞춰줌 + character.height = rig.cameraInRigSpaceHeight + additionalHeight; + + //수평(x, z) 좌표는 capsuleCenter를 따라가고, y는 character.height의 절반으로 해서 발을 땅에 붙이도록 해줌 + character.center = new Vector3(capsuleCenter.x, character.height / 2f, capsuleCenter.z); + } +} diff --git a/FadeTeleportationProvider.cs b/FadeTeleportationProvider.cs new file mode 100644 index 0000000..d356c31 --- /dev/null +++ b/FadeTeleportationProvider.cs @@ -0,0 +1,80 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.XR.Interaction.Toolkit; +using DG.Tweening; //DOTween 네임스페이스 +using UnityEngine.Assertions; + +public class FadeTeleportationProvider : TeleportationProvider +{ + public Image fadeScreen; + public Color fadeColor = Color.black; + public float fadeDuration = 0.5f; + + // Start is called before the first frame update + void Start() + { + startLocomotion += FadeTeleportationProvider_startLocomotion; + endLocomotion += FadeTeleportationProvider_endLocomotion; + fadeScreen.color = fadeColor; + } + + private void FadeTeleportationProvider_startLocomotion(LocomotionSystem obj) + { + print("Start Locomotion"); + //페이드아웃이 끝나면 Teleport 함수를 실행해라 + fadeScreen.DOFade(1f, fadeDuration).OnComplete(Teleport); + } + + private void FadeTeleportationProvider_endLocomotion(LocomotionSystem obj) + { + print("End Locomotion"); + fadeScreen.DOFade(0f, fadeDuration); + } + + protected override void Update() + { + //텔레포트 요청 + if (!validRequest || !BeginLocomotion()) + return; + } + + void Teleport() + { + print("Begin TeleporT!!!"); + + //실제 텔레포트 하는 부분 + var xrRig = system.xrRig; + if (xrRig != null) + { + switch (currentRequest.matchOrientation) + { + case MatchOrientation.WorldSpaceUp: + xrRig.MatchRigUp(Vector3.up); + break; + case MatchOrientation.TargetUp: + xrRig.MatchRigUp(currentRequest.destinationRotation * Vector3.up); + break; + case MatchOrientation.TargetUpAndForward: + xrRig.MatchRigUpCameraForward(currentRequest.destinationRotation * Vector3.up, currentRequest.destinationRotation * Vector3.forward); + break; + case MatchOrientation.None: + // Change nothing. Maintain current rig rotation. + break; + default: + Assert.IsTrue(false, $"Unhandled {nameof(MatchOrientation)}={currentRequest.matchOrientation}."); + break; + } + + var heightAdjustment = xrRig.rig.transform.up * xrRig.cameraInRigSpaceHeight; + + var cameraDestination = currentRequest.destinationPosition + heightAdjustment; + + xrRig.MoveCameraToWorldLocation(cameraDestination); + } + + EndLocomotion(); + validRequest = false; + } +} diff --git a/LocomotionController.cs b/LocomotionController.cs new file mode 100644 index 0000000..4de9609 --- /dev/null +++ b/LocomotionController.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.Interaction.Toolkit; + +public class LocomotionController : MonoBehaviour +{ + public XRController leftTeleportRay; + public XRController rightTeleportRay; + public InputHelpers.Button teleportActivationButton; //텔레포트 활성화 버튼 + public float activationThreshold = 0.1f; //살짝만 누르면 텔레포트 활성화 되도록 + + //각 손의 텔레포트 활성화 할것인가? => UnityEvent 에서 설정 + public bool EnableLeftTeleport { get; set; } = true; + public bool EnableRightTeleport { get; set; } = true; + + public XRRayInteractor leftRayInteractor; + public XRRayInteractor rightRayInteractor; + + // Update is called once per frame + void Update() + { + //각 컨트롤러의 텔레포트를 활성화/비활성화 + if (leftTeleportRay) + { + //InteractorRay가 무엇에 닿았는가? + bool isLeftInteractorRayHovering = leftRayInteractor.TryGetHitInfo(out Vector3 pos, out Vector3 norm, out int posInLine, out bool isValid); + leftTeleportRay.gameObject.SetActive(CheckIfActivated(leftTeleportRay) && EnableLeftTeleport && !isLeftInteractorRayHovering); + } + + + if (rightTeleportRay) + { + bool isRightInteractorRayHovering = rightRayInteractor.TryGetHitInfo(out Vector3 pos, out Vector3 norm, out int posInLine, out bool isValid); + rightTeleportRay.gameObject.SetActive(CheckIfActivated(rightTeleportRay) && EnableRightTeleport && !isRightInteractorRayHovering); + } + + } + + //특정 컨트롤러의 특정 버튼이 눌렀는가? 를 리턴하는 함수 + public bool CheckIfActivated(XRController controller) + { + //controller의 활성화 버튼을 activationThreshold 이상으로 눌렀으면, 그 값을 저장하고 리턴 + InputHelpers.IsPressed(controller.inputDevice, teleportActivationButton, out bool isActivated, activationThreshold); + + return isActivated; + } +} diff --git a/ObjectActivator.cs b/ObjectActivator.cs new file mode 100644 index 0000000..b3b8fcf --- /dev/null +++ b/ObjectActivator.cs @@ -0,0 +1,43 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +public class ObjectActivator : MonoBehaviour +{ + public bool Toggle; + public UnityEvent OnActivated; + public UnityEvent OnDeactivated; + public AudioClip audioClip; + public AudioSource audioSource; + + bool m_Activated = false; + + public void Activated() + { + if (Toggle) + { + if (m_Activated) + OnDeactivated.Invoke(); + else + OnActivated.Invoke(); + m_Activated = !m_Activated; + audioSource.PlayOneShot(audioClip); + } + else + { + OnActivated.Invoke(); + m_Activated = true; + + } + } + + public void Deactivated() + { + if (!Toggle) + { + OnDeactivated.Invoke(); + m_Activated = false; + } + } +} diff --git a/SceneLoad.cs b/SceneLoad.cs new file mode 100644 index 0000000..471b9ae --- /dev/null +++ b/SceneLoad.cs @@ -0,0 +1,17 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.SceneManagement; + +public class SceneLoad : MonoBehaviour +{ + public void ChangeScene(string sceneName) + { + SceneManager.LoadScene(sceneName); + } + + public void OnApplicationQuit() + { + Application.Quit(); + } +} diff --git a/SocketTarget.cs b/SocketTarget.cs new file mode 100644 index 0000000..5fc8ae7 --- /dev/null +++ b/SocketTarget.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.Interaction.Toolkit; + + +/// +/// Special script that work with the XRExclusiveSocket script. This allow to define a SocketType and if that SocketType +/// does not match the XRExclusiveSocket SocketType, this won't be accepted by the socket as a valid target +/// +[RequireComponent(typeof(XRBaseInteractable))] +public class SocketTarget : MonoBehaviour +{ + public string SocketType; + public XRInteractableEvent SocketedEvent; + public bool DisableSocketOnSocketed; + + void Awake() + { + var interactable = GetComponent(); + + interactable.onSelectEntered.AddListener(SelectedSwitch); + } + + public void SelectedSwitch(XRBaseInteractor interactor) + { + var socketInteractor = interactor as XRExclusiveSocketInteractor; + + if(socketInteractor == null) + return; + + if(SocketType != socketInteractor.AcceptedType) + return; + + if (DisableSocketOnSocketed) + { + //TODO : find a better way, delay feel very wrong + StartCoroutine(DisableSocketDelayed(socketInteractor)); + } + + SocketedEvent.Invoke(interactor); + } + + IEnumerator DisableSocketDelayed(XRExclusiveSocketInteractor interactor) + { + yield return new WaitForSeconds(0.5f); + interactor.socketActive = false; + } +} diff --git a/TwoHandGrabInteractable.cs b/TwoHandGrabInteractable.cs new file mode 100644 index 0000000..62ac4ed --- /dev/null +++ b/TwoHandGrabInteractable.cs @@ -0,0 +1,108 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.Interaction.Toolkit; + +public class TwoHandGrabInteractable : XRGrabInteractable +{ + public List secondHandGrabPoints = new List(); //두번째 손 잡이용 XRSimpleInteractable들 + private XRBaseInteractor secondInteractor; //두 번째 손의 Interactor + private Quaternion attachInitialRotation; //첫 번째 손의 원래 회전값 + + public enum TwoHandRotationType { None, First, Second } //어느 손의 기준으로 총을 회전할 것인가? + public TwoHandRotationType twoHandRotationType; + + private Quaternion initialRotationOffset; //두번째 손으로 잡을 때, 회전각의 차이값을 저장 + + void Start() + { + //두 번째 손을 잡았을 때 / 손았을 때 실행할 리스너 함수 등록 + foreach (var item in secondHandGrabPoints) + { + item.onSelectEntered.AddListener(OnSecondHandGrab); + item.onSelectExited.AddListener(OnSecondHandRelease); + } + } + + //사물을 잡고 있는 동안 계쏙 실행 + public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) + { + //양 손으로 잡고 있을 때만 회전값을 계산하도록 + if(selectingInteractor && secondInteractor) + { + //회전값을 계산 + //오브젝트의 Pivot중심점이 아닌, 첫 번째 손을 잡은 AttachTransform을 기준으로 회전하기 위해 잡은 손의 attachTransform의 회전각을 변경해줌 + selectingInteractor.attachTransform.rotation = GetTwoHandRotation() * initialRotationOffset; + } + + base.ProcessInteractable(updatePhase); + } + + + private Quaternion GetTwoHandRotation() + { + Quaternion targetRotation; + + switch (twoHandRotationType) + { + default: + case TwoHandRotationType.None: + //회전하지 않음 + targetRotation = Quaternion.LookRotation(secondInteractor.transform.position - selectingInteractor.transform.position); + break; + case TwoHandRotationType.First: + targetRotation = Quaternion.LookRotation(secondInteractor.transform.position - selectingInteractor.transform.position, selectingInteractor.attachTransform.up); + break; + case TwoHandRotationType.Second: + targetRotation = Quaternion.LookRotation(secondInteractor.transform.position - selectingInteractor.transform.position, secondInteractor.attachTransform.up); + break; + } + + return targetRotation; + } + + public override bool IsSelectableBy(XRBaseInteractor interactor) + { + //이미 다른 Interactor가 잡고 있는가? + //현재 잡고 있는 Interactor가 있고, 그게 지금 새로 잡으려는 interactor가 아닌 경우에만 true + bool isAlreadyGrabbed = selectingInteractor && !interactor.Equals(selectingInteractor); + + //'다른 Interactor'가 잡고 있지 않을 때'라는 조건 추가 + return base.IsSelectableBy(interactor) && !isAlreadyGrabbed; + } + + public void OnSecondHandGrab(XRBaseInteractor interactor) + { + print("Second Hand Grab"); + secondInteractor = interactor; //두번째 Interactor 설정 + + //Quaternion의 회전값의 차이를 구하기 위해, 한 Quaternion의 Inverse값에 다른 Quaternion을 곱해줌 + initialRotationOffset = Quaternion.Inverse(GetTwoHandRotation()) * selectingInteractor.attachTransform.rotation; + } + + public void OnSecondHandRelease(XRBaseInteractor interactor) + { + print("Second Hand Release"); + secondInteractor = null; //두번쨰 Interactor 비워줌 + } + + protected override void OnSelectEntered(XRBaseInteractor interactor) + { + base.OnSelectEntered(interactor); + + print("First Hand Enter"); + + attachInitialRotation = interactor.attachTransform.localRotation; //첫 번째 손의 원래 회전값 저장 + } + + protected override void OnSelectExited(XRBaseInteractor interactor) + { + base.OnSelectExited(interactor); + + print("First Hand Exit"); + + secondInteractor = null; //첫번째 손을 놓으면, 두번째 손도 놓게 + + interactor.attachTransform.localRotation = attachInitialRotation; //첫 번째 손의 회전값 리셋 + } +} diff --git a/XRExclusiveSocketInteractor.cs b/XRExclusiveSocketInteractor.cs new file mode 100644 index 0000000..c696d9a --- /dev/null +++ b/XRExclusiveSocketInteractor.cs @@ -0,0 +1,30 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.Interaction.Toolkit; + +public class XRExclusiveSocketInteractor : XRSocketInteractor +{ + public string AcceptedType; //SocketTarget의 SocketType과 비교할 값 + + + public override bool CanSelect(XRBaseInteractable interactable) + { + //이 소켓과 인터렉션하려는 사물의 SocketTarget을 가져오고 + SocketTarget socketTarget = interactable.GetComponent(); + + //SocketTarget이 없으면 선택 불가 + if (socketTarget == null) + return false; + + //baseClass에서도 선택할 수 있고, SocketTarget의 타입도 같으면 선택 가능하게 + return base.CanSelect(interactable) && (socketTarget.SocketType == AcceptedType); + } + + //선택 가능한 사물만이 Hover 가능하도록 + public override bool CanHover(XRBaseInteractable interactable) + { + return CanSelect(interactable); + } + +} diff --git a/XROffsetGrabInteractable.cs b/XROffsetGrabInteractable.cs new file mode 100644 index 0000000..c411a59 --- /dev/null +++ b/XROffsetGrabInteractable.cs @@ -0,0 +1,44 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.XR.Interaction.Toolkit; + +public class XROffsetGrabInteractable : XRGrabInteractable +{ + //Pivot을 리셋시키기 위해 원래 로컬 위치, 회전 저장 + private Vector3 initialAttachLocalPos; + private Quaternion initialAttachLocalRot; + + protected virtual void Start() + { + //attachTransform 이 없다면 만들어준다. + if(!attachTransform) + { + GameObject pivot = new GameObject("Attach Pivot"); //피봇용 빈 게임오브젝트를 만들어서 + pivot.transform.SetParent(transform, false); //자신의 자식으로 넣고, 위치를 0, 0, 0 으로 + attachTransform = pivot.transform; + } + + initialAttachLocalPos = attachTransform.localPosition; + initialAttachLocalRot = attachTransform.localRotation; + } + + //잡는 순간 실행되는 함수 오버라이드 + protected override void OnSelectEntering(XRBaseInteractor interactor) + { + //직접 손으로 잡으면 Offset Grabbing + if(interactor is XRDirectInteractor) + { + attachTransform.position = interactor.attachTransform.position; + attachTransform.rotation = interactor.attachTransform.rotation; + } + else + { + //그 외에는 Pivot값을 원래대로 리셋시켜줌 + attachTransform.localPosition = initialAttachLocalPos; + attachTransform.localRotation = initialAttachLocalRot; + } + + base.OnSelectEntering(interactor); + } +}