Unity/Project : Cursed Treasure

Unity 팀프 21.05.27 조작 - 3인칭 조이스틱

HappyFrog 2021. 5. 27. 23:53

결론부터 말하자면 캐릭터의 이동은 조이스틱으로 조작하고 카메라의 시야는 화면 드래그로 조작한다.

카메라는 언제나 캐릭터를 쫓아다니며 캐릭터를 조이스틱으로 전진(상단)시키면 언제나 카메라의 정면으로 향한다.

 

 

후술하겠지만 주의해야 할 점으로는 빈 오브젝트 아래에 카메라를 넣어두어야한다.

그렇지 않는다면 카메라의 위치가 RotateAround메서드로 인해 변하는 Position과 캐릭터를 쫓아가도록 코딩한 target.position + offset이 겹쳐서 결국 카메라가 캐릭터를 쫓아가지 않게 되거나 이상한 움직임을 보이게 된다.

배경이 까만 이유는 카메라의 BackGround설정을 SolidColor로 했기 때문.

 

카메라가 하늘을 향할수록 -값을 가지고

 

카메라가 아래를 볼수록 +값을 가진다.

 

 

핵심은 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)을 초기화시켜준다.