같은 룸에 입장한 네트워크 유저간의 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 함수를 호출한다.

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

 

 

https://github.com/IndieGameMaker/UnityBook

 

GitHub - IndieGameMaker/UnityBook: 절대강좌! 유니티 2021 - 리소스

절대강좌! 유니티 2021 - 리소스. Contribute to IndieGameMaker/UnityBook development by creating an account on GitHub.

github.com

 

이제 네트워크를 통해 다중 접속해 주인공 캐릭터의 이동, 회전및 애니메이션이 제대로 동기화되는지 확인해보자. 지금까지 구현된 상태로 빌드해 실행 파일을 생성한다..

메뉴에서 [Edit]-[Project Settings..]를 선택해 Project Settings 윈도우를 연 후 [Player]섹션을 선택한다. Resolution and Presentation에서  Fullscreen Mode를 [Windowed]로 변경하고 가로 및 세로 해상도를 적절히 설정한다. 

메뉴에서 [File]-[Build Settings...]를 선택해 Build Settings 창을 연다. [Build And Run] 버튼을 클릭하면 저장할 경로를 물으면 Builds라는 폴더를 새로 생성하고 실행 파일명을 입력한 다음 Save를 눌러 저장한다. 빌드과정이 끝나면 자동으로 게임이 실행된다. 이제 유니티도 실행하면 2번째로 입장하게 되고 2개의 윈도우를 보면 각자의 게임속에서 네트워크 유저가 움직이는걸 확인할 수 있다

OnPhotonSerializeView 콜백 함수

같은 룸에 입장한 네트워크 객체간 데이터를 동기화 시키는 2번째 방법은 OnPhotonSerializeView 콜백 함수를 사용하는 것이다. 앞서 구현한것 처럼 PhotonTransformView와 PhotonAnimatorview는 컴포넌트만 추가하면 간단히 동기화 처리가 되지만, 좀 더 세밀한 보정이 필요할 때는 이 콜백 함수가 유용하다.

우선 Resources폴더에 있는 Player프리팹을 선택하고 PhotonTransform 컴포넌트를 제거한다. 그다음 Movement 스크립트를 다음과 같이 수정한다.

 

먼저 Movement 클래스에 IPunObservable 인터페이스를 추가한다.

public class Movement : MonoBehaviourPunCallbacks, IPunObservable {

OnPhotonSerializeView 콜백 함수의 첫 번째 인자인 PhotonStream.IsWriting 속성이 true이면 데이터를 전송하는 것을 의미한다. PhotronView.IsMine 속성이 true일 경우 네트워크 객체는 자신의 캐릭터이다. 자신의 캐릭터 transform정보는 같은 룸에 입장한 모든 객체에게 전송되어야 하고 false일 경우 다른 네트워크의 객체의 transform 정보를 수신해야 한다.

public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
    // 자신의 로컬 캐릭터인 경우 자신의 데이터를 다른 네트워크 유저에게 송신 
    if (stream.IsWriting) {
        stream.SendNext(transform.position);
        stream.SendNext(transform.rotation);
    } else {
        receivePos = (Vector3)stream.ReceiveNext();
        receiveRot = (Quaternion)stream.ReceiveNext();
    }
}

데이터를 전송하는 것은 PhotonStream.SendNext() 함수를 이용해 전달하며 데이터를 수신할 때는 PhotonStream.ReceiveNext()를 이용한다. 전송하는 데이터의 개수와 데이터타입은 수신할 데이터의 개수와 타입이 일치해야 한다. 수신한 데이터는 receivePos, receiveRot에 저장되고 Update() 함수에서 사용된다.

Update()함수에서 PhtonView.IsMine 속성으로 자신의 캐릭터는 직접 컨트롤 하고 타 네트워크 유저 캐릭터는 수신받은 데이터로 이용해 이동 시킨다.  이때 이동할 좌표는 Vector3.Lerp 함수를 이용해 보간하고 회전값은 Quaternion.Slerp함수를 사용해 보간한다.

