[SKKU DT] 38일차 -유니티(Unity) 포톤 클라우드(Photon Cloud)로 멀티플레이 슈팅 게임 만들기2 -Cinemachine/카메라 설정/PhotonView/캐릭터 이동/총알 생성/이펙트 추가

2023. 12. 21. 17:58SKKU DT

728x90
반응형

Cinemachine 설치

 

 

Virtual Camera 생성

컴포넌트 안에 CinemachineVirtualCamera가 생성되어 있는 것을 볼 수 있다.

 

 

Follow와 Look At에 Hierarchy상의 Player 프리팹을 끌어다 놓는다.

 

 

밑에 Body와 Aim을 수정한다.

데드존을 0.2까지도 설정하면서 카메라의 움직임을 본다.

 

 

가운데 데드존에서 캐릭터가 움직이면 카메라가 가만히 있고 주변 소프트존에 캐릭터가 닿으면 부드럽게 카메라가 움직인다.(카메라 회전 스크립트가 들어가있으면 제대로 보여지지 않을 수 있음)

 

 


 

 

Player Component에 Photon View 컴포넌트 스크립트를 추가한다. 플레이어 데이터를 서로 송수신하는 중요한 컴포넌트이다. Synchronization에서 동기화 기능을 볼 수 있다.

  • Off: 동기화 필요없지만 RPC를 불러와야할때
  • Reliable Delta Compressed: 마지막 호출을 받았을 때 변경사항이 없다면 송신하지 않는다.
  • Unreliable: 송신한 패킷의 수신 여부를 확인하지 않는다.
  • Unreliable On Change: 두 번째와 세 번째를 섞은 느낌.

 

 

Transform(위치, 회전) 동기화를 위해 Photon Transform View 스크립트 컴포넌트 추가하면 바로 전에 추가했던 Photon View 스크립트 컴포넌트 Observed Components에 Player가 하나 생기는 것을 볼 수 있다.

 

 

플레이어의 Animator를 동기화 하기 위해서는 Photon Animator View 스크립트 컴포넌트도 추가해야 한다. 자동으로 Animator의 파라미터들이 불러와지는 것을 볼 수 있다. 각 파라미터를 RPC 호출로 불러오는 아래 두 트리거를 제외하고 Discrete으로 바꾼다. 트리거는 불러오는 방식이 다르다.

 

 


 

 

테스트 빌드 -창모드로 변경한다.

Fullscreen Mode - Windowed

Resizable Window 체크

 

 


 

 

Turn() 함수가 조금 이상해서 스크립트를 수정한다.

주석 처리된 부분은 이전에 썼던 회전이 이상한 코드이다.

//플레이어 카메라 회전
public float rotSpeed = 200f;
//회전값 변수
float mx, my = 0;
void Turn()
{
    /*
    ray = camera.ScreenPointToRay(Input.mousePosition);
    //Plane에 쏠건데 Plane에 맞는 부분
    float enter = 0f;
    plane.Raycast(ray, out enter);
    hitPoint = ray.GetPoint(enter);

    Vector3 lookDir = hitPoint - transform.position;//바라보는 방향 목적지 - 플레이어의 위치
    lookDir.y = 0.0f;//y축은 필요 없음
    transform.localRotation = Quaternion.LookRotation(lookDir);
    */

    //축 받아오기
    float horizontal = Input.GetAxis("Mouse X");
    //값 누적
    mx += horizontal * rotSpeed * Time.deltaTime;
    //x, y축 회전 Vector3에 유의해서 작성
    transform.eulerAngles = new Vector3(0, mx, 0);
}

 

Movement 전체 코드

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

public class Movement : MonoBehaviour
{
    CharacterController controller;
    private new Transform transform;
    Animator animator;//애니메이션 컨트롤러 가져오기
    private new Camera camera;

    //가상으로 만든 plane
    //Plane plane;
    //Ray ray;
    //Vector3 hitPoint;
    public float moveSpeed = 10f;

    //플레이어 카메라 회전
    public float rotSpeed = 200f;
    //회전값 변수
    float mx = 0;

    void Start()
    {
        //캐릭터에 포함된 컴포넌트 가져오기
        controller = GetComponent<CharacterController>();
        transform = GetComponent<Transform>();
        animator = GetComponent<Animator>();
        camera = Camera.main; //카메라는 Tag에 MainCamera로 설정되어 있는 것을 가져온다.
        //plane = new Plane(transform.up, transform.position); //노멀 방향은 위쪽으로, 플레이어의 위치에 가상 평면을 만든다.
    }
    void Update()
    {
        Move();
        Turn();
    }

