상세 컨텐츠

본문 제목

19장. 좀비 서바이벌 멀티 플레이어( 네트워크 게임 월드 구현 )

유니티/게임만들기

by MJ_119 2024. 5. 1. 01:24

본문

이 장에서 다루는 내용

 - 싱글플레이어 게임을 멀티플레이어로 포팅하는 전반적인 방향

 - 로컬과 리모트를 코드로 구별하는 방법 

 - 호스트에서만 특정 처리를 실행하고, 결과를 클라이언트에 전파하는 방법

 - PUN의 다양한 컴포넌트

 - Photon View 컴포넌트를 사용해 로컬과 리모트 사이의 동기화

 - [PunRPC] 속성

 - 직렬화와 역직렬화

 

@ Main 씬을 열고 네트워크 플레이어 캐릭터 준비

 - Player Character 게임 오브젝트에 새로 추가된 컴포넌트

 - Camera Setup

 - Photon View

 - Photon Transform View

 - Photon Animator View

 

 

 @ 네트워크를 통해 동기화될 모든 게임 오브젝트는 Photon View 컴포넌트를 가져야 함.

 - Photon View 컴포넌트가 관측할 컴포넌트는 Observed Components 리스트에 할당하면 됨.

 단, 모든 컴포넌트가 관측 가능한 것은 아니며, IPunObservable 인터페이스를 상속한 컴포넌트만 관측 가능.

 

 

 @ Photon Transform View 컴포넌트

 - 자신의 게임 오브젝트에 추가된 트랜스폼 컴포넌트 값의 변화를 측정하고, Photon View 컴포넌트를 사용해 동기화 함.

즉, 네트워크를 넘어 로컬과 리모트 오브젝트 사이에서 트랜스폼 컴포넌트의 위치, 회전, 스케일을 동기화 할 수 있음.

< Photon Transform View 컴포넌트는 Photon View 컴포넌트 없이는 동작할 수 없음 >

 

 

 @ Photon Animator View 컴포넌트

 - 네트워크를 넘어 로컬과 리모트 오브젝트 사이에서 애니메이터 컴포넌트의 파라미터를 동기화하여 서로 같은 애니메이션을 재생하도록 함.

 

 

 @ CameraSetup 스크립트

 - 씬의 시네머신 가상 카메라가 로컬 플레이어인지 검사해서 맞으면 추적하도록 설정.

 

using Cinemachine; // 시네머신 관련 코드
using Photon.Pun; // PUN 관련 코드
using UnityEngine;

// 시네머신 카메라가 로컬 플레이어를 추적하도록 설정
public class CameraSetup : MonoBehaviourPun {
    void Start() {
        // 만약 자신이 로컬 플레이어라면
        if ( photonView.IsMine )
        {
            // 씬에 있는 시네머신 가상 카메라를 찾고
            CinemachineVirtualCamera followCam = FindObjectOfType<CinemachineVirtualCamera>();

            // 가상 카메라의 추적 대상을 자신의 트랜스폼으로 변경
            followCam.Follow = transform;
            followCam.LookAt = transform;
        }
    }
}

 

현재 게임 오브젝트가 로컬이면 photonView.IsMine은 true이다.

 

@ PlayerInput 스크립트 

 - 로컬 플레이어인 경우에만 사용자 입력을 감지하고 움직이도록 변경.

 

using Photon.Pun;
using UnityEngine;

// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviourPun {
    public string moveAxisName = "Vertical"; // 앞뒤 움직임을 위한 입력축 이름
    public string rotateAxisName = "Horizontal"; // 좌우 회전을 위한 입력축 이름
    public string fireButtonName = "Fire1"; // 발사를 위한 입력 버튼 이름
    public string reloadButtonName = "Reload"; // 재장전을 위한 입력 버튼 이름

    // 값 할당은 내부에서만 가능
    public float move { get; private set; } // 감지된 움직임 입력값
    public float rotate { get; private set; } // 감지된 회전 입력값
    public bool fire { get; private set; } // 감지된 발사 입력값
    public bool reload { get; private set; } // 감지된 재장전 입력값

    // 매프레임 사용자 입력을 감지
    private void Update() {
        // 로컬 플레이어가 아닌 경우 입력을 받지 않음
        if (!photonView.IsMine)
        {
            return;
        }

        // 게임오버 상태에서는 사용자 입력을 감지하지 않는다
        if (GameManager.instance != null
            && GameManager.instance.isGameover)
        {
            move = 0;
            rotate = 0;
            fire = false;
            reload = false;
            return;
        }

        // move에 관한 입력 감지
        move = Input.GetAxis(moveAxisName);
        // rotate에 관한 입력 감지
        rotate = Input.GetAxis(rotateAxisName);
        // fire에 관한 입력 감지
        fire = Input.GetButton(fireButtonName);
        // reload에 관한 입력 감지
        reload = Input.GetButtonDown(reloadButtonName);
    }
}

 

 

