룸에 입장한후 로드된 BattleField씬에서는 아무런 동작도 없이 단순히 씬만 전환된것 뿐이다 .Lobby씬에서 유저명을 입력받아 네트워크상 식별자를 부여한 후 무작위 추출된 룸에 입장하고 BattleField씬으로 전환된것 이다. 따라서 BattleField씬으로 넘어왔다는 것은 이미 룸에 입장한 상태이므로 주인공 캐릭터를 생성하는 로직을 구현해야 한다.

배틀필드 씬을 열어 하이라키뷰에 빈 게임오브젝트를 하나 만들고 이름을 GameManager로 지정한다. 주인공을 생성하는 로직을 처리할 GameManager스크립트를 만들고 추가한다.

using UnityEngine;
using Photon.Pun;

public class GameManager : MonoBehaviourPunCallbacks {

    void Awake() {
        CreatePlayer();
    }

    void CreatePlayer() {
        // 출현 위치 정보를 배열에 저장
        Transform[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
        int idx = Random.Range(1, points.Length);

        // 네트워크상에 캐릭터 생성
        PhotonNetwork.Instantiate("Player",
                                  points[idx].position,
                                  points[idx].rotation,
                                  0);
    }
}

메뉴 [File][Build settings..]를 선택해 Lobby와 BattleField씬을 끌어다 놓는다. 위쪽의 씬이 먼저 실행된다

빌드후 유니티는 로비씬에서 실행한다.

2개의 게임은 로그인 상태이고 로그인 버튼을 먼저 누른 우저가 룸을 생성해 입장한다.

 

하이라키뷰의 Player(Clone)를 선택해 인스펙터뷰에서 PhotonView컴포넌트를 보면 owner속성에 User ID값이 설정된 것을 확인할 수 있다. 

네트워크 게임상 각 플레이어를 식별하기 위해 유저명을 입력받아야 한다. 먼저 UI를 구성하자. 로비 UI구현에 사용할 텍스트는 TextMeshPro를 사용해보자. 메뉴에서 [Window][TextMeshPro][Import TMP Essential Resources]를 선택해 초기화한다.

Canvas를 생성한 후 다음과 같이 로비 UI에서 유저명과 생성할 룸의 이름을 입력받을 Text 항목과 Button을 각각 추가한다. Canvas Scaler의 UI Scale Mode 속성은 Scale With Screen Size로 설정하고 진행한다. UI가 너무 커서 Resolution을 조정후

하이라키뷰의 Canvas를 더블클릭하고 씬뷰를 2D로 만든후 보기 좋게 하고 다음과 같이 배치한다.

로그인 UI구성을 완료한 후 유저명을 입력받아 설정하고 룸을 생성하는 로직을 구현해 본다. PhotoneManager스크립트를 다음과 같이 수정한다.

using TMpro 스페이스를 추가한다.

using System.Collections.Generic;
using UnityEngine;
using TMPro;  //TextMeshPro UI를 위한 네임스페이스
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스

 UI 필드를 참조할 변수를 설정한다.

public TMP_InputField userIF; // 유저명을 입력할 TextMeshPro Input Field
public TMP_InputField roomNameIF; // 룸 이름을 입력할 TextMeshPro Input Field

Awake()내의 NickName은 참조처리한다.

//PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정

처음 실행했을때 유저명을 자동으로 InputField에 표시하기 위해 PlayerPrefab을 이용 유저명을 로드하고 표시한다. 저장된 유저명이 없다면 랜덤으로 지정한다.

void Start()
{
    // 저장된 유저명을 로드
    userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1,21):00}");
    userIF.text = userId;
    // 접속 유저의 닉네임 등록
    PhotonNetwork.NickName = userId;  
}

로그인 UI의 [Login]버튼 또는 [Make Room]버튼을 클릭했을때, 유저명의 변경 사항을 최종확인하고 PlayerPrefs를 사용해 유저명을 저장한후PhotoneNetwork.NickName을 설정한다.

