레이블이 Bluetooth Controller인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Bluetooth Controller인 게시물을 표시합니다. 모든 게시물 표시

2017년 10월 25일 수요일

[VR] Unity : Bluetooth Controller를 사용하여 캐릭터 컨트롤하기

이번엔 지난 글과 저번 글의 내용을 합쳐서 Bluetooth Controller를 통해 1인칭 캐릭터를 컨트롤할 수 있는 VR앱을 만들 것이다.

배경까지 다 만들기는 귀찮으니까 Assets Store에서 괜찮은 꽁짜 프로젝트를 받아온다.
- 2018년 11월 현재 위 프로젝트는 서비스되지 않아서 받아올 수 없다. 다른 프로젝트를 사용해도 무방하니 적당한 것을 찾아보자


필요없는 부분은 다 지우고 먼저 1인칭 캐릭터 역할을 할 GameObject를 적당한 위치에 적당한 크기로 하나 만든다.



벽을(심지어 바닥도!) 뚫고 다니면 안 되니까 Capsule Collider를 추가하고 물리엔진과 상호작용(중력이라던가 다른 물체와 충돌이라던가)을 할 수 있도록 Rigidbody를 추가한다.
중요한게 Rigidbody에서 Constraints안에 Freeze Rotation의 모든 축을 체크해야 한다. 물리엔진에 의해 캐릭터가 회전하지 않도록 제약을 걸어 두는 것으로, 이거 안 해주면 Capsule Collider의 곡면 때문에 캐릭터가 자기 맘대로 막 굴러다닌다.

그 다음엔 캐릭터 GameObject의 하위 Object로 포함되도록 Camera를 추가한다.



위치는 적당히 잡아주면 된다.
단, 비대칭을 좋아하는 사람이 아니면 X와 Z 좌표는 0으로 해 두자.

이제 스크립트를 하나 추가한다. 이거 참고해서 작성했다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent (typeof (Rigidbody))]
[RequireComponent (typeof (CapsuleCollider))]

public class BlueControlManager : MonoBehaviour
{
  private GameObject cameraObject;
  private Rigidbody CharacterRigidbody;

  private bool jumpPushed = false;

  // contants
  private float speed = 2.2f;
  private float gravity = 10.0f;
  private bool grounded = false;

  private float maxVelocityChange = 1.5f;

  private float jumpHeight = 0.5f;
  private Vector3 jumpVelocity;

  void Start()
  {
  cameraObject = GameObject.Find("Camera");
  CharacterRigidbody = GetComponent<Rigidbody>();

  // From the jump height and gravity we deduce the upwards speed 
  // for the character to reach at the apex.
  jumpVelocity = new Vector3(0, Mathf.Sqrt(2 * jumpHeight * gravity), 0);
  }

  void Update()
  {
  if(Input.GetKeyDown(KeyCode.JoystickButton0))
  jumpPushed = true;
  }

  void FixedUpdate()
  {
  if(grounded)
  {
  // get joystick data
  Vector2 joystickPosition = new Vector2();
  joystickPosition.x = Input.GetAxis("Horizontal");
  joystickPosition.y = Input.GetAxis("Vertical");
  joystickPosition = joystickPosition.normalized;

  // Calculate how fast we should be moving
  Vector3 targetVelocity = new Vector3(0, 0, 0);
  Vector3 forward = new Vector3(0, 0, 0);
  Vector3 right = new Vector3(0, 0, 0);

  forward.x = cameraObject.transform.forward.x;
  forward.z = cameraObject.transform.forward.z;
  right.x = cameraObject.transform.right.x;
  right.z = cameraObject.transform.right.z;
  forward = forward.normalized;
  right = right.normalized;
  targetVelocity = forward * joystickPosition.y + right * joystickPosition.x;
  targetVelocity *= speed;

  // Apply a force that attempts to reach our target velocity
  Vector3 velocity = CharacterRigidbody.velocity;
  Vector3 velocityChange = (targetVelocity - velocity);
  velocityChange.x = Mathf.Clamp(velocityChange.x,
                -maxVelocityChange, maxVelocityChange);
  velocityChange.z = Mathf.Clamp(velocityChange.z,
                -maxVelocityChange, maxVelocityChange);
  velocityChange.y = 0;
  CharacterRigidbody.AddForce(velocityChange, ForceMode.VelocityChange);

  // Jump
  if(jumpPushed)
  CharacterRigidbody.AddForce(jumpVelocity, ForceMode.VelocityChange);
  }

  grounded = false;
  jumpPushed = false;
  }

  void OnCollisionStay(Collision collisionInfo)
  {
  foreach(ContactPoint contact in collisionInfo.contacts)
  {
  if(contact.normal.y > 0.7f)
  {
  grounded = true;
  break;
  }
  }
  }
}

주요 특징

1. UI 처리는 Update() 함수에서, 물리적 처리는 FixedUpdate() 함수에서 한다.
Update() 함수는 프레임마다 한 번 호출되며, 초당 프레임의 변화에 따라 호출 주기가 달라지는 반면 FixedUpdate() 함수는 호출 주기가 일정하다. Input.GetKeyDown() 함수는 버튼이 눌린 시점의 프레임에서만 true를 반환한다. 따라서 FixedUpdate() 함수에서 Input.GetKeyDown() 함수로 값을 읽어오는 경우 프레임 타이밍에 따라 버튼을 눌러도 true값을 읽지 못할 수도 있다. 반면에 물리적 처리는 호출 주기가 일정한 FixedUpdate() 함수에서 처리해야 자연스럽게 동작하며 그렇지 않고 Update() 함수에서 처리할 경우 초당 프레임 변화에 따라 다르게 동작할 것이다.

