본문 바로가기
프로그래밍

Zombie Survive 전체 스크립트

by HyunS_PG 2020. 6. 1.
반응형

 

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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");
    }
}
cs

 

 

 

 

UIManager.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
using UnityEngine;
using UnityEngine.SceneManagement; // 씬 관리자 관련 코드
using UnityEngine.UI; // UI 관련 코드
 
// 필요한 UI에 즉시 접근하고 변경할 수 있도록 허용하는 UI 매니저
public class UIManager : MonoBehaviour {
    // 싱글톤 접근용 프로퍼티
    public static UIManager instance
    {
        get
        {
            if (m_instance == null)
            {
                m_instance = FindObjectOfType<UIManager>();
            }
 
            return m_instance;
        }
    }
 
    private static UIManager m_instance; // 싱글톤이 할당될 변수
 
    public Text ammoText; // 탄약 표시용 텍스트
    public Text scoreText; // 점수 표시용 텍스트
    public Text waveText; // 적 웨이브 표시용 텍스트
    public GameObject gameoverUI; // 게임 오버시 활성화할 UI 
 
    // 탄약 텍스트 갱신
    public void UpdateAmmoText(int magAmmo, int remainAmmo) {
        ammoText.text = magAmmo + "/" + remainAmmo;
    }
 
    // 점수 텍스트 갱신
    public void UpdateScoreText(int newScore) {
        scoreText.text = "Score : " + newScore;
    }
 
    // 적 웨이브 텍스트 갱신
    public void UpdateWaveText(int waves, int count) {
        waveText.text = "Wave : " + waves + "\nEnemy Left : " + count;
    }
 
    // 게임 오버 UI 활성화
    public void SetActiveGameoverUI(bool active) {
        gameoverUI.SetActive(active);
    }
 
    // 게임 재시작
    public void GameRestart() {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}
cs

 

 

 

 

LobbyManager.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
65
66
67
68
69
70
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

 

 

 

 

CameraSetup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
        }
    }
}
cs

 

 

 

 

LivingEntity.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System;
using Photon.Pun;
using UnityEngine;
using UnityEngine.UI; // UI 관련 코드
 
// 생명체로서 동작할 게임 오브젝트들을 위한 뼈대를 제공
// 체력, 데미지 받아들이기, 사망 기능, 사망 이벤트를 제공
public class LivingEntity : MonoBehaviourPun, IDamageable
{
    public GameObject damageUIPrefab;   // 데미지 팝업 UI
    protected FloatingText popupText;   // 데미지 수치
 
    public float startingHealth = 100f; // 시작 체력
    public float health { get; protected set; } // 현재 체력
    public Slider healthSlider;     // 체력을 표시할 UI 슬라이더
 
    public bool dead { get; protected set; } // 사망 상태
    public event Action onDeath; // 사망시 발동할 이벤트
 
    public float fireDamage = 10f;          // 불 데미지
    public float timeBetFireDamage = 0.5f;  // 불 데미지 간격
    private float lastFireDamageTime;       // 마지막 불 데미지 시점
 
    // 호스트->모든 클라이언트 방향으로 체력과 사망 상태를 동기화 하는 메서드
    [PunRPC]
    public void ApplyUpdatedHealth(float newHealth, bool newDead)
    {
        health = newHealth;
        dead = newDead;
    }
 
    // 생명체가 활성화될때 상태를 리셋
    protected virtual void OnEnable()
    {
        // 사망하지 않은 상태로 시작
        dead = false;
        // 체력을 시작 체력으로 초기화
        health = startingHealth;
 
        healthSlider.gameObject.SetActive(true);
        healthSlider.maxValue = startingHealth;
        healthSlider.value = health;
 
        popupText = damageUIPrefab.GetComponent<FloatingText>();
    }
 
