카테고리 없음

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

MJ_119 2024. 5. 23. 01:55

@ 변경된 Zombie 스크립트

 

 - 기존 기능 : 경로 계산, 목표 추적 및 공격

 - 변경된 기능 : 호스트에서만 경로 계산, 추적, 공격을 실행

using System.Collections;
using Photon.Pun;
using UnityEngine;
using UnityEngine.AI; // AI, 내비게이션 시스템 관련 코드를 가져오기

// 좀비 AI 구현
public class Zombie : LivingEntity {
    public LayerMask whatIsTarget; // 공격 대상 레이어

    private LivingEntity targetEntity; // 추적할 대상
    private NavMeshAgent navMeshAgent; // 경로계산 AI 에이전트

    public ParticleSystem hitEffect; // 피격시 재생할 파티클 효과
    public AudioClip deathSound; // 사망시 재생할 소리
    public AudioClip hitSound; // 피격시 재생할 소리

    private Animator zombieAnimator; // 애니메이터 컴포넌트
    private AudioSource zombieAudioPlayer; // 오디오 소스 컴포넌트
    private Renderer zombieRenderer; // 렌더러 컴포넌트

    public float damage = 20f; // 공격력
    public float timeBetAttack = 0.5f; // 공격 간격
    private float lastAttackTime; // 마지막 공격 시점


    // 추적할 대상이 존재하는지 알려주는 프로퍼티
    private bool hasTarget
    {
        get
        {
            // 추적할 대상이 존재하고, 대상이 사망하지 않았다면 true
            if (targetEntity != null && !targetEntity.dead)
            {
                return true;
            }

            // 그렇지 않다면 false
            return false;
        }
    }

    private void Awake() {
        // 게임 오브젝트로부터 사용할 컴포넌트들을 가져오기
        navMeshAgent = GetComponent<NavMeshAgent>();
        zombieAnimator = GetComponent<Animator>();
        zombieAudioPlayer = GetComponent<AudioSource>();

        // 렌더러 컴포넌트는 자식 게임 오브젝트에게 있으므로
        // GetComponentInChildren() 메서드를 사용
        zombieRenderer = GetComponentInChildren<Renderer>();
    }

    // 좀비 AI의 초기 스펙을 결정하는 셋업 메서드
    [PunRPC]
    public void Setup(float newHealth, float newDamage,
        float newSpeed, Color skinColor) {
        // 체력 설정
        startingHealth = newHealth;
        health = newHealth;
        // 공격력 설정
        damage = newDamage;
        // 내비메쉬 에이전트의 이동 속도 설정
        navMeshAgent.speed = newSpeed;
        // 렌더러가 사용중인 머테리얼의 컬러를 변경, 외형 색이 변함
        zombieRenderer.material.color = skinColor;
    }
    
    private void Start() {
        // 호스트가 아니라면 AI의 추적 루틴을 실행하지 않음
        if (!PhotonNetwork.IsMasterClient)
        {
            return;
        }

        // 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작
        StartCoroutine(UpdatePath());
    }

    private void Update() {
        // 호스트가 아니라면 애니메이션의 파라미터를 직접 갱신하지 않음
        // 호스트가 파라미터를 갱신하면 클라이언트들에게 자동으로 전달되기 때문.
        if (!PhotonNetwork.IsMasterClient)
        {
            return;
        }

        // 추적 대상의 존재 여부에 따라 다른 애니메이션을 재생
        zombieAnimator.SetBool("HasTarget", hasTarget);
    }