2. 캐릭터를 움직일 때는 rigidbody의 AddForce() 함수 사용
캐릭터를 움직이는 방법, 즉 캐릭터의 위치를 바꾸는 방법은 위치 자체를 바꾸는 방법, 속도를 바꾸는 방법, 힘(가속도)를 주는 방법의 세 가지가 있다. 위치 자체를 바꾸는 것은 캐릭터가 순간이동하는 것으로 구현될 것이며, 속도를 바꾸거나 힘을 주어야 위치가 연속적으로 변할 것이다. 힘을 주는 방법이 위치와 속도가 연속적으로 변하는 것이므로 가장 자연스럽게 동작할 것이다.

3.  grounded (캐릭터가 땅을 밟고 서 있는지) 판단 조건
캐릭터의 Collider가 다른 Collider와 접촉할 때 호출되는 OnCollisionStay() 함수에서 항상 grounded를 true로 바꾸도록 하면 벽과 접촉할 때에도 땅을 밟고 있다고 판단하여 벽타기가 가능하다.. 따라서 OnCollisionStay() 함수의 인자로 주어지는 Collision 정보 내에서 다른 Collider와 접촉한 방향이 옆쪽이 아닌 아래쪽 방향이 있는 경우에만 땅을 밟고 있다고 판단하게 한다.



플레이 영상!

2017년 10월 18일 수요일

[VR] Unity에서 Bluetooth Controller 제어

VR 앱을 개발하기 위해 아이페가 PG-9068 TOMAHAWK 모델의 블루투스 컨트롤러를 구입하였다.


집에 도착한 모습!



Xbox 컨트롤러와 유사한 구조를 갖고 있다.

Unity에서 컨트롤러의 입력값을 읽어오기 위해서는 각 버튼이 어떻게 매핑되어 있는지 알아야 한다.
직접 테스트해본 결과 다음 그림과 같이 매핑되어 있었다.


OS와 컨트롤러에 따라 매핑 정보는 달라질 수 있다. 따라서 출시되는 VR 앱에는 이를 잘 파악하여 동적으로 매핑 정보를 파악할 수 있도록 해야 한다.

그림에서 "JoystickButton*"로 매핑되는 버튼은 Digital 값이고, "*th axis"로 매핑되는 조이스틱이나 버튼은 Analog 값이다. Digital인가 Analog인가에 따라 Unity에서 값을 읽어오는 방식이 다르다.
(LT, RT 버튼은 Digital과 Analog 방식 모두 동작한다.)

먼저 Digital 값은 스크립트에서 다음과 같이 쉽게 읽어올 수 있다.

bool key_value = Input.GetKey(KeyCode.JoystickButton0);

Unity reference를 확인해 보면 KeyCode의 다양한 값을 볼 수 있다. 최대 8개까지의 조이스틱(컨트롤러)를 구분할 수 있으며 각 컨트롤러마다 최대 20개의 버튼을 구분할 수 있다.
이 값을 Input.GetKey() 함수의 인자로 주면 버튼 값을 True나 False로 돌려 준다.
물론 Input.GetKeyDown() 이나 Input.GetKeyUp() 등의 다른 함수도 사용할 수 있다.

Analog 값은 조금 복잡한데, 우선 스크립트에서 읽는 방법은 다음과 같다.
-1.0 ~ 1.0 범위의 값을 얻을 수 있다.

float key_value = Input.GetAxis("Horizontal");

Input.GetAxis() 함수를 사용하는데, 인자로 주는 string은 읽으려는 axis의 이름이다. Unity는 기본적으로 컨트롤러의 첫 번째 조이스틱에 "Horizontal", "Vertical" axis가 매핑되어 있다. 이러한 정보는 Edit -> Project Settings -> Input에서 설정 가능하다.


설정 창을 열게 되면 Inspector에 다음과 같이 InputManager가 보일 것이다.


다양한 Axis 이름들이 정의되어 있는데, 그 중에서 "Horizontal", "Vertical" axis가 미리 정의되어 있는 것을 볼 수 있다. 또 다른 Analog 입력을 추가하고 싶다면 InputManager의 가장 위에 있는 속성인 Size 값을 늘린 후에 추가된 항목에 값을 채워넣으면 된다.

여기서는 다음과 같이 오른쪽 조이스틱을 입력으로 추가했다.


각 속성에 대한 상세한 내용은 여기 참조

가장 위에 있는 Name 속성에 입력하는 string을 스크립트에서 Input.GetAxis() 등의 함수에 인자로 사용하여 해당 Analog 입력 값을 받아올 수 있다.
Axis 속성에는 앞서 언급했던, 버튼과 조이스틱이 매핑된 axis를 선택하면 된다. 오른쪽 조이스틱은 X 축이 3rd axis에, Y 축이 4th axis에 매핑되어 있다고 했으므로 그에 맞게 설정하였다.