    //함수 밖에 정의하기 위해서 "=>" 사용
    float h => Input.GetAxis("Horizontal");
    float v => Input.GetAxis("Vertical");

    void Move()
    {
        //카메라 설정
        Vector3 cameraForward = camera.transform.forward;
        Vector3 cameraRight = camera.transform.right;
        cameraForward.y = 0.0f; //y축을 쓰지 않기 위해서
        cameraRight.y = 0.0f;
        //두 값 벡터 합
        Vector3 moveDir = (cameraForward * v) + (cameraRight * h);
        moveDir.Set(moveDir.x, 0.0f, moveDir.z);
        controller.SimpleMove(moveDir * moveSpeed); //SimpleMove: 스피드만큼 캐릭터를 이동한다.

        float forward = Vector3.Dot(moveDir, transform.forward);
        float strafe = Vector3.Dot(moveDir, transform.right);

        //Animator Controller에 들어있는 float형태의 두 parameter Forward와 Strafe 가져오기
        animator.SetFloat("Forward", forward); //float 타입 파라미터라 SetFloat를 쓴다.
        animator.SetFloat("Strafe", strafe);
    }

    void Turn()
    {
        /*
        ray = camera.ScreenPointToRay(Input.mousePosition);
        //Plane에 쏠건데 Plane에 맞는 부분
        float enter = 0f;
        plane.Raycast(ray, out enter);
        hitPoint = ray.GetPoint(enter);

        Vector3 lookDir = hitPoint - transform.position;//바라보는 방향 목적지 - 플레이어의 위치
        lookDir.y = 0.0f;//y축은 필요 없음
        transform.localRotation = Quaternion.LookRotation(lookDir);
        */

        //축 받아오기
        float horizontal = Input.GetAxis("Mouse X");
        //값 누적
        mx += horizontal * rotSpeed * Time.deltaTime;
        //x, y축 회전 Vector3에 유의해서 작성
        transform.eulerAngles = new Vector3(0, mx, 0);
    }
}

 

 


 

 

Player 랜덤 Instantiate 생성하기

PointGroup을 만들어서 p1, p2, p3... 자식 오브젝트를 만든다. 배열로 저장할 예정.

빈 오브젝트 만들어서 포인트를 위치한다. 기즈모를 사용하면 위치를 표시할 수 있다.

 

다른 방법으로는 스크립트로 기즈모를 생성할 수 있다.

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

public class myGizmo : MonoBehaviour
{
    public Color _color = Color.yellow; //색깔 선택
    public float _radius = 0.2f;

    private void OnDrawGizmos()
    {
        //기즈모 색상 설정
        Gizmos.color = _color;
        //기즈모 생성(생성 위치, 반지름)
        Gizmos.DrawSphere(transform.position, _radius);
    }
}

크기 설정 가능, 색 변경 가능

 

대략 5군데 정도에 Point를 배치한다.

 

 

네트워크를 통해서 Instantiate 할 예정. PhotonManager 스크립트에 추가한다. 방에 들어가면 추가 되도록.

//룸에 들어왔을 때 콜백 함수
public override void OnJoinedRoom()
{
    Debug.Log($"In Room = {PhotonNetwork.InRoom}");
    Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
    //접속한 사용자 닉네임 확인
    foreach(var player in PhotonNetwork.CurrentRoom.Players)
    {
        //플레이어 닉네임, 유저의 고유값 가져오기
        Debug.Log($"플레이어 닉네임: {player.Value.NickName}, 유저 고유값: {player.Value.ActorNumber}");
    }

    //플레이어 생성 포인트 그룹 배열을 받아오기. 포인트 그룹의 자식 오브젝트의 Transform 받아오기.
    Transform[] points = GameObject.Find("PointGroup").GetComponentsInChildren<Transform>();
    //1부터 배열의 길이까지의 숫자 중 Random한 값을 추출
    int idx = Random.Range(1, points.Length);
    //플레이어 프리팹을 추출한 idx 위치와 회전 값에 생성. 네트워크를 통해서.
    PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
}

 

전체 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Photon 네임스페이스 추가
using Photon.Pun;
using Photon.Realtime;

