본문 바로가기
프로그래밍

Zombie Survive 스크립트, 기능 설명

by HyunS_PG 2020. 6. 1.
반응형

 

※목차

1. 멀티플레이 - Photon, 동기화

2. 적 - NavMesh, 추적연산, 기타

3. 플레이어 - 애니메이션, 공격

4. 기타

5. 전체 스크립트 및 게임 다운로드

 

 

 

 

 

1. 멀티플레이 - Photon, 동기화              

스크립트)

1) Photon 멀티환경 구축

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using Photon.Pun; // 유니티용 포톤 컴포넌트들
using Photon.Realtime; // 포톤 서비스 관련 라이브러리
using UnityEngine;
using UnityEngine.UI;
 
// 마스터(매치 메이킹) 서버와 룸 접속을 담당
public class LobbyManager : MonoBehaviourPunCallbacks
{
    private string gameVersion = "1"// 게임 버전
 
    public Text connectionInfoText; // 네트워크 정보를 표시할 텍스트
    public Button joinButton; // 룸 접속 버튼
 
    // 게임 실행과 동시에 마스터 서버 접속 시도
    private void Start() {
        PhotonNetwork.GameVersion = gameVersion;
        PhotonNetwork.ConnectUsingSettings();
 
        joinButton.interactable = false;
        connectionInfoText.text = "마스터 서버에 접속 중...";
    }
 
    // 마스터 서버 접속 성공시 자동 실행
    public override void OnConnectedToMaster() {
        joinButton.interactable = true;
        connectionInfoText.text = "온라인 : 마스터 서버와 연결됨";
    }
 
    // 마스터 서버 접속 실패시 자동 실행
    public override void OnDisconnected(DisconnectCause cause) {
        joinButton.interactable = false;
        connectionInfoText.text = "오프라인 : 마스터 서버와 연결되지 않음/n" + "접속 재시도 중...";
 
        PhotonNetwork.ConnectUsingSettings();
    }
 
    // 룸 접속 시도
    public void Connect() {
        joinButton.interactable = false;
 
        if(PhotonNetwork.IsConnected) {
            connectionInfoText.text = "룸에 접속...";
            PhotonNetwork.JoinRandomRoom();
        }
        else {
            connectionInfoText.text = "오프라인 : 마스터 서버와 연결되지 않음/n" + "접속 재시도 중...";
            PhotonNetwork.ConnectUsingSettings();
        }
    }
 
    // (빈 방이 없어)랜덤 룸 참가에 실패한 경우 자동 실행
    public override void OnJoinRandomFailed(short returnCode, string message) {
        connectionInfoText.text = "빈 방이 없음, 새로운 방 생성...";
        PhotonNetwork.CreateRoom(nullnew RoomOptions { MaxPlayers = 4 });
    }
 
    // 룸에 참가 완료된 경우 자동 실행
    public override void OnJoinedRoom() {
        connectionInfoText.text = "방 참가 성공";
        PhotonNetwork.LoadLevel("Main");
    }
}
cs

Photon 서버에 로그인하여 Unity와 연결

 

 

 

2) stream.IsWriting을 통해 PhotonStream의 원격 데이터 값 쓰고 읽기(게임 점수, 적 남은 수, 웨이브 데이터)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GameManager : MonoBehaviourPunCallbacks, IPunObservable {
    (생략)
    // 주기적으로 자동 실행되는, 동기화 메서드
    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);
        }
    }
    (생략)
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EnemySpawner : MonoBehaviourPun, IPunObservable {
    (생략)
    // 주기적으로 자동 실행되는, 동기화 메서드
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        // 로컬 오브젝트라면 쓰기 부분이 실행됨
        if (stream.IsWriting) {
            // 적의 남은 수를 네트워크를 통해 보내기
            stream.SendNext(enemies.Count);
            // 현재 웨이브를 네트워크를 통해 보내기
            stream.SendNext(wave);
        }
        else {
            // 리모트 오브젝트라면 읽기 부분이 실행됨
            // 적의 남은 수를 네트워크를 통해 받기
            enemyCount = (int) stream.ReceiveNext();
            // 현재 웨이브를 네트워크를 통해 받기 
            wave = (int) stream.ReceiveNext();
        }
    }
    (생략)
}
cs

 

 

