1. Grip Objects
[참고] https://circuitstream.com/blog/oculus-unity-setup/
[참고] https://www.youtube.com/watch?v=sxvKGVDmYfY
oculus의 컨트롤러를 이용하여 물건을 잡아본다.유니티에서 oculus 개발 설정을 한다.( 앞부분 참조)
Scene에 평면을 만들고 2개의 구와 1개의 6면체를 아래와 같이 만든다.
바닥의 평면에는 mesh collider를 설치한다. 2개의 구에는 각각 Sphere Collider와 Rigidbody를 설치한다. Box 6면체에도 Box Collider와 Rigidbody를 설치한다. 모두 gravity를 채크하여 중력의 영향을 받도록 하자. 이들 3개의 객체가 컨트롤러에 의해 Grab이 되기 위해서는 OvrGrabbale 스크립트(Integration Package) 컴포넌트를 붙여준다.
Main Camera를 삭제하거나 비활성화를 하고 OvrPlayerController 프리팹을 Scene에 배치한다.
OvrPlayerController에는 OverCameraRig 카메라가 부착되어 있으며 사용자가 HMD를 쓰고 이동하면 같이 연동되어 이동된다. OverCameraRig/TrackingSpace 자식으로 LocalAvatar 프리팹을 붙여준다. Controller모양을 보여줄 수 있다
LeftHandAnchor와 RightHandAnchor에 각각 Rigidbody, Sphere Collider 및 OverGrabber(스크립트)를 추가한다.
설정은 아래 그림과 같이 한다. isTrigger를 채크한다. 채크를 안하면 물체를 잡으려고 할때 충돌하여 팅겨 나간다.
LocalAvatar의 기본 설정을 사용한다.
실행하고 테스트한다. 왼쪽 컨트롤러는 앞뒤로 이동이 가능하며 우측 컨트롤러는 좌우측 회전이 가능하다. 양쪽 2개 컨트롤러 모두 객체를 잡아서 던질 수 있다.
2. Button Select (Laser Point Select)
화면에 버튼이 여러개 있고 이를 컨트롤러에 의해 선택하여 보자. 위의 실습 결과에 계속 이어서 진행하여 보자
OVRCameraRig에 OVRPhysicsRaycaster 스크립트를 붙여준다.
UIHelper 프리팹을 검색하여 Hierarchy에 Drag하여 끌어다 놓는다.
UIHelper > EventSystem 에 OVRInputModule 스크립트 추가하고 Ray Transform대상을 RightHandAnchor로 설정하여 준다.
Canvas를 추가하고 Render Mode를 World Space로 변경한다. 위치를 적절히 배치한다. Layer를 UI 에서 Default로 변경한다. Event Camera를 현재사용하는 CenterEyeAnchor로 설정한다. OVRRayCaster 스크립트를 추가하여 준다.
Canvas에 버튼을 여러개 추가한다. 버튼의 기본색상을 변경하여 주자.
UIHelper > LaserPointer 의 Line Renderer를 채크하여준다.
프로그램을 테스트하여보자.
3. Console for Debugging
프로그램을 개발하다 보면 프로그램 실행정보(변수값이나 이벤트 정보 등)를 표시하고 학인할 필요가 있다. HMD 화면에 콘솔 디스플레이를 만들고 정보를 표현하여 보자. 여기에서는 Canvas에 Text를 보여주는 방식으로 구현한다.
Cavas를 추가한다. 이름을 CanvasConsole이라고 변경하였다. 그 자식으로 Text (UI)를 추가하였다. Canvas의 Render Mode는 Screen Space -Camera로 설정하였다. RenderCamera는 CenterEyeAnchor로 설정하였다. Text UI는 가로세로 크기 조정과 위치를 수정한다.
CanvasConsole 객체에 스크립트 CanvasConsole을 만들어 붙였다.
화면에 가장 최근 20개의 메시지만 출력하도록 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Collections;
public class CanvasConsole : MonoBehaviour
{
// Start is called before the first frame update
ArrayList board = new ArrayList();
GameObject textObj;
//int Count = 0;
void Start()
{
}
public void Log(string msg)
{
board.Add(msg);
while (board.Count > 20)
{
board.RemoveAt(0);//delete oldest one!!
}
if (textObj == null) textObj = GameObject.Find("CanvasConsole/Text");
if (textObj == null) return;
string consoleMsgs = "";
for (int i=0;i< board.Count; i++)
{
consoleMsgs = consoleMsgs + board[i] + "\r\n";
}
textObj.GetComponent<UnityEngine.UI.Text>().text = consoleMsgs;
}
// Update is called once per frame
void Update()
{
//test code
//Count = (Count + 1)%100;
//Log("Count=" + Count);
}
}
Empty GameObject를 추가하고 이름을 MainGameObject 변경한다.
이곳에 스크립트를 붙여 콘솔객체에 테스트 메시지를 출력하여본다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainGameObject : MonoBehaviour
{
// Start is called before the first frame update
int Count = 0;
GameObject consoleObj;
void Start()
{
}
// Update is called once per frame
void Update()
{
//test code
Count = (Count + 1)%100;
if(consoleObj==null) consoleObj= GameObject.Find("CanvasConsole");
if (consoleObj == null) return;
consoleObj.GetComponent<CanvasConsole>().Log("Message from MainGameobject Count=" + Count);
}
}
4. Custom hands
컨트롤러를 들고 있는 손이 아닌 아무것도 들고 있지 않은 손을 표현하여 보자. 컨트롤러 대신에 다른 사물을 잡고 있어야 할 때 유용하게 사용될 수 있다.
제공하는 CustomHands 프리팹을 검색하고 OVRPlayerController >OVRCameraRig > TrackingSpace > LeftHandAnchor >CustomHandLeft 설치, 오른손은 OVRPlayerController >OVRCameraRig > TrackingSpace > RightHandAnchor >CustomHandRight 설치를 위하여 드래그한다.
CustomHandLeft/CustomHandRight에는 Inspector창에서 Parent Transform에 TrackingSpace 를 설정한다. 왼쪽 손에는 L Touch를 오른손에는 R Touch를 설치한다. OverGrabber이 설치되어 있으므로 물건을 잡을 수 있다. CustomHand를 이용하여 물건을 잡을 수 있다. 테스트를 하여본다.
제공하는 CustomHand 대신에 다른 메쉬를 사용하여 보자. 예를 들어 권총 등의 매쉬를 컨트롤러 대신에 사용할 수 있다. CustomHandRight을 비활성화하고 적절한 모델을 구하여 설치하여 보자. 여기서는 Gun을 설치한다. Gun에는 Rigidbody, BoxCollider, Hand(Script), OVRGrabber(Script)를 설치한다. 즉 다양한 3D모델을 컨트롤러 모습 대신으로 사용할 수 있다. 또는 제공하는 핸드와 모델 Gun을 동시에 보여줄 수 있는 방법으로 제작할 수도 있다. (핸드의 위치와 애니메이션과 어울리도록 하는 작업이 어려움)
5. Controller 입력받기
사용하는 컨트롤러로 부터 입력을 받아보도록 하자.
// returns true if the primary button (typically "A") is currently pressed.
OVRInput.Get(OVRInput.Button.One);
// returns true if the primary button (typically "A") was pressed this frame.
OVRInput.GetDown(OVRInput.Button.One);
// returns true if the "X" button was released this frame.
OVRInput.GetUp(OVRInput.RawButton.X);
// returns a Vector2 of the primary (typically the Left) thumbstick's current state.
// (X/Y range of -1.0f to 1.0f)
OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick);
// returns true if the primary thumbstick is currently pressed (clicked as a button)
OVRInput.Get(OVRInput.Button.PrimaryThumbstick);
// returns true if the primary thumbstick has been moved upwards more than halfway.
// (Up/Down/Left/Right - Interpret the thumbstick as a D-pad).
OVRInput.Get(OVRInput.Button.PrimaryThumbstickUp);
// returns a float of the secondary (typically the Right) index finger trigger's current state.
// (range of 0.0f to 1.0f)
OVRInput.Get(OVRInput.Axis1D.SecondaryIndexTrigger);
// returns a float of the left index finger trigger's current state.
// (range of 0.0f to 1.0f)
OVRInput.Get(OVRInput.RawAxis1D.LIndexTrigger);
// returns true if the left index finger trigger has been pressed more than halfway.
// (Interpret the trigger as a button).
OVRInput.Get(OVRInput.RawButton.LIndexTrigger);
// returns true if the secondary gamepad button, typically "B", is currently touched by the user.
OVRInput.Get(OVRInput.Touch.Two);
기존 MainGameObject의 스크립트를 수정한다. 만일 없다면 Empty GameObject를 추가하고 이름을 MainGameObject 변경한다.
이곳에 스크립트(MainGameObject)를 붙인다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
public class MainGameObject : MonoBehaviour
{
// Start is called before the first frame update
GameObject consoleObj;
public GameObject gun;//인스펙터 창에서 설정
Animation gunAnimation;
string deviceInfo;
void Start()
{
if (gun != null)
{
//gunAnimation = gun.GetComponent<Animation>();
//gunAnimation.clip.wrapMode = WrapMode.Once;
}
List<InputDevice> devices = new List<InputDevice>();
InputDeviceCharacteristics rc = InputDeviceCharacteristics.Right | InputDeviceCharacteristics.Controller;
InputDevices.GetDevicesWithCharacteristics(rc, devices);
deviceInfo = "";
foreach (var item in devices)
{
deviceInfo = deviceInfo + item.name + " " + item.characteristics + "\r\n";
}
}
// Update is called once per frame
void Update()
{
if(consoleObj==null) consoleObj= GameObject.Find("CanvasConsole");
if (consoleObj == null) return;
float val = OVRInput.Get(OVRInput.Axis1D.SecondaryIndexTrigger);
consoleObj.GetComponent<CanvasConsole>().Log("OVRInput.Axis1D.SecondaryIndexTrigger val=" + val);
if(val>0.7f)
{
consoleObj.GetComponent<CanvasConsole>().Log(deviceInfo);
OVRInput.SetControllerVibration(0.1f/*frequency*/, 0.1f/*amplitude*/, OVRInput.Controller.RTouch);
//gunAnimation.Play();
}
else {
//gunAnimation.Stop();
}
}
}
6. Shooting Simulation
[참고] https://www.youtube.com/watch?v=I0gDcSphoMw
간단한 슈팅기능을 구현하여 보자. 가급적 기존 Scene을 그대로 활용한다. 권총의 총구 앞부분에 empty gameObject를 만든다. z축이 총알이 나가는 방향으로 설정한다.
총알을 구를 이용하여 적정크기로 만든다. 이를 Prefab으로 만들어 bullet이란 이름으로 저장한다. 총알에는 Bullet이라는 스크립트를 만들어 준다. 일정기간이 지나거나 뭔가에 충돌되면 총알을 삭제한다.
public class Bullet : MonoBehaviour
{
// Start is called before the first frame update
float endPlayTime = 10.0f;
bool isPlaying = false;
void Start()
{
endPlayTime = Time.time + 5.0f;
}
// Update is called once per frame
void Update()
{
if (isPlaying && Time.time > endPlayTime)
{
isPlaying = false;
Destroy(transform.gameObject);
}
}
void OnCollisionEnter(Collision collision)
{
if (collision.transform.tag == "target")
{
//ToDo:
}
Destroy(transform.gameObject);
}
}
MainGameObject 객체에 AudioSource 컴포넌트를 추가하고 AudioClip에 총소리 wave를 설정하자.
스크립트를 아래와 같이 만들어 보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
public class MainGameObject : MonoBehaviour
{
public Transform muzzle;
public Rigidbody bullet;
GameObject consoleObj;
AudioSource audioSource;
Animation gunAnimation;
string deviceInfo;
float endPlayTime = 0.5f;
bool isPlaying = false;
bool keyPressed = false;
void Start()
{
//if (gun != null)
//{
//gunAnimation = gun.GetComponent<Animation>();
//gunAnimation.clip.wrapMode = WrapMode.Once;
//}
List<InputDevice> devices = new List<InputDevice>();
InputDeviceCharacteristics rc = InputDeviceCharacteristics.Right | InputDeviceCharacteristics.Controller;
InputDevices.GetDevicesWithCharacteristics(rc, devices);
deviceInfo = "";
foreach (var item in devices)
{ //테스트코드(무시)
deviceInfo = deviceInfo + item.name + " " + item.characteristics + "\r\n";
}
audioSource = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
if(consoleObj==null) consoleObj= GameObject.Find("CanvasConsole");
if (consoleObj == null) return;
float val = OVRInput.Get(OVRInput.Axis1D.SecondaryIndexTrigger);
consoleObj.GetComponent<CanvasConsole>().Log("OVRInput.Axis1D.SecondaryIndexTrigger val=" + val);
if (Input.GetMouseButton(0)) keyPressed = true;//마우스로 테스트
if (val>0.7f && isPlaying==false)
{
consoleObj.GetComponent<CanvasConsole>().Log(deviceInfo);
Rigidbody bb = (Rigidbody)Instantiate(bullet, muzzle.transform.position, muzzle.transform.rotation);
bb.velocity = muzzle.TransformDirection(new Vector3(0, 0, 20.0f));
endPlayTime = Time.time + 0.5f;//객체 소거시간 설정
isPlaying = true;
keyPressed = false;
//gunAnimation.Play();
PlayShoot(true);
}
else {
//gunAnimation.Stop();
}
}
IEnumerator Haptics(float frequency, float amplitude, float duration, bool rightHand, bool leftHand)
{
if (rightHand) OVRInput.SetControllerVibration(frequency, amplitude, OVRInput.Controller.RTouch);
if (leftHand) OVRInput.SetControllerVibration(frequency, amplitude, OVRInput.Controller.LTouch);
yield return new WaitForSeconds(duration);
if (rightHand) OVRInput.SetControllerVibration(0, 0, OVRInput.Controller.RTouch);
if (leftHand) OVRInput.SetControllerVibration(0, 0, OVRInput.Controller.LTouch);
isPlaying = false;
}
public void PlayShoot(bool rightHanded)
{
audioSource.Play();
if (rightHanded) StartCoroutine(Haptics(1, 1, 0.3f, true, false));
else StartCoroutine(Haptics(1, 1, 0.3f, false, true));
}
}
인스펙터 창에서 Muzzle과 Bullet을 설정한다. AudioClip도 설정한다
실행하고 테스트하여 보자.
7. Line Renderer
MainGameObject 에 Line Renderer를 부착하고 이를 이용하여 빔을 생성하여보자. 총구가 겨누는 위치에 Collider가 부착된 객체(moon)가 있으면 빔을 보여줄것이다. 빔을 받은 객체 moon은 자전을 더 빨리하도록 하여본다.
moon에는 다음의 스크립트를 붙여준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Moon : MonoBehaviour
{
float rot = 1.0f;
void Start()
{
}
void Update()
{
transform.Rotate(0, rot, 0);
}
public void OnRaycastReceived(float para01)
{
StartCoroutine( RaycastReceivedAction(para01) );
}
IEnumerator RaycastReceivedAction(float frequency)
{
rot = frequency;
yield return new WaitForSeconds(1.3f);
rot = 1.0f;
}
}
MainGameObject에 다음의 스크립트를 추가로 붙여준다.
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System;
public class MyLaserPointer : MonoBehaviour
{
float maxLength = 10.0f;
public Transform startTransform;
Vector3 endPosition;
LineRenderer lineRenderer;
GameObject consoleObj;
void Start()
{
lineRenderer = GetComponent<LineRenderer>();
}
void Update()
{
if (consoleObj == null) consoleObj = GameObject.Find("CanvasConsole");
if (consoleObj == null) return;
RaycastHit hit;
Ray ray = new Ray(startTransform.position, startTransform.forward);
Physics.Raycast(ray, out hit, maxLength);
if (hit.collider)
{
consoleObj.GetComponent<CanvasConsole>().Log("Raycast hit=" + hit.transform.tag);
endPosition = hit.point;
lineRenderer.SetPosition(0, startTransform.position);
lineRenderer.SetPosition(1, endPosition);
lineRenderer.enabled = true;
//hit.collider.gameObject.SendMessage("OnRaycastReceived",1.0f, SendMessageOptions.DontRequireReceiver);
if (hit.transform.tag == "moon")
{
consoleObj.GetComponent<CanvasConsole>().Log("hit.transform.tag is moon");
hit.collider.gameObject.GetComponent<Moon>().OnRaycastReceived(3.0f);
}
}
else lineRenderer.enabled=false;
//_endPoint = startTransform.position + startTransform.forward * hitDistance;
}
}