




DefaultNetworkPrefabs에 네트워크에 존재하는 애들을 미리정의해야함


5 - 1. Multi Play Mode에서 Player2 를 활성화 한다. -> 싱크 맞추는 용도(매우 중요함)

5 - 2. 디버그 중에 NetworkManager를 StartHost 버튼을 누른다.

5 - 3. Player2 에 StartClient 버튼을 누른다.

5 - 4. 그 결과 Player2에도 플레이어 프리펩이 생성되었다.


using UnityEngine;
public class PlayeNetwork : MonoBehaviour
{
void Update()
{
Vector3 movedir = Vector3.zero;
movedir.x = Input.GetAxis("Horizontal");
movedir.z = Input.GetAxis("Vertical");
float movespeed = 3;
transform.Translate(movedir * movespeed * Time.deltaTime);
}
}



using Unity.Netcode;
using UnityEngine;
public class NetworkManagerUI : MonoBehaviour
{
//네트워크 매니저한테 호스트로 시작할거야? 클라이언트로 시작할거야? 에 대한 정보를 알려주기 위해 간단한 버튼
public void HostPreesed()
{
NetworkManager.Singleton.StartHost();
}
public void ClientPressed()
{
NetworkManager.Singleton.StartClient();
}
}


실행 화면
이전에, 수동으로 NetworkManager에서 StartHost, StartClient을 직접 클릭하였는데, 그것이 아닌 버튼 이벤트를 통해 host와 Client를 나눴다.
하지만, 모두 똑같이 움직인다. 왜냐하면 각 플레이어에 PlayerNetwork.cs가 할당 되어있기에 똑같이 반응한다.



using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
void Update()
{
if (IsOwner == false) //이 스크립트의 주인이 내가 아니라면
{
return; //아무것도 하지마.
}
Vector3 movedir = Vector3.zero;
movedir.x = Input.GetAxis("Horizontal");
movedir.z = Input.GetAxis("Vertical");
float movespeed = 3;
transform.Translate(movedir * movespeed * Time.deltaTime);
}
}









public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
NetworkVariable<int> randomNumber = new NetworkVariable<int>(1);
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.T))
{
randomNumber.Value = Random.Range(0, 100);
}
Debug.Log("random number : " + randomNumber.Value);
Vector3 moveDir = Vector3.zero;
moveDir.x = Input.GetAxis("Horizontal");
moveDir.z = Input.GetAxis("Vertical");
float moveSpeed = 3;
transform.Translate(moveDir * moveSpeed * Time.deltaTime);
}
}
NetworkVariable : 이 변수는 네트워크 전체가 변수 하나이다. 모든 네트워크가 randomNumber 변수의 값이 같게 세팅이 된다.
실행 결과

T를 눌러서 randomNumber가 53으로 바뀌었지만, 클라이언트 에서는 코드에서 초기화 했던 1로 바뀌고 있다. 즉, 오류이다. 이유는 Host에서 NetworkVariable 을 변형시켜서 숫자를 바뀐거지, Client에서는 NetworkVariable을 변형시키지 않았다. -> Owner가 아닌 녀석의 NetworkVariable을 변형시킨 것이 아니다.
public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
NetworkVariable<int> randomNumber = new NetworkVariable<int>(1);
void Update()
{
Debug.Log("OwnerClientid" + OwnerClientid + "random number : " + randomNumber.Value);
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.T))
{
randomNumber.Value = Random.Range(0, 100);
}
Vector3 moveDir = Vector3.zero;
moveDir.x = Input.GetAxis("Horizontal");
moveDir.z = Input.GetAxis("Vertical");
float moveSpeed = 3;
transform.Translate(moveDir * moveSpeed * Time.deltaTime);
}
}
두 개의 플레이어 중에서 Owner만 NetworkVariable을 변형시킨 것이다. 그러는 것이 아닌 위에 디버그 로그를 찍는다.
실행 결과

위 사진은 T를 누르지 않은 상태이다. 즉, 초기 사진이다. 각각의 다른 OwnerId가 출력되었고, randomNumber는 1로 동일하다. Host -> OwnerId는 0 , Client -> OwnerId는 1이다.

위 사진은 Host에서 T를 누른 상태인데, Host의 randomNumber가 99로 바뀌었다. Client의 randomNumber는 그대로 1이다. 왜냐하면 Host에서 T를 눌렀기 때문

위 사진은 전체 사진인데, Client에서 Host의 randomNumber가 99로 바뀐것을 알고 있다.

위 사진은 Client에서 T를 눌렀을 때 NetworkVariable가 변형되지 않고 Error가 난다. 왜냐하면 아래 코드 때문이다.
NetworkVariable<int> randomNumber = new NetworkVariable<int>(1);










public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
struct myStruct : INetworkSerializable
{
public bool _bool;
public int _int;
public FixedString64Bytes _string;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref _bool);
serializer.SerializeValue(ref _int);
serializer.SerializeValue(ref _string);
}
}
NetworkVariable<myStruct> randomNumber = new NetworkVariable<myStruct>(
new myStruct
{
_bool = true,
_int = 1
}, // 수
NetworkVariableReadPermission.Everyone, // 모든 대상자가 읽을 수 있는 권한
NetworkVariableWritePermission.Owner // 소지자만 작성할 수 있는 권한
);
// OnEnable의 NetworkBehaviour 버전
public override void OnNetworkSpawn() //⭐
{
randomNumber.OnValueChanged += HandleNumber;
}
// OnDisable의 NetworkBehaviour 버전
public override void OnNetworkDespawn() //⭐
{
randomNumber.OnValueChanged -= HandleNumber;
}
void HandleNumber(myStruct oldValue, myStruct newValue) //⭐이전 값과 새로운 값 매개변수 목록
{
Debug.Log("Owner id : " + OwnerClientId + ", random number : "
+ randomNumber.Value._int + ", "
+ randomNumber.Value._string); //⭐
}
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.T))
{
randomNumber.Value = new myStruct { _int = Random.Range(0, 100), _string = "일이삼다오륙칠팔구" };
}
Vector3 moveDir = Vector3.zero;
moveDir.x = Input.GetAxis("Horizontal");
moveDir.z = Input.GetAxis("Vertical");
float moveSpeed = 3;
transform.Translate(moveDir * moveSpeed * Time.deltaTime);
}
}
OnValueChanged 는 델리게이트이며, 매개변수 목록은 이전 값과 새로운 값이 필요하다.

실행 결과


using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
struct myStruct : INetworkSerializable
{
public bool _bool;
public int _int;
public FixedString64Bytes _string;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref _bool);
serializer.SerializeValue(ref _int);
serializer.SerializeValue(ref _string);
}
}
NetworkVariable<myStruct> randomNumber = new NetworkVariable<myStruct>(
new myStruct
{
_bool = true,
_int = 1
}, // 수
NetworkVariableReadPermission.Everyone, // 모든 대상자가 읽을 수 있는 권한
NetworkVariableWritePermission.Owner // 소지자만 작성할 수 있는 권한
);
// OnEnable의 NetworkBehaviour 버전
public override void OnNetworkSpawn()
{
randomNumber.OnValueChanged += HandleNumber;
}
// OnDisable의 NetworkBehaviour 버전
public override void OnNetworkDespawn()
{
randomNumber.OnValueChanged -= HandleNumber;
}
void HandleNumber(myStruct oldValue, myStruct newValue)
{
Debug.Log("Owner id : " + OwnerClientId + ", random number : "
+ randomNumber.Value._int + ", "
+ randomNumber.Value._string);
}
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.Z)) //⭐ 추가
{
TestServerRpc(); //클라이언트가 아닌 서버의 함수를 호출. //⭐ 추가
}
if (Input.GetKeyDown(KeyCode.T))
{
randomNumber.Value = new myStruct { _int = Random.Range(0, 100), _string = "이것은 스트링이다." };
}
Vector3 moveDir = Vector3.zero;
moveDir.x = Input.GetAxis("Horizontal");
moveDir.z = Input.GetAxis("Vertical");
float moveSpeed = 3;
transform.Translate(moveDir * moveSpeed * Time.deltaTime);
}
////RPC : 플레이어들 간의 함수를 호출. 즉, 멀리서 함수 호출.
[ServerRpc] //서버에서 실행. 함수 이름 = 함수 이름 + ServerRpc 로 끝나야 함.
void TestServerRpc() //⭐ 추가 -> 함수 이름 주의
{
Debug.Log("RPC 메서드 호출");
}
}
함수 이름은 무조건 ServerRpc가 뒤에 있어야 한다.
실행 결과

Host에서 z를 누르면 로그가 찍힌다. 하지만 Client에서 z를 누르면 Host에서 로그가 찍힌다. 왜냐하면 ServerRpc이므로 무조건 Server 함수를 호출하므로 Host에서만 로그가 찍힌다.
ClientRPC도 있다. Host에서 Client에 함수를 호출할 때 사용한다. 동시에 함수 호출은 불가능하다.