// 유저명을 설정하는 로직
public void SetUserId()
{
    if (string.IsNullOrEmpty(userIF.text))
    {
        userId = $"USER_{Random.Range(1,21):00}";
    }
    else
    {
        userId = userIF.text;
    }

    // 유저명 저장
    PlayerPrefs.SetString("USER_ID", userId);
    // 접속 유저의 닉네임 등록
    PhotonNetwork.NickName = userId;
}

게임 실행시 자동 입장을 방지하기 위해 PhotonNetwork.JoinRandomRoom() 주석처리한다

// 로비에 접속 후 호출되는 콜백 함수
public override void OnJoinedLobby() {
    Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
    //PhotonNetwork.JoinRandomRoom();     수동입장을 위해 주석처리
}

무작위 룸에 입장을 시도했다가 실패했을때 호출되는 OnJoinedRandomFailed 콜백 함수도 룸을 생성하는 로직을 주석 처리하고 OnMakeRoomClick 함수를 호출하도록 수정한다.

// 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
public override void OnJoinRandomFailed(short returnCode, string message) {
    Debug.Log($"JoinRandom Filed {returnCode}:{message}");
    OnMakeRoomClick();
      // 룸의 속성 정의
    //RoomOptions ro = new RoomOptions();
    //ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
    //ro.IsOpen = true;       // 룸의 오픈 여부
    //ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

    // 룸 생성
    //PhotonNetwork.CreateRoom("My Room", ro);
}

룸에 입장한후 호출되는 OnJoinedRoom 콜백함수에서 캐릭터를 생성하는 로직은 주석처리하고 BattleField씬을 호출한다. Phtoneview가 추가된 주인공 캐릭터는 BattleField씬에서 생성한다. 씬을 로딩하는 함수는 유니티가 제공하는 SceneManagement.SceneManager.LoadScene함수대신 PhotoneNetwork.LoadLevel함수를 이용한다.  이 함수는 씬을 로딩전 데이터 송수신을 잠시 멈추고 재개하는 로직이 포함되어 있다. 만일 수동으로 처리한다면 PhotoneNetwork.IsMessageQueueRunning 속성을 false로 지정한후 로딩후 true로 변경해야한다.

씬의 로딩은 마스터 클라이언트만 호출해야 한다. 룸에 입장한 다른 네트워크유저는 PhotonNetwork.AutomaticallySyncScene을 true로 설정했기 때문에 마스터 클라이언트가 다른 씬을 로딩하면 자동으로 씬이 로딩된다.

// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom() {
    Debug.Log($"PhotonNetwork.InRoom = {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[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
    //int idx = Random.Range(1, points.Length);
    //네트워크에 캐릭터 생성
    //PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
    // 마스터 클라이언트인 경우에 룸에 입장한 후 전투 씬을 로딩한다.
    if (PhotonNetwork.IsMasterClient) {
        PhotonNetwork.LoadLevel("BattleField");
    }
}

OnLoginClick()함수는 UI의[Login]버튼의 클릭 이벤트에 연결할 함수로 유저명을 설정하고 무작위로 선택한 룸에 입장한다.

public void OnLoginClick() {
    // 유저명 저장
    SetUserId();

    // 무작위로 추출한 룸으로 입장
    PhotonNetwork.JoinRandomRoom();
}

OnMakeRoomClick() 함수 역시 로그인 UI [Maker Room] 버튼에 연결할 함수 이며 입력한 룸 이름으로 룸을 생성한다.

public void OnMakeRoomClick() {
    // 유저명 저장
    SetUserId();

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

    // 룸 생성
    PhotonNetwork.CreateRoom(SetRoomName(), ro);
}

PhotonManager.cs
0.00MB

전체코드

using System.Collections.Generic;
using UnityEngine;
using TMPro;  //TextMeshPro UI를 위한 네임스페이스
using Photon.Pun;  //포톤 API를 위한 네임스페이스
using Photon.Realtime; //포톤 API를 위한 네임스페이스