3) 적의 추적하는 연산은 호스트만 연산, 애니메이션은 Photon Animator View에서 갱신되므로 호스트만 재생.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Enemy : LivingEntity {
    (생략)
    private void Start() {
        // 호스트가 아니라면 AI의 추적 루틴을 실행하지 않음
        if (!PhotonNetwork.IsMasterClient) {
            return;
        }
 
        // 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작
        StartCoroutine(UpdatePath());
    }
 
    private void Update() {
        // 호스트가 아니라면 애니메이션의 파라미터를 직접 갱신하지 않음
        // 호스트가 파라미터를 갱신하면 클라이언트들에게 자동으로 전달되기 때문.
        if (!PhotonNetwork.IsMasterClient) {
            return;
        }
 
        // 추적 대상의 존재 여부에 따라 다른 애니메이션을 재생
        enemyAnimator.SetBool("HasTarget", hasTarget);
    }
    (생략)
}
cs
Zombie Prefab에 Photon View 적용

 

4) RPC(Remote Procedure Call)를 사용하여 호스트가 연산 후 다른 클라이언트에서도 연산할 수 있도록 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LivingEntity : MonoBehaviourPun, IDamageable {
    (생략)
    // 데미지 처리
    // 호스트에서 먼저 단독 실행되고, 호스트를 통해 다른 클라이언트들에서 일괄 실행됨
    [PunRPC]
    public virtual void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        if (PhotonNetwork.IsMasterClient) {
            // 데미지만큼 체력 감소
            health -= damage;
 
            healthSlider.value = health;
 
            // 호스트에서 클라이언트로 동기화
            photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);
 
            // 다른 클라이언트들도 OnDamage를 실행하도록 함
            photonView.RPC("OnDamage", RpcTarget.Others, damage, hitPoint, hitNormal);
        }
 
        // 체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
        if (health <= 0 && !dead) {
            Die();
        }
    }
    (생략)
}
cs

 

 

 

 

2. 적 - NavMesh, 추적연산      

스크립트 및 기능) 

1) Nav Mesh Agent를 사용하여 장애물이 놓여도 목적지/목표물까지 최단거리로 갈 수 있도록 함.

Zombie Prefab에 Nav Mesh Agent 적용
맵에 오브젝트가 다닐 수 있는 공간을 설정

 

 

2) 적이 목표물(플레이어)를 추적할 때 가장 가까운 대상을 목표로 추적할 수 있도록 연산.
 - whatIsTarget을 플레이어로 설정하여 플레이어만 추적할 수 있도록 함.
 - OverlapSphere를 사용하여 범위 안에 있으면서 LivingEntity 컴포넌트를 가진 데이터들(살아있는 플레이어들) 저장.
 - Vector3와 foreach를 사용하여 가장 가까운 플레이어를 찾아서 추적.
 - 추적 대상 존재시 pathFinder 추적 진행.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    // 주기적으로 추적할 대상의 위치를 찾아 경로를 갱신
    private IEnumerator UpdatePath() {
        // 살아있는 동안 무한 루프
        while (!dead) {
            if (hasTarget) {
                // 추적 대상 존재 : 경로를 갱신하고 AI 이동을 계속 진행
                pathFinder.isStopped = false;
                pathFinder.SetDestination(targetEntity.transform.position);
            }
            else {
                // 추적 대상 없음 : AI 이동 중지
                pathFinder.isStopped = true;
            }
 
            // 20 유닛의 반지름을 가진 가상의 구를 그렸을때, 구와 겹치는 모든 콜라이더를 가져옴
            // 단, targetLayers에 해당하는 레이어를 가진 콜라이더만 가져오도록 필터링
            Collider[] colliders = Physics.OverlapSphere(transform.position, 20f, whatIsTarget);
 
            //모든 콜라이더 중 LivingEntity 컴포넌트가 존재하는 개체를 저장
            LivingEntity livingEntity = null;
            LivingEntity[] livingEntities = new LivingEntity[colliders.Length];
            float shortDistance;    //가장 짧은 거리
            int n = 0;
 
            // 모든 콜라이더들을 순회하면서, 살아있는 플레이어를 찾기
            for (int i = 0; i < colliders.Length; i++) {
                // 콜라이더로부터 LivingEntity 컴포넌트 가져오기
                livingEntity = colliders[i].GetComponent<LivingEntity>();
 
                // LivingEntity 컴포넌트가 존재하며, 해당 LivingEntity가 살아있다면,
                if (livingEntity != null && !livingEntity.dead) {
                    livingEntities[n] = livingEntity;
                    n++;
                }
            }
 
            //제일 가까운 대상으로 목표 변경
            if (n != 0) {
                shortDistance = Vector3.Distance(gameObject.transform.position, livingEntities[n - 1].transform.position);
 
                foreach (LivingEntity livEnt in livingEntities) {
                    if (livEnt != null) {
                        float distance = Vector3.Distance(gameObject.transform.position, livEnt.transform.position);
 
                        if (distance <= shortDistance) {
                            // 추적 대상을 해당 LivingEntity로 설정
                            targetEntity = livEnt;
                            shortDistance = distance;
                        }
                    }
                }
            }
 
            // 0.25초 주기로 처리 반복
            yield return new WaitForSeconds(0.25f);
        }
    }