    // 주기적으로 추적할 대상의 위치를 찾아 경로를 갱신
    private IEnumerator UpdatePath() {
        // 살아있는 동안 무한 루프
        while (!dead)
        {
            if (hasTarget)
            {
                // 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
                navMeshAgent.isStopped = false;
                navMeshAgent.SetDestination(targetEntity.transform.position);
            }
            else
            {
                // 추적 대상 없음 : AI 이동 중지
                navMeshAgent.isStopped = true;

                // 20 유닛의 반지름을 가진 가상의 구를 그렸을때, 구와 겹치는 모든 콜라이더를 가져옴
                // 단, targetLayers에 해당하는 레이어를 가진 콜라이더만 가져오도록 필터링
                Collider[] colliders =
                    Physics.OverlapSphere(transform.position, 20f, whatIsTarget);

                // 모든 콜라이더들을 순회하면서, 살아있는 플레이어를 찾기
                for (int i = 0; i < colliders.Length; i++)
                {
                    // 콜라이더로부터 LivingEntity 컴포넌트 가져오기
                    LivingEntity livingEntity = colliders[i].GetComponent<LivingEntity>();

                    // LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아있다면,
                    if (livingEntity != null && !livingEntity.dead)
                    {
                        // 추적 대상을 해당 LivingEntity로 설정
                        targetEntity = livingEntity;

                        // for문 루프 즉시 정지
                        break;
                    }
                }
            }

            // 0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }


    // 데미지를 입었을때 실행할 처리
    [PunRPC]
    public override void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        // 아직 사망하지 않은 경우에만 피격 효과 재생
        if (!dead)
        {
            // 공격 받은 지점과 방향으로 파티클 효과를 재생
            hitEffect.transform.position = hitPoint;
            hitEffect.transform.rotation = Quaternion.LookRotation(hitNormal);
            hitEffect.Play();

            // 피격 효과음 재생
            zombieAudioPlayer.PlayOneShot(hitSound);
        }

        // LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage, hitPoint, hitNormal);
    }

    // 사망 처리
    public override void Die() {
        // LivingEntity의 Die()를 실행하여 기본 사망 처리 실행
        base.Die();

        // 다른 AI들을 방해하지 않도록 자신의 모든 콜라이더들을 비활성화
        Collider[] zombieColliders = GetComponents<Collider>();
        for (int i = 0; i < zombieColliders.Length; i++)
        {
            zombieColliders[i].enabled = false;
        }

        // AI 추적을 중지하고 내비메쉬 컴포넌트를 비활성화
        navMeshAgent.isStopped = true;
        navMeshAgent.enabled = false;

        // 사망 애니메이션 재생
        zombieAnimator.SetTrigger("Die");

        // 사망 효과음 재생
        zombieAudioPlayer.PlayOneShot(deathSound);
    }

    private void OnTriggerStay(Collider other) {
        // 호스트가 아니라면 공격 실행 불가
        if (!PhotonNetwork.IsMasterClient)
        {
            return;
        }

        // 자신이 사망하지 않았으며,
        // 최근 공격 시점에서 timeBetAttack 이상 시간이 지났다면 공격 가능
        if (!dead && Time.time >= lastAttackTime + timeBetAttack)
        {
            // 상대방으로부터 LivingEntity 타입을 가져오기 시도
            LivingEntity attackTarget
                = other.GetComponent<LivingEntity>();

            // 상대방의 LivingEntity가 자신의 추적 대상이라면 공격 실행
            if (attackTarget != null && attackTarget == targetEntity)
            {
                // 최근 공격 시간을 갱신
                lastAttackTime = Time.time;

                // 상대방의 피격 위치와 피격 방향을 근삿값으로 계산
                Vector3 hitPoint = other.ClosestPoint(transform.position);
                Vector3 hitNormal = transform.position - other.transform.position;

                // 공격 실행
                attackTarget.OnDamage(damage, hitPoint, hitNormal);
            }
        }
    }
}

 

- 변경 사항

 Setup(), OnDamage() 메서드에 [PunRPC] 선언

 Start(), Update(), OnTriggerStay()를 호스트에서만 실행

 

생성한 좀비가 모든 클라이언트에서 동일한 능력치를 가지게 하려면 모든 클라이언트에서 좀비의 Setup() 메서드가 실행되어야 함. 따라서 Setup() 메서드는 [PunRPC] 속성으로 선언되어야 함.

 

 

 

@ 네트워크 아이템

 - 기존 기능 : 플레이어의 탄알 추가, 효과 적용 후 스스로를 파괴