- MonoBehaviourPun 상속과 if문을 통해 로컬플레이어를 확인하고 맞으면 입력 감지하도록 구성.

// 로컬 플레이어가 아닌 경우 입력을 받지 않음
        if (!photonView.IsMine)
        {
            return;
        }

 

 

 

@ PlayerMovement 스크립트

 

using Photon.Pun;
using UnityEngine;

// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviourPun {
    public float moveSpeed = 5f; // 앞뒤 움직임의 속도
    public float rotateSpeed = 180f; // 좌우 회전 속도

    private Animator playerAnimator; // 플레이어 캐릭터의 애니메이터
    private PlayerInput playerInput; // 플레이어 입력을 알려주는 컴포넌트
    private Rigidbody playerRigidbody; // 플레이어 캐릭터의 리지드바디

    private void Start() {
        // 사용할 컴포넌트들의 참조를 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerRigidbody = GetComponent<Rigidbody>();
        playerAnimator = GetComponent<Animator>();
    }

    // FixedUpdate는 물리 갱신 주기에 맞춰 실행됨
    private void FixedUpdate() {
        // 로컬 플레이어만 직접 위치와 회전을 변경 가능
        if (!photonView.IsMine)
        {
            return;
        }

        // 회전 실행
        Rotate();
        // 움직임 실행
        Move();

        // 입력값에 따라 애니메이터의 Move 파라미터 값을 변경
        playerAnimator.SetFloat("Move", playerInput.move);
    }

    // 입력값에 따라 캐릭터를 앞뒤로 움직임
    private void Move() {
        // 상대적으로 이동할 거리 계산
        Vector3 moveDistance =
            playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
        // 리지드바디를 통해 게임 오브젝트 위치 변경
        playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
    }

    // 입력값에 따라 캐릭터를 좌우로 회전
    private void Rotate() {
        // 상대적으로 회전할 수치 계산
        float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
        // 리지드바디를 통해 게임 오브젝트 회전 변경
        playerRigidbody.rotation =
            playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
    }
}

 

- MonoBehaviourPun 상속과 if문을 통해 로컬플레이어를 확인하고 맞으면 이동, 회전, 애니메이터 파라미터 지정하도록 구성.

 

@ PlayerShooter 스크립트

 

using Photon.Pun;
using UnityEngine;

// 주어진 Gun 오브젝트를 쏘거나 재장전
// 알맞은 애니메이션을 재생하고 IK를 사용해 캐릭터 양손이 총에 위치하도록 조정
public class PlayerShooter : MonoBehaviourPun {
    public Gun gun; // 사용할 총
    public Transform gunPivot; // 총 배치의 기준점
    public Transform leftHandMount; // 총의 왼쪽 손잡이, 왼손이 위치할 지점
    public Transform rightHandMount; // 총의 오른쪽 손잡이, 오른손이 위치할 지점

    private PlayerInput playerInput; // 플레이어의 입력
    private Animator playerAnimator; // 애니메이터 컴포넌트

    private void Start() {
        // 사용할 컴포넌트들을 가져오기
        playerInput = GetComponent<PlayerInput>();
        playerAnimator = GetComponent<Animator>();
    }

    private void OnEnable() {
        // 슈터가 활성화될 때 총도 함께 활성화
        gun.gameObject.SetActive(true);
    }

    private void OnDisable() {
        // 슈터가 비활성화될 때 총도 함께 비활성화
        gun.gameObject.SetActive(false);
    }