cs

 

 

3) 적 Spawn과 Destroy 구현.

 - PhotonNetwork.Instantiate와 RPC를 사용하여 적을 생성하고 Mathf.Lerp로 랜덤 능력치 부여

 - 적 사망시 onDeath 이벤트를 사용하여 사망시 필요한 함수들을 일괄적으로 실행. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
    // 현재 웨이브에 맞춰 적을 생성
    private void SpawnWave() {
        // 웨이브 1 증가
        wave++;
 
        // 현재 웨이브 * 1.5에 반올림 한 개수 만큼 적을 생성
        int spawnCount = Mathf.RoundToInt(wave * 1.5f);
 
        // spawnCount 만큼 적을 생성
        for (int i = 0; i < spawnCount; i++)
        {
            // 적의 세기를 0%에서 100% 사이에서 랜덤 결정
            float enemyIntensity = Random.Range(0f, 1f);
            // 적 생성 처리 실행
            CreateEnemy(enemyIntensity);
        }
    }
 
    // 적을 생성하고 생성한 적에게 추적할 대상을 할당
    private void CreateEnemy(float intensity) {
        // intensity를 기반으로 적의 능력치 결정
        float health = Mathf.Lerp(healthMin, healthMax, intensity);
        float damage = Mathf.Lerp(damageMin, damageMax, intensity);
        float speed = Mathf.Lerp(speedMin, speedMax, intensity);
        int enemySort = Random.Range(0, enemyPrefab.Length);
 
        // intensity를 기반으로 하얀색과 enemyStrength 사이에서 적의 피부색 결정
        Color skinColor = Color.Lerp(Color.white, strongEnemyColor, intensity);
 
        // 생성할 위치를 랜덤으로 결정
        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
 
        // 적 프리팹으로부터 적을 생성, 네트워크 상의 모든 클라이언트들에게 생성됨
        GameObject createdEnemy = PhotonNetwork.Instantiate(enemyPrefab[enemySort].gameObject.name, spawnPoint.position, spawnPoint.rotation);
        
        // 생성한 적을 셋업하기 위해 Enemy 컴포넌트를 가져옴
        Enemy enemy = createdEnemy.GetComponent<Enemy>();
 
        // 생성한 적의 능력치와 추적 대상 설정
        enemy.photonView.RPC("Setup", RpcTarget.All, health, damage, speed, skinColor);
        Debug.Log(skinColor);
 
        // 생성된 적을 리스트에 추가
        enemies.Add(enemy);
 
        // 적의 onDeath 이벤트에 익명 메서드 등록
        // 사망한 적을 리스트에서 제거
        enemy.onDeath += () => enemies.Remove(enemy);
        // 사망한 적을 10 초 뒤에 파괴
        enemy.onDeath += () => StartCoroutine(DestroyAfter(enemy.gameObject, 10f));
        // 적 사망시 점수 상승
        enemy.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);
        }
    }
cs

 

 

 

 

 

3. 플레이어 - 애니메이션, 공격              

스크립트 및 기능)

1) Layers로 기본 애니메이션(뛰기, 죽음)과 상체 애니메이션을 나누어,

   재장전시 상체 애니메이션이 독립적으로 실행되게 하여 뛰면서도 상체 애니메이션(재장전)이 재생.

플레이어 애니메이터

 

2) IK Pass를 사용하여 총을 든 상태에서 애니메이션이 자연스럽게 동작하도록 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PlayerShooter : MonoBehaviourPun {
    (생략) 
    // 애니메이터의 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);
    }
}
cs

 

 