 - 변경된 기능 : 탄알 추가를 모든 클라이언트에서 실행, 모든 클라이언트에서 스스로를 파괴

 

using Photon.Pun;
using UnityEngine;

// 총알을 충전하는 아이템
public class AmmoPack : MonoBehaviourPun, IItem {
    public int ammo = 30; // 충전할 총알 수

    public void Use(GameObject target) {
        // 전달 받은 게임 오브젝트로부터 PlayerShooter 컴포넌트를 가져오기 시도
        PlayerShooter playerShooter = target.GetComponent<PlayerShooter>();

        // PlayerShooter 컴포넌트가 있으며, 총 오브젝트가 존재하면
        if (playerShooter != null && playerShooter.gun != null)
        {
            // 총의 남은 탄환 수를 ammo 만큼 더하기, 모든 클라이언트에서 실행
            playerShooter.gun.photonView.RPC("AddAmmo", RpcTarget.All, ammo);
        }

        // 모든 클라이언트에서의 자신을 파괴
        PhotonNetwork.Destroy(gameObject);
    }
}

 

변경 사항

 MonoBehaviourPun 사용

 AddAmmo()를 RPC로 원격 실행하여 탄알 추가

Destroy() 대신 PhotonNetwork.Destroy() 메서드 사용

 

 

@ HealthPack 스크립트

 

 - 기존 기능 : 플레이어의 체력 추가, 효과 적용 후 스스로를 파괴

 - 변경된 기능 : 모든 클라이언트에서 스스로를 파괴

 

using Photon.Pun;
using UnityEngine;

// 체력을 회복하는 아이템
public class HealthPack : MonoBehaviourPun, IItem {
    public float health = 50; // 체력을 회복할 수치

    public void Use(GameObject target) {
        // 전달받은 게임 오브젝트로부터 LivingEntity 컴포넌트 가져오기 시도
        LivingEntity life = target.GetComponent<LivingEntity>();

        // LivingEntity컴포넌트가 있다면
        if (life != null)
        {
            // 체력 회복 실행
            life.RestoreHealth(health);
        }

        // 모든 클라이언트에서의 자신을 파괴
        PhotonNetwork.Destroy(gameObject);
    }
}

 

 

@ ItemSpawner 스크립트

Photon View 컴포넌트 추가, Player Transform 필드 삭제.

 

using System.Collections;
using Photon.Pun;
using UnityEngine;
using UnityEngine.AI; // 내비메쉬 관련 코드

// 주기적으로 아이템을 플레이어 근처에 생성하는 스크립트
public class ItemSpawner : MonoBehaviourPun {
    public GameObject[] items; // 생성할 아이템들

    public float maxDistance = 5f; // 플레이어 위치로부터 아이템이 배치될 최대 반경

    public float timeBetSpawnMax = 7f; // 최대 시간 간격
    public float timeBetSpawnMin = 2f; // 최소 시간 간격

    private float timeBetSpawn; // 생성 간격
    private float lastSpawnTime; // 마지막 생성 시점

    private void Start() {
        // 생성 간격과 마지막 생성 시점 초기화
        timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
        lastSpawnTime = 0;
    }

    // 주기적으로 아이템 생성 처리 실행
    private void Update() {
        // 호스트에서만 아이템 직접 생성 가능
        if (!PhotonNetwork.IsMasterClient)
        {
            return;
        }

        if (Time.time >= lastSpawnTime + timeBetSpawn)
        {
            // 마지막 생성 시간 갱신
            lastSpawnTime = Time.time;
            // 생성 주기를 랜덤으로 변경
            timeBetSpawn = Random.Range(timeBetSpawnMin, timeBetSpawnMax);
            // 실제 아이템 생성
            Spawn();
        }
    }