    // 데미지 처리
    // 호스트에서 먼저 단독 실행되고, 호스트를 통해 다른 클라이언트들에서 일괄 실행됨
    [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();
        }
    }
 
    //데미지 수치를 캐릭터 머리위에 표시
    [PunRPC]
    public virtual void DamagePopup(float damage)
    {
        if (!dead)
        {
            Vector3 randVecter = new Vector3(UnityEngine.Random.Range(0f, 0.6f), 2.3f, UnityEngine.Random.Range(0f, 0.6f));
            popupText.Text = damage.ToString("F1");
            GameObject d = Instantiate(damageUIPrefab, transform.position + randVecter, transform.rotation, transform);
        }
    }
 
    // 체력을 회복하는 기능
    [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;
 
        healthSlider.gameObject.SetActive(false);
    }
 
    // 생명체가 불에 닿으면 체력 소모
    private void OnTriggerStay(Collider other)
    {
        if (other.tag == "Fire")
        {
            if (!dead && Time.time >= lastFireDamageTime + timeBetFireDamage)
            {
                lastFireDamageTime = Time.time;
 
                Vector3 hitPoint = other.ClosestPoint(transform.position);
                Vector3 hitNormal = transform.position - other.transform.position;
 
                OnDamage(fireDamage, hitPoint, hitNormal);
            }
        }
    }
}
cs

 

 

 

 

IDamageable.cs

1
2
3
4
5
6
7
8
using UnityEngine;
 
// 데미지를 입을 수 있는 타입들이 공통적으로 가져야 하는 인터페이스
public interface IDamageable {
    // 데미지를 입을 수 있는 타입들은 IDamageable을 상속하고 OnDamage 메서드를 반드시 구현해야 한다
    // OnDamage 메서드는 입력으로 데미지 크기(damage), 맞은 지점(hitPoint), 맞은 표면의 방향(hitNormal)을 받는다
    void OnDamage(float damage, Vector3 hitPoint, Vector3 hitNormal);
}
cs

 

 

 

 

FloatingText.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
65
using UnityEngine;
using System.Collections;
 
public class FloatingText : MonoBehaviour
{
    public GUISkin CustomSkin;// GUISkin
    public string Text = "";// Text
    public float LifeTime = 1;// Life time
    public bool FadeEnd = false;// Fade out at last 1 second before destroyed
    public Color TextColor = Color.white; // Text color
    public bool Position3D = false// enabled when you need the text along with world 3d position
    public Vector2 Position; // 2D Position
    
    private float alpha = 1;
    private float timeTemp = 0;
    
    void Start ()
    {
        timeTemp = Time.time;
        GameObject.Destroy(this.gameObject,LifeTime);
        if(Position3D)
        {
            Vector3 screenPos = Camera.main.WorldToScreenPoint(this.transform.position);
            Position = new Vector2(screenPos.x,Screen.height - screenPos.y);
        }
    }
 
    void Update ()
    {
 
        if(FadeEnd)
        {
            if(Time.time >= ((timeTemp + LifeTime) - 1)){
                alpha = 1.0f - (Time.time - ((timeTemp + LifeTime) - 1));
            }
        }
        else
        {
            alpha = 1.0f - ((1.0f / LifeTime) * (Time.time - timeTemp));
        }
    
        if(Position3D)
        {
            Vector3 screenPos = Camera.main.WorldToScreenPoint(this.transform.position);
            Position = new Vector2(screenPos.x,Screen.height - screenPos.y);
        }
    
    }
 
    void OnGUI(){
        
        GUI.color = new Color(GUI.color.r,GUI.color.g,GUI.color.b,alpha);
        if(CustomSkin)
        {
            GUI.skin = CustomSkin;
        }
        
        Vector2 textsize = GUI.skin.label.CalcSize(new GUIContent(Text));
        Rect rect = new Rect(Position.x - (textsize.x/2), Position.y,textsize.x,textsize.y);
 
        GUI.skin.label.normal.textColor = TextColor;
        GUI.Label(rect,Text);
    }
}
 
cs

 

 

 

 