    private void Update() {
        // 로컬 플레이어만 총을 직접 사격, 탄약 UI 갱신 가능
        if (!photonView.IsMine)
        {
            return;
        }

        // 입력을 감지하고 총 발사하거나 재장전
        if (playerInput.fire)
        {
            // 발사 입력 감지시 총 발사
            gun.Fire();
        }
        else if (playerInput.reload)
        {
            // 재장전 입력 감지시 재장전
            if (gun.Reload())
            {
                // 재장전 성공시에만 재장전 애니메이션 재생
                playerAnimator.SetTrigger("Reload");
            }
        }

        // 남은 탄약 UI를 갱신
        UpdateUI();
    }

    // 탄약 UI 갱신
    private void UpdateUI() {
        if (gun != null && UIManager.instance != null)
        {
            // UI 매니저의 탄약 텍스트에 탄창의 탄약과 남은 전체 탄약을 표시
            UIManager.instance.UpdateAmmoText(gun.magAmmo, gun.ammoRemain);
        }
    }

    // 애니메이터의 IK 갱신
    private void OnAnimatorIK(int layerIndex) {
        // 총의 기준점 gunPivot을 3D 모델의 오른쪽 팔꿈치 위치로 이동
        gunPivot.position = playerAnimator.GetIKHintPosition(AvatarIKHint.RightElbow);

        // IK를 사용하여 왼손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandMount.rotation);

        // IK를 사용하여 오른손의 위치와 회전을 총의 오른쪽 손잡이에 맞춘다
        playerAnimator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1.0f);
        playerAnimator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1.0f);

        playerAnimator.SetIKPosition(AvatarIKGoal.RightHand, rightHandMount.position);
        playerAnimator.SetIKRotation(AvatarIKGoal.RightHand, rightHandMount.rotation);
    }
}

 

- MonoBehaviourPun 상속과 if문을 통해 로컬플레이어를 확인하고 맞으면 사용자 입력에 의한 탄알 발사와 UI 갱신을 하도록 구성.

 

@ LivingEntity 스크립트

 - 호스트에서만 체력관리와 데미지 처리 실행.

 

using System;
using Photon.Pun;
using UnityEngine;

// 생명체로서 동작할 게임 오브젝트들을 위한 뼈대를 제공
// 체력, 데미지 받아들이기, 사망 기능, 사망 이벤트를 제공
public class LivingEntity : MonoBehaviourPun, IDamageable {
    public float startingHealth = 100f; // 시작 체력
    public float health { get; protected set; } // 현재 체력
    public bool dead { get; protected set; } // 사망 상태
    public event Action onDeath; // 사망시 발동할 이벤트


    // 호스트->모든 클라이언트 방향으로 체력과 사망 상태를 동기화 하는 메서드
    [PunRPC]
    public void ApplyUpdatedHealth(float newHealth, bool newDead) {
        health = newHealth;
        dead = newDead;
    }

    // 생명체가 활성화될때 상태를 리셋
    protected virtual void OnEnable() {
        // 사망하지 않은 상태로 시작
        dead = false;
        // 체력을 시작 체력으로 초기화
        health = startingHealth;
    }

    // 데미지 처리
    // 호스트에서 먼저 단독 실행되고, 호스트를 통해 다른 클라이언트들에서 일괄 실행됨
    [PunRPC]
    public virtual void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        if (PhotonNetwork.IsMasterClient)
        {
            // 데미지만큼 체력 감소
            health -= damage;

            // 호스트에서 클라이언트로 동기화
            photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);

            // 다른 클라이언트들도 OnDamage를 실행하도록 함
            photonView.RPC("OnDamage", RpcTarget.Others, damage, hitPoint, hitNormal);
        }

        // 체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
        if (health <= 0 && !dead)
        {
            Die();
        }
    }


    // 체력을 회복하는 기능
    [PunRPC]
    public virtual void RestoreHealth(float newHealth) {
        if (dead)
        {
            // 이미 사망한 경우 체력을 회복할 수 없음
            return;
        }

        // 호스트만 체력을 직접 갱신 가능
        if (PhotonNetwork.IsMasterClient)
        {
            // 체력 추가
            health += newHealth;
            // 서버에서 클라이언트로 동기화
            photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);

            // 다른 클라이언트들도 RestoreHealth를 실행하도록 함
            photonView.RPC("RestoreHealth", RpcTarget.Others, newHealth);
        }
    }

    public virtual void Die() {
        // onDeath 이벤트에 등록된 메서드가 있다면 실행
        if (onDeath != null)
        {
            onDeath();
        }

        // 사망 상태를 참으로 변경
        dead = true;
    }
}

 