public class PhotonManager : MonoBehaviourPunCallbacks //상속 클래스 변경
{
    //변수 선언
    private readonly string version = "1.0";//게임 버전 체크. 유저가 건드리지 못하게 private, readonly
    private string userId = "Victor"; //아무거나 userId 생성

    //네트워크 접속은 Start()보다 먼저 실행되어야한다. Awake() 함수 사용
    private void Awake()
    {
        //씬 동기화. 맨 처음 접속한 사람이 방장이 된다.
        PhotonNetwork.AutomaticallySyncScene = true;
        //버전 할당. 위에 string으로 만들었던 version을 쓴다.
        PhotonNetwork.GameVersion = version;
        //App ID 할당. 위에 userId로 만들었던 userId를 쓴다.
        PhotonNetwork.NickName = userId;
        //포톤 서버와의 통신 횟수를 로그로 찍기. 기본값 : 30
        Debug.Log(PhotonNetwork.SendRate); //제대로 통신이 되었다면 30이 출력된다.
        //포톤 서버에 접속
        PhotonNetwork.ConnectUsingSettings();
    }

    //CallBack 함수
    public override void OnConnectedToMaster() //정상적으로 마스터 서버에 접속이 되면 호출된다.
    {
        //마스터 서버에 접속이 되었는지 디버깅 한다.
        Debug.Log("Connected to Master");
        Debug.Log($"In Lobby = {PhotonNetwork.InLobby}"); //로비에 들어와 있으면 True, 아니면 False 반환. Master 서버에는 접속했지만 로비에는 아니므로 False 반환된다.
        //로비 접속
        PhotonNetwork.JoinLobby();
    }

    public override void OnJoinedLobby() //로비에 접속이 제대로 되었다면 해당 콜백함수 호출
    {
        Debug.Log($"In Lobby = {PhotonNetwork.InLobby}"); //로비에 접속이 되었다면 True가 반환 될 것이다.
        //방 접속 방법은 두 가지. 1.랜덤 매치메이킹, 2.선택된 방 접속
        PhotonNetwork.JoinRandomRoom();
    }

    //방 생성이 되지 않았으면 오류 콜백 함수 실행
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.Log($"JoinRandom Failed {returnCode}: {message}");

        //룸 속성 설정
        RoomOptions roomOptions = new RoomOptions();
        //룸의 접속할 수 있는 최대 접속자 수 최대 제한을 해놔야 CCU를 제한할 수 있다.
        roomOptions.MaxPlayers = 20;
        //룸 오픈 여부
        roomOptions.IsOpen = true;
        //로비에서 룸의 목록에 노출시킬지 여부. 공개방 생성
        roomOptions.IsVisible = true;
        //룸 생성
        PhotonNetwork.CreateRoom("Room1", roomOptions); //룸 이름과 룸 설정. 우리는 roomOptions에 설정을 이미 해놓았다.
    }

    //제대로 룸이 있다면 다음의 콜백 함수를 호출한다.
    public override void OnCreatedRoom()
    {
        Debug.Log("Created Room");
        Debug.Log($"Room Name: {PhotonNetwork.CurrentRoom.Name}");
    }

    //룸에 들어왔을 때 콜백 함수
    public override void OnJoinedRoom()
    {
        Debug.Log($"In Room = {PhotonNetwork.InRoom}");
        Debug.Log($"Player Count = {PhotonNetwork.CurrentRoom.PlayerCount}");
        //접속한 사용자 닉네임 확인
        foreach(var player in PhotonNetwork.CurrentRoom.Players)
        {
            //플레이어 닉네임, 유저의 고유값 가져오기
            Debug.Log($"플레이어 닉네임: {player.Value.NickName}, 유저 고유값: {player.Value.ActorNumber}");
        }

        //플레이어 생성 포인트 그룹 배열을 받아오기. 포인트 그룹의 자식 오브젝트의 Transform 받아오기.
        Transform[] points = GameObject.Find("PointGroup").GetComponentsInChildren<Transform>();
        //1부터 배열의 길이까지의 숫자 중 Random한 값을 추출
        int idx = Random.Range(1, points.Length);
        //플레이어 프리팹을 추출한 idx 위치와 회전 값에 생성. 네트워크를 통해서.
        PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
    }
}

 

**Photon에서 생성하는 프리팹은 Resources 폴더에서 불러올 수 있다.

플레이어는 Hierarchy 상에서 지운다.

 