    // 실제 아이템 생성 처리
    private void Spawn() {
        // (0,0,0)을 기준으로 maxDistance 안에서 내비메시위의 랜덤 위치 지정
        Vector3 spawnPosition = GetRandomPointOnNavMesh(Vector3.zero, maxDistance);
        // 바닥에서 0.5만큼 위로 올리기
        spawnPosition += Vector3.up * 0.5f;

        // 생성할 아이템을 무작위로 하나 선택
        GameObject itemToCreate = items[Random.Range(0, items.Length)];

        // 네트워크의 모든 클라이언트에서 해당 아이템 생성
        GameObject item =
            PhotonNetwork.Instantiate(itemToCreate.name, spawnPosition,
                Quaternion.identity);

        // 생성한 아이템을 5초 뒤에 파괴
        StartCoroutine(DestroyAfter(item, 5f));
    }

    // 포톤의 PhotonNetwork.Destroy()를 지연 실행하는 코루틴 
    IEnumerator DestroyAfter(GameObject target, float delay) {
        // delay 만큼 대기
        yield return new WaitForSeconds(delay);

        // target이 파괴되지 않았으면 파괴 실행
        if (target != null)
        {
            PhotonNetwork.Destroy(target);
        }
    }

    // 네브 메시 위의 랜덤한 위치를 반환하는 메서드
    // center를 중심으로 distance 반경 안에서 랜덤한 위치를 찾는다.
    private Vector3 GetRandomPointOnNavMesh(Vector3 center, float distance) {
        // center를 중심으로 반지름이 maxDinstance인 구 안에서의 랜덤한 위치 하나를 저장
        // Random.insideUnitSphere는 반지름이 1인 구 안에서의 랜덤한 한 점을 반환하는 프로퍼티
        Vector3 randomPos = Random.insideUnitSphere * distance + center;

        // 네브 메시 샘플링의 결과 정보를 저장하는 변수
        NavMeshHit hit;

        // randomPos를 기준으로 maxDistance 반경 안에서, randomPos에 가장 가까운 네브 메시 위의 한 점을 찾음
        NavMesh.SamplePosition(randomPos, out hit, distance, NavMesh.AllAreas);

        // 찾은 점 반환
        return hit.position;
    }
}

 

주요 변경 사항

 - 플레이어 캐릭터 위치를 사용하지 않음

 - 호스트에서만 아이템 생성

 - 아이템 생성은 PhotonNetwork.Instantiate() 사용

 - 아이템 파괴는 PhotonNetwork.Destroy() 사용

 

 @ 아이템을 월드의 중심에서 maxDistance 반경 내의 랜덤 위치에 생성.

 

 @ PhotonNetwork.Instantiate() 사용

 - Instantiate() 메서드를 사용하면 로컬에서만 게임 오브젝트가 생성됨.

 단, PhotonNetwork.Instantiate() 메서드는 프리팹을 직접 받지 못하고 프리팹의 이름을 받기 때문에 여러 개의 아이템 프리팹 중 선택한 아이템 프리팹의 이름을 넣도록 코드를 수정.

// 생성할 아이템을 무작위로 하나 선택
GameObject itemToCreate = items[Random.Range(0, items.Length)];

// 네트워크의 모든 클라이언트에서 해당 아이템 생성
GameObject item = PhotonNetwork.Instantiate(itemToCreate.name, spawnPosition, Quaternion.identity);

 

 

 

@ PhotonNetwork.Destroy() 사용

// 생성한 아이템을 5초 뒤에 파괴
        StartCoroutine(DestroyAfter(item, 5f));
        
        

    // 포톤의 PhotonNetwork.Destroy()를 지연 실행하는 코루틴 
    IEnumerator DestroyAfter(GameObject target, float delay) {
        // delay 만큼 대기
        yield return new WaitForSeconds(delay);

        // target이 파괴되지 않았으면 파괴 실행
        if (target != null)
        {
            PhotonNetwork.Destroy(target);
        }
    }

 

 - 멀티플레이 게임에서는 생성한 아이템을 모든 클라이언트에서 동시에 파괴해야 하기 때문에 PhotonNetwork.Destroy() 메서드 사용.

 단, PhotonNetwork.Destroy() 메서드는 지연시간을 받지 못하므로, 코루틴을 추가해서 delay 시간 만큼 지연한뒤 실행.

 

@ 네트워크 게임 매니저( GameManager 스크립트 )