using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
//..생략
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
Debug.Log("is server : " + IsServer + "is client : " + IsClient);
if (Input.GetKeyDown(KeyCode.Z))
{
//TestServerRpc(); //클라이언트가 아닌 서버의 함수를 호출.
TestClientRpc(); //⭐
}
//..생략
}
////RPC : 플레이어들 간의 함수를 호출. 즉, 멀리서 함수 호출.
[ServerRpc] //서버에서 실행. 함수 이름 = 함수 이름 + ServerRpc 로 끝나야 함.
void TestServerRpc()
{
Debug.Log("ServerRpc 메서드 진입");
}
[ClientRpc] //⭐
void TestClientRpc() //⭐
{
Debug.Log("ClientRpc 메서드 진입"); //⭐
}
}



Unity의 네트워크 아키텍처에서 Host는 "서버"와 "클라이언트"를 동일한 프로세스 내에서 실행할 수 있도록 설계되어 있습니다. 이는 다음과 같은 이유에서 가능합니다:
하나의 네트워크 인스턴스에서 두 역할 구현:
Unity는 네트워크를 처리할 때, 같은 프로세스에서 Server와 Client를 동시에 동작시킬 수 있도록 설계되었습니다.
이를 통해 네트워크 통신이 내부적으로 처리되며, 추가적인 연결이나 네트워크 비용이 필요하지 않습니다.로컬 클라이언트 최적화:
Host는 자신의 데이터를 로컬에서 바로 처리하므로, 네트워크 지연(latency)이 없습니다.
실제 클라이언트-서버 구조처럼 동작하지만, Host와 자신의 클라이언트 간의 통신은 내부적으로 최적화됩니다.권위(authority) 시스템:
Host는 서버와 클라이언트 모두를 제어할 권위를 가지며, 자신의 데이터를 즉각적으로 처리할 수 있습니다.

void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
Debug.Log("is server : " + IsServer + "is client : " + IsClient);
if (Input.GetKeyDown(KeyCode.Z))
{
TestServerRpc(new ServerRpcParams()); //⭐
}
//..생략
}
////RPC : 플레이어들 간의 함수를 호출. 즉, 멀리서 함수 호출.
[ServerRpc] //서버에서 실행. 함수 이름 = 함수 이름 + ServerRpc 로 끝나야 함.
void TestServerRpc(ServerRpcParams rpcParams) //⭐ ServerRpcParams : RPC를 호출한 클라이언트를 식별 할 때 사용하는 파라미터.
{
// Debug.Log("ServerRpc 메서드 진입" + number);
Debug.Log("ServerRpc 메서드 진입" + rpcParams.Receive.SenderClientId); //⭐
}
[ClientRpc]
void TestClientRpc()
{
Debug.Log("ClientRpc 메서드 진입");
}



void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
Debug.Log("is server : " + IsServer + "is client : " + IsClient);
if (Input.GetKeyDown(KeyCode.Z))
{
TestClientRpc(new ClientRpcParams //⭐
{
Send = new ClientRpcSendParams
{
TargetClientIds = new List<ulong> { 1 },
}
});
}
//..생략
}
////RPC : 플레이어들 간의 함수를 호출. 즉, 멀리서 함수 호출.
[ClientRpc]
void TestClientRpc(ClientRpcParams rpcParams) //⭐
{
Debug.Log("ClientRpc 메서드 진입");
}

TargetClientIds = new List<ulong> { 1 }


public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
[SerializeField]
GameObject spawnedObjectPrefab; // ⭐
//..생략
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.X)) // ⭐
{
Instantiate(spawnedObjectPrefab);
}
//..생략
}
}

실행 결과

Host에서 x키를 눌렀으나 Host에서만 생성되고, Client에서는 생성되지 않았다.

반대로, Client에서 x키를 눌렀으나 Client에서만 생성되고, Host에서 생성되지 않았다.
public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
[SerializeField]
GameObject spawnedObjectPrefab;
//..생략
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.X))
{
GameObject go = Instantiate(spawnedObjectPrefab); // ⭐
go.GetComponent<NetworkObject>().Spawn(true); // ⭐Spawn의 권한은 서버에 있다.
}
//..생략
}
}


public class PlayeNetwork : NetworkBehaviour //네트워크 기능들을 좀 더 사용할 수 있는 모노비해비어의 한정판
{
//네트워크 비해비어에만 존재하는 것들
// 1. IsOwner
// 2. OwnerClientId
// 3. [ServerRpc]
[SerializeField]
GameObject spawnedObjectPrefab; //⭐
//..생략
void Update()
{
if (!IsOwner) { return; } // 해당 오브젝트의 Network Owner가 아니면 작동X
if (Input.GetKeyDown(KeyCode.X))
{
spawnedObject = Instantiate(spawnedObjectPrefab);
spawnedObject.GetComponent<NetworkObject>().Spawn(true);
}
if (Input.GetKeyDown(KeyCode.C)) //⭐
{
//DeSpawn하는 방법은 2가지. 모두 똑같지만 2번째 방법이 좋다.
//Destroy(spawnedObject); //⭐ 네트워크에서 스폰된 것들은 자동으로 Despawn한다.
spawnedObject.GetComponent<NetworkObject>().Despawn(true); //⭐ 네트워크에서는 Despawn이 된다.
}
//..생략
}
}








실행 화면

생성도 되고, 기본 이동도 물론 된다.

일단 이 상태로 실행하면

Host에서는 움직여지만, Client에서 움직여지지 않다.
왜냐하면 Client에서 PlayerInput을 먹어버리고 있다. 즉, PlayerInput 컴포넌트가 입력을 처리한 뒤, 다른 시스템에 전달되지 않는 상태이다.

using Unity.Netcode;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/* Note: animations are called via the controller for both the character and capsule using animator null checks
*/
namespace StarterAssets
{
[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM
[RequireComponent(typeof(PlayerInput))]
#endif
public class ThirdPersonController : NetworkBehaviour
{
//..생략
public override void OnNetworkSpawn() //⭐
{
if (IsOwner == false) //내가 주인이 아니면
{
GetComponent<PlayerInput>().enabled = false; //⭐ 인풋 시스템을 끈다.
}
}
//..생략
private void Update()
{
if (IsOwner == false) //⭐ 주인이 아니라면
{
return;
}
//..생략
}
//..생략
}
실행 결과

Host에서 플레이어가 움직이면 Client에서도 움직인다. 하지만 Client에서 움직이는 애니메이션이 좀 이상하다.
반대로, Client에서 플레이어가 움직이면 Host에서도 움직인다. 하지만 Host에서 움직이는 애니메이션이 좀 이상하다.
Host에서 Owner가 아닌 플레이어를 보면 PlayerInput이 비활성화 됨.

마찬가지로 Client에서 Owner가 아닌 플레이어를 보면 PlayerInput이 비활성화 됨.



using Unity.Netcode.Components;
using UnityEngine;
public class ClientNetworkAnimator : NetworkAnimator
{
protected override bool OnIsServerAuthoritative()
{
return false;
}
}

기존 Network Animator 컴포넌트를 없애고 스크립트를 할당하여야 한다.
실행 화면

정상적으로 고쳐졌다. 문제가 뭐였을까? 클라이언트는 애니메이터를 건들일 수 없다.
아까 아래 사진처럼 Network Transform 컴포넌트에 Authority Mode가 Owenr가 되어있었다. 즉, 위치를 클라이언트도 변경할 수 있었다. 옛날에는 Owner가 없었고 오로지 Server만 가능했다. 즉, Client도 Animator을 접근할 수 있게 해야 한다.

이 얘기를 왜 하냐면은, Network Animator에 정의를 따라가면 아래 사진처럼 나오게 되는데 그 중에서 IsServerAuthoritative() 메서드를 볼 수 있다.

그래서 아래 코드 처럼 NetworkAnimator을 상속 받아서 OnIsServerAuthoritative()을 false처리 하는 것이다. 즉, 서버만 애니메이션을 바꿀 수 있는 것을 false하는 것이다.
네트워크 애니메이터에 정보를 전달하는 역할을 Client도 가능하게 한 것.


이렇게 하지 않으면, 애니메이션이 없이 클라이언트는 서버한테 저 무슨 동작(애니메이션)을 해야 되요? 라고 요청해야 됨.


실행 화면

그래프가 생겼다.
이 컴포넌트는 네트워크 성능 데이터를 시각적으로 보여주는 컴포넌트이다.
이 컴포넌트를 추가하면 게임 화면의 좌측 상단에 실시간 네트워크 성능을 나타내는 그래프가 표시됩니다.
그래프와 데이터는 네트워크 성능을 이해하고 최적화하는 데 중요한 지표를 제공합니다.
나중에 여기서 Address Port 옵션 설정해서 뚫어주면 된다. 같은망으로 연결해 주는건 유니티 백앤드 서비스를 어떻게 써야하는 듯

가브리엘 어쩌고 에셋


위자드 에셋


텍스처 에셋





아래 폴더를 열고 압축을 푼다.

결과.

이 씬에서 에셋이 제대로 나오면 성공









using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
float rotationSpeed = 10;
[SerializeField]
float moveSpeed = 5;
Vector3 targetPos; //목표 위치
private void Start()
{
targetPos = transform.position;
}
void Update()
{
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if(Input.GetMouseButtonDown(1))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit))
{
targetPos = hit.point;
}
}
//2. 내 위치에서 targetPos 위치로 회전 및 이동 해야 함.
Vector3 dir = targetPos - transform.position;
if(dir.magnitude > moveSpeed * Time.deltaTime) //한 프레임에 이동하는 크기가 moveSpeed * Time.deltaTime보다 크다면 이동
{
Quaternion targerRot = Quaternion.LookRotation(dir); //가야할 방향으로 쳐다 봄
transform.rotation =
Quaternion.Lerp(transform.rotation, targerRot, rotationSpeed * Time.deltaTime); //현재 회전 위치에서 쳐다 봐야할 회전 값까지 서서히 회전
transform.position += dir.normalized * moveSpeed * Time.deltaTime;
}
}
}