void Update() {
    // 자신이 생성한 네트워크 객체만 컨트롤
    if (pv.IsMine) {
        Move();
        Turn();
    }  else
    {
        // 수신된 좌표로 보간한 이동처리
        transform.position = Vector3.Lerp(transform.position,
                                          receivePos,
                                          Time.deltaTime * damping);

        // 수신된 회전값으로 보간한 회전처리
        transform.rotation = Quaternion.Slerp(transform.rotation,
                                              receiveRot,
                                              Time.deltaTime * damping);
    }
}

스크립트를 수정한 후 Player 프리팹의 PhotonView컴포넌트를 보면 Observable Serch 속성이 [Auto]로 되어 있기 때문에 Player(Movement) 스크립트가 자동 적용되어 있다. [Manual]이라면 직접 드래그해서 추가해야 한다.

로직을 변경했으므로 다시 Build해서 테스트 해보자

큰 차이는 없어보이지만 OnPhotonSerializeView 콜백 함수를 이용해 직접 데이터를 송수신해 처리했다.

Movement.cs
0.00MB

 

지금까지 포톤 서버에 접속하고, 룸을 생성한 다음 입장하는 기능을 구현했다. 이제 주인공 캐릭터를 네트워크 통신이 가능하게 수정해보자. 하이라키뷰에서 Player를 선택하고 메뉴에서 [Component]->[Photon Networking]->[Photon View]를 선택해 PhotonView 컴포넌트를 추가한다.

PhotonView 컴포넌트

플레이어간 데이터를 송수신하는 통신 모듈이다. 동일한 룸에 입장한 다른 플레이어에게 자신의 위치와 회전 정보를 동기화시키고 특정 데이터를 송수신하려면 이 컴포넌트가 필요하다.

Synchronization

  • None : 동기화 처리한지 않는다.
  • Reliable Dellta Compress : 마지막 데이터가 변경되지 않았을 때 데이터를 송신하지 않는다
  • Unreliable : 송신한 패킷의 수신 여부를 확인하지 않는다.
  • Unreliable On Change : Unreliable과 동일한데 변경사항이 발생했을 때만 송신한다.

Observed Components : Photon View 컴포넌트가 관찰해 데이터를 송수신할 대상을 등록하는 속성이다.

주인공 캐릭터는 같은 룸에 입장한 다른 유저에게 자신의 위치와 회전값, 애니메이션에 대한 정보를 동기화해야한다.

  • Photon Transform View, Photon Animation View 컴포넌트를 사용하는 방식
  • OnPhotonSerializeView 콜백 함수를 사용하는 방식

첫번째 방식은 가장 쉽게 네트워크를 동기화 할 수 있지만. 세밀한 조정을 할 수 없으며 레이턴시 발생시 수동으로 보정할 수 없다.  2번째 방식은 Photon의 컴포넌트를 사용하지 않고 수동으로 관리하는 방식이다.

 

Photon Transform View, Photon Animator View

먼저 첫번째 방식으로 컴포넌트를 이용해 구현해보자. 하이라키뷰의 Player를 선택하고 [Component]-[Photon Networking] 하위 Photon Transform View와 Photon Animator View 컴포넌트를 추가해 동기화할 데이터및 속도를 지정할 수 있다.

Photon Animatior View에서 동기화 속도를 설정해야한다. 모드 Discrete로 지정한다.

네트워크 환경에서 생성하기 위한 준비

프로젝트뷰에서 Resouces 폴더를 생성한 후 하이라키 뷰의 Player를 Resources 폴더로 드래그해 프리팹으로 생성한후 하이라키뷰의 Palyer는 삭제한다. 포톤에서 네트워크로 동기화할 대상은 PhotonNetwork.Instantiate 함수를 사용하며 모두 Resources 폴더에 위치해야 한다.

 

다수의 네트워크 유저가 접속하기 때문에 미리 스폰 포인트를 설정하고 그중에서 랜덤한 위치에 생성되게 한다. 8장의 SpawnPointGroup을 생성했던 내용을 참고해 동일하게 생성한다.

