※목차
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(null, new RoomOptions { MaxPlayers = 4 });
}
// 룸에 참가 완료된 경우 자동 실행
public override void OnJoinedRoom() {
connectionInfoText.text = "방 참가 성공";
PhotonNetwork.LoadLevel("Main");
}
}
|
cs |
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 |
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를 사용하여 장애물이 놓여도 목적지/목표물까지 최단거리로 갈 수 있도록 함.
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
스크립트 다운로드 : drive.google.com/file/d/12mRIEAwA3Qw4DhK_YJSqJBuNrTKjyfWg/view?usp=sharing
게임 다운로드 : drive.google.com/file/d/1P8-oAXZtVGQF9EmOS_QmWpG1l3gK-4hj/view?usp=sharing
'프로그래밍' 카테고리의 다른 글
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 |