실행 화면

정상적으로 이동 및 회전이 구현 되었다. 하지만, 아직 카메라와 애니메이션은 구현되지 않았다.
애니메이션은 데모용이므로 나중에 조정할 것 이다.



//..생략
void Update()
{
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if(Input.GetMouseButtonDown(1))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit,1000, LayerMask.GetMask("Ground"))) //⭐ 추가.
{
targetPos = hit.point;
}
}
//..생략
}

점프를 하지 않을 것이므로 pos y축 해제, rot은 y축 기준으로 회전하므로 y축만 체크 , 크기 조정 안할 것이므로 scale은 모두 해제 . 그리고 Authority Mode는 Owner로 하여 Host와 Client가 독립된 Owner를 각각 존재할 수 있도록 한다.


그리고 UnityTransport을 해준다.




using Unity.Netcode;
using UnityEngine;
public class NetworkManagerUI : MonoBehaviour
{
//네트워크 매니저한테 호스트로 시작할거야? 클라이언트로 시작할거야? 에 대한 정보를 알려주기 위해 간단한 버튼
public void HostPreesed()
{
NetworkManager.Singleton.StartHost();
}
public void ClientPressed()
{
NetworkManager.Singleton.StartClient();
}
}


using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour //⭐
{
//..생략
public override void OnNetworkSpawn()
{
if(IsOwner == false) //내가 Owner가 아니라면
{
return;
}
targetPos = transform.position; //Owner일 때만 실행
}
private void Start()
{
}
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//..생략
}


실행 결과

정상적으로 Host의 Owner만 움직이고 회전 하고, Client의 Owner만 움직이고 회전하게 되었다. 즉, 싱크가 잘 되었다.
using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
//..생략
public override void OnNetworkSpawn()
{
if(IsOwner == false) //내가 Owner가 아니라면
{
return;
}
targetPos = transform.position; //Owner일 때만 실행
}
private void Start()
{
}
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//..생략
Camera.main.transform.position = transform.position + new Vector3(0, 9 , -9); //⭐플레이어에 후방 위치를 카메라 위치로 저장
}

using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
[SerializeField]
float rotationSpeed = 10;
[SerializeField]
float moveSpeed = 5;
Vector3 targetPos; //목표 위치
public override void OnNetworkSpawn()
{
if(IsOwner == false) //내가 Owner가 아니라면
{
return;
}
targetPos = transform.position; //Owner일 때만 실행
}
private void Start()
{
}
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if (Input.GetMouseButtonDown(1))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit,1000, LayerMask.GetMask("Ground")))
{
targetPos = hit.point;
}
}
//2. 내 위치에서 targetPos 위치로 회전 및 이동 해야 함.
Vector3 dir = targetPos - transform.position;
if(dir.magnitude > moveSpeed * Time.deltaTime) //한 프레임에 이동하는 크기가 moveSpeed * Time.deltaTime보다 크다면 이동
{
Quaternion targerRot = Quaternion.LookRotation(dir); //가야할 방향으로 쳐다 봄
transform.rotation =
Quaternion.Lerp(transform.rotation, targerRot, rotationSpeed * Time.deltaTime); //현재 회전 위치에서 쳐다 봐야할 회전 값까지 서서히 회전
transform.position += dir.normalized * moveSpeed * Time.deltaTime;
}
Camera.main.transform.position = transform.position + new Vector3(0, 9, -9); //플레이어에 후방 위치를 카메라 위치로 저장
}
}

아래와 같이 세팅.

using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
[SerializeField]
float rotationSpeed = 10;
[SerializeField]
float moveSpeed = 5;
Vector3 targetPos; //목표 위치
public override void OnNetworkSpawn()
{
if(IsOwner == false) //내가 Owner가 아니라면
{
return;
}
targetPos = transform.position; //Owner일 때만 실행
}
private void Start()
{
}
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if (Input.GetMouseButtonDown(1))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit,1000, LayerMask.GetMask("Ground")))
{
targetPos = hit.point;
}
}
}
private void FixedUpdate()
{
if (IsOwner == false) //⭐내가 Owner가 아니라면 반환
{
return;
}
//2. 내 위치에서 targetPos 위치로 회전 및 이동 해야 함.
Vector3 dir = targetPos - transform.position;
if (dir.magnitude > moveSpeed * Time.fixedDeltaTime) //한 프레임에 이동하는 크기가 moveSpeed * Time.deltaTime보다 크다면 이동
{
Quaternion targerRot = Quaternion.LookRotation(dir); //가야할 방향으로 쳐다 봄
transform.rotation =
Quaternion.Lerp(transform.rotation, targerRot, rotationSpeed * Time.fixedDeltaTime); //현재 회전 위치에서 쳐다 봐야할 회전 값까지 서서히 회전
Vector3 movementVector = dir.normalized * moveSpeed * Time.fixedDeltaTime; //⭐
GetComponent<Rigidbody>().MovePosition(transform.position + movementVector);//⭐
}
Camera.main.transform.position = transform.position + new Vector3(0, 9, -9); //플레이어에 후방 위치를 카메라 위치로 저장
}
}

파라미터 세팅도 해보자.





using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
enum State //⭐
{
Idle,
Move,
Attack,
Dying
}
[SerializeField]
float rotationSpeed = 10;
[SerializeField]
float moveSpeed = 5;
Vector3 targetPos; //목표 위치
State state; //⭐
Animator animator; //⭐ 여러번 쓸 거면 캐싱한다.
//OnNetworkSpawn() 와 Start() 중 누가 먼저 실행될까? 경우에 따라 다르다.
//미리 Spawn 해 놓을 수도 있고 나중에 Spawn 해 놓을 수 있으므로 순서가 다르다.
// Awake()을 사용하는 것이 좋음
private void Start()
{
state = State.Idle; //⭐Idle이긴 하지만 명시적으로 사용
animator = GetComponent<Animator>(); //⭐ 캐싱 초기화.
}
public override void OnNetworkSpawn()
{
//..생략
}
void Update()
{
//..생략
}
private void FixedUpdate()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//2. 내 위치에서 targetPos 위치로 회전 및 이동 해야 함.
Vector3 dir = targetPos - transform.position;
if (dir.magnitude > moveSpeed * Time.fixedDeltaTime) //한 프레임에 이동하는 크기가 moveSpeed * Time.deltaTime보다 크다면 이동
{
SetState(State.Move); //⭐ Move 상태로 전환
//..생략
}
else
{
SetState(State.Idle); //⭐ Idle 상태로 전환
}
Camera.main.transform.position = transform.position + new Vector3(0, 9, -9); //플레이어에 후방 위치를 카메라 위치로 저장
}
void SetState(State newstate) //⭐ 상태에 따른 애니메이터 관리 메서드.
{
// 원래 State머신은 각 state을 스크립트로 만들어 버리는데 용량이 작기 때문에 그냥 이렇게 ㄱ
switch(state)
{
case State.Idle:
if(newstate == State.Attack)
{
animator.SetTrigger("Attack");
}
else if (newstate == State.Move)
{
animator.SetTrigger("Move");
}
break;
case State.Move:
if (newstate == State.Attack)
{
animator.SetTrigger("Attack");
}
else if (newstate == State.Idle)
{
animator.SetTrigger("Idle");
}
break;
case State.Attack:
animator.SetTrigger("Idle");
break;
}
state = newstate;
}
}
using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
//..생략
Vector3 attackTargetPos; //⭐ 공격 대상 위치.
bool isWaitingAttackInput; //⭐ 공격 키 입력 여부
//..생략
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if (Input.GetMouseButtonDown(1) && (state == State.Idle || state == State.Move)) //⭐
{
isWaitingAttackInput = false; //⭐
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit,1000, LayerMask.GetMask("Ground")))
{
targetPos = hit.point;
}
}
// 2. 공격
if(Input.GetKeyDown(KeyCode.A)) //⭐
{
isWaitingAttackInput = true; //⭐
}
if (Input.GetMouseButtonDown(0) && isWaitingAttackInput && (state == State.Idle || state == State.Move)) //⭐
{
isWaitingAttackInput = false; //⭐
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //⭐
RaycastHit hit; //⭐
if (Physics.Raycast(ray, out hit, 1000, LayerMask.GetMask("Ground"))) //⭐
{
attackTargetPos = hit.point; //해당 마우스 지점을 attakcTargetPos에 저장. //⭐
SetState(State.Attack); //⭐ 상태 전환
targetPos = transform.position; //⭐멈춘다.
//실제 발사
}
}
}
public void AttackEnd() //⭐ Attack 애니메이션 끝자락에 이벤트로 호출. Attack 애니메이션 끝나면 Idle로
{
SetState(State.Idle);
}
private void FixedUpdate()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
if (state != State.Attack) //⭐ Attack 상태가 아닐 때만 플레이어 이동 및 회전 가능. 없으면 중복상태 오류
{
//2. 내 위치에서 targetPos 위치로 회전 및 이동 해야 함.
Vector3 dir = targetPos - transform.position;
if (dir.magnitude > moveSpeed * Time.fixedDeltaTime) //한 프레임에 이동하는 크기가 moveSpeed * Time.deltaTime보다 크다면 이동
{
SetState(State.Move);
Quaternion targerRot = Quaternion.LookRotation(dir); //가야할 방향으로 쳐다 봄
transform.rotation =
Quaternion.Lerp(transform.rotation, targerRot, rotationSpeed * Time.fixedDeltaTime); //현재 회전 위치에서 쳐다 봐야할 회전 값까지 서서히 회전
Vector3 movementVector = dir.normalized * moveSpeed * Time.fixedDeltaTime;
GetComponent<Rigidbody>().MovePosition(transform.position + movementVector);
}
else //⭐ 이동이 끝났다면 Idle로
{
SetState(State.Idle);
}
}
else //⭐
{
Vector3 direction = attackTargetPos - transform.position;
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime);
}
Camera.main.transform.position = transform.position + new Vector3(0, 9, -9); //플레이어에 후방 위치를 카메라 위치로 저장
}
void SetState(State newstate)
{
//..생략
}
}