 기존 기능 : 게임 점수와 게임오버 상태 관리

 변경된 기능 : 네트워크 플레이어 캐릭터 생성, 게임 점수 동기화, 룸 나가기 구현

 

using Photon.Pun;
using UnityEngine;
using UnityEngine.SceneManagement;

// 점수와 게임 오버 여부, 게임 UI를 관리하는 게임 매니저
public class GameManager : MonoBehaviourPunCallbacks, IPunObservable {
    // 외부에서 싱글톤 오브젝트를 가져올때 사용할 프로퍼티
    public static GameManager instance
    {
        get
        {
            // 만약 싱글톤 변수에 아직 오브젝트가 할당되지 않았다면
            if (m_instance == null)
            {
                // 씬에서 GameManager 오브젝트를 찾아 할당
                m_instance = FindObjectOfType<GameManager>();
            }

            // 싱글톤 오브젝트를 반환
            return m_instance;
        }
    }

    private static GameManager m_instance; // 싱글톤이 할당될 static 변수

    public GameObject playerPrefab; // 생성할 플레이어 캐릭터 프리팹

    private int score = 0; // 현재 게임 점수
    public bool isGameover { get; private set; } // 게임 오버 상태

    // 주기적으로 자동 실행되는, 동기화 메서드
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        // 로컬 오브젝트라면 쓰기 부분이 실행됨
        if (stream.IsWriting)
        {
            // 네트워크를 통해 score 값을 보내기
            stream.SendNext(score);
        }
        else
        {
            // 리모트 오브젝트라면 읽기 부분이 실행됨         

            // 네트워크를 통해 score 값 받기
            score = (int) stream.ReceiveNext();
            // 동기화하여 받은 점수를 UI로 표시
            UIManager.instance.UpdateScoreText(score);
        }
    }


    private void Awake() {
        // 씬에 싱글톤 오브젝트가 된 다른 GameManager 오브젝트가 있다면
        if (instance != this)
        {
            // 자신을 파괴
            Destroy(gameObject);
        }
    }

    // 게임 시작과 동시에 플레이어가 될 게임 오브젝트를 생성
    private void Start() {
        // 생성할 랜덤 위치 지정
        Vector3 randomSpawnPos = Random.insideUnitSphere * 5f;
        // 위치 y값은 0으로 변경
        randomSpawnPos.y = 0f;

        // 네트워크 상의 모든 클라이언트들에서 생성 실행
        // 단, 해당 게임 오브젝트의 주도권은, 생성 메서드를 직접 실행한 클라이언트에게 있음
        PhotonNetwork.Instantiate(playerPrefab.name, randomSpawnPos, Quaternion.identity);
    }

    // 점수를 추가하고 UI 갱신
    public void AddScore(int newScore) {
        // 게임 오버가 아닌 상태에서만 점수 증가 가능
        if (!isGameover)
        {
            // 점수 추가
            score += newScore;
            // 점수 UI 텍스트 갱신
            UIManager.instance.UpdateScoreText(score);
        }
    }

    // 게임 오버 처리
    public void EndGame() {
        // 게임 오버 상태를 참으로 변경
        isGameover = true;
        // 게임 오버 UI를 활성화
        UIManager.instance.SetActiveGameoverUI(true);
    }

    // 키보드 입력을 감지하고 룸을 나가게 함
    private void Update() {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            PhotonNetwork.LeaveRoom();
        }
    }

    // 룸을 나갈때 자동 실행되는 메서드
    public override void OnLeftRoom() {
        // 룸을 나가면 로비 씬으로 돌아감
        SceneManager.LoadScene("Lobby");
    }
}

 

주요 변경 사항

 - MonoBehaviourPunCallbacks 상속

 - 룸 나가기(로비로 돌아가기) 구현

 - IPunObservable 상속, OnPhotonSerializeView() 구현

 - Start()에서 로컬 플레이어 캐릭터 생성

 