접속한 유저가 출현할 불규칙한 위치 정보는 게임 매니저가 알고 있어야 한다. 먼저 빈 게임오브젝트를 생성하고 SpawnPointGroup으로 지정한다. Position은 (0,0,0)이다 SpawnPointGroup를 선택하고 하위에 빈게임오브젝트를 생성하고 Point라고 이름을 바꾼다.

시각적으로 표현하기 위해 MyGizmo.cs를 작성해서 연결한다.

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

public class MyGizmo : MonoBehaviour
{
    public Color _color = Color.yellow;
    public float _radius = 0.1f;
    private void OnDrawGizmos() {
        Gizmos.color = _color;
        Gizmos.DrawSphere(transform.position, _radius);
    }
}

이제 유저가 접속했을때 여러 Spawn Point중에서 랜덤으로 발생시킬것이다. . 

포인트를 Ctr-D로 복사해서 여러군대 배치한다. 이름을 Point_1 ~ Point_9로 변경한다.

PhotonManager 스크립트에서 룸에 입장한 후 주인공 캐릭터를 네트워크상에서 동기화되게 생성한다. OnJoinedRoom  함수를 다음과 같이 수정한다. 

 

// 룸에 입장한 후 호출되는 콜백 함수
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);
}

유니티를 실행후 Player(Clone)이 생성되었는지 확인한다.

런타임(동적)으로 생성되었기 때문에 시네머신 카메라의 Follow와 LookAt속성이 모두 연결이 끊어져 주인공을 따라가지는 않는다. 다음과 같이 Movement.cs를 수정해서 따라가도록 한다. 

using Photon.Realtime 네임스페이스를 추가한다.

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

우선 .Movement Class의 MonoBehaviour속성을 MonoBehaviourPunCallbacks로 변경한다.

public class Movement : MonoBehaviourPunCallbacks{

네트워크에 의해 생성한 캐릭터 참조 pv와 시네머신 가상 카메라를 저장할 변수를 전역변수로 지정한다.

// PhotonView 컴포넌트 캐시처리를 위한 변수
private PhotonView pv;
// 시네머신 가상 카메라를 저장할 변수
private CinemachineVirtualCamera virtualCamera;

자신이 생성한 캐릭터와 네트워크 유저는 Photonview.IsMine속성으로 구별할 수 있다. true일 경우 자신이다.  

Start()에서 연결한 virtualCamera를 pv.IsMine==true일 경우 연결한다.

void Start() {
    controller = GetComponent<CharacterController>();
    transform = GetComponent<Transform>();
    animator = GetComponent<Animator>();
    camera = Camera.main;

    pv = GetComponent<PhotonView>();
    virtualCamera = GameObject.FindObjectOfType<CinemachineVirtualCamera>();
    //PhotonView가 자신의 것일 경우 시네머신 가상카메라를 연결
    if (pv.IsMine) {
        virtualCamera.Follow = transform;
        virtualCamera.LookAt = transform;
    }
    // 가상의 바닥을 주인공의 위치를 기준으로 생성
    plane = new Plane(transform.up, transform.position);
}

네트워크 유저 캐릭터는 PhotonView에 의해서 관리되므로 자신이 생성한 캐릭터만 관리한다

    void Update() {
        // 자신이 생성한 네트워크 객체만 컨트롤
        if (pv.IsMine) {
            Move();
            Turn();
        }
    }

실행후 룸에 접속했을때 주인공 캐릭터가 불규칙한 위치에 생성되고 카메라가 주인공을 따라가는 것을 볼 수 있다. 하이라키뷰에서 Player(Clone)을 선택하고 인스펙터뷰에서 PhotonView의 IsMine속성과 Controller, Owner, Creator에 대한 정보를 확인할 수 있다.

 

PhotonManager.cs
0.00MB
Movement.cs
0.00MB
MyGizmo.cs
0.00MB

+ Recent posts