public class PhotonManager : MonoBehaviourPunCallbacks {
    private readonly string version = "1.0"; // 게임의 버전
    private string userId = "Zack"; // 유저의 닉네임  
                                    
    public TMP_InputField userIF; // 유저명을 입력할 TextMeshPro Input Field
    public TMP_InputField roomNameIF; // 룸 이름을 입력할 TextMeshPro Input Field

    void Awake() {      
        PhotonNetwork.AutomaticallySyncScene = true;  // 마스터 클라이언트의 씬 자동 동기화 옵션
        PhotonNetwork.GameVersion = version; // 게임 버전 설정
        //PhotonNetwork.NickName = userId;  //접속 유저의 닉네임 설정
        Debug.Log(PhotonNetwork.SendRate);// 포톤 서버와의 데이터의 초당 전송 횟수
        PhotonNetwork.ConnectUsingSettings(); // 포톤 서버 접속
    }
    void Start() {
        // 저장된 유저명을 로드
        userId = PlayerPrefs.GetString("USER_ID", $"USER_{Random.Range(1, 21):00}");
        userIF.text = userId;
        // 접속 유저의 닉네임 등록
        PhotonNetwork.NickName = userId;
    }

    // 유저명을 설정하는 로직
    public void SetUserId() {
        if (string.IsNullOrEmpty(userIF.text)) {
            userId = $"USER_{Random.Range(1, 21):00}";
        } else {
            userId = userIF.text;
        }

        // 유저명 저장
        PlayerPrefs.SetString("USER_ID", userId);
        // 접속 유저의 닉네임 등록
        PhotonNetwork.NickName = userId;
    }
    // 룸 명의 입력여부를 확인하는 로직
    string SetRoomName() {
        if (string.IsNullOrEmpty(roomNameIF.text)) {
            roomNameIF.text = $"ROOM_{Random.Range(1, 101):000}";
        }

        return roomNameIF.text;
    }
    public override void OnConnectedToMaster() {
        Debug.Log("Connected to Master!");
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        PhotonNetwork.JoinLobby();
    }

    // 로비에 접속 후 호출되는 콜백 함수
    public override void OnJoinedLobby() {
        Debug.Log($"PhotonNetwork.InLobby = {PhotonNetwork.InLobby}");
        //PhotonNetwork.JoinRandomRoom();     수동입장을 위해 주석처리
    }

    // 랜덤한 룸 입장이 실패했을 경우 호출되는 콜백 함수
    public override void OnJoinRandomFailed(short returnCode, string message) {
        Debug.Log($"JoinRandom Filed {returnCode}:{message}");
        OnMakeRoomClick();
          // 룸의 속성 정의
        //RoomOptions ro = new RoomOptions();
        //ro.MaxPlayers = 20;     // 룸에 입장할 수 있는 최대 접속자 수
        //ro.IsOpen = true;       // 룸의 오픈 여부
        //ro.IsVisible = true;    // 로비에서 룸 목록에 노출시킬 여부

        // 룸 생성
        //PhotonNetwork.CreateRoom("My Room", ro);
    }

    // 룸 생성이 완료된 후 호출되는 콜백 함수
    public override void OnCreatedRoom() {
        Debug.Log("Created Room");
        Debug.Log($"Room Name = {PhotonNetwork.CurrentRoom.Name}");
    }