방에 들어오는 데에 성공하면 랜덤 위치에 플레이어가 생성된다.

 

 


 

 

Movement 스크립트 수정 포톤 불러오기

네임스페이스 추가

using Photon.Pun;
using Photon.Realtime;
using Cinemachine;

 

변수 추가

//포톤뷰 가져오기
PhotonView pv;
//시네머신 카메라 가져오기, 컴포넌트 중 Follow 접근
CinemachineVirtualCamera virtualCamera;

 

Start 함수 수정

void Start()
{
    //캐릭터에 포함된 컴포넌트 가져오기
    controller = GetComponent<CharacterController>();
    transform = GetComponent<Transform>();
    animator = GetComponent<Animator>();
    camera = Camera.main; //카메라는 Tag에 MainCamera로 설정되어 있는 것을 가져온다.

    //포톤뷰 가져오기
    pv = GetComponent<PhotonView>();
    //virtualCamera 불러오기. 스크립트 외부에서 가져오기 위해 FindObjectOfType을 사용했다.
    virtualCamera = GameObject.FindObjectOfType<CinemachineVirtualCamera>();

    if (pv.IsMine)//내 클라이언트 라면, (남의 캐릭터를 움직일 수 없게 함)
    {
        virtualCamera.Follow = transform; //나 자신의 transform
        virtualCamera.LookAt = transform; //나 자신의 transform
    } 
}

 

전체 코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Cinemachine;

public class Movement : MonoBehaviour
{
    CharacterController controller;
    private new Transform transform;
    Animator animator;//애니메이션 컨트롤러 가져오기
    private new Camera camera;

    public float moveSpeed = 10f;

    //플레이어 카메라 회전
    public float rotSpeed = 200f;
    //회전값 변수
    float mx = 0;

    //포톤뷰 가져오기
    PhotonView pv;
    //시네머신 카메라 가져오기, 컴포넌트 중 Follow 접근
    CinemachineVirtualCamera virtualCamera;

    void Start()
    {
        //캐릭터에 포함된 컴포넌트 가져오기
        controller = GetComponent<CharacterController>();
        transform = GetComponent<Transform>();
        animator = GetComponent<Animator>();
        camera = Camera.main; //카메라는 Tag에 MainCamera로 설정되어 있는 것을 가져온다.

        //포톤뷰 가져오기
        pv = GetComponent<PhotonView>();
        //virtualCamera 불러오기. 스크립트 외부에서 가져오기 위해 FindObjectOfType을 사용했다.
        virtualCamera = GameObject.FindObjectOfType<CinemachineVirtualCamera>();

        if (pv.IsMine)//내 클라이언트 라면, (남의 캐릭터를 움직일 수 없게 함)
        {
            virtualCamera.Follow = transform; //나 자신의 transform
            virtualCamera.LookAt = transform; //나 자신의 transform
        } 
    }
    void Update()
    {
        Move();
        Turn();
    }

    //함수 밖에 정의하기 위해서 "=>" 사용
    float h => Input.GetAxis("Horizontal");
    float v => Input.GetAxis("Vertical");

    void Move()
    {
        //카메라 설정
        Vector3 cameraForward = camera.transform.forward;
        Vector3 cameraRight = camera.transform.right;
        cameraForward.y = 0.0f; //y축을 쓰지 않기 위해서
        cameraRight.y = 0.0f;
        //두 값 벡터 합
        Vector3 moveDir = (cameraForward * v) + (cameraRight * h);
        moveDir.Set(moveDir.x, 0.0f, moveDir.z);
        controller.SimpleMove(moveDir * moveSpeed); //SimpleMove: 스피드만큼 캐릭터를 이동한다.

        float forward = Vector3.Dot(moveDir, transform.forward);
        float strafe = Vector3.Dot(moveDir, transform.right);

        //Animator Controller에 들어있는 float형태의 두 parameter Forward와 Strafe 가져오기
        animator.SetFloat("Forward", forward); //float 타입 파라미터라 SetFloat를 쓴다.
        animator.SetFloat("Strafe", strafe);
    }

    void Turn()
    {
        //축 받아오기
        float horizontal = Input.GetAxis("Mouse X");
        //값 누적
        mx += horizontal * rotSpeed * Time.deltaTime;
        //x, y축 회전 Vector3에 유의해서 작성
        transform.eulerAngles = new Vector3(0, mx, 0);
    }
}

 