실행 화면

이동 중 마우스 커서 방향으로 공격 방향이 설정되고, 공격 도중에는 움직일 수 없다. 그리고 공격 상태가 끝나면 Idle상태가 된다.


회전은 안할거므로 x,y,z Freeze 시킨다.
중력 끄고, Angular Drag을 0으로.

using UnityEngine;
public class LifeTime : MonoBehaviour
{
[SerializeField]
float lifeTime = 2;
private void Start()
{
Destroy(gameObject, lifeTime);
}
}
using UnityEngine;
public class DestroyOnContact : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
if(other != null)
{
Destroy(gameObject);
}
}
}




총 2개. ClientProjectile과 ProjectileBase을 만듦.
using Unity.Netcode;
using UnityEngine;
public class ProjectileLancuher : NetworkBehaviour
{
//클라이언트나 서버에서 이 스크립트를 사용할 수 있으므로 구분해야함
[SerializeField]
GameObject serverProjectilePrefab; //서버에서 사용하는 발사체 프리팹
[SerializeField]
GameObject clientProjectilePrefab; //클라이언트에서 사용하는 발사체 프리팹입니다
[SerializeField]
float projectileSpeed = 3; //발사체 속도
[SerializeField]
float coolTime = 1; //쿨타임
[SerializeField]
float initialDistance = 1; //발사체가 생성되는 위치를 발사 원점으로부터 얼마나 떨어뜨릴지 설정
float coolTimer = 0; //현재 쿨타임 -> 시간으로 증가 시킬 것임
private void Update()
{
if(IsOwner == false)
{
return;
}
coolTimer += Time.deltaTime;
}
public void Attack(Vector3 targetPos)
{
if(coolTimer < coolTime) //쿨타임이 아직 지나지 않았으면 발사를 중단합니다.
{
return;
}
coolTimer = 0; //발사가 성공적으로 이루어진 후 쿨타이머를 초기화
//발사
}
}
using System.Collections;
using Unity.Netcode;
using UnityEngine;
public class ProjectileLancuher : NetworkBehaviour
{
//클라이언트나 서버에서 이 스크립트를 사용할 수 있으므로 구분해야함
[SerializeField]
GameObject serverProjectilePrefab;
[SerializeField]
GameObject clientProjectilePrefab;
[SerializeField]
float projectileSpeed = 3;
[SerializeField]
float coolTime = 1;
[SerializeField]
float initialDistance = 1;
float coolTimer = 0;
private void Update()
{
if(IsOwner == false)
{
return;
}
coolTimer += Time.deltaTime;
}
public void Attack(Vector3 targetPos)
{
if(coolTimer < coolTime)
{
return;
}
coolTimer = 0;
//⭐ 발사 구현 시작
Vector3 dir = (targetPos - transform.position).normalized; //방향 벡터.
Vector3 spawnPoint = dir * initialDistance + transform.position; //발사체 스폰 위치.
spawnPoint.y = spawnPoint.y + 1;
StartCoroutine(FireAfterDelay(spawnPoint, dir));
}
IEnumerator FireAfterDelay(Vector3 spawnPoint, Vector3 dir) //⭐ 딜레이 주는 메서드
{
yield return new WaitForSeconds(0.5f);
SpawnDummyProjectile(spawnPoint, dir);
}
void SpawnDummyProjectile(Vector3 spawnPoint,Vector3 dir) //⭐ 모양은 있으나, 실제로 데미지 계산은 하지 않는.
{
GameObject projectile = Instantiate(clientProjectilePrefab, spawnPoint, Quaternion.identity);
Physics.IgnoreCollision(GetComponent<Collider>(),projectile.GetComponent<Collider>()); // 내 콜라이더와 발사체 콜라이더를 무시.
if(projectile.TryGetComponent<Rigidbody>(out Rigidbody rb)) //TryGetComponent : 컴포넌트 찾아내면 True -> rb에 저장
{
rb.linearVelocity = dir * projectileSpeed;
}
}
}
using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
//..생략
// 2. 공격
if(Input.GetKeyDown(KeyCode.A))
{
isWaitingAttackInput = true;
}
if (Input.GetMouseButtonDown(0) && isWaitingAttackInput && (state == State.Idle || state == State.Move))
{
isWaitingAttackInput = false;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 1000, LayerMask.GetMask("Ground")))
{
attackTargetPos = hit.point; //해당 마우스 지점을 attakcTargetPos에 저장.
SetState(State.Attack); //상태 전환
targetPos = transform.position; //멈춘다.
//실제 발사
GetComponent<ProjectileLancuher>().Attack(attackTargetPos); // ⭐ Attack 메서드 호출
}
}
}
//..생략
}










using System.Collections;
using Unity.Netcode;
using UnityEngine;
public class ProjectileLancuher : NetworkBehaviour
{
//클라이언트나 서버에서 이 스크립트를 사용할 수 있으므로 구분해야함
[SerializeField]
GameObject serverProjectilePrefab;
[SerializeField]
GameObject clientProjectilePrefab;
[SerializeField]
float projectileSpeed = 3;
[SerializeField]
float coolTime = 1;
[SerializeField]
float initialDistance = 1;
float coolTimer = 0;
private void Update()
{
if(IsOwner == false)
{
return;
}
coolTimer += Time.deltaTime;
}
public void Attack(Vector3 targetPos)
{
if(coolTimer < coolTime)
{
return;
}
coolTimer = 0;
//발사
Vector3 dir = (targetPos - transform.position).normalized; //방향 벡터.
Vector3 spawnPoint = dir * initialDistance + transform.position; //발사체 스폰 위치.
spawnPoint.y = spawnPoint.y + 1;
StartCoroutine(FireAfterDelay(spawnPoint, dir));
}
IEnumerator FireAfterDelay(Vector3 spawnPoint, Vector3 dir) //딜레이 주는 메서드
{
yield return new WaitForSeconds(0.5f);
SpawnDummyProjectile(spawnPoint, dir); //⭐여기서 서버한테도 알려야 함.
FireServerRpc(spawnPoint, dir); //⭐
}
[ServerRpc] //⭐
void FireServerRpc(Vector3 spawnPoint, Vector3 dir) //⭐서버 -> 클라이언트
{
GameObject projectile = Instantiate(serverProjectilePrefab, spawnPoint, Quaternion.identity); //serverProjectilePrefab 생성
Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>()); // 내 콜라이더와 발사체 콜라이더를 무시.
if (projectile.TryGetComponent<Rigidbody>(out Rigidbody rb)) //찾아내면 True -> rb에 저장
{
rb.linearVelocity = dir * projectileSpeed;
}
FireClientRpc(spawnPoint, dir);
}
[ClientRpc] //⭐
void FireClientRpc(Vector3 spawnPoint, Vector3 dir) //⭐
{
if(IsOwner)
{
return;
}
SpawnDummyProjectile(spawnPoint, dir);
}
void SpawnDummyProjectile(Vector3 spawnPoint,Vector3 dir) //모양은 있으나, 실제로 데미지 계산은 하지 않는.
{
GameObject projectile = Instantiate(clientProjectilePrefab, spawnPoint, Quaternion.identity);
Physics.IgnoreCollision(GetComponent<Collider>(),projectile.GetComponent<Collider>()); // 내 콜라이더와 발사체 콜라이더를 무시.
if(projectile.TryGetComponent<Rigidbody>(out Rigidbody rb)) //찾아내면 True -> rb에 저장
{
rb.linearVelocity = dir * projectileSpeed;
}
}
}