PlayerShooter.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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);
    }
}
cs

 

 

 

 

PlayerInput.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
using Photon.Pun;
using UnityEngine;
 
// 플레이어 캐릭터를 조작하기 위한 사용자 입력을 감지
// 감지된 입력값을 다른 컴포넌트들이 사용할 수 있도록 제공
public class PlayerInput : MonoBehaviourPun
{
    public string verticalAxisName = "Vertical";
    public string horizontalAxisName = "Horizontal";
    public string fireButtonName = "Fire1"// 발사를 위한 입력 버튼 이름
    public string reloadButtonName = "Reload"// 재장전을 위한 입력 버튼 이름
 
    // 값 할당은 내부에서만 가능
    public float verticalMove { get; private set; } // 감지된 움직임 입력값
    public float horizontalMove { 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)
        {
            verticalMove = 0;
            horizontalMove = 0;
            rotate = 0;
            fire = false;
            reload = false;
            return;
        }
 
        // move에 관한 입력 감지
        verticalMove = Input.GetAxis(verticalAxisName);
        horizontalMove = Input.GetAxis(horizontalAxisName);
        //move = Input.GetAxis(moveAxisName);
        // rotate에 관한 입력 감지
        //rotate = Input.GetAxis(rotateAxisName);
        // fire에 관한 입력 감지
        fire = Input.GetButton(fireButtonName);
        // reload에 관한 입력 감지
        reload = Input.GetButtonDown(reloadButtonName);
    }
}
cs

 

 

 

 

PlayerMovement.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using Photon.Pun;
using UnityEngine;
 
// 플레이어 캐릭터를 사용자 입력에 따라 움직이는 스크립트
public class PlayerMovement : MonoBehaviourPun
{
    public float moveSpeed = 150f; // 앞뒤 움직임의 속도
    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.verticalMove + playerInput.horizontalMove);
    }
 
    // 입력값에 따라 캐릭터를 앞뒤로 움직임
    private void Move()
    {
        //// 상대적으로 이동할 거리 계산
        //Vector3 moveDistance = playerInput.move * transform.forward * moveSpeed * Time.deltaTime;
        //// 리지드바디를 통해 게임 오브젝트 위치 변경
        //playerRigidbody.MovePosition(playerRigidbody.position + moveDistance);
        
        playerRigidbody.velocity = new Vector3(playerInput.verticalMove * moveSpeed * Time.deltaTime + playerInput.horizontalMove * moveSpeed * Time.deltaTime,
                                               playerRigidbody.velocity.y,
                                               playerInput.verticalMove * moveSpeed * Time.deltaTime - playerInput.horizontalMove * moveSpeed * Time.deltaTime);
    }
 
    // 입력값에 따라 캐릭터를 좌우로 회전
    private void Rotate()
    {
        //// 상대적으로 회전할 수치 계산
        //float turn = playerInput.rotate * rotateSpeed * Time.deltaTime;
        //// 리지드바디를 통해 게임 오브젝트 회전 변경
        //playerRigidbody.rotation =
        //    playerRigidbody.rotation * Quaternion.Euler(0, turn, 0f);
 
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
 
        int layerMask = 1 << LayerMask.NameToLayer("Ground");
 
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask))
        {
            Vector3 target = new Vector3(hit.point.x, 0f, hit.point.z);
 
            Vector3 distance = target - transform.position;
 
            if (distance.magnitude > 0.5f)
            {
                transform.LookAt(target);
            }
        }
    }
}
cs

 

 

 

 

PlayerHealth.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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);
 
        DamagePopup(damage);
 
        // 갱신된 체력을 체력 슬라이더에 반영
        healthSlider.value = health;
    }
 
    //데미지 수치 팝업
    public override void DamagePopup(float damage) {
        popupText.TextColor = Color.white;
        base.DamagePopup(damage);
    }
 
    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);
    }
}
cs

 

 

 

 