< 스크립트의 주요 변경 사항 >

 - MonoBehaviourPun 사용

 - 체력, 사망 상태 동기화를 위한 ApplyUpdatedHealth() 메서드 추가

 - Ondamage (), RestoreHealth()에 [PunRPC] 선언

 - Ondamage ()에서 데미지 처리는 호스트에서만 실행 

 - RestoreHealth()에서 체력 추가 처리는 호스트에서만 실행

 

 

- [PunRPC] 

[PunRPC] 로 선언된 메서드는 다른 클라이언트에서 원격 실행할 수 있음.

RPC를 통해 어떤 메서드를 다른 클라이언트에서 원격 실행할 때는 Photon View 컴포넌트의 RPC() 메서드를 사용함.

 - 원격 실행할 메서드 이름( string 타입)

 - 원격 실행할 대상 클라이언트 ( RpcTarget 타입 )

 - 원격 실행할 메서드에 전달할 값( 필요한 경우 )

ex. photonView.RPC("DoSomething", RpcTarget.All);

 

- ApplyUpdateHealth()

 : 새로운 체력값과 새로운 사망 상탯값을 받아 기존 변숫값을 갱신하는 메서드

 호스트 측 LivingEntity의 체력, 사망 상탯값을 다른 클라이언트의 LivingEntity에 전달하기 위해 사용.

ex. photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);

 

 

- Ondamage()

 : 호스트인 경우에만 데미지 수치를 적용하고, 그것을 호스트에서 다른 클라이언트로 전파하는 처리를 수행.

if (PhotonNetwork.IsMasterClient)
        {
            // 데미지만큼 체력 감소
            health -= damage;

            // 호스트에서 클라이언트로 동기화
            photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);

            // 다른 클라이언트들도 OnDamage를 실행하도록 함
            photonView.RPC("OnDamage", RpcTarget.Others, damage, hitPoint, hitNormal);
        }

PhotonNetwork.IsMasterClient : 현재 코드를 실행하는 클라이언트가 마스터 클라이언트(호스트)인지 반환하는 프로퍼티.

호스트면 true, 아니면 false 반환.

 

- RestoreHealth() 메서드

 : 어떤 클라이언트가 다른 클라이언트에서 원격 실행할 수 있음.

 Ondamage()와 같은 원리로 동작함.

 

 

 @ PlayerHealth 스크립트

 - 기존 기능 : 플레이어 캐릭터의 체력 관리, 체력 UI, 갱신

 - 변경 기능 : 리스폰 기능 추가, 아이템을 호스트에서만 사용.

 

using Photon.Pun;
using UnityEngine;
using UnityEngine.UI; // UI 관련 코드

// 플레이어 캐릭터의 생명체로서의 동작을 담당
public class PlayerHealth : LivingEntity {
    public Slider healthSlider; // 체력을 표시할 UI 슬라이더

    public AudioClip deathClip; // 사망 소리
    public AudioClip hitClip; // 피격 소리
    public AudioClip itemPickupClip; // 아이템 습득 소리

    private AudioSource playerAudioPlayer; // 플레이어 소리 재생기
    private Animator playerAnimator; // 플레이어의 애니메이터

    private PlayerMovement playerMovement; // 플레이어 움직임 컴포넌트
    private PlayerShooter playerShooter; // 플레이어 슈터 컴포넌트

    private void Awake() {
        // 사용할 컴포넌트를 가져오기
        playerAnimator = GetComponent<Animator>();
        playerAudioPlayer = GetComponent<AudioSource>();

        playerMovement = GetComponent<PlayerMovement>();
        playerShooter = GetComponent<PlayerShooter>();
    }