    // 룸에 입장한 후 호출되는 콜백 함수
    public override void OnJoinedRoom() {
        Debug.Log($"PhotonNetwork.InRoom = {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[] points = GameObject.Find("SpawnPointGroup").GetComponentsInChildren<Transform>();
        //int idx = Random.Range(1, points.Length);
        //네트워크에 캐릭터 생성
        //PhotonNetwork.Instantiate("Player", points[idx].position, points[idx].rotation, 0);
        // 마스터 클라이언트인 경우에 룸에 입장한 후 전투 씬을 로딩한다.
        if (PhotonNetwork.IsMasterClient) {
            PhotonNetwork.LoadLevel("BattleField");
        }
    }
    #region UI_BUTTON_EVENT

    public void OnLoginClick() {
        // 유저명 저장
        SetUserId();

        // 무작위로 추출한 룸으로 입장
        PhotonNetwork.JoinRandomRoom();
    }

    public void OnMakeRoomClick() {
        // 유저명 저장
        SetUserId();

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

        // 룸 생성
        PhotonNetwork.CreateRoom(SetRoomName(), ro);
    }

    #endregion
}

스크립트를 작성 저장후  로그인 UI의 InputField(TMP)-UserID와 InputField(TMP)-Room을 PhtonManager의 User IF, Room Name IF에 각각 드래그해 연결한다.

하이라키뷰의 [Canvas][Button-Login]을 선택하고 인스펙터의 OnClick에서 +를 눌러 이벤트를 하나 만들어 주고 하이라키뷰에서 PhotonManager를 끌어다 놔주면 활성화되는데 

No Fuction을 선택해 OnLoginClick()을 지정해준다.

같은 방법으로 Room Button도 OnMakerRoomClick을 지정해준다.

게임룸 입장

이제 실행해보면 씬이 로드되지 않았다는 에러가 나서

File-BuildSetting에서 우리가 만든 2개의 씬을 추가해주었다

이제 실행해보면 User ID 입력필드에 무작위 이름이 표시된다.

여러분의 이름을 직접입력하고 [Login]버튼을 누르면 입장되고 콘솔뷰에 유저명과 룸에 접속한 접속자 수가 표시된다.

또한 씬이 BattleField로 변경된것을 확인할 수 있다.

룸에 입장한후 로드된 BattleField씬에서는 아무런 동작도 없이 단순히 씬만 전환된것 뿐이다 .Lobby씬에서 유저명을 입력받아 네트워크상 식별자를 부여한 후 무작위 추출된 룸에 입장하고 BattleField씬으로 전환된것 이다. 따라서 BattleField씬으로 넘어왔다는 것은 이미 룸에 입장한 상태이므로 주인공 캐릭터를 생성하는 로직을 구현해야 한다.

로비는 네트워크에 접속한 모든 플레이어가 대기하며 방을 생성하거나 다른방에 입장할 수 있는 기능을 제공해야 한다. 포톤 클라우드에서 로비에 접속해야만 현재 생성된 룸의 정보를 서버로부터 받아올수 있다. 예제 게임에서는 이미 로비에 입장했기 때문에 룸 정보를 받을 준비가 끝났다.