PlayerBuff.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
65
66
67
68
69
70
using System.Collections;
using Photon.Pun;
using System.Collections.Generic;
using UnityEngine;
 
public class PlayerBuff : MonoBehaviourPun { 
    public Renderer playerRenderer;     //플레이어 색깔
    public GameObject gun;              //총
    public GameObject buffEffect;       //버프 효과
    
    float buffSpeed;
    float buffFireRate;
    //float buffDefense = 0f;
    bool isBuff;
 
    float multiple = 1.5f;  //증가 배수
 
    float timeBetBuff = 5f; //버프지속시간
    [HideInInspector] public float lastBuffTime;
 
    private void OnEnable() {
        buffSpeed = GetComponent<PlayerMovement>().moveSpeed * multiple;
        buffFireRate = gun.GetComponent<Gun>().timeBetFire / multiple;
        lastBuffTime = 0f;
        isBuff = true;
    }
 
    public void test(float _lastBuffTime)
    {
        photonView.RPC("ApplyBuff", RpcTarget.All, _lastBuffTime);
    }
 
 
    [PunRPC]
    public void ApplyBuff(float _lastBuffTime)
    {
        lastBuffTime = _lastBuffTime;
        StartCoroutine(OnBuff());
    }
 
    public IEnumerator OnBuff()
    {
        while (isBuff)
        {
            // 버프 해제
            if (Time.time >= lastBuffTime + timeBetBuff) {
                lastBuffTime = Time.time;
 
                GetComponent<PlayerMovement>().moveSpeed = buffSpeed / multiple;    //이동속도 원상복귀
                gun.GetComponent<Gun>().timeBetFire = buffFireRate * multiple;      //연사력 원상복귀
 
                playerRenderer.material.color = Color.white;                        //캐릭터색 원상복귀
                buffEffect.SetActive(false);                                        //이펙트 효과 off
                isBuff = false;
            }
            else {
                GetComponent<PlayerMovement>().moveSpeed = buffSpeed;       //이동속도 업
                gun.GetComponent<Gun>().timeBetFire = buffFireRate;         //연사력 업
 
                playerRenderer.material.color = Color.green / multiple;     //캐릭터색 변경
                buffEffect.SetActive(true);                                 //이펙트 효과 on
            }
 
            // 0.1초 주기로 처리 반복
            yield return new WaitForSeconds(0.1f);
        }
 
    }
}
 
cs

 

 

 

 

Gun.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using System.Collections;
using Photon.Pun;
using UnityEngine;
 
// 총을 구현한다
public class Gun : MonoBehaviourPun, IPunObservable {
    // 총의 상태를 표현하는데 사용할 타입을 선언한다
    public enum State {
        Ready, // 발사 준비됨
        Empty, // 탄창이 빔
        Reloading // 재장전 중
    }
 
    public State state { get; private set; } // 현재 총의 상태
 
    public Transform fireTransform; // 총알이 발사될 위치
 
    public ParticleSystem muzzleFlashEffect; // 총구 화염 효과
    public ParticleSystem shellEjectEffect; // 탄피 배출 효과
 
    private LineRenderer bulletLineRenderer; // 총알 궤적을 그리기 위한 렌더러
 
    private AudioSource gunAudioPlayer; // 총 소리 재생기
    public AudioClip shotClip; // 발사 소리
    public AudioClip reloadClip; // 재장전 소리
 
    public float damage = 25// 공격력
    private float fireDistance = 50f; // 사정거리
 
    public int ammoRemain = 100// 남은 전체 탄약
    public int magCapacity = 25// 탄창 용량
    public int magAmmo; // 현재 탄창에 남아있는 탄약
 
    public float timeBetFire = 0.12f; // 총알 발사 간격
    public float reloadTime = 1.8f; // 재장전 소요 시간
    private float lastFireTime; // 총을 마지막으로 발사한 시점
 