    protected override void OnEnable() {
        // LivingEntity의 OnEnable() 실행 (상태 초기화)
        base.OnEnable();

        // 체력 슬라이더 활성화
        healthSlider.gameObject.SetActive(true);
        // 체력 슬라이더의 최대값을 기본 체력값으로 변경
        healthSlider.maxValue = startingHealth;
        // 체력 슬라이더의 값을 현재 체력값으로 변경
        healthSlider.value = health;

        // 플레이어 조작을 받는 컴포넌트들 활성화
        playerMovement.enabled = true;
        playerShooter.enabled = true;
    }

    // 체력 회복
    [PunRPC]
    public override void RestoreHealth(float newHealth) {
        // LivingEntity의 RestoreHealth() 실행 (체력 증가)
        base.RestoreHealth(newHealth);
        // 체력 갱신
        healthSlider.value = health;
    }


    // 데미지 처리
    [PunRPC]
    public override void OnDamage(float damage, Vector3 hitPoint,
        Vector3 hitDirection) {
        if (!dead)
        {
            // 사망하지 않은 경우에만 효과음을 재생
            playerAudioPlayer.PlayOneShot(hitClip);
        }

        // LivingEntity의 OnDamage() 실행(데미지 적용)
        base.OnDamage(damage, hitPoint, hitDirection);
        // 갱신된 체력을 체력 슬라이더에 반영
        healthSlider.value = health;
    }

    public override void Die() {
        // LivingEntity의 Die() 실행(사망 적용)
        base.Die();

        // 체력 슬라이더 비활성화
        healthSlider.gameObject.SetActive(false);

        // 사망음 재생
        playerAudioPlayer.PlayOneShot(deathClip);

        // 애니메이터의 Die 트리거를 발동시켜 사망 애니메이션 재생
        playerAnimator.SetTrigger("Die");

        // 플레이어 조작을 받는 컴포넌트들 비활성화
        playerMovement.enabled = false;
        playerShooter.enabled = false;

        // 5초 뒤에 리스폰
        Invoke("Respawn", 5f);
    }

    private void OnTriggerEnter(Collider other) {
        // 아이템과 충돌한 경우 해당 아이템을 사용하는 처리
        // 사망하지 않은 경우에만 아이템 사용가능
        if (!dead)
        {
            // 충돌한 상대방으로 부터 Item 컴포넌트를 가져오기 시도
            IItem item = other.GetComponent<IItem>();

            // 충돌한 상대방으로부터 Item 컴포넌트가 가져오는데 성공했다면
            if (item != null)
            {
                // 호스트만 아이템 직접 사용 가능
                // 호스트에서는 아이템을 사용 후, 사용된 아이템의 효과를 모든 클라이언트들에게 동기화시킴
                if (PhotonNetwork.IsMasterClient)
                {
                    // Use 메서드를 실행하여 아이템 사용
                    item.Use(gameObject);
                }

                // 아이템 습득 소리 재생
                playerAudioPlayer.PlayOneShot(itemPickupClip);
            }
        }
    }

    // 부활 처리
    public void Respawn() {
        // 로컬 플레이어만 직접 위치를 변경 가능
        if (photonView.IsMine)
        {
            // 원점에서 반경 5유닛 내부의 랜덤한 위치 지정
            Vector3 randomSpawnPos = Random.insideUnitSphere * 5f;
            // 랜덤 위치의 y값을 0으로 변경
            randomSpawnPos.y = 0f;

            // 지정된 랜덤 위치로 이동
            transform.position = randomSpawnPos;
        }

        // 컴포넌트들을 리셋하기 위해 게임 오브젝트를 잠시 껐다가 다시 켜기
        // 컴포넌트들의 OnDisable(), OnEnable() 메서드가 실행됨
        gameObject.SetActive(false);
        gameObject.SetActive(true);
    }
}

 

주요 변경사항

 - RestorHealth(), OnDamage()에 [PunRPC] 선언

 - Respawn() 메서드 추가

 - Die() 메서드 하단에서 Respawn() 실행

 - OnTriggerEnter()의 item.Use()를 if 문으로 감싸기

 