3) 플레이어의 공격 연산

 - Raycast로 탄환이 충돌된 곳의 정보를 가져온 뒤 LineRenderer로 총알 궤적을 그려 발사 이펙트를 재생.

 - 실제 탄환이 발사되는 연산은 호스트가 연산하고 발사 이펙트는 RPC로 다른 클라이언트도 실행되게 함.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class Gun : MonoBehaviourPun, IPunObservable {
    (생략)
    private void Shot() {
        // 실제 발사 처리는 호스트에게 대리
        photonView.RPC("ShotProcessOnServer", RpcTarget.MasterClient);
 
        // 남은 탄환의 수를 -1
        magAmmo--;
        if (magAmmo <= 0) {
            // 탄창에 남은 탄약이 없다면, 총의 현재 상태를 Empty으로 갱신
            state = State.Empty;
        }
    }
 
    // 호스트에서 실행되는, 실제 발사 처리
    [PunRPC]
    private void ShotProcessOnServer() {
        // 레이캐스트에 의한 충돌 정보를 저장하는 컨테이너
        RaycastHit hit;
        // 총알이 맞은 곳을 저장할 변수
        Vector3 hitPosition = Vector3.zero;
 
        // 레이캐스트(시작지점, 방향, 충돌 정보 컨테이너, 사정거리)
        if (Physics.Raycast(fireTransform.position, fireTransform.forward, out hit, fireDistance)) {
            // 레이가 어떤 물체와 충돌한 경우
 
            // 충돌한 상대방으로부터 IDamageable 오브젝트를 가져오기 시도
            IDamageable target = hit.collider.GetComponent<IDamageable>();
 
            // 상대방으로 부터 IDamageable 오브젝트를 가져오는데 성공했다면
            if (target != null && hit.collider.tag != "Player") {
                // 상대방의 OnDamage 함수를 실행시켜서 상대방에게 데미지 주기
                target.OnDamage(damage, hit.point, hit.normal);
            }
 
            // 레이가 충돌한 위치 저장
            hitPosition = hit.point;
        }
        else {
            // 레이가 다른 물체와 충돌하지 않았다면
            // 총알이 최대 사정거리까지 날아갔을때의 위치를 충돌 위치로 사용
            hitPosition = fireTransform.position + fireTransform.forward * fireDistance;
        }
 
        // 발사 이펙트 재생, 이펙트 재생은 모든 클라이언트들에서 실행
        photonView.RPC("ShotEffectProcessOnClients", RpcTarget.All, hitPosition);
    }
 
    // 이펙트 재생 코루틴을 랩핑하는 메서드
    [PunRPC]
    private void ShotEffectProcessOnClients(Vector3 hitPosition) {
        StartCoroutine(ShotEffect(hitPosition));
    }
 
    // 발사 이펙트와 소리를 재생하고 총알 궤적을 그린다
    private IEnumerator ShotEffect(Vector3 hitPosition) {
        // 총구 화염 효과 재생
        muzzleFlashEffect.Play();
        // 탄피 배출 효과 재생
        shellEjectEffect.Play();
 
        // 총격 소리 재생
        gunAudioPlayer.PlayOneShot(shotClip);
 
        // 선의 시작점은 총구의 위치
        bulletLineRenderer.SetPosition(0, fireTransform.position);
        // 선의 끝점은 입력으로 들어온 충돌 위치
        bulletLineRenderer.SetPosition(1, hitPosition);
        // 라인 렌더러를 활성화하여 총알 궤적을 그린다
        bulletLineRenderer.enabled = true;
 
        // 0.03초 동안 잠시 처리를 대기
        yield return new WaitForSeconds(0.03f);
 
        // 라인 렌더러를 비활성화하여 총알 궤적을 지운다
        bulletLineRenderer.enabled = false;
    }
    (생략)
}
cs

 

 

 

 

 

4. 기타              

스크립트)

1) 플레이어와 적은 공통적으로 LivingEntity 부모 클래스를 상속받음.

 - 인터페이스 IDamageable을 사용하여 생명체인 오브젝트는 반드시 OnDamage(~~)를 구현하도록 했음.

 - virtual를 사용하여 OnDamage(~~)나 Die() 등과 같은 플레이어와 적의 공통적인 부분을 처리.

 - override를 사용하여 각각 자식 클래스에서 재정의. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public interface IDamageable {
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}