    // 주기적으로 자동 실행되는, 동기화 메서드
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        // 로컬 오브젝트라면 쓰기 부분이 실행됨
        if (stream.IsWriting) {
            // 남은 탄약수를 네트워크를 통해 보내기
            stream.SendNext(ammoRemain);
            // 탄창의 탄약수를 네트워크를 통해 보내기
            stream.SendNext(magAmmo);
            // 현재 총의 상태를 네트워크를 통해 보내기
            stream.SendNext(state);
        }
        else {
            // 리모트 오브젝트라면 읽기 부분이 실행됨
            // 남은 탄약수를 네트워크를 통해 받기
            ammoRemain = (int) stream.ReceiveNext();
            // 탄창의 탄약수를 네트워크를 통해 받기
            magAmmo = (int) stream.ReceiveNext();
            // 현재 총의 상태를 네트워크를 통해 받기
            state = (State) stream.ReceiveNext();
        }
    }
 
    // 남은 탄약을 추가하는 메서드
    [PunRPC]
    public void AddAmmo(int ammo) {
        ammoRemain += ammo;
    }
 
    private void Awake() {
        // 사용할 컴포넌트들의 참조를 가져오기
        gunAudioPlayer = GetComponent<AudioSource>();
        bulletLineRenderer = GetComponent<LineRenderer>();
 
        // 사용할 점을 두개로 변경
        bulletLineRenderer.positionCount = 2;
        // 라인 렌더러를 비활성화
        bulletLineRenderer.enabled = false;
    }
 
 
    private void OnEnable() {
        // 현재 탄창을 가득채우기
        magAmmo = magCapacity;
        // 총의 현재 상태를 총을 쏠 준비가 된 상태로 변경
        state = State.Ready;
        // 마지막으로 총을 쏜 시점을 초기화
        lastFireTime = 0;
    }
 
    // 발사 시도
    public void Fire() {
        // 현재 상태가 발사 가능한 상태
        // && 마지막 총 발사 시점에서 timeBetFire 이상의 시간이 지남
        if (state == State.Ready && Time.time >= lastFireTime + timeBetFire)
        {
            // 마지막 총 발사 시점을 갱신
            lastFireTime = Time.time;
            // 실제 발사 처리 실행
            Shot();
        }
    }
 
    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;
    }
 
    // 재장전 시도
    public bool Reload() {
        if (state == State.Reloading || ammoRemain <= 0 || magAmmo >= magCapacity) {
            // 이미 재장전 중이거나, 남은 총알이 없거나
            // 탄창에 총알이 이미 가득한 경우 재장전 할수 없다
            return false;
        }
 
        // 재장전 처리 실행
        StartCoroutine(ReloadRoutine());
        return true;
    }
 
    // 실제 재장전 처리를 진행
    private IEnumerator ReloadRoutine() {
        // 현재 상태를 재장전 중 상태로 전환
        state = State.Reloading;
        // 재장전 소리 재생
        gunAudioPlayer.PlayOneShot(reloadClip);
 
        // 재장전 소요 시간 만큼 처리를 쉬기
        yield return new WaitForSeconds(reloadTime);
 
        // 탄창에 채울 탄약을 계산한다
        int ammoToFill = magCapacity - magAmmo;
 
        // 탄창에 채워야할 탄약이 남은 탄약보다 많다면,
        // 채워야할 탄약 수를 남은 탄약 수에 맞춰 줄인다
        if (ammoRemain < ammoToFill) {
            ammoToFill = ammoRemain;
        }
 
        // 탄창을 채운다
        magAmmo += ammoToFill;
        // 남은 탄약에서, 탄창에 채운만큼 탄약을 뺸다
        ammoRemain -= ammoToFill;
 
        // 총의 현재 상태를 발사 준비된 상태로 변경
        state = State.Ready;
    }
}
cs

 

 

 

 

EnemySpawner.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Pun;
using UnityEngine;
 
 
// 적 게임 오브젝트를 주기적으로 생성
public class EnemySpawner : MonoBehaviourPun, IPunObservable
{
    public Enemy[] enemyPrefab; // 생성할 적 AI
    public Transform[] spawnPoints; // 적 AI를 소환할 위치들
 
