Este documento describe los pasos para configurar un nuevo proyecto multijugador desde cero utilizando el nuevo sistema de networking (comunicación/redes). Este proceso paso-por-paso es genérico, pero puede ser personalizado para muchos tipos de juegos multijugador una vez haya empezado.
Para empezar, cree un nuevo proyecto de Unity vacío.
El primer paso es crear un objeto NetworkManager en el proyecto:
Para más detalles, ver Utilizando el NetworkManager.
El siguiente paso es configurar el Unity Prefab que representa el jugador en el juego. Por defecto, el NetworkManger instancia un objeto para cada jugador al clonar el player prefab. En este ejemplo, el objeto jugador será un simple cubo.
Una vez el prefab del jugador es creado, esta debe ser registrado con el sistema del network (red).
Ahora es un buen momento para guardar el proyecto por primera vez. Del menú File -> Save Project, guarde el proyecto. Usted también debería guardar la escena. Llamemos esta escena la escena “offline”.
Una de las funcionalidades principales del juego es mover el objeto jugador. Esto se hará primero sin el networking (red), por lo que solo funcionará en el modo de un solo jugador.
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);
}
}
Esto hace que el cubo sea controlado por las teclas flechas o un controlador pad. El cubo solamente se mueve en el cliente ahorita - no está networked (en red).
Guarde el proyecto nuevamente.
Ingrese al modo de juego en el editor oprimiendo el botón de reproducción. Usted debe ver la interfaz de usuario por defecto del NetworkManagerHUD:
Presione “Host” para iniciar el juego como el host (anfitrión) del juego. Esto va a causar que un objeto jugador sea creado, y el HUD va a cambiar para mostrar que el servidor está activo. Este juego está corriendo como “host” (anfitrión) - el cual es un servidor y un cliente en el mismo proceso.
Ver Conceptos de Network (red).
Al oprimir las teclas de flecha el objeto player cube debe moverse.
Salga del modo de juego al oprimir el botón de stop en el editor.
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);
}
}
Los cubos en el juego están actualmente todos en blanco, entonces el usuario no puede decir cuál es su cubo. Para identificar el jugador, haremos el cubo local el jugador rojo.
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
Esta función solamente se llama en el jugador local en su cliente. Esto hará que el usuario vea su cubo como rojo. La función OnStartLocalPlayer es un buen lugar para dar una inicialización que es solamente para el jugador local, tal como configurar cámaras e input.
También hay otras funciones virtuales útiles en la clase base NetworkBehaviour. Ver Spawning.
Una característica común en los juegos multijugador es hacer que los jugadores disparen balas. Esta sección agrega balas sin estar networked (en red) para mostrar el ejemplo. El networking para las balas se agrega en la siguiente sección.
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);
}
}
Esta sección agrega el networking a las balas en el ejemplo.
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();
}
}
}
Este código utiliza un Command para disparar la bullet (bala) en el servidor. Para más información ver Networked Actions.
Esto agrega un manejador de colisiones para que las balas desaparezcan cuando golpeen el objeto player cube.
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
Destroy(gameObject);
}
}
}
Ahora cuando una bala golpea un objeto jugador esta será destruida. Cuando la bala en el servidor sea destruida, este será destruido en los clientes también debido a que es un objeto generado y manejado por el network (red).
Una característica común de las balas es que el objeto jugador tiene una propiedad “health” (saludo) que inicia en un valor completo y luego es reducido cuando el jugador toma daños de una bala que lo está golpeando. Esta sección agrega salud no networked (en red) al objeto jugador.
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!");
}
}
}
El script bullet necesita ser actualizado para llamar la función TakeDamage en un golpe. * Abra el script bullet * Agregue un llamado a TakeDamage() del script Combat en la función que maneja colisiones
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);
}
}
}
Esto hará que la health (salud) del jugador disminuya cuando sea golpeado por una bala. Pero usted no puede ver que esto suceda en el juego. Necesitamos agregar una barra de salud simple.
Esto es mucho código que utiliza el sistema anterior de GUI. Esto no es muy importante para el networking por lo que lo utilizaremos sin explicación por ahora.
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;
}
}
Los cambios a la salud están siendo aplicados ahora en todo lado - independientemente del cliente y el anfitrión. Esto permite que la salud se vea diferente en los diferentes jugadores. La salud se debe solamente aplicar en el servidor y los cambios replicados a los clientes. Nosotros llamamos esto “server authority” (autoridad del servidor) para la salud.
Para más información acerca de SyncVars, ver State Synchronization.
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!");
}
}
}
Actualmente, nada sucede cuando la salud de un jugador es cero excepto un mensaje largo. Para hacerlo más juego, cuando la salud sea cero, el jugador debería ser tele-transportado de-vuelta a la ubicación inicial con salud completa.
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;
}
}
}
En este juego, el cliente controla la posición del objeto jugador - el objeto jugador tiene “local authority” (autoridad local) en el cliente. Si el servidor justamente configuro la posición a la posición inicial, esta sería anulada por la del cliente, ya que el cliente tiene autoridad. Para evitar esto, el servidor le dice al cliente dueño que mueva el objeto jugador a la posición inicial.
Mientras los objetos jugador son generados cuando el cliente se conectada al host (anfitrión), la mayoría de juegos tienen objetos no jugadores que existen en el mundo del juego, tal como enemigos. En esta sección un spawner (generador) es agregado y crea objetos no jugador que pueden ser disparados.
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);
}
}
}
Ahora cree un prefab Enemy:
El script bullet está configurado para que solamente le funcione a los jugadores. Ahora actualice el script bullet para trabajar con cualquier objeto que tenga el script Combat adjunto:
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);
}
}
}
Relacione el EnemySpawner con el objeto Enemy:
Pruebe los enemigos:
Mientras que los enemigos pueden ser disparados por balas y la salud de cada uno disminuye de acuerdo a esto, estos respawn (se generan) como jugadores. Los enemigos deben ser destruidos cuando su salud alcance en cero en vez de volver a ser generados.
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;
}
}
}
Ahora el enemy será destruido cuando su salud alcance cero, pero los jugadores se volverán a generan.
Los jugadores actualmente todos aparecen en el punto cero cuando fueron creados. Esto significa que potencialmente están el uno encima del otro. El jugador debe spawn (generarse) en diferentes ubicaciones. El componente NetworkStartPosition puede ser usado para hacer esto.
Cree un nuevo GameObject vacío
Re-nombre el objeto a “Pos1”
Oprima el botón Add Component y agregue el componente NetworkStartPosition
Mueva el objeto Pos1 a la posición (–3,0,0)
Cree un segundo GameObject vacío
Re-nombre el objeto a “Pos2”
Oprima el botón Add Component y agregue el componente NetworkStartPosition
Mueva el objeto Pos2 a la posición (3,0,0)
Encuentre el NetworkManger y seleccionelo.
Abra el desplegable “Spawn Info”
cambie el “Player Spawn Method” a “Round Robin”
Construya y ejecute el juego
Los objetos jugadores deberían ser creados en las ubicaciones de los objetos Pos1 y Pos2 en vez de cero.