[SKKU DT] 39일차 -유니티(Unity) 포톤 클라우드(Photon Cloud)로 멀티플레이 슈팅 게임 만들기3 -죽는 모션/로비 구현/방 만들기

2023. 12. 22. 21:19SKKU DT

728x90
반응형
 

[SKKU DT] 37일차 -유니티(Unity) 포톤 클라우드(Photon Cloud)로 멀티플레이 슈팅 게임 만들기 -포톤 설정

포톤 클라우드를 활용한 네트워크 게임 제작 https://www.photonengine.com/ko-kr# 글로벌 크로스 플랫폼 실시간 게임 개발 | Photon Engine EssentialPhoton Details Discover a summary of our product range, notable features, the pow

lightbakery.tistory.com

 

 

[SKKU DT] 38일차 -유니티(Unity) 포톤 클라우드(Photon Cloud)로 멀티플레이 슈팅 게임 만들기2 -Cinemachine/

Cinemachine 설치 Virtual Camera 생성 컴포넌트 안에 CinemachineVirtualCamera가 생성되어 있는 것을 볼 수 있다. Follow와 Look At에 Hierarchy상의 Player 프리팹을 끌어다 놓는다. 밑에 Body와 Aim을 수정한다. 데드존

lightbakery.tistory.com


 

 

이제 캐릭터끼리 죽일 수 있게 하기 위해 Damage 스크립트를 생성한다.

캐릭터를 Destroy하면 플레이어를 다시 생성하는 과정이 필요하기 때문에 다른 방식을 사용해야 한다. 렌더러를 끄고 리스폰 장소에서 다시 나오도록.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using Player = Photon.Realtime.Player;

public class Damage : MonoBehaviourPunCallbacks
{
    //MeshRenderer 컴포넌트 배열로 가져오기. 죽은 다음 투명 처리를 하기 위해서.
    //배열로 가져오는 이유는 플레이어의 Skinned MeshRenderer, 총의 MeshRenderer 두 개 있기 때문.
    Renderer[] renderers;
    //초기 Hp
    int initialHp = 100;
    //현재 Hp
    public int currentHp = 100;
    //죽을 때 애니메이션
    Animator anim;
    //죽었을 때 CharacterController 막아야한다. 죽은 캐릭터가 움직이면 안되기 때문.
    CharacterController cc;

    private readonly int hashDie = Animator.StringToHash("Die"); //애니메이터 두 파라미터를 해시데이터 값을 만들어서 뒤에서 불러올 예정
    private readonly int hashRespawn = Animator.StringToHash("Respawn");

    private void Awake()
    {
        //자식 오브젝트의 렌더러를 가져온다.
        renderers = GetComponentsInChildren<Renderer>();
        anim = GetComponent<Animator>();
        cc = GetComponent<CharacterController>();
        //시작 전 체력 동일하게 맞추기
        currentHp = initialHp;
    }

    private void OnCollisionEnter(Collision collision)
    {
        //총알과 닿으면 체력이 깎이게 한다. 특정 부위에 collider 넣고 Tag로 구별해서 해당 collider에 닿으면 데미지를 추가로 넣도록 응용할 수도 있다.
        if(currentHp > 0 && collision.collider.CompareTag("BULLET")) //현재 hp가 양수이고 충돌체의 태그가 BULLET이면
        {
            currentHp -= 10;
            if(currentHp <= 0) //만약 현재 hp가 음수이면 Die 코루틴 실행
            {
                StartCoroutine(PlayerDie());
            }
            Debug.Log($"현재 체력: {currentHp}");
        } 
    }

    IEnumerator PlayerDie()
    {
        //캐릭터 컨트롤러 컴포넌트 비활성화
        cc.enabled = false;
        //리스폰 애니메이션 비활성화. Respawn 파라미터의 타입은 bool
        anim.SetBool(hashRespawn, false); //string 타입("Respawn")으로 가져와도 되고 hash 타입으로 가져와도 된다.
        //사망 애니메이션 활성화
        anim.SetTrigger(hashDie);
        //죽고 나서 조금 대기
        yield return new WaitForSeconds(3f);
        //리스폰 애니메이션 활성화
        anim.SetBool(hashRespawn, true);
        //캐릭터 렌더러 비활성화 함수
        SetPlayerVisible(false);
        //리스폰까지 대기
        yield return new WaitForSeconds(3f);

        //플레이어를 안보이게만 해놓았으므로 위에서 비활성화 했던 것들을 반대로 바꿔야 한다.
        //체력 재설정
        currentHp = 100;
        //캐릭터 렌더러 활성화
        SetPlayerVisible(true);
        //캐릭터 컨트롤러 활성화
        cc.enabled = true;

    }