이렇게 짜고 빌드를 하고 실행 파일을 두 개 생성하면 내가 움직이는 클라이언트에서 원래 있던 캐릭터랑 내가 같이 움직인다.

 

 

이 움직임을 수정하려면 움직임을 막으면 된다. 간단하게 Update함수에서 pv.IsMine 안에 모든 움직임을 넣으면 된다.

void Update()
{
    if (pv.IsMine)
    {
        Move();
        Turn();
    }
}

 

 


 

 

여러 명이 들어오면 아래처럼 보인다.

발전시키면 Firebase로 회원가입 기능, 메타버스 플랫폼으로 닉네임 기능 등을 추가하여 만들 수 있다.

 

 


 

 

RPC를 만들면 서로 죽일 수 있다.

 

 

총알이 나가도록 구현

원격 네트워크 유저에게 총알이 생성돼서 나가도록.

총알 프리팹은 프로젝트의 안에 들어있다. Tag로 구분을 할 것이기 때문에 BULLET으로 되어있는지 확인.

 

 

Bullet 스크립트 생성

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

public class Bullet : MonoBehaviour
{
    public GameObject effect;
    void Start()
    {
        //총알에 힘을 주는 함수
        GetComponent<Rigidbody>().AddRelativeForce(Vector3.forward * 1000f); //앞으로 1000만큼의 힘을 줌
        //3초 뒤 총알 삭제 (내 오브젝트)
        Destroy(this.gameObject, 3f);
    }

    //부딪혔을 때 콜백 함수
    private void OnCollisionEnter(Collision collision)
    {
        //충돌 지점 추출
        var contact = collision.GetContact(0);//처음 충돌한 지점
        //충돌 지점에 이펙트 생성
        var obj = Instantiate(effect, contact.point, Quaternion.LookRotation(-contact.normal));//RayCast처럼 point로 가져온다. normal 방향은 쏜 방향의 반대 방향으로 이펙트 생성
        //이펙트 제거
        Destroy(obj, 2f);
        //총알 제거
        Destroy(this.gameObject);
    }
}

 

 

Bullet 스크립트의 Effect에 파티클 프리팹 넣기

 

 


 

 

총알 발사를 위한 Fire 스크립트 생성

플레이어 안에 들어있는 총구 위치와 파티클 프리팹을 사용할 예정이다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;//아래에서 PhotonView만 불러올 때는 Pun만 가져와도 된다.

public class Fire : MonoBehaviour
{
    //총구 위치 총알이 시작될 위치
    public Transform firePos;
    //총알 프리팹
    public GameObject bulletProfab;
    //파티클 시스템
    ParticleSystem muzzleFlash;
    //포톤뷰 컴포넌트
    PhotonView pv;
    //마우스 왼쪽 클릭 이벤트. 클릭이 되었는지 ture/false
    bool isMouseClick => Input.GetMouseButtonDown(0);
    void Start()
    {
        //포톤뷰 가져오기
        pv = GetComponent<PhotonView>();
        //파티클 시스템 muzzleFlash 가져오기. Player 오브젝트 아래에 있음.
        muzzleFlash = firePos.Find("MuzzleFlash").GetComponent<ParticleSystem>(); //firePos 밑에서 Find한다.
    }
    void Update()
    {
        //마우스 왼쪽 버튼을 클릭했고 && 로컬 유저라면(내 캐릭터라면) 총알 발사
        if(isMouseClick && pv.IsMine) //이미 위에 isMouseClick을 bool로 만들어놨다.
        {
            //포톤뷰에서 actorNo 가져오기
            FireBullet(pv.Owner.ActorNumber);
            //RPC로 원격지에 있는 함수를 호출. 괄호 안에 (메서드 이름, 타겟, 파라미터)
            pv.RPC("FireBullet", RpcTarget.Others, pv.Owner.ActorNumber);
        }
    }
    //RPC 함수 생성
    [PunRPC]
    void FireBullet(int actorNo)
    {
        //총구 화염 효과가 실행 중이 아닐 때 효과 실행
        if (!muzzleFlash.isPlaying) muzzleFlash.Play(true);
        //총구에 총알 생성
        GameObject bullet = Instantiate(bulletProfab, firePos.position, firePos.rotation);
    }
}

 

 

 Player에게 Fire 스크립트 넣고 다른 컴포넌트도 넣는다.

 

 

총알이 잘 나온다!

728x90
반응형