 로비씬 제작

로비를 구성하기 위해 현재 열려있는 SampleScene을 저장후 프로젝트뷰 Scenes폴더에서 이름을 BattleField로 변경한후 복제해 로비씬으로 사용한다.  씬을 선택하고  Ctrl-D으로 씬을 복제하낟. 복제한 씬의 이름을 Lobby로 변경한다.

이제부터 포톤 클라우드에 접속하는 과정은 Lobby씬에서 처리한다. 따라서 BattleField씬에 있는 PhotonManager 게임오브젝트는 삭제한다.

이제 로비씬을 선택해 수정한다.

로비 씬에서는 주인공을 추적하는 기능이 필요없기 때문에 CM vcam1 게임오브젝트와 Main Camera의 cinemachine Brain 컴포넌트도 삭제한다.

프로젝트뷰의 AngryBotResource/Prefabs폴더의 Player를 하이라키뷰에 추가하고 적절한 위치에 배치한다. 또한 게임 뷰를 보면서 Main Camera 위치와 각도를 다음과 같이 설정한다.

*  씬뷰에서 적당한 화면 구도를 잡는다. 하이라뷰의 Main Camera를 선택후 Ctrl-Shit-F를 누르면 Align with view가 실행된다.

로비씬에는 시각적 효과를 위해 피사계심도와 색보정 효과를 추가해보겠다.

로비씬은 BattleField씬을 복사했기 때문에 Global Volume은 같은 Profile을 공유하고 있다.  따라서 New옆의 Clone을 클릭해 Lobby씬의 Post Processing 효과를 위한 새로운 Profile을 만들어야 한다.

Add Override로 Depth of Field, Color Adjustment후처리를 추가한다 

효과는 씬뷰에서는 확인이 안되고 게임뷰에서 확인할 수 있다

 

같은 룸에 입장한 네트워크 유저간의 RPC를 이용해 총알이 발사되는 로직을 완성했다. 이제 상대편 네트워크 유저가 발사한 총알에 데미지를 입고 사망하는 로직을 구현하자.  그 반대도 같은 로직이다. 또 사망한 후 일정시간이 지난 후 리스폰(재생,부활) 되게 한다.  이 처리는 새로운 스크립트에서 처리한다. 스크립트를 새로 만들고 이름을 Damage라고 하자 프로젝트뷰의 Resoures폴더에 있는 Player프리팹에 추가한 후 다음과 같이 작성한다.

핵심은 총알을 맞아서 생명수치 hp값이 0이하일 때 캐릭터의 Die 애니메이션을 실행한 후 화면상에 보이지 않게 모든 Renderer 컴포넌트를 비활성화 하는 것이다. 리스폰 시간이 지난 후 불규칙한 위치로 이동시키고 다시 Renderer 컴포넌트를 활성화해 화면에 표시한다.

using System.Collections;
using UnityEngine;


public class Damage : MonoBehaviour { // MonoBehaviourPunCallbacks {
    private Renderer[] renderers;  // 사망 후 투명 처리를 위한 MeshRenderer 컴포넌트의 배열
    private int initHp = 100;  // 캐릭터의 초기 생명치
    public int currHp = 100;  // 캐릭터의 현재 생명치
     private Animator anim;
    private CharacterController cc;

    // 애니메이터 뷰에 생성한 파라미터의 헤시값 추출
    private readonly int hashDie = Animator.StringToHash("Die");
    private readonly int hashRespawn = Animator.StringToHash("Respawn");



    void Awake() {
        // 캐릭터 모델의 모든 Renderer 컴포넌트를 추출한 후 배열에 할당
        renderers = GetComponentsInChildren<Renderer>();
        anim = GetComponent<Animator>();
        cc = GetComponent<CharacterController>(); 
        currHp = initHp;  //현재 생명치를 초기 생명치로 초깃값 설정

    }

    void OnCollisionEnter(Collision coll) {
        // 생명 수치가 0보다 크고 충돌체의 태그가 BULLET인 경우에 생명 수치를 차감
        if (currHp > 0 && coll.collider.CompareTag("BULLET")) {
            currHp -= 20;
            if (currHp <= 0) {
                // 자신의 PhotonView 일 때만 메시지를 출력

                StartCoroutine(PlayerDie());
            }
        }
    }

    IEnumerator PlayerDie() {
        cc.enabled = false; // CharacterController 컴포넌트 비활성화
        anim.SetBool(hashRespawn, false);  // 리스폰 비활성화
        anim.SetTrigger(hashDie); // 캐릭터 사망 애니메이션 실행
        yield return new WaitForSeconds(3.0f);   
        anim.SetBool(hashRespawn, true); // 리스폰 활성화     
        SetPlayerVisible(false); // 캐릭터 투명 처리
        yield return new WaitForSeconds(1.5f);

        currHp = 100;    // 리스폰 시 생명 초깃값 설정       
        SetPlayerVisible(true); // 캐릭터를 다시 보이게 처리
        cc.enabled = true;  // CharacterController 컴포넌트 활성화
    }