[ServerRpc]

[ClientRpc]




발사체를 소유한 클라이언트가 중복 생성하지 않도록 하기 위해 작성된 코드



using UnityEngine;
public class SpawnOnDestroy : MonoBehaviour
{
//발사체가 사라지는 순간에 이펙트 생성 슈우웃~
[SerializeField]
GameObject dustPrefab;
private void OnDestroy()
{
Instantiate(dustPrefab).transform.position = transform.position; //발사체가 사라질 때 사라진 위치에 이펙트 생성.
}
}





최종 세팅

using UnityEngine;
public class LookAtCamera : MonoBehaviour
{
void Update()
{
transform.LookAt(transform.position + Camera.main.transform.forward);
}
}

실행 결과

사라지는 이펙트, 체력 바 , 마나 바 모두 잘 작동한다.

using Unity.Netcode;
using UnityEngine;
public class Health : NetworkBehaviour
{
public int maxHealth = 100;
public NetworkVariable<int> currentHealth = new NetworkVariable<int>();
bool isDead;
public override void OnNetworkSpawn()
{
if(IsServer == false) //클라이언트의 체력은 서버가 책임져야 한다.
{
return;
}
currentHealth.Value = maxHealth;
}
public void TakeDamage(int damage) //체력 감소
{
ModifyHealth(-damage);
}
public void RestoreHealth(int health) //체력 증가
{
ModifyHealth(health);
}
void ModifyHealth(int value)
{
if(isDead)
{
return;
}
int newHealth = currentHealth.Value + value; //현재 체력 + 변화된 체력 = 새로운 체력
currentHealth.Value = Mathf.Clamp(newHealth, 0, maxHealth); //새로운 체력은 maxHealth 값 까지만 도달할 수 있ㅇ므.
if(currentHealth.Value == 0)
{
isDead = true;
}
}
}


using Unity.Netcode;
using UnityEngine;
public class DealDamageContact : MonoBehaviour
{
[SerializeField]
int damage = 10;
ulong ownerClientid;
public void SetOwner(ulong owerClientid)
{
this.ownerClientid = owerClientid;
}
private void OnTriggerEnter(Collider other)
{
if(other.attachedRigidbody == null) //충돌한 콜라이더의 부모 오브젝트의 리지드바디
{
return;
}
if(other.TryGetComponent<NetworkObject>(out NetworkObject obj))
{
if(ownerClientid == obj.OwnerClientId) //자해를 막고
{
return;
}
}
if(other.TryGetComponent<Health>(out Health health))
{
health.TakeDamage(damage);
}
}
}
using System.Collections;
using Unity.Netcode;
using UnityEngine;
public class ProjectileLancuher : NetworkBehaviour
{
//클라이언트나 서버에서 이 스크립트를 사용할 수 있으므로 구분해야함
//..생략
[ServerRpc]
void FireServerRpc(Vector3 spawnPoint, Vector3 dir) //2. 클라이언트 -> 서버
{
GameObject projectile = Instantiate(serverProjectilePrefab, spawnPoint, Quaternion.identity); //serverProjectilePrefab 생성
projectile.GetComponent<DealDamageContact>().SetOwner(OwnerClientId); //⭐ 호출
Physics.IgnoreCollision(GetComponent<Collider>(), projectile.GetComponent<Collider>()); // 내 콜라이더와 발사체 콜라이더를 무시.
if (projectile.TryGetComponent<Rigidbody>(out Rigidbody rb)) //찾아내면 True -> rb에 저장
{
rb.linearVelocity = dir * projectileSpeed;
}
FireClientRpc(spawnPoint, dir);
}
//..생략
}


서버용 발사체는 서버에만 있는 거고, 딜 계산은 서버만 할 것이다. 그래서 서버가 딜 계산을 하기 위해서 서버용 발사체가 따로 있는거다. 호스트는 서버와 클라이언트 모두 있지만, 다른 클라이언트에게 발사체를 보여주기 위해서 클라이언트용 발사체가 있는 것이다.

using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
public class StatDisPlayer : NetworkBehaviour
{
[SerializeField]
Health health;
[SerializeField]
Image healthBarImage;
[SerializeField]
Image mpBarImage;
private void Start()
{
if(IsClient == false)
{
return;
}
if(health != null)
{
health.currentHealth.OnValueChanged += HandleHealthChange;
}
}
void HandleHealthChange(int oldHelath,int newHealth)
{
healthBarImage.fillAmount = newHealth / (float)health.maxHealth;
}
}

실행 결과

몬스터를 잡으면 마나 아이템을 파밍할 수 있다.
마나가 다 떨어지면, 마나가 점차 증가하는 것을 구현

using Unity.Netcode;
using UnityEngine;
public class MagicPoint : NetworkBehaviour
{
public float maxMagicPoint = 100;
public NetworkVariable<float> currentMagic = new NetworkVariable<float>(0,NetworkVariableReadPermission.Everyone,NetworkVariableWritePermission.Owner);
//클라이언트에서 관리할 때 즉시즉시 반영 -> 이제 클라이언트가 마나를 관리
// public NetworkVariable<float> currentMagic = new NetworkVariable<float>();
[SerializeField]
float regerateRate = 6.0f;
public override void OnNetworkSpawn() //스폰되지마자 실행
{
if(IsOwner == false) //서버가 관리해야 하므로 서버가 아니라면 실행 x
{
return;
}
currentMagic.Value = maxMagicPoint;
}
private void Update()
{
if (IsOwner == false) //서버가 관리해야 하므로 서버가 아니라면 실행 x
{
return;
}
currentMagic.Value = Mathf.Clamp(currentMagic.Value + regerateRate * Time.deltaTime, 0, maxMagicPoint); //마나 업데이트 코드
}
public bool UseMagic(float magic) //마법을 사용할 때 마나 깎기
{
//마나가 쓸 수 있는 상황과 쓸 수 없는 상황을 true,false로 판별.
if(currentMagic.Value < magic) // 현재 마나 30 < 사용해야 될 마나 50 => 30마나로 50마나를 사용할 수 없음 => false
{
return false;
}
ModifyMagicPoint(-magic);
return true;
}
public void RestoreMagic(float magic)
{
ModifyMagicPoint(magic);
}
private void ModifyMagicPoint(float value)
{
currentMagic.Value = Mathf.Clamp(currentMagic.Value + value, 0, maxMagicPoint);
}
}
using TMPro;
using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
//..생략
void Update()
{
if (IsOwner == false) //내가 Owner가 아니라면 반환
{
return;
}
//1. 마우스 우 클릭하면 이동해야 하므로 , 우클릭 했을 때 마우스 위치를 targetPos에 저장해야 함
if (Input.GetMouseButtonDown(1) && (state == State.Idle || state == State.Move))
{
isWaitingAttackInput = false;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit,1000, LayerMask.GetMask("Ground")))
{
targetPos = hit.point;
}
}
// 2. 공격
if(Input.GetKeyDown(KeyCode.A))
{
isWaitingAttackInput = true;
}
if (Input.GetMouseButtonDown(0) && isWaitingAttackInput && (state == State.Idle || state == State.Move))
{
isWaitingAttackInput = false;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 1000, LayerMask.GetMask("Ground")))
{
if(GetComponent<MagicPoint>().UseMagic(20)) //⭐ 마나 20을 사용할 수 있다면?
{
attackTargetPos = hit.point; //해당 마우스 지점을 attakcTargetPos에 저장.
SetState(State.Attack); //상태 전환
targetPos = transform.position; //멈춘다.
//실제 발사
GetComponent<ProjectileLancuher>().Attack(attackTargetPos);
}
}
}
}
public void AttackEnd() //Attack 애니메이션 끝자락에 이벤트로 호출
{
SetState(State.Idle);
}
//..생략
}