    //캐릭터 렌더러 컴포넌트 배열을 활성화/비활성화 함수
    void SetPlayerVisible(bool isVisible)
    {
        for(int i = 0; i < renderers.Length; i++)
        {
            //렌더러 배열을 활성화 할지 여부
            renderers[i].enabled = isVisible;
        }
    }
}

Die, Respawn 파라미터 확인. 각각 Trigger와 bool이다.

**특정 부위에 collider 넣고 Tag로 구별해서 해당 collider에 닿으면 데미지를 추가로 넣도록 응용할 수도 있다.

 

 

 


 

 

로비에서 유저 이름과 방 이름을 설정

먼저 로비에서 닉네임을 생성한다.

로비씬은 게임씬의 환경을 가져다 쓰기 위해 씬 복사한다. Virtual Camera 삭제, Main Camera 씨네머신 컴포넌트 삭제

 

 

로비 UI 구현

 

 

PhotonManager 스크립트 수정

유저 ID 설정, 룸ID 설정, 랜덤 룸 함수 끄기

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

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

    //유저 ID를 입력할 인풋 필드
    public TMP_InputField userInputField;
    //룸 ID를 입력할 인풋 필드
    public TMP_InputField roomInputField;

    //네트워크 접속은 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}");

        OnMakeRoomClick(); //오류 나는 것을 방지하기 위해서.

        //룸 속성 설정
        //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);

        //마스터 클라이언트인 경우 게임 씬 로딩
        if(PhotonNetwork.IsMasterClient)
        {
            PhotonNetwork.LoadLevel("GameScene"); //씬 이름으로 불러오기
        }
    }

    private void Start()
    {
        //유저 ID 랜덤 설정
        userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1, 21):00}"); //20명까지 밖에 못들어오므로 1~21 설정. :00은 한 자리도 두 자리로 만들어주려고.
        userInputField.text = userId;
        //접속 닉네임 네트워크 등록
        PhotonNetwork.NickName = userId;
    }

    //유저명을 설정하는 로직
    public void SetUserId()
    {
        //인풋 필드가 비어있으면 랜덤한 값, 그렇지 않으면 유저가 생성한 값. 다시 접속했을 때 닉네임 보존
        if (string.IsNullOrEmpty(userInputField.text))
        {
            userId = $"USER_{Random.Range(1, 21):00}";
        }
        else
        {
            userId = userInputField.text;
        }
        //유저명 저장. 로비에서 만든 개체를 메인에서도 쓸 수 있다.
        PlayerPrefs.SetString("USER_ID", userId);
        PhotonNetwork.NickName = userId; //네트워크에도 반영
    }
    
    string SetRoomName()
    {
        //비어있으면 랜덤한 룸 이름. 그렇지 않으면 가져오도록.
        if (string.IsNullOrEmpty(roomInputField.text))
        {
            roomInputField.text = $"ROOM_{Random.Range(1, 101):000}";
        }
        return roomInputField.text;
    }

    public void OnLoginClick() //로그인 버튼 매핑 함수
    {
        //유저 ID 저장
        SetUserId();
        //무작위 룸으로 입장
        PhotonNetwork.JoinRandomRoom();
    }

    public void OnMakeRoomClick() //방 생성 버튼 매핑 함수
    {
        //유저 ID 저장
        SetUserId();
        //룸 속성 정의
        RoomOptions ro = new RoomOptions();
        ro.MaxPlayers = 20;
        ro.IsOpen = true;
        //공개방 설정
        ro.IsVisible = true;
        //룸 생성
        PhotonNetwork.CreateRoom(SetRoomName(), ro); //고정된 값이 아니라 유저가 타이핑한 값을 받아온다.
    }
}

 

버튼 매핑을 한다.

 

바로 실행하면 User ID에 자동으로 이름이 생성되는 것을 볼 수 있다.

 

하지만 플레이어 생성은 되지 않는다.

 

 


 

 

GameScene에서 더이상 PhotonManager는 필요하지 않다. 삭제.

 

 

GameManager 빈 오브젝트 생성, GameManager 스크립트 생성

GameManager에서 플레이어를 생성할 예정이다.

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

public class GameManager : MonoBehaviour
{
    private void Awake()
    {
        CreatePlayer();
    }

    void CreatePlayer()
    {
        //PhotonManager 스크립트에서 주석처리 되어있던 줄을 가져왔다.
        //플레이어 생성 포인트 그룹 배열을 받아오기. 포인트 그룹의 자식 오브젝트의 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);
    }
}

 

 


 

 

룸 목록 만들기

패널 하나, 스크롤 뷰 하나, 버튼 하나 만들어서 사이즈를 조절한다.

 

RoomItem 버튼 위치 수정

 

Content에 Grid Layout Group 컴포넌트 추가

 