    public float damageMax = 40f; // 최대 공격력
    public float damageMin = 20f; // 최소 공격력
 
    public float healthMax = 200f; // 최대 체력
    public float healthMin = 100f; // 최소 체력
 
    public float speedMax = 3f; // 최대 속도
    public float speedMin = 1f; // 최소 속도
 
    public Color strongEnemyColor = Color.red; // 강한 적 AI가 가지게 될 피부색
 
    private List<Enemy> enemies = new List<Enemy>(); // 생성된 적들을 담는 리스트
 
    private int enemyCount = 0// 남은 적의 수
    private int wave; // 현재 웨이브
 
    // 주기적으로 자동 실행되는, 동기화 메서드
    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();
        }
    }
 
    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 (enemies.Count <= 0)
            {
                SpawnWave();
            }
        }
 
        // UI 갱신
        UpdateUI();
    }
 
    // 웨이브 정보를 UI로 표시
    private void UpdateUI() {
        if (PhotonNetwork.IsMasterClient) {
            // 호스트는 직접 갱신한 적 리스트를 통해 남은 적의 수를 표시함
            UIManager.instance.UpdateWaveText(wave, enemies.Count);
        }
        else {
            // 클라이언트는 적 리스트를 갱신할 수 없으므로, 호스트가 보내준 enemyCount를 통해 적의 수를 표시함
            UIManager.instance.UpdateWaveText(wave, enemyCount);
        }
    }
 
    // 현재 웨이브에 맞춰 적을 생성
    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

 

 

 

 

Enemy.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
using System.Collections;
using System.Collections.Generic;
using Photon.Pun;
using UnityEngine;
using UnityEngine.AI; // AI, 내비게이션 시스템 관련 코드를 가져오기
 
// 적 AI를 구현한다
public class Enemy : LivingEntity {
 
    public LayerMask whatIsTarget; // 추적 대상 레이어
 
    private LivingEntity targetEntity; // 추적할 대상
    private NavMeshAgent pathFinder; // 경로계산 AI 에이전트
 
    public ParticleSystem hitEffect; // 피격시 재생할 파티클 효과
    public AudioClip deathSound; // 사망시 재생할 소리
    public AudioClip hitSound; // 피격시 재생할 소리
 
    private Animator enemyAnimator; // 애니메이터 컴포넌트
    private AudioSource enemyAudioPlayer; // 오디오 소스 컴포넌트
    private Renderer enemyRenderer; // 렌더러 컴포넌트
 
    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() {
        // 게임 오브젝트로부터 사용할 컴포넌트들을 가져오기
        pathFinder = GetComponent<NavMeshAgent>();
        enemyAnimator = GetComponent<Animator>();
        enemyAudioPlayer = GetComponent<AudioSource>();
 
        // 렌더러 컴포넌트는 자식 게임 오브젝트에게 있으므로
        // GetComponentInChildren() 메서드를 사용
        enemyRenderer = GetComponentInChildren<Renderer>();
    }
 
    // 적 AI의 초기 스펙을 결정하는 셋업 메서드
    [PunRPC]
    public void Setup(float newHealth, float newDamage, float newSpeed, Color skinColor) {
        // 체력 설정
        startingHealth = newHealth;
        health = newHealth;
        // 공격력 설정
        damage = newDamage;
        // 내비메쉬 에이전트의 이동 속도 설정
        pathFinder.speed = newSpeed;
        // 렌더러가 사용중인 머테리얼의 컬러를 변경, 외형 색이 변함
        enemyRenderer.material.color = skinColor;
    }
 
    private void Start() {
        // 호스트가 아니라면 AI의 추적 루틴을 실행하지 않음
        if (!PhotonNetwork.IsMasterClient) {
            return;
        }
 
        // 게임 오브젝트 활성화와 동시에 AI의 추적 루틴 시작
        StartCoroutine(UpdatePath());
    }
 