using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
public class StatDisPlayer : NetworkBehaviour
{
[SerializeField]
Health health;
[SerializeField]//⭐
MagicPoint magicPoint;//⭐
[SerializeField]
Image healthBarImage;
[SerializeField]
Image mpBarImage;
public override void OnNetworkSpawn() //⭐
{
if (IsClient == false)
{
return;
}
if (health != null)
{
health.currentHealth.OnValueChanged += HandleHealthChange;
}
if (magicPoint != null) //⭐
{
magicPoint.currentMagic.OnValueChanged += HandleMagicPointChange;
}
}
public override void OnNetworkDespawn() //⭐
{
if (IsClient == false)
{
return;
}
if (health != null)
{
health.currentHealth.OnValueChanged -= HandleHealthChange;
}
if (magicPoint != null) //⭐
{
magicPoint.currentMagic.OnValueChanged -= HandleMagicPointChange;
}
}
void HandleHealthChange(int oldHelath,int newHealth)
{
healthBarImage.fillAmount = newHealth / (float)health.maxHealth;
}
void HandleMagicPointChange(float oldMagic, float newMagic) //⭐
{
mpBarImage.fillAmount = newMagic / magicPoint.maxMagicPoint;
}
}

실행 결과

마나 감소 또한 마나 ui가 감소되고 증가가 잘 되는 것을 볼 수 있다.
public NetworkVariable<float> currentMagic = new NetworkVariable<float>();
public NetworkVariable<float> currentMagic = new NetworkVariable<float>();
[SerializeField]
float regerateRate = 6.0f;
public override void OnNetworkSpawn() //스폰되지마자 실행
{
if(IsServer == false) //⭐ 서버가 관리해야 하므로 서버가 아니라면 실행 x
{
return;
}
currentMagic.Value = maxMagicPoint;
}
private void Update()
{
if (IsServer == false) //⭐ 서버가 관리해야 하므로 서버가 아니라면 실행 x
{
return;
}
currentMagic.Value = Mathf.Clamp(currentMagic.Value + regerateRate * Time.deltaTime, 0, maxMagicPoint); //마나 업데이트 코드
}


public NetworkVariable<float> currentMagic = new NetworkVariable<float>();






결과. 이 아이템으로 마나를 증가 시킬 것이다.



Monster 레이어를 만든다.

Monster 레이어 설정.

레이어 충돌체 상호작용 설정.

인스펙터에서 값 설정한다.

using Unity.Netcode;
using UnityEngine;
public class MonsterSpawner : NetworkBehaviour
{
[SerializeField]
GameObject[] monsterPrefabs; //몬스터가 여러개이므로 배열로.
[SerializeField]
int maxMonsters = 20; //최대 몬스터 생성 갯수
[SerializeField]
Vector2 minSpawnPos;
[SerializeField]
Vector2 maxSpawnPos;
//몬스터가 겹치지 않기 위해서. 즉, 피아할 레이어
[SerializeField]
LayerMask layerMask;
float monsterRadius; //몬스터 생성 범위.
Collider[] monsterBuffer = new Collider[1]; //1개 짜리 버퍼를 만들어도 ㄱㅊ음.
public override void OnNetworkSpawn()
{
//뿌려준다. 서버만 관리해야 함. 클라이언트는 접근 불가.
if(IsServer == false)
{
return;
}
monsterRadius = monsterPrefabs[0].GetComponent<SphereCollider>().radius;
for(int i = 0; i< maxMonsters; i++)
{
SpawnMonster();
}
}
void SpawnMonster()
{
GameObject go = Instantiate(monsterPrefabs[Random.Range(0, monsterPrefabs.Length)], GetSpawnPos(), Quaternion.identity);
go.GetComponent<NetworkObject>().Spawn();//서버에서 스폰을 하긴 하지만 네트워크 오브젝트를 통해서 전송
}
Vector3 GetSpawnPos()//몬스터가 생성될 때 겹치지 않아야 하므로 관련 메서드 생성
{
float x = 0;
float z = 0;
while(true)
{
x = Random.Range(minSpawnPos.x, maxSpawnPos.x);
z = Random.Range(minSpawnPos.y, maxSpawnPos.y);
Vector3 spawnPos = new Vector3(x, 0, z); //y는 0으로 해야함. 3d에서는
int numColliders = Physics.OverlapSphereNonAlloc(spawnPos, monsterRadius, monsterBuffer,layerMask);
//return 값이 int. int 겹친 갯수를 numColliders에 저장. 충돌체 정보는 monsterBuffer에 담겨져있음.
if(numColliders == 0)
{
return spawnPos;
}
}
}
}



Rigidbody에서 x,z축 회전을 막고, y축으로 위치 이동하는 것을(미끄러짐) 막는다.
Network Transform에서 x,z축으로 만 위치 싱크를 맞출 수 있게 하고, y축 회전(3d기준)을 할 수 있게 싱크한다. scale은 조정 안할 것이므로 모두 해제.
NetworkRigidbody는 일반적인 리지드 바디를 갖고 있는 것을 서버가 갖고와서 서버가 관리한다.

실행 결과



Cavas에 StatDisplayer 스크립트와 LookAtCamera 스크립트를 추가한다. 아래와 같이 세팅


using Unity.Netcode;
using UnityEngine;
public class DealDamageContact : MonoBehaviour
{
[SerializeField]
int damage = 10;
ulong ownerClientid;
public void SetOwner(ulong owerClientid)
{
this.ownerClientid = owerClientid;
}
private void OnTriggerEnter(Collider other)
{
if(other.attachedRigidbody == null) //충돌한 콜라이더의 부모 오브젝트의 리지드바디
{
return;
}
if(other.TryGetComponent<NetworkObject>(out NetworkObject obj))
{
if(ownerClientid == obj.OwnerClientId && other.GetComponent<Monster>() == null) //⭐ 자해를 막고
{
return;
}
}
if(other.TryGetComponent<Health>(out Health health))
{
health.TakeDamage(damage);
}
}
}

using System;
using Unity.Netcode;
using UnityEngine;
public class Health : NetworkBehaviour
{
public int maxHealth = 100;
public NetworkVariable<int> currentHealth = new NetworkVariable<int>();
bool isDead;
public Action OnDie; //⭐
public override void OnNetworkSpawn()
{
if(IsServer == false) //클라이언트의 체력은 서버가 책임져야 한다.
{
return;
}
currentHealth.Value = maxHealth;
}
public void TakeDamage(int damage) //체력 감소
{
ModifyHealth(-damage);
}
public void RestoreHealth(int health) //체력 증가
{
ModifyHealth(health);
}
void ModifyHealth(int value)
{
if(isDead)
{
return;
}
int newHealth = currentHealth.Value + value; //현재 체력 + 변화된 체력 = 새로운 체력
currentHealth.Value = Mathf.Clamp(newHealth, 0, maxHealth); //새로운 체력은 maxHealth 값 까지만 도달할 수 있ㅇ므.
if(currentHealth.Value == 0)
{
isDead = true;
OnDie?.Invoke(); //⭐ OnDie가 null 아니면, 즉, OnDie라면 이벤트 뿌리기.
}
}
}
using Unity.Netcode;
using UnityEngine;
public class Monster : NetworkBehaviour //⭐
{
public override void OnNetworkSpawn() //⭐
{
//체력이 떨어져서 죽을 때 이벤트 받기
GetComponent<Health>().OnDie += OnDie;
}
public override void OnNetworkDespawn() //⭐
{
GetComponent<Health>().OnDie -= OnDie;
}
void OnDie() //⭐
{
//죽을 때 뭔가 애니메이션 또는 특수효과를 넣고 싶다면, 여기에다 ㄱㄱ
Destroy(gameObject);
}
}



using UnityEngine;
public class SpawnOnDestroy : MonoBehaviour
{
//발사체가 사라지는 순간에 이펙트 생성 슈우웃~
[SerializeField]
GameObject dustPrefab;
private void OnDestroy()
{
Instantiate(dustPrefab).transform.position = transform.position; //발사체가 사라질 때 사라진 위치에 이펙트 생성.
}
}


using Unity.Netcode;
using UnityEngine;
public class MagicPotionItem : NetworkBehaviour
{
[SerializeField]
float magicPoint = 50; //먹으면 마나 50증가
public float Collect()
{
if(IsServer == false) //서버가 아니라면 실행 x.
{
return 0;
}
Destroy(gameObject);
return magicPoint;
}
}

using Unity.Netcode;
using UnityEngine;
public class NetworkSpawnDestroy : NetworkBehaviour
{
[SerializeField]
GameObject prefab;
public override void OnDestroy() //NetworkBehaviour 용 OnDestroy()임.
{
base.OnDestroy();
if(IsServer == false)
{
return;
}
GameObject go = Instantiate(prefab); //prefab생성
go.transform.position = transform.position; //프리펩 위치를 이 스크립트 주인의 위치로
go.GetComponent<NetworkObject>().Spawn(); //프리펩 스폰.
}
}

실행 결과

몬스터가 죽었을 때 프리펩(마나 아이템)이 정상적으로 생성된다.



using Unity.Netcode;
using UnityEngine;
public class ItemCollecter : NetworkBehaviour
{
private void OnTriggerEnter(Collider other)
{
if(!other.TryGetComponent<MagicPotionItem>(out MagicPotionItem item))
{
return;
}
float magic = item.Collect();
if (IsServer == false)
{
return;
}
GetComponent<MagicPoint>().RestoreMagicClientRpc(magic,
new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = new[] { OwnerClientId }
}
});
}
}

아이템 레이어 충돌 상호작용 설정.

using Unity.Netcode;
using UnityEngine;
public class NetworkSpawnDestroy : NetworkBehaviour
{
[SerializeField]
GameObject prefab;
public override void OnDestroy() //NetworkBehaviour 용 OnDestroy()임.
{
base.OnDestroy();
if(NetworkManager.Singleton.IsListening && !NetworkManager.Singleton.ShutdownInProgress) //⭐ 추가.
{
if (IsServer == false)
{
return;
}
GameObject go = Instantiate(prefab); //prefab생성
go.transform.position = transform.position; //프리펩 위치를 이 스크립트 주인의 위치로
go.GetComponent<NetworkObject>().Spawn(); //프리펩 스폰.
}
}
}

using System;
using Unity.Netcode;
using UnityEngine;
public class Health : NetworkBehaviour
{
public int maxHealth = 100;
public NetworkVariable<int> currentHealth = new NetworkVariable<int>();
bool isDead;
public Action<Health> OnDie; //⭐ this는 곧 이 스크립트인 Health이므로 Health 타입.
public override void OnNetworkSpawn()
{
if(IsServer == false) //클라이언트의 체력은 서버가 책임져야 한다.
{
return;
}
currentHealth.Value = maxHealth;
}
public void TakeDamage(int damage) //체력 감소
{
ModifyHealth(-damage);
}
public void RestoreHealth(int health) //체력 증가
{
ModifyHealth(health);
}
void ModifyHealth(int value)
{
if(isDead)
{
return;
}
int newHealth = currentHealth.Value + value; //현재 체력 + 변화된 체력 = 새로운 체력
currentHealth.Value = Mathf.Clamp(newHealth, 0, maxHealth); //새로운 체력은 maxHealth 값 까지만 도달할 수 있ㅇ므.
if(currentHealth.Value == 0)
{
isDead = true;
// OnDie?.Invoke(); //OnDie가 null 아니면, 즉, OnDie라면 이벤트 뿌리기.
OnDie?.Invoke(this); //⭐ this 키워드를 붙이면 누가 죽었는지 알 수 있다.
}
}
}
using Unity.Netcode;
using UnityEngine;
public class Monster : NetworkBehaviour
{
public override void OnNetworkSpawn()
{
//체력이 떨어져서 죽을 때 이벤트 받기
GetComponent<Health>().OnDie += OnDie;
}
public override void OnNetworkDespawn()
{
GetComponent<Health>().OnDie -= OnDie;
}
void OnDie(Health sender) //⭐Health sender 추가.
{
//죽을 때 뭔가 애니메이션 또는 특수효과를 넣고 싶다면, 여기에다 ㄱㄱ
Destroy(gameObject);
}
}
using Unity.Netcode;
using UnityEngine;
public class MonsterSpawner : NetworkBehaviour
{
[SerializeField]
GameObject[] monsterPrefabs; //몬스터가 여러개이므로 배열로.
[SerializeField]
int maxMonsters = 20; //최대 몬스터 생성 갯수
[SerializeField]
Vector2 minSpawnPos;
[SerializeField]
Vector2 maxSpawnPos;
//몬스터가 겹치지 않기 위해서. 즉, 피아할 레이어
[SerializeField]
LayerMask layerMask;
float monsterRadius; //몬스터 생성 범위.
Collider[] monsterBuffer = new Collider[1]; //1개 짜리 버퍼를 만들어도 ㄱㅊ음.
public override void OnNetworkSpawn()
{
//뿌려준다. 서버만 관리해야 함. 클라이언트는 접근 불가.
if(IsServer == false)
{
return;
}
monsterRadius = monsterPrefabs[0].GetComponent<SphereCollider>().radius; //0.5
for(int i = 0; i< maxMonsters; i++)
{
SpawnMonster();
}
}
void SpawnMonster()
{
GameObject go = Instantiate(monsterPrefabs[Random.Range(0, monsterPrefabs.Length)], GetSpawnPos(), Quaternion.identity);
go.GetComponent<NetworkObject>().Spawn();//서버에서 스폰을 하긴 하지만 네트워크 오브젝트를 통해서 전송
go.GetComponent<Health>().OnDie += OnDie; //⭐
}
private void OnDie(Health sender) //⭐
{
sender.OnDie -= OnDie; //⭐
SpawnMonster(); //⭐
}
Vector3 GetSpawnPos()//몬스터가 생성될 때 겹치지 않아야 하므로 관련 메서드 생성
{
float x = 0;
float z = 0;
while(true)
{
x = Random.Range(minSpawnPos.x, maxSpawnPos.x);
z = Random.Range(minSpawnPos.y, maxSpawnPos.y);
Vector3 spawnPos = new Vector3(x, 0, z); //y는 0으로 해야함. 3d에서는
int numColliders = Physics.OverlapSphereNonAlloc(spawnPos, monsterRadius, monsterBuffer,layerMask);
//return 값이 int. int 겹친 갯수를 numColliders에 저장. 충돌체 정보는 monsterBuffer에 담겨져있음.
if(numColliders == 0)
{
return spawnPos;
}
}
}
}



유저는 네트워크가 필요한 객체이므로, NetworkObject 컴포넌트도 추가.

using System;
using System.Collections;
using Unity.Netcode;
using Unity.Services.Lobbies.Models;
using UnityEngine;
public class PlayerRespawnManager : NetworkBehaviour
{
[SerializeField]
NetworkObject playerPrefab;
public override void OnNetworkSpawn()
{
//죽은 이벤트를 갖다가 리스폰하겠다.
if(IsServer == false) //플레이어 리스폰은 무조건 서버가 할 것이므로 서버가 아니면 return;
{
return;
}
- 4.
//Manager가 생성되기 전에 생성된 Player 친구도 이벤트를 뿌려야 함. -> 뭔말?
PlayerController[] players = FindObjectsByType<PlayerController>(FindObjectsSortMode.None); //모든 PlayerController 컴포넌트를 모두 찾음
foreach(PlayerController player in players)
{
HandlePlayerSpawn(player);
}
PlayerController.OnPlayerSpawn += HandlePlayerSpawn;
PlayerController.OnPlayerDespawn += HandlePlayerDeSpawn;
}
public override void OnNetworkDespawn()
{
PlayerController.OnPlayerSpawn -= HandlePlayerSpawn;
PlayerController.OnPlayerDespawn -= HandlePlayerDeSpawn;
}
private void HandlePlayerSpawn(PlayerController player)
{
//너의 죽음 이벤트를 받겠노라
player.GetComponent<Health>().OnDie += HandlePlayerDie;
}
private void HandlePlayerDeSpawn(PlayerController player)
{
player.GetComponent<Health>().OnDie -= HandlePlayerDie;
}
private void HandlePlayerDie(Health sender)
{
PlayerController player = sender.GetComponent<PlayerController>();
StartCoroutine(RespawnPlayerRoutine(player.OwnerClientId)); //주인은 바뀌면 안되므로 주인을 인자로 넘김.
Destroy(player.gameObject);
}
IEnumerator RespawnPlayerRoutine(ulong ownerClientid)
{
yield return null;
NetworkObject playerObj = Instantiate(playerPrefab,SpawnPoint.GetRandomSpawnPoint(),Quaternion.identity); //NetworkObject타입의 playerPrefab을 Instantiate하면 NetworkObject을 반환
playerObj.SpawnAsPlayerObject(ownerClientid);
}
}
using System;
using TMPro;
using Unity.Netcode;
using UnityEngine;
public class PlayerController : NetworkBehaviour
{
enum State
{
Idle,
Move,
Attack,
Dying
}
[SerializeField]
float rotationSpeed = 10;
[SerializeField]
float moveSpeed = 5;
Vector3 targetPos; //목표 위치
State state;
Animator animator; //여러번 쓸 거면 캐싱한다.
//OnNetworkSpawn() 와 Start() 중 누가 먼저 실행될까? 경우에 따라 다르다.
//미리 Spawn 해 놓을 수도 있고 나중에 Spawn 해 놓을 수 있으므로 순서가 다르다.
// Awake()을 사용하는 것이 좋음
public static Action<PlayerController> OnPlayerSpawn; //⭐ static 이므로 플레이어가 죽어도 OnPlayerSpawn , OnPlayerDespawn 을 호출할 수있다.
public static Action<PlayerController> OnPlayerDespawn; //⭐static이므로 플레이어가 죽어도 OnPlayerSpawn , OnPlayerDespawn 을 호출할 수있다.
Vector3 attackTargetPos; //공격 대상 위치.
bool isWaitingAttackInput; //공격 키 입력 여부
private void Start()
{
state = State.Idle; //Idle이긴 하지만 명시적으로 사용
animator = GetComponent<Animator>();
}
public override void OnNetworkSpawn()
{
if(IsServer) //⭐
{
OnPlayerSpawn?.Invoke(this); //⭐ this 키워드를 통해 어느 플레이어가 죽었는지 알 수 있음.
}
if(IsOwner == false) //내가 Owner가 아니라면
{
return;
}
targetPos = transform.position; //Owner일 때만 실행
}
public override void OnNetworkDespawn()
{
if (IsServer) //⭐
{
OnPlayerDespawn?.Invoke(this); //⭐this 키워드를 통해 어느 플레이어가 죽었는지 알 수 있음.
}
}
void Update()
{
//..생략
}
public void AttackEnd() //Attack 애니메이션 끝자락에 이벤트로 호출
{
//..생략
}
private void FixedUpdate()
{
//..생략
}
void SetState(State newstate)
{ //..생략
}
}



Window - Multiplayer - Multiplayer Center . 이 탭은 상황에 맞춰서 만들고자 하는 게임의 장르와 설정을 선택하면 필요한 패키지를 설치할 수 있게 해줍니다.



Server 탭에서 활성화 시킨 옵션들은 서버에서 UI,Audio,렌더링 컴포넌트 같은게 필요없을 경우 활성화 시킨다. 마지막 Apply 버튼 클릭


Linux Dedicated Server Build Support 설치

이것도 설치


결과

임포트를 하고 나면 7개의 에러메시지가 나타난다. 나중에 수정할 예정이니 걱정 ㄴ


userId를 userAuthId로 수정한다. 그래도 에러메시지가 나타나면 패키지에 경고 아이콘이 뜨는 패키지를 Remove한다.
using System;
using UnityEngine;
public enum GameMode //⭐ 추가
{
Default//⭐ 추가
}
public enum GameQueue //⭐ 추가
{
Solo, //⭐ 추가
Team //⭐ 추가
}
public enum Map //⭐ 추가
{
Default //⭐ 추가
}
[Serializable]
public class UserData
{
public string userName;
public string userAuthId;
public GameInfo userGamePreferences; //⭐ 추가
}
[Serializable] //⭐ 추가
public class GameInfo //⭐ 추가
{
public Map map; //⭐ 추가
public GameMode gameMode; //⭐ 추가
public GameQueue gameQueue; //⭐ 추가
public string ToMultiplayQueue() //⭐ 추가
{
return ""; //⭐ 추가
}
}

using System.Threading.Tasks;
using Unity.Android.Gradle.Manifest;
using Unity.Multiplayer;
using Unity.Services.Authentication;
using Unity.Services.Core;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ApplicationManager : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
async void Start()
{
DontDestroyOnLoad(gameObject);
// await LaunchInMode(SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null); //⭐ 주석
await LaunchInMode(MultiplayerRolesManager.ActiveMultiplayerRoleMask == MultiplayerRoleFlags.Server); //⭐ 추가
}
//서버일 경우와 아닐 경우 나눔.
async Task LaunchInMode(bool isDedicatedServer)
{
if(isDedicatedServer)
{
}
else
{
//임시 코드
bool authenticated = await ClientSingleTon.Instance.InitAsync();
HostSingleTon hostSingleTon = HostSingleTon.Instance;
//await HostSingleTon.Instance.StartHostAsync(); //버튼 눌렀을 대 사용할 코드
if(authenticated)
{
GotoMenu();
}
}
}
public void GotoMenu()
{
SceneManager.LoadScene("MenuScene");
}
}

데드케이트 서버는 게임 시작 시 ServerInstance가 생성되고, 게임 종료시 인스턴스는 사라진다.

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Unity.Netcode;
using UnityEngine;
public class ServerGameManager : IDisposable
{
string serverIp;
ushort serverPort; // 게임 데이터를 얻을 때 사용하는 포트
ushort queryPort; // 서버 정보를 얻을 때 사용하는 포트
MultiplayAllocationService multiplayAllocationService;
public ServerGameManager(string serverIp, ushort serverPort, ushort queryPort, NetworkManager manager)
{
this.serverIp = serverIp;
this.serverPort = serverPort;
this.queryPort = queryPort;
multiplayAllocationService = new MultiplayAllocationService();
}
public async Task StartGameServerAsync()
{
await multiplayAllocationService.BeginServerCheck();
if (!ServerSingleTon.Instance.OpenConnection(serverIp, serverPort))
{
Debug.LogWarning("Network Server not started");
return;
}
// 서버가 열렸으면 BattleScene으로 이동
NetworkManager.Singleton.SceneManager.LoadScene("BattleScene",
UnityEngine.SceneManagement.LoadSceneMode.Single);
}
public void Dispose()
{
multiplayAllocationService?.Dispose();
}
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Core;
using UnityEditor.EditorTools;
using UnityEngine;
public class ServerSingleTon : MonoBehaviour
{
static ServerSingleTon instance;
public ServerGameManager serverManager; //⭐ 추가
public static ServerSingleTon Instance
{
get
{
if (instance == null)
{
GameObject singleTon = new GameObject("ServerSingleTon");
instance = singleTon.AddComponent<ServerSingleTon>();
DontDestroyOnLoad(singleTon);
}
return instance;
}
}
public void Init() //인스턴스의 생성을 위한 메서드
{
}
public Action<string> OnClientLeft;
private void OnEnable()
{
NetworkManager.Singleton.ConnectionApprovalCallback += ApprovalCheck;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisConnect; //클라이언트가 접속을 종료 했을 때. -> 실행해야할 함수를 지정
}
private void OnDisable()
{
if(NetworkManager.Singleton != null)
{
NetworkManager.Singleton.ConnectionApprovalCallback -= ApprovalCheck;
NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisConnect;
}
}
private void OnClientDisConnect(ulong clientid) //클라이언트가 호스트 서버에서 접속 종료했을 때 메서드
{
if(ClientToUserData.ContainsKey(clientid))
{
//로비에 호스트가 있고, 클라이언트가 호스트에 접속. 로비에서 클라이언트들이 접속을 종료했다고 호스트에 알려줘야함
// -> 로비를 만드는 HostSingleTon.cs에서 구현해야 함.
string authld = ClientToUserData[clientid].userAuthId;
ClientToUserData.Remove(clientid);
if (clientid != 0) //클라이언트 id가 0이 아닐 경우 -> 즉, 서버가 아닐 경우
{
OnClientLeft?.Invoke(authld);
}
}
}
public Dictionary<ulong, UserData> ClientToUserData = new Dictionary<ulong, UserData>();
private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
//
string payload = Encoding.UTF8.GetString(request.Payload);
UserData userData = JsonConvert.DeserializeObject<UserData>(payload);
Debug.Log("User Data : " + userData.userName);
//저장. 들어온 유저의 클라이언트id를 저장한다. 딕셔너리로 관리
ClientToUserData[request.ClientNetworkId] = userData;
response.Approved = true; //승인
response.CreatePlayerObject = true; //플레이어 오브젝트 만들어줌.
response.Position = SpawnPoint.GetRandomSpawnPoint(); //이 클라이언트가 들어올 때 스폰포인트로 지정해서 들어올 수 있음.
response.Rotation = Quaternion.identity;
}
public async Task CreateServer() //⭐ 추가
{
await UnityServices.InitializeAsync(); //⭐ 추가
serverManager = new ServerGameManager(
ApplicationData.IP(),
(ushort)ApplicationData.Port(),
(ushort)ApplicationData.QPort(),
NetworkManager.Singleton); //⭐ 추가
}
public bool OpenConnection(string ip, ushort port) //⭐ 추가
{
// transport는 서로 데이터를 주고 받는 역할
UnityTransport transport = NetworkManager.Singleton.GetComponent<UnityTransport>(); //⭐ 추가
transport.SetConnectionData(ip, port); //⭐ 추가
return NetworkManager.Singleton.StartServer();//⭐ 추가
}
}
using System.Threading.Tasks;
using Unity.Android.Gradle.Manifest;
using Unity.Multiplayer;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ApplicationManager : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
async void Start()
{
DontDestroyOnLoad(gameObject);
// await LaunchInMode(SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null); //널이면 데드케이트 서버임.
await LaunchInMode(MultiplayerRolesManager.ActiveMultiplayerRoleMask == MultiplayerRoleFlags.Server);
}
//서버일 경우와 아닐 경우 나눔.
async Task LaunchInMode(bool isDedicatedServer)
{
if(isDedicatedServer)
{
ServerSingleTon.Instance.Init(); //⭐ 추가
await ServerSingleTon.Instance.CreateServer(); //⭐ 추가
await ServerSingleTon.Instance.serverManager.StartGameServerAsync(); //⭐ 추가
}
else
{
//임시 코드
bool authenticated = await ClientSingleTon.Instance.InitAsync();
HostSingleTon hostSingleTon = HostSingleTon.Instance;
//await HostSingleTon.Instance.StartHostAsync(); //버튼 눌렀을 대 사용할 코드
if(authenticated)
{
GotoMenu();
}
}
}
public void GotoMenu()
{
SceneManager.LoadScene("MenuScene");
}
}