@ MonoBehaviourPunCallbacks를 상속한 스크립트는 여러 Photon 이벤트를 감지할 수 있음.

 GameManager 스크립트는 OnLeftRoom() 이벤트를 감지하고 해당 메서드를 자동 실행하기 위해 MonoBehaviourPunCallbacks을 상속함.

 

 @ OnLeftRoom() 메서드 : 로컬 플레이어가 현재 게임 룸을 나갈 때 자동 실행. ( 로비로 돌아감 )

// 룸을 나갈때 자동 실행되는 메서드
    public override void OnLeftRoom() {
        // 룸을 나가면 로비 씬으로 돌아감
        SceneManager.LoadScene("Lobby");
    }

 

@ PhotonNetwork.LeaveRoom() : 현재 네트워크 룸을 나가는 메서드. 룸을 나가고 네트워크 접속이 종료된다고 해도 이것이 씬을 전환한다는 의미는 아니므로 OnLeftRoom()을 구현하여 로비 씬으로 돌아감.

 

 @ 호스트 입장에서 Game Manager 게임 오브젝트는 로컬임. 따라서 IPunObservable 인터페이스를 상속하고 OnPhotonSerializeView 메서드를 구현하여 로컬에서 리모트로 점수 동기화를 구현하면 호스트의 갱신된 점수가 다른 클라이언트에도 자동 반영 됨.

 

 @ 호스트에서는 AddScore() 메서드가 실행되면서 UI가 갱신됨, 그러나 다른 클라이언트에서는 AddScore() 메서드가 실행되지 못하므로 동기화가 실행되는 시점에 UI를 갱신하도록 함.

 

@ 좀비 생성기 포팅 ( Zombie Spawner )

 

- ZombieSpawner 스크립트

 

 기존 기능 : 좀비 생성과 사망 시의 처리 등록, 남은 좀비를 UI로 표시

 변경된 기능 : 네트워크상에서 좀비 생성, 남은 종비 수 동기화

 

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


// 좀비 게임 오브젝트를 주기적으로 생성
public class ZombieSpawner : MonoBehaviourPun, IPunObservable {
    public Zombie zombiePrefab; // 생성할 좀비 원본 프리팹
    
    public ZombieData[] zombieDatas; // 사용할 좀비 셋업 데이터들
    public Transform[] spawnPoints; // 좀비 AI를 소환할 위치들
    
    private List<Zombie> zombies = new List<Zombie>(); // 생성된 좀비들을 담는 리스트

    private int zombieCount = 0; // 남은 좀비 수
    private int wave; // 현재 웨이브