    private void Update() {
        // 호스트가 아니라면 애니메이션의 파라미터를 직접 갱신하지 않음
        // 호스트가 파라미터를 갱신하면 클라이언트들에게 자동으로 전달되기 때문.
        if (!PhotonNetwork.IsMasterClient)
        {
            return;
        }
 
        // 추적 대상의 존재 여부에 따라 다른 애니메이션을 재생
        enemyAnimator.SetBool("HasTarget", hasTarget);
    }
 
    // 주기적으로 추적할 대상의 위치를 찾아 경로를 갱신
    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);
        }
    }
 
    // 데미지를 입었을때 실행할 처리
    [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;
    }
 
    //데미지 수치 팝업
    public override void DamagePopup(float damage)
    {
        popupText.TextColor = Color.red;
        base.DamagePopup(damage);
    }
 
    // 사망 처리
    public override void Die() {
        // LivingEntity의 Die()를 실행하여 기본 사망 처리 실행
        base.Die();
 
        // 다른 AI들을 방해하지 않도록 자신의 모든 콜라이더들을 비활성화
        Collider[] enemyColliders = GetComponents<Collider>();
        for (int i = 0; i < enemyColliders.Length; i++)
        {
            enemyColliders[i].enabled = false;
        }
 
        // AI 추적을 중지하고 내비메쉬 컴포넌트를 비활성화
        pathFinder.isStopped = true;
        pathFinder.enabled = false;
 
        // 사망 애니메이션 재생
        enemyAnimator.SetTrigger("Die");
 
        // 사망 효과음 재생
        enemyAudioPlayer.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);
            }
        }
    }
}
cs

 

 

 

 

ColorSerialization.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using ExitGames.Client.Photon;
using UnityEngine;
 
public class ColorSerialization 
{
    public static byte[] SerializeColor(object targetObject) {
        Color color = (Color) targetObject;
 
        Quaternion colorToQuaterinon = new Quaternion(color.r, color.g, color.b, color.a);
        byte[] bytes = Protocol.Serialize(colorToQuaterinon);
 
        return bytes;
    }
 
    public static object DeserializeColor(byte[] bytes) {
        Quaternion quaterinon = (Quaternion) Protocol.Deserialize(bytes);
 
        Color color = new Color(quaterinon.x, quaterinon.y, quaterinon.z, quaterinon.w);
 
        return color;
    }
}
cs

 

 

 

 

IItem.cs

1
2
3
4
5
6
7
using UnityEngine;
 
// 아이템 타입들이 반드시 구현해야하는 인터페이스
public interface IItem {
    // 입력으로 받는 target은 아이템 효과가 적용될 대상
    void Use(GameObject target);
}
cs

 

 

 

 

ItemSpawner.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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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;
    }
}
cs

 

 

 

 

Rotator.cs

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
 
// 게임 오브젝트를 지속적으로 회전하는 스크립트
public class Rotator : MonoBehaviour {
    public float rotationSpeed = 60f;
 
    private void Update() {
        transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
    }
}
cs

 

 

 

 

AmmoPack.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}
cs

 

 

 

 

Coin.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using Photon.Pun;
using UnityEngine;
 
// 게임 점수를 증가시키는 아이템
public class Coin : MonoBehaviourPun, IItem {
    public int score = 200// 증가할 점수
 
    public void Use(GameObject target) {
        // 게임 매니저로 접근해 점수 추가
        GameManager.instance.AddScore(score);
        // 모든 클라이언트에서의 자신을 파괴
        PhotonNetwork.Destroy(gameObject);
    }
}
cs

 

 

 

 

HealthPack.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}
cs

 

 

 

 

ImprovingBottle.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
 
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);
    }
}
 
cs

 

 

 

 

 

 

반응형

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

Zombie Survive 스크립트, 기능 설명  (0) 2020.06.01
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

댓글0