Unity 팀프 21.05.27 조작 - 3인칭 조이스틱
결론부터 말하자면 캐릭터의 이동은 조이스틱으로 조작하고 카메라의 시야는 화면 드래그로 조작한다.
카메라는 언제나 캐릭터를 쫓아다니며 캐릭터를 조이스틱으로 전진(상단)시키면 언제나 카메라의 정면으로 향한다.

후술하겠지만 주의해야 할 점으로는 빈 오브젝트 아래에 카메라를 넣어두어야한다.
그렇지 않는다면 카메라의 위치가 RotateAround메서드로 인해 변하는 Position과 캐릭터를 쫓아가도록 코딩한 target.position + offset이 겹쳐서 결국 카메라가 캐릭터를 쫓아가지 않게 되거나 이상한 움직임을 보이게 된다.





핵심은 RotateAround를 사용해서 카메라가 캐릭터 주위의 궤도를 돌도록했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
public class InGame : MonoBehaviourPun, IPunObservable
{
public Transform spawnPoint;
public List<Transform> spawnPoints;
public CameraController camController;
bool s1 = true;
GameObject character;
Vector3 pos;
[HideInInspector]
public int idx;
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
stream.SendNext(this.character.transform.position);
pos = (Vector3)stream.ReceiveNext();
}
private void Awake()
{
PhotonNetwork.SendRate = 60;
PhotonNetwork.SerializationRate = 30;
}
void Start()
{
Debug.Log(PhotonNetwork.AutomaticallySyncScene);
idx = 0;
Init();
}
void Update()
{
if (Input.GetKey(KeyCode.W))
{
this.character.transform.Translate(0, 0, 0.1f);
}
if (Input.GetKey(KeyCode.S))
{
this.character.transform.Translate(0, 0, -0.1f);
}
if (Input.GetKey(KeyCode.A))
{
this.character.transform.Translate(-0.1f, 0, 0);
}
if (Input.GetKey(KeyCode.D))
{
this.character.transform.Translate(0.1f, 0, 0);
}
}
void Init()
{
SpawnCharacter();
}
void SpawnCharacter()
{
foreach (var player in PhotonNetwork.PlayerList)
{
if (player.NickName == PhotonNetwork.NickName)
{
break;
}
idx++;
}
this.character = PhotonNetwork.Instantiate("TestPlayer", spawnPoint.position, Quaternion.identity);
this.character.name = this.character.name + idx.ToString();
this.character.GetComponent<PlayerController>().Init();
camController.Init(idx.ToString(), spawnPoint.position);
}
}
1.우선 InGame씬으로 넘어오자마자 InGame오브젝트에 할당된 InGame스크립트에서 서버에 동기화 시키는 프레임을 설정하고 IPunObservable의 인터페이스 메서드를 통해 움직이는 좌표를 보내고, 받아서 화면에 동기화시킬 수 있도록 했다.
2.그리고 Init()을 통해 플레이어 리스트의 인덱스를 통해 얻은 상수로 캐릭터를 포톤네트워크에 생성하면서 그 위치는 4개의 스폰위치 중 해당 인덱스 값을 가진 위치에 생성하도록 했다. *ex) idx = 3, 스폰위치 = spawnPoints[3]이 된다.
3.그리고 해당 캐릭터를 구분하기위해 이름에 인덱스를 더해주고 (*ex) 이름이 A이고 idx = 0이라면 이름이 A0로 바뀐다.)
4.이름까지 바뀌었다면, 해당 캐릭터프리팹에 할당되어 있던 PlayerController를 GetComponent해주며 바로 Init()한다.
5.마지막으로 camController스크립트가 할당된 Panel을 Init(인덱스의 string값, 스폰위치)를 하며 InGame은 끝이난다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
public class PlayerController : MonoBehaviourPun
{
public Transform camera;
public Rigidbody rigid;
[SerializeField]
float speed;
VariableJoystick joystick;
private void Start()
{
speed = 3f;
}
public void Init()
{
var cameraGo = GameObject.Find("Main Camera");
camera = cameraGo.GetComponent<Transform>();
var joystickGo = GameObject.Find("Variable Joystick");
joystick = joystickGo.GetComponent<VariableJoystick>();
}
private void FixedUpdate()
{
Vector3 joystickDir = Vector3.forward * joystick.Vertical + Vector3.right * joystick.Horizontal;
if (joystickDir == Vector3.zero) return;
Vector3 playerAngle = Quaternion.LookRotation(joystickDir).eulerAngles;
Vector3 camPivotAngle = camera.eulerAngles;
Vector3 towardAngle = Vector3.up * (playerAngle.y + camPivotAngle.y);
rigid.rotation = Quaternion.Euler(towardAngle);
rigid.transform.Translate(Vector3.forward * Time.deltaTime * speed);
}
}
1.다음으로는 PlayerController스크립트에서 Init이 됨과 동시에 이름으로 Camera와 Joystick을 찾는다. 그리고 해당 오브젝트들의 Transform컴포넌트 또는 조이스틱 컴포넌트를 사용할 수 있도록 GetComponent한다.
2.조이스틱 조작을 통해 내가 보는 화면에서 언제나 조이스틱 조작방향으로 이동할 수 있도록 코드를 짰다.
3.진행 방향은 (조이스틱의 수평값, 0, 조이스틱의 수직값)이고, 진행 방향이 없을 때(조작을 하고있지 않을 때)는 return시켜서 이동시키거나 회전시키지 않는다.
ㄴy가 0인 이유는 Vector3.forward와 right 둘 모두 y값이 0이기 때문이다.
4.플레이어의 회전각(rotation 또는 eulerAngles) 은 Quaternion.LookRotation(진행방향).eulerAngles이며, 이는 진행방향을 바라보는 플레이어의 eulerAngles값을 반환한다.
5.카메라의 오일러각을 camPivotAngle이라는 변수에 저장해준다.
6. 4와 5를 더하여 플레이어가 바라보는 방향을 정해준다.
ㄴ이게 4+5가 플레이어가 바라보는 방향인 이유는 4는 플레이어가 바라보는 방향 5는 카메라의 회전값이기 때문이다.
ㄴ만약 캐릭터가 카메라가 x+방향을 바라보고 있고(0,90,0) 조이스틱을 3시(90도)방향으로 움직인다면 캐릭터는 z-방향(0,180,0)으로 바라보게 된다. (90 + 90 = 180)
7.구한 towardAngle을 Rigidbody의 회전각에 할당하고(Quaternion.Euler를 사용한 이유는 짐벌락을 방지하기 위해서.) 단순 테스트용으로 만들었으므로 transform.Translate(Vector.forward) 해준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using Photon.Pun;
public class CameraController : MonoBehaviourPun, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
public Transform camFollow;
public Transform camera;
public GameObject target;
[SerializeField]
float speedHorizontal = 100;
[SerializeField]
float speedVertical = 40;
[SerializeField]
Vector3 offset;
[SerializeField]
float angleUp;
[SerializeField]
float angleDown;
Vector2 lastPosition;
void Start()
{
speedHorizontal = 100f;
speedVertical = 40f;
offset = new Vector3(0, 1, -5);
angleUp = 20f;
angleDown = -10f;
}
public void Init(string idx, Vector3 spawnPos)
{
Debug.Log("Init");
target = GameObject.Find("TestPlayer(Clone)" + idx);
camFollow.position = target.transform.position + offset;
}
void Update()
{
camFollow.position = target.transform.position + offset;
Debug.LogFormat("camera : {0} / target : {1}", camera.position, target.transform.position);
var angle = camera.transform.eulerAngles;
if(angleUp < angle.x && angle.x < 180)
{
camera.transform.eulerAngles = new Vector3(angleUp, angle.y, angle.z);
}
else if(angle.x < angleDown + 360 && angle.x > 180)
{
camera.transform.eulerAngles = new Vector3(angleDown, angle.y, angle.z);
}
}
public void OnDrag(PointerEventData eventData)
{
var targetDir = (eventData.position - lastPosition).normalized;
if (targetDir.x != 0)
{
camera.RotateAround(target.transform.position, Vector3.up, targetDir.x * speedHorizontal * Time.deltaTime);
}
if (targetDir.y != 0)
{
camera.Rotate(-targetDir.y * Time.deltaTime * speedVertical, 0, 0);
}
lastPosition = eventData.position;
}
public void OnPointerDown(PointerEventData eventData)
{
lastPosition = eventData.position;
}
public void OnPointerUp(PointerEventData eventData)
{
lastPosition = Vector2.zero;
}
}
1.마찬가지로 타겟을 이름으로 찾아서 Camera의 Parent인 CamFollow를 피벗으로 할당해준다.
2.드래그를 통해 값을 받아야 하므로 드래그핸들러, 포인터다운, 포인터업 핸들러를 모두 부착한다.
3.화면을 터치하는 순간 해당 위치를 기억하도록 lastPosition변수에 저장해준다.
4.드래그를 할 때는 (마우스의 현재위치 - 가장 최근위치)값을 노멀벡터화 시켜서 방향벡터로 만든다.
5.방향벡터의 x값이 0이 아니라면 target.transform.position을 중심점으로 두고 Vector3.up(0,1,0)을 축으로 삼아 회전하도록 했다.
ㄴtargetDir.x의 값이 음수 또는 양수가 나올 수 있으므로 그대로 할당하면 화면을 왼쪽으로 드래그 시(x<0) 왼쪽(-)으로 회전하고 오른쪽으로 드래그 시(x>0) 오른쪽(+)으로 회전한다.
6.카메라의 위 아래 시점같은 경우는 굳이 축을 정해서 궤도처럼 돌릴 필요를 못느꼈다. 따라서 Rotate함수를 사용햇으며, 화면이 하늘을 바라보기 위해선 x축의 rotation값이 작아져야 하므로 -를 붙여줌으로써 값의 부호를 반전시켜주었다.
ㄴ반전시켜준다면 화면을 위쪽으로 드래그할 때(y>0) rotation.x에는 (-)값이 더해지므로 하늘을 볼 수 있게된다. 반대의 경우도 마찬가지.
7.터치를 끝내는 순간 최근 터치위치(lastPosition)을 초기화시켜준다.