Grid Layout Group 설정 수정

 

 

PhotonManage 스크립트 수정

OnRoomListUpdate(List<RoomInfo> roomList) 콜백 함수 추가

public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
    foreach(var room in roomList)
    {
        Debug.Log($"Room = {room.Name} ({room.PlayerCount}/{room.MaxPlayers}");
    }
}

 

룸 정보를 Dictionary 형태로 저장

변수 부분 추가

//룸 목록에 대한 데이터 저장
Dictionary<string, GameObject> rooms = new Dictionary<string, GameObject>();
//룸 목록을 표시할 프리팹
GameObject roomItemPrefab;
//룸 목록이 표시될 scroll content
public Transform scrollContent;

 

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이 출력된다.
    
    //RoomItem 프리팹 로드 Resources 폴더로부터...
    roomItemPrefab = Resources.Load<GameObject>("RoomItem");
    
    //포톤 서버에 접속
    if (PhotonNetwork.IsConnected == false)
    {
        PhotonNetwork.ConnectUsingSettings();
    }
}

 

 

OnRoomListUpdate 콜백 함수 수정

//방 리스트를 수신하는 콜백 함수 생성
public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
    // 삭제된 RoomItem 프리팹을 저장할 임시변수
    GameObject tempRoom = null;
    foreach (var roomInfo in roomList)
    {
        // 룸이 삭제된 경우
        if (roomInfo.RemovedFromList == true)
        {
            // 딕셔너리에서 룸 이름으로 검색해 저장된 RoomItem 프리팹을 추출
            rooms.TryGetValue(roomInfo.Name, out tempRoom);
            // RoomItem 프리팹 삭제
            Destroy(tempRoom);
            // 딕셔너리에서 해당 룸 이름의 데이터를 삭제
            rooms.Remove(roomInfo.Name);

        }
        else // 룸 정보가 변경된 경우
        {
            // 룸 이름이 딕셔너리에 없는 경우 새로 추가
            if (rooms.ContainsKey(roomInfo.Name) == false)
            {
                // RoomInfo 프리팹을 scrollContent 하위에 생성
                GameObject roomPrefab = Instantiate(roomItemPrefab, scrollContent);
                // 룸 정보를 표시하기 위해 RoomInfo 정보 전달
                roomPrefab.GetComponent<RoomData>().RoomInfo = roomInfo;
                // 딕셔너리 자료형에 데이터 추가
                rooms.Add(roomInfo.Name, roomPrefab);
            }
            else  // 룸 이름이 딕셔너리에 없는 경우에 룸 정보를 갱신
            {
                rooms.TryGetValue(roomInfo.Name, out tempRoom);
                tempRoom.GetComponent<RoomData>().RoomInfo = roomInfo;
            }
        }
        Debug.Log($"Room={roomInfo.Name} ({roomInfo.PlayerCount}/{roomInfo.MaxPlayers})");
    }
}

 

 

RoomData 스크립트 생성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;
public class RoomData : MonoBehaviour
{
    private RoomInfo _roomInfo;
    // 하위에 있는 TMP_Text를 저장할 변수
    private TMP_Text roomInfoText;
    // PhotonManager 접근 변수
    private PhotonManager photonManager;
    // 프로퍼티 정의
    public RoomInfo RoomInfo
    {
        get
        {
            return _roomInfo;
        }

        set
        {
            _roomInfo = value;
            // 룸 정보 표시
            roomInfoText.text = $"{_roomInfo.Name} ({_roomInfo.PlayerCount}/{_roomInfo.MaxPlayers})";
            // 버튼 클릭 이벤트에 함수 연결
            GetComponent<UnityEngine.UI.Button>().onClick.AddListener(() => OnEnterRoom(_roomInfo.Name));
        }
    }

    void Awake()
    {
        roomInfoText = GetComponentInChildren<TMP_Text>();
        photonManager = GameObject.Find("PhotonManager").GetComponent<PhotonManager>();
    }

    void OnEnterRoom(string roomName)
    {
        // 유저명 설정
        photonManager.SetUserId();

        // 룸 속성 정의
        RoomOptions ro = new RoomOptions();
        ro.MaxPlayers = 20;     // 룸에 접속할 수 있는 최대 접속자 수
        ro.IsOpen = true;       // 룸의 오픈 여부
        ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬지 여부

        // 룸 접속
        PhotonNetwork.JoinOrCreateRoom(roomName, ro, TypedLobby.Default);
    }
}

 

 

RoomData 스크립트를 프리팹에 추가

 

Content를 PhotonManager 빈 컴포넌트에 추가

 

왼쪽 플레이어가 들어간 방이 오른쪽 플레이어의 룸 리스트에 뜬다.

728x90
반응형