    //Renderer 컴포넌트를 활성/비활성화하는 함수
    void SetPlayerVisible(bool isVisible) {
        for (int i = 0; i < renderers.Length; i++) {
            renderers[i].enabled = isVisible;
        }
    }
}

소스코드와 프로젝트를 모두 저장후 빌드후 캐릭터가 피격후 사망후 리스폰되는지 확인해보자.

블러거는 실행후 BULLET Tag가 없다고 해서 Resources/Prefab/Bullet 에서 BULLET Tag를 만든후 적용해 주었다

 

 

원격 네트워크 유저에게 총알을 발사하는 로직을 만들어 보자. 앞서 이동 로직에서 구현했듯이 총알 프리팹에 PhotonView컴포넌트를 추가해 생상하면 아주 간단하게 만들수 있겠지만 이는 잘못된 방법이다.

PhotonView 컴포넌트는 초당 20회 데이터를 전송하기 때문에 스테이지에 많은 총알을 생성하면 생성된 모든 총알에서 트래픽이 발생하기 때문에 이런 이벤트성 동작을 네트워크 유저와 공유할 때는 RPC(Remote Procedure Calls)를 통해 구현하는 것이 일방적인 방식이다.

RPC : 원격 프로시저 호출은 물리적으로 떨어져 있는 다른 디바이스의 함수를 호출하는 기능으로 RPC함수를 호출하면 네트워크를 통해 다른 사용자의 스크립트에서 해당 함수가 호출된다. 비슷한 개념으로  RMI(Remote Method Invocation)가 있다.

총알로 사용할 Bullet은 미리 만들어진 프리팹을 사용한다. 임포트한 AngryBotResorces/Prefabs 하위의 Bullet 프리팹을 씬뷰로 드래그해 확인해보자. Bullet프리팹은 Capsulre Collider와 Rigidbody 컴포넌트로 구성된다.  BULLET태그룰 생성하고 지정한다.

Bullet Prefab원본에 Bullet 스크립트를 연결한다. 

Start()함수에서 Force로 발사  되게 만들고 충돌시 충돌지점에 스파크이펙트를 생성하는 스크립트이다.

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

public class Bullet : MonoBehaviour {
    public GameObject effect;
    // 총알을 발사한 플레이어의 고유번호
    public int actorNumber;

    void Start() {
        GetComponent<Rigidbody>().AddRelativeForce(Vector3.forward * 1000.0f);
        // 일정시간이 지난 후 총알을 삭제
        Destroy(this.gameObject, 3.0f);
    }

    void OnCollisionEnter(Collision coll) {
        // 충돌 지점 추출
        var contact = coll.GetContact(0);
        // 충돌 지점에 스파크 이펙트 생성
        var obj = Instantiate(effect,
                              contact.point,
                              Quaternion.LookRotation(-contact.normal));
        Destroy(obj, 2.0f);
        Destroy(this.gameObject);
    }
}

public GameObject effect는 AngryBotResources/Particles/Prefab하위의 Bullet_Impact_Wall프리팹을 연결하고 저장한다.

 

유니티를 실행시키면 총알이 발사된다. 그런 하이라키뷰의 총알은 지운다. 이제 총알을 발사할 Fire 스크립트를 생성한다.

void Update() {
    // 로컬 유저여부와 마우스 왼쪽 버튼을 클릭했을 때 총알을 발사
    if (pv.IsMine && isMouseClick) {
        FireBullet(pv.Owner.ActorNumber);
        //RPC로 원격지에 있는 함수를 호출
        pv.RPC("FireBullet", RpcTarget.Others, pv.Owner.ActorNumber);
    }
}

포톤서버에서 일반적인 RPC호출은 PhtoneView.RPC(호출함수명, 호출대상, 전달할 데이터) 함수를 사용한다. 원격으로 호출할 함수명 인자는 string 타입으로 전달하고 호출 대상은 특정 플레이어를 지정하거나 RpcTarget옵션으로 전달 대상의 범위를 지정할 수 있다.