public class LivingEntity : MonoBehaviourPun, IDamageable {
    // 데미지 처리
    // 호스트에서 먼저 단독 실행되고, 호스트를 통해 다른 클라이언트들에서 일괄 실행됨
    [PunRPC]
    public virtual void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal) {
        if (PhotonNetwork.IsMasterClient) {
            // 데미지만큼 체력 감소
            health -= damage;
 
            healthSlider.value = health;
 
            // 호스트에서 클라이언트로 동기화
            photonView.RPC("ApplyUpdatedHealth", RpcTarget.Others, health, dead);
 
            // 다른 클라이언트들도 OnDamage를 실행하도록 함
            photonView.RPC("OnDamage", RpcTarget.Others, damage, hitPoint, hitNormal);
        }
 
        // 체력이 0 이하 && 아직 죽지 않았다면 사망 처리 실행
        if (health <= 0 && !dead) {
            Die();
        }
    }
}
 
 
 
 
public class Enemy : LivingEntity {
    // 데미지를 입었을때 실행할 처리
    [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();
 
            // 피격 효과음 재생
            enemyAudioPlayer.PlayOneShot(hitSound);
        }
 
        // LivingEntity의 OnDamage()를 실행하여 데미지 적용
        base.OnDamage(damage, hitPoint, hitNormal);
        DamagePopup(damage);
 
        healthSlider.value = health;
    }
}
cs

 

 

2) 아이템

 - Photon View로 동기화되므로 호스트에서만 아이템을 직접 생성.

 - Random.insideUnitSphere으로 플레이어를 중심으로 정해진 반경 안에 랜덤한 좌표를 가져온 뒤,

   그 좌표와 가장 가까운 NavMesh위의 한 점에 아이템을 PhotonNetwork.Instantiate으로 생성.

 

맵에 아이템 오브젝트가 생성될 수 있는 곳 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ItemSpawner : MonoBehaviourPun {
    (생략)
    // 주기적으로 아이템 생성 처리 실행
    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));
    }
 
    // 네브 메시 위의 랜덤한 위치를 반환하는 메서드
    // 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;
    }
    (생략)
}
cs

 

 

3) 인터페이스 IItem을 사용하여 아이템 오브젝트는 반드시 Use()함수를 구현하도록 했음.

 -발사, 이동속도 증가 버프 물약 / 탄창 아이템 / 체력 아이템 / 점수 아이템 구현.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public interface IItem {
    // 입력으로 받는 target은 아이템 효과가 적용될 대상
    void Use(GameObject target);
}
 
 
 
 
//버프 아이템
public class ImprovingBottle : MonoBehaviourPun, IItem {
    public void Use(GameObject target) {
        PlayerBuff playerbuff = target.GetComponent<PlayerBuff>();
 
        if (playerbuff != null) {
            playerbuff.test(Time.time);
        }
 
        // 사용되었으므로, 자신을 파괴
        PhotonNetwork.Destroy(gameObject);
    }
}
 
 
 
 
// 총알을 충전하는 아이템
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);
    }
}
 
 
 
 
// 게임 점수를 증가시키는 아이템
public class Coin : MonoBehaviourPun, IItem {
    public int score = 200// 증가할 점수
 
    public void Use(GameObject target) {
        // 게임 매니저로 접근해 점수 추가
        GameManager.instance.AddScore(score);
        // 모든 클라이언트에서의 자신을 파괴
        PhotonNetwork.Destroy(gameObject);
    }
}
 
 
 
 
// 체력을 회복하는 아이템
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);
    }
}
cs

 

 

 

 

 

 

 

 

5. 전체 스크립트 및 게임 다운로드              

 

전체 스크립트 : https://nemesis32.tistory.com/113

 

Zombie Survive 전체 스크립트

GameManager.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64..

nemesis32.tistory.com

 

스크립트 다운로드 : drive.google.com/file/d/12mRIEAwA3Qw4DhK_YJSqJBuNrTKjyfWg/view?usp=sharing

 

Google 드라이브 - 모든 파일을 한 곳에서

하나의 계정으로 모든 Google 서비스를 Google 드라이브로 이동하려면 로그인하세요.

accounts.google.com

 

게임 다운로드 : drive.google.com/file/d/1P8-oAXZtVGQF9EmOS_QmWpG1l3gK-4hj/view?usp=sharing

 

Google 드라이브 - 모든 파일을 한 곳에서

하나의 계정으로 모든 Google 서비스를 Google 드라이브로 이동하려면 로그인하세요.

accounts.google.com

반응형

'프로그래밍' 카테고리의 다른 글

Zombie Survive 전체 스크립트  (0) 2020.06.01
Line Drawing 스크립트 설명  (0) 2020.05.13
Line Drawing 전체 스크립트  (0) 2020.05.13
DigDog 스크립트 설명  (0) 2020.05.08
DigDog 전체 스크립트  (0) 2020.05.07