19장. 좀비 서바이벌 멀티 플레이어2( 네트워크 게임 월드 구현 )
@ 변경된 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);