  • All : 자기자신을 포함한 모든 유저에게 함수를 뿌림
  • Others : 자신을 제외한 모든 유저에게 함수를 뿌림
  • MasterClient : 방장에게 함수를 뿌림
  • AllBuffered : 자신을 포함한 모든유저에게 함수를 뿌림, 또한 나중에 입장한 유저는 버퍼에 저장된 RPC를 전달받는다.
  • OtherBuffered : 자신을 제외한 모든유저에게 함수를 뿌림, 나중에 입장한 유저는 버퍼에 저장된 RPC를 전달받는다.
  • AllViaServer : 모든 네트워크 유저에게 거의 동일한 시간에 RPC를 전송하기위해 서버의 모든클라이언트에게 RPC를 동시전송
  • AllBufferedViaServer : AllViaServer와 동일하며, 버퍼에 저장된 RPC를 나중에 입장한 유저에게 전달

총알 발사 로직에서 로컬 FireBullet함수를 호출하지 않고 다음과 같이 RpcTarget.All 옵션을 사용해도 동일한 결과를 볼 수 있다. RpcTarget.All은 Rpc함수를 룸에 입장한 모든 네트워크 유저에 대해 호출하고 로컬 유저는 해당 함수를 즉시 호출한다.

pv.RPC("FireBullet",RpcTarget.All, null);

RPC로 호출할때는 반드시 [PunRPC] 어트리뷰트를 함수앞에 명시해야한다.

[PunRPC]
void FireBullet(int actorNo) {
    // 총구화염 효과가 실행 중이 아닌 경우에 총구 화염효과 실행
    if (!muzzleFlash.isPlaying) muzzleFlash.Play(true);

    GameObject bullet = Instantiate(bulletPrefab,
                                    firePos.position,
                                    firePos.rotation);
    bullet.GetComponent<Bullet>().actorNumber = actorNo;
}

 

Fire.cs 전체코드

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

public class Fire : MonoBehaviour
{
    public Transform firePos;
    public GameObject bulletPrefab;
    private ParticleSystem muzzleFlash;

    private PhotonView pv;
    // 왼쪽 마우스 버튼 클릭 이벤트 저장
    private bool isMouseClick => Input.GetMouseButtonDown(0);

    void Start()
    {
        // 포톤뷰 컴포넌트 연결
        pv = GetComponent<PhotonView>();  
        // FirePos 하위에 있는 총구 화염 효과 연결  
        muzzleFlash = firePos.Find("MuzzleFlash").GetComponent<ParticleSystem>();
    }

    void Update()
    {
        // 로컬 유저여부와 마우스 왼쪽 버튼을 클릭했을 때 총알을 발사
        if (pv.IsMine && isMouseClick)
        {
            FireBullet(pv.Owner.ActorNumber);
            //RPC로 원격지에 있는 함수를 호출
            pv.RPC("FireBullet", RpcTarget.Others, pv.Owner.ActorNumber);
        }        
    }

    [PunRPC]
    void FireBullet(int actorNo)
    {
        // 총구화염 효과가 실행 중이 아닌 경우에 총구 화염효과 실행
        if (!muzzleFlash.isPlaying) muzzleFlash.Play(true);

        GameObject bullet = Instantiate(bulletPrefab,
                                        firePos.position,
                                        firePos.rotation);
        bullet.GetComponent<Bullet>().actorNumber = actorNo;
    }
}

작성된 스크립트를 Resources폴더에 있는 Player프리팹에 추가한다. Resources폴더에 있는 Player 프리팹을 선택한 다음 인스펙터 뷰에서 [Open Prefab] 버튼을 클릭한다. 프리팹 에디터 뷰로 진입하면 하이라키뷰에 있는 Gun/Fire Pos속성에 연결한다. Bullet Prefab 속성에는 프로젝트뷰의 AngryBotResources/Prefabs하위에 있는 Bullet프리팹을 연결한다.

* RPC호출 목적으로만 사용하려면 PhotonView컴포넌트의 Synchronization 속성을 Off로 설정해야 한다.

총알의 발사는 자신의 캐릭터에서만 동작해야 하기 때문에 PhotonView.IsMine 속성을 체크해야 한다. 자신의 경우 로컬 FireBullet함수를 호출하고 원격유저의 캐릭터는 RPC함수를 사용해 원격 FireBullet 함수를 호출한다.

게임을 빌드후 실행해 서로 총알을 발사해 확인해보자.

 

 

+ Recent posts