@ [PunRPC] 선언

 - LivingEntity에서 RestoreHealth()와 OnDamage() 메서드는 [PunRPC] 속성이 선언되어있음.

 오버라이드하는 측에서도 원본 메서드와 동일하게 [PunRPC] 속성을 선언해야 정상적으로 RPC를 통해 원격 실행 가능.

 

 @ Die() 메서드

 기존 Die() 메서드에 Invoke("Respawn", 5f);를 추가.

 - Invoke() 메서드는 특정 메서드를 지연실행하는 메서드. Invoke ( 메서드이름, 지연시간 );

 

 @ Respawn() 메서드

 - 사망한 플레이어 캐릭터를 부활시켜 재배치(리스폰)하는 메서드

 - 부활처리는 게임오브젝트를 끄고 켜는 간단한 방식으로 구현

// 컴포넌트들을 리셋하기 위해 게임 오브젝트를 잠시 껐다가 다시 켜기
        // 컴포넌트들의 OnDisable(), OnEnable() 메서드가 실행됨
        gameObject.SetActive(false);
        gameObject.SetActive(true);

 

이게 가능한 이유는 6부에서 캐릭터 관련 스크립트를 작성할 때 수치 초기화를 Awake() 또는 Start() 대신에 OnEnable() 메서드에 몰아뒀기 때문임.

 - 게임 오브젝트를 끄고 다시 켜면  PlayerHealth, LivingEntity 등의 스크립트의 OnEnable() 메서드가 자동 실행되고, 체력 등 각종 수치가 기본값으로 다시 리셋 됨.

 

public void Respawn() {
        // 로컬 플레이어만 직접 위치를 변경 가능
        if (photonView.IsMine)
        {
            // 원점에서 반경 5유닛 내부의 랜덤한 위치 지정
            Vector3 randomSpawnPos = Random.insideUnitSphere * 5f;
            // 랜덤 위치의 y값을 0으로 변경
            randomSpawnPos.y = 0f;

            // 지정된 랜덤 위치로 이동
            transform.position = randomSpawnPos;
        }
        }

 

@ OnTriggerEnter() 메서드

  기존엔 충돌한 아이템을 감지하고 사용하는 처리를 구현.

 변경은 기존 아이템 사용처리 item.use(gameObject);를 if문으로 감싸서 호스트에서만 실행.

 - 호스트가 아이템 사용 결과를 다른 클라이언트로 전파할 수 있도록 아이템 스크립트를 수정해야함.

 

 @ 네트워크 Gun

  - 부모 게임 오브젝트에 Photon View 컴포넌트가 이미 추가되어 있다고 해도 자식게임 오브젝트에서 독자적으로 실행할 네트워크 처리가 있다면 자식 게임 오브젝트에서도 Photon View 컴포넌트를 추가해서 View ID를 부여해야 함.

 

 기존기능 : 사격실행, 사격이펙트 재생, 재장전 실행, 탄알 관리 

 변경기능 : 실제 사격 처리 부분을 호스트에서만 실행, 상태 동기화

 

@ 변경사항

 - MonoBehaviourPun 사용

 - IPunObservable 인터페이스 상속, OnPhotonSerializeView() 메서드 구현

 - 새로운 RPC 메서드 AddAmmo() 추가

 - Shot()의 사격 처리 부분을 새로운 RPC 메서드 ShotProcessOnServer()로 옮김

 - ShotEffect()를 새로운 RPC 메서드 ShotEffectProcessOnClients()로 감쌈

 

 - OnPhotonSerializeView 메서드의 입력으로 들어오는 stream은 현재 클라이언트에서 다른 클라이언트로 보낼 값을 쓰거나, 다른 클라이언트가 보내온 값을 읽을 때 사용할 스트림 형태의 데이터 컨테이너.

 - stram.IsWriting은 현재 스트림이 쓰기모드인지 반환함.

// 로컬 게임오브젝트 = 쓰기 모드 = true, 리모트 게임 오브젝트 = 읽기 모드 = false

 

 @ ReceiveNext() 메서드를 통해 값이 들어올 때는 범용적인 object 타입으로 들어오기 때문에 읽는 측에서 원본 타입으로 형변환 함. 

 - 또한 스트림에 삽입한 순서대로 값이 도착하기 때문에 SendNext()와 ReceiveNext() 메서드간 주고받는 변수들의 나열 순서가 일치해야 함.

 

 

 

관련글 더보기