이 문서는 새로운 네트워킹 시스템을 사용하여 신규 멀티플레이어 프로젝트를 설정하는 방법을 다룹니다. 이 단계별 과정은 일반적인 것이지만 커스텀화하여 다양한 타입의 멀티플레이어 게임에 적용할 수 있습니다.
우선 새로운 빈 Unity 프로젝트를 생성해야 합니다.
첫 단계는 프로젝트에 NetworkManager 오브젝트를 생성하는 것입니다.
자세한 내용은 NetworkManager 사용을 참조하십시오.
다음 단계는 게임에서 플레이어를 나타내는 Unity 프리팹을 만드는 것입니다. NetworkManager는 디폴트로 플레이어 프리팹을 복제하는 방식으로 각 플레이어에 대해 오브젝트를 인스턴스화합니다. 이 예제에서 플레이어 오브젝트는 단순한 큐브입니다.
플레이어 오브젝트를 참조하십시오.
플레이어 프리팹을 생성했으면 이제 네트워크 시스템에 등록해야 합니다.
이제 프로젝트를 처음으로 저장할 때입니다. 메뉴에서 File -> Save Project로 가서 프로젝트를 저장해야 합니다. 씬 역시 저장할 수 있습니다. 이 씬을 “오프라인” 씬이라고 합시다.
구현할 게임의 첫 기능은 플레이어 오브젝트를 움직이는 것입니다. 우선 네트워킹 없이 구현할 것이므로, 싱글플레이어 모드에서만 작동합니다.
using UnityEngine;
public class PlayerMove : MonoBehaviour
{
void Update()
{
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
이 코드는 방향키 또는 컨트롤러 패드를 통해 큐브를 조종할 수 있게 연결합니다. 큐브는 아직 네트워크 연결이 되어있지 않기 때문에 클라이언트에서만 움직입니다.
프로젝트를 다시 저장합니다.
에디터에서 플레이(Play) 버튼을 클릭하여 플레이 모드로 진입합니다. 아래와 같이 NetworkManagerHUD 디폴트 사용자 인터페이스가 나타날 것입니다.
“호스트”를 누르면 게임 호스트로서 게임을 시작하게 됩니다. 이제 플레이어 오브젝트가 생성되고 HUD가 변경되어 서버가 활성화된 것으로 나타날 것입니다. 게임은 서버와 클라이언트가 동일 프로세스에 있는 “호스트”로서 실행됩니다.
네트워크 개념을 참조하십시오.
방향키를 누르면 플레이어 큐브 오브젝트가 이동합니다.
에디터에서 중지 버튼을 눌러 플레이 모드를 종료해야 합니다.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
이 게임의 큐브는 현재 모두 흰색이어서, 사용자가 어느 것이 자신의 큐브인지 알 수 없습니다. 플레이어를 식별하기 위해 로컬 플레이어의 큐브를 빨간색으로 만들 것입니다.
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
함수는 해당 클라이언트에서 로컬 플레이어에만 호출됩니다. 이를 통해 사용자에게는 자신의 큐브가 빨간색으로 보이게 됩니다. OnStartLocalPlayer 함수는 카메라 및 입력 환경설정 등과 같은 로컬 플레이어에만 해당되는 초기화 작업을 수행하기에 적합합니다.
NetworkBehaviour 베이스 클래스에는 기타 유용한 가상 함수들이 있습니다. 스포닝을 참조하십시오.
멀티플레이어 게임에서 플레이어가 탄환을 발사하는 것은 흔한 기능입니다. 이 섹션은 예제에 네트워크가 적용되지 않는 탄환을 추가합니다. 탄환에 네트워크를 적용하는 것은 다음 섹션에서 다룹니다.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
// create the bullet object from the bullet prefab
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
// make the bullet move away in front of the player
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// make bullet disappear after 2 seconds
Destroy(bullet, 2.0f);
}
}
이 섹션에서는 직전의 예제에 네트워킹을 적용합니다.
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
[Command]
void CmdFire()
{
// This [Command] code is run on the server!
// create the bullet object locally
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// spawn the bullet on the clients
NetworkServer.Spawn(bullet);
// when the bullet is destroyed on the server it will automaticaly be destroyed on clients
Destroy(bullet, 2.0f);
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
// Command function is called from the client, but invoked on the server
CmdFire();
}
}
}
코드는 [커맨드]를 사용해 서버에서 탄환을 발사합니다. 자세한 내용은 네트워크 액션을 참조하십시오.
이제 탄환에 충돌 핸들러를 추가하여 플레이어 큐브 오브젝트와 충돌하면 사라지도록 합니다.
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
Destroy(gameObject);
}
}
}
이제 탄환이 플레이어 오브젝트를 맞추면 파괴됩니다. 서버에서 탄환이 제거되면, 탄환은 네트워크가 관리하는 스폰된 오브젝트이므로 클라이언트에서도 제거됩니다.
보통 플레이어는 “체력” 프로퍼티가 있어서 처음에는 최대치로 시작하지만 총알에 피격되어 플레이어가 피해를 입으면 해당 프로퍼티의 값이 줄어들게 됩니다. 이 섹션에서는 플레이어 오브젝트에 네트워크가 적용되지 않은 체력 프로퍼티를 추가합니다.
using UnityEngine;
public class Combat : MonoBehaviour
{
public const int maxHealth = 100;
public int health = maxHealth;
public void TakeDamage(int amount)
{
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
Bullet 스크립트를 업데이트하여 피격시 TakeDamage 함수를 호출하도록 해야 합니다. * Bullet 스크립트를 엽니다. * 충돌 핸들러 함수에서 Combat 스크립트의 TakeDamage()를 호출하도록 추가합니다.
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
var combat = hit.GetComponent<Combat>();
combat.TakeDamage(10);
Destroy(gameObject);
}
}
}
이제 플레이어 오브젝트 체력은 탄환에 피격될 때 줄어듭니다. 하지만 게임에서는 이 현상을 볼 수 없으므로, 간단한 체력 바를 추가해야 합니다. * PlayerCube 프리팹을 선택합니다. * 컴포넌트 추가 버튼을 선택하여 HealthBar라는 새 스크립트를 추가합니다. * HealthBar 스크립트를 엽니다.
이 코드는 이전 GUI 시스템을 사용하기에 길이가 깁니다. 네트워킹과는 큰 관련이 없으므로 특별히 설명하지 않고 그냥 사용하도록 하겠습니다.
using UnityEngine;
using System.Collections;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
Combat combat;
void Awake()
{
combat = GetComponent<Combat>();
}
void OnGUI()
{
InitStyles();
// Draw a Health Bar
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// draw health bar background
GUI.color = Color.grey;
GUI.backgroundColor = Color.grey;
GUI.Box(new Rect(pos.x-26, Screen.height - pos.y + 20, Combat.maxHealth/2, 7), ".", backStyle);
// draw health bar amount
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x-25, Screen.height - pos.y + 21, combat.health/2, 5), ".", healthStyle);
}
void InitStyles()
{
if( healthStyle == null )
{
healthStyle = new GUIStyle( GUI.skin.box );
healthStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 1f, 0f, 1.0f ) );
}
if( backStyle == null )
{
backStyle = new GUIStyle( GUI.skin.box );
backStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 0f, 0f, 1.0f ) );
}
}
Texture2D MakeTex( int width, int height, Color col )
{
Color[] pix = new Color[width * height];
for( int i = 0; i < pix.Length; ++i )
{
pix[ i ] = col;
}
Texture2D result = new Texture2D( width, height );
result.SetPixels( pix );
result.Apply();
return result;
}
}
체력 변경은 클라이언트와 호스트에 독립적으로 각각 모두 적용됩니다. 따라서 체력이 플레이어에 따라 다르게 보일 수 있습니다. 체력은 서버에서만 적용되어야 하며 그 변경은 클라이언트에 복사되어야 합니다. 이를 체력의 “서버 권한”이라고 합니다.
SyncVars에 대한 자세한 내용은 상태 동기화를 참조하십시오.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
현재까지는 플레이어의 체력이 0이 되더라도 로그 메시지 이외에는 아무것도 일어나지 않습니다. 게임을 더 현실적으로 만드려면 체력이 0이 되는 시점에서 플레이어가 다시 체력이 전부 채워진 상태로 시작 지점으로 돌아가도록 해야 합니다.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = maxHealth;
// called on the server, will be invoked on the clients
RpcRespawn();
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// move back to zero location
transform.position = Vector3.zero;
}
}
}
게임에서 클라이언트는 플레이어 오브젝트의 포지션을 제어합니다. 플레이어 오브젝트는 클라이언트에서 “로컬 권한”을 가지기 때문입니다. 만일 서버가 플레이어 포지션을 시작 포지션로 설정하면, 클라이언트가 권한이 있으므로 이를 오버라이드하게 됩니다. 이를 방지하려면 서버가 권한이 있는 클라이언트에 플레이어 오브젝트를 시작 포지션으로 이동하라고 요청해야 합니다.
플레이어 오브젝트는 클라이언트가 호스트에 연결되면 생성되지만, 대부분의 게임은 게임 월드에 존재하는, 적과 같은 비플레이어 오브젝트가 있습니다. 이 섹션에서는 스포너를 추가하여 피격하고 죽일 수 있는 비플레이어 오브젝트를 생성합니다.
using UnityEngine;
using UnityEngine.Networking;
public class EnemySpawner : NetworkBehaviour {
public GameObject enemyPrefab;
public int numEnemies;
public override void OnStartServer()
{
for (int i=0; i < numEnemies; i++)
{
var pos = new Vector3(
Random.Range(-8.0f, 8.0f),
0.2f,
Random.Range(-8.0f, 8.0f)
);
var rotation = Quaternion.Euler( Random.Range(0,180), Random.Range(0,180), Random.Range(0,180));
var enemy = (GameObject)Instantiate(enemyPrefab, pos, rotation);
NetworkServer.Spawn(enemy);
}
}
}
이제 Enemy 프리팹을 생성합니다.
Bullet 스크립트는 플레이어에 대해서만 작동하도록 설정되었으므로, 이를 업데이트하여 Combat 스크립트가 있는 모든 오브젝트에 작동하도록 해야 합니다.
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitCombat = hit.GetComponent<Combat>();
if (hitCombat != null)
{
hitCombat.TakeDamage(10);
Destroy(gameObject);
}
}
}
EnemySpawner를 Enemy 오브젝트에 연결합니다.
적을 테스트합니다.
적은 피격당하면 체력이 줄어들기는 하지만, 플레이어와 같이 리스폰합니다. 적은 체력이 0이 되면 리스폰하지 않고 파괴되어야 합니다.
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
public bool destroyOnDeath;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}
else
{
health = maxHealth;
// called on the server, will be invoked on the clients
RpcRespawn();
}
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// move back to zero location
transform.position = Vector3.zero;
}
}
}
이제 적은 체력이 0이 되면 파괴되고, 플레이어는 체력이 0이 되면 리스폰됩니다.
현재 플레이어는 생성될 때 모두 영점에 나타나게 됩니다. 즉, 서로 겹칠 가능성이 있다는 의미입니다. 플레이어는 서로 다른 위치에 스폰되어야 합니다. NetworkStartPosition 컴포넌트를 사용하여 이 문제를 해결할 수 있습니다.
비어 있는 새 게임 오브젝트를 생성합니다.
오브젝트 이름을 “Pos1”으로 변경합니다.
컴포넌트 추가 버튼을 선택하여 NetworkStartPosition 컴포넌트를 추가합니다.
Pos1 오브젝트를 (–3,0,0) 포지션으로 이동합니다.
비어 있는 두 번째 게임 오브젝트를 생성합니다.
이 오브젝트 이름을 “Pos2”로 변경합니다.
컴포넌트 추가 버튼을 선택하여 NetworkStartPosition 컴포넌트를 추가합니다.
Pos2 오브젝트를 (3,0,0) 포지션으로 이동합니다.
NetworkManager를 찾아 선택합니다.
“Spawn Info” 폴드아웃을 엽니다.
“Player Spawn Method”를 “Round Robin”으로 변경합니다.
게임을 빌드하고 실행합니다.
플레이어 오브젝트는 이제 영점이 아닌 Pos1과 Pos2 오브젝트의 위치에 생성됩니다.