    // 주기적으로 자동 실행되는, 동기화 메서드
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        // 로컬 오브젝트라면 쓰기 부분이 실행됨
        if (stream.IsWriting)
        {
            // 남은 좀비 수를 네트워크를 통해 보내기
            stream.SendNext(zombies.Count);
            // 현재 웨이브를 네트워크를 통해 보내기
            stream.SendNext(wave);
        }
        else
        {
            // 리모트 오브젝트라면 읽기 부분이 실행됨
            // 남은 좀비 수를 네트워크를 통해 받기
            zombieCount = (int) stream.ReceiveNext();
            // 현재 웨이브를 네트워크를 통해 받기 
            wave = (int) stream.ReceiveNext();
        }
    }

    private void Awake() {
        PhotonPeer.RegisterType(typeof(Color), 128, ColorSerialization.SerializeColor,
            ColorSerialization.DeserializeColor);
    }

    private void Update() {
        // 호스트만 좀비를 직접 생성할 수 있음
        // 다른 클라이언트들은 호스트가 생성한 좀비를 동기화를 통해 받아옴
        if (PhotonNetwork.IsMasterClient)
        {
            // 게임 오버 상태일때는 생성하지 않음
            if (GameManager.instance != null && GameManager.instance.isGameover)
            {
                return;
            }

            // 좀비들을 모두 물리친 경우 다음 스폰 실행
            if (zombies.Count <= 0)
            {
                SpawnWave();
            }
        }

        // UI 갱신
        UpdateUI();
    }

    // 웨이브 정보를 UI로 표시
    private void UpdateUI() {
        if (PhotonNetwork.IsMasterClient)
        {
            // 호스트는 직접 갱신한 좀비 리스트를 통해 남은 좀비의 수를 표시함
            UIManager.instance.UpdateWaveText(wave, zombies.Count);
        }
        else
        {
            // 클라이언트는 좀비 리스트를 갱신할 수 없으므로, 호스트가 보내준 zombieCount를 통해 좀비의 수를 표시함
            UIManager.instance.UpdateWaveText(wave, zombieCount);
        }
    }

    // 현재 웨이브에 맞춰 좀비를 생성
    private void SpawnWave() {
        // 웨이브 1 증가
        wave++;

        // 현재 웨이브 * 1.5에 반올림 한 개수 만큼 좀비를 생성
        int spawnCount = Mathf.RoundToInt(wave * 1.5f);

        // spawnCount 만큼 좀비 생성
        for (int i = 0; i < spawnCount; i++)
        {
            // 좀비 생성 처리 실행
            CreateZombie();
        }
    }

    // 좀비 생성
    private void CreateZombie() {
        // 사용할 좀비 데이터 랜덤으로 결정
        ZombieData zombieData = zombieDatas[Random.Range(0, zombieDatas.Length)];

        // 생성할 위치를 랜덤으로 결정
        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];

        // 좀비 프리팹으로부터 좀비 생성, 네트워크 상의 모든 클라이언트들에게 생성됨
        GameObject createdZombie = PhotonNetwork.Instantiate(zombiePrefab.gameObject.name,
            spawnPoint.position,
            spawnPoint.rotation);
        
        // 생성한 좀비를 셋업하기 위해 Zombie 컴포넌트를 가져옴
        Zombie zombie = createdZombie.GetComponent<Zombie>();

        // 생성한 좀비의 능력치 설정
        zombie.photonView.RPC("Setup", RpcTarget.All, zombieData.health, zombieData.damage, zombieData.speed, zombieData.skinColor);

        // 생성된 좀비를 리스트에 추가
        zombies.Add(zombie);

        // 좀비의 onDeath 이벤트에 익명 메서드 등록
        // 사망한 좀비를 리스트에서 제거
        zombie.onDeath += () => zombies.Remove(zombie);
        // 사망한 좀비를 10 초 뒤에 파괴
        zombie.onDeath += () => StartCoroutine(DestroyAfter(zombie.gameObject, 10f));
        // 좀비 사망시 점수 상승
        zombie.onDeath += () => GameManager.instance.AddScore(100);
    }

    // 포톤의 Network.Destroy()는 지연 파괴를 지원하지 않으므로 지연 파괴를 직접 구현함
    IEnumerator DestroyAfter(GameObject target, float delay) {
        // delay 만큼 쉬고
        yield return new WaitForSeconds(delay);
    
        // target이 아직 파괴되지 않았다면
        if (target != null)
        {
            // target을 모든 네트워크 상에서 파괴
            PhotonNetwork.Destroy(target);
        }
    }
}

 

주요 변경 사항

 - zombieCount 변수 추가

 - IPunObservable 상속, OnPhotonSerializeView () 구현

 - CreateZombie()에서 Zombie의 Setup() 메서드를 RPC로 원격 실행

 - DestroyAfter() 코루틴 메서드 추가

 - Awake() 메서드에서 Photon.Peer.RegisterType() 실행

 

 @ 좀비 생성은 호스트의 로컬에서만 생성됨. 다른 클라이언트는 호스트의 좀비 오브젝트의 복제본을 네트워크를 통해 건네받음.

 

 @ 좀비의 능력치와 피부색을 모든 클라이언트에서 같게 함

기존 코드 :

zombie.Setup(zombieData);

 

변경된 코드 : 

// 생성한 좀비의 능력치 설정
        zombie.photonView.RPC("Setup", RpcTarget.All, zombieData.health, zombieData.damage, zombieData.speed, zombieData.skinColor);