Multiplayer en UE4

El multiplayer en videojuegos es un terreno peligroso y pantanoso. Pero UE4 ofrece una cantidad de características muy poderosas para hacerte la vida más fácil. Y dado que UE4 fue concebido con el multijugador en mente, conocer como funciona el multiplayer te da un conocimiento más profundo de como funciona el motor. Por lo que entender el multiplayer en UE4, independientemente de si vas a hacer juegos multijugador o no, es una buena idea.

¿Qué voy a aprender aquí?

Este post va a ser un resumen y a la vez una referencia de todo lo que nos ofrece UE4.

Esta pensado para el que no tenga ni idea se haga una idea, para el que tenga algunas lagunas vea la imagen global y para el que ya haya trabajado en multiplayer tenga una referencia a modo de resumen.

Y si no vas a hacer juegos multiplayer, aquí obtendrás un conocimiento más profundo de como funciona el motor.

Cliente - Servidor

En UE4 todos los jugadores son los clientes y todas las acciones que hacen se envían a un servidor. El servidor es la autoridad. El servidor valida las acciones y envía una confirmación a los clientes que actuarán en consecuencia.

Arquitectura cliente servidor

Esto quiere decir una cosa, cuando aprietas el gatillo y disparas en un shooter multiplayer, realmente tu no eres quién dispara. Tu eres el cliente (un cliente). La intención de disparar es enviada al servidor. El servidor comprueba si puedes disparar y si tu disparo ha dado en algo. Este cálculo es devuelto a ti, como cliente. Y entonces reaccionas como cliente a tal información; mostrando la animación de disparar, el VFX del disparo, etc.,

Lo mismo ocurre cuando te mueves. Cuando mueves tu personaje, como cliente, es el servidor quién lo mueve. El cliente, osea tu, envías la información del movimiento que quieres hacer al servidor (mover tantos metros hacía la derecha, por ejemplo):

El servidor lo valida, y es él quién mueve el personaje. Este cálculo es enviado de vuelta y tu personaje reacciona haciendo el movimiento.

Es decir, en un juego multiplayer el juego se juega en el servidor. Los clientes reproducen una simulación en plan matrix e intentan mantener el estado del juego actualizado lo más aproximado al estado del juego en el servidor.

Los clientes nunca "hablan" entre ellos. Es el servidor quién valida toda la información.

Hay una excepción a esta separación entre cliente y servidor. Y es que, excepcionalmente, si no se cuenta con un servidor dedicado, un cliente también puede ser, a la vez, el servidor. Y esto es lo que precisamente ocurre en los juegos no-multiplayer.

Entorno de pruebas

UE4 ofrece distintas opciones para probar el juego en modo multiplayer mientras estás desarrollando.

Cuandole das a Play en el editor el juego asume el papel de servidor y cliente.
Si quieres que el juego asuma el papel de sólo cliente y se conecte a un servidor, puedes marcar la opción Run Dedicaded Server bajo las opciones de Play.

Con Run Dedicaded Server marcado, UE4 arranca un servidor independiente y el juego asuma un papel de cliente.

También puedes cambiar el número de jugadores (clientes) que se conectan al servidor.

Set Number of Players

Cuando le des a Play se abrirán tantas ventanas como jugadores hayas escogido.

Para aclarar aún más estos dos settings, el de Run Dedicaded Server y Number of players, veamos algunas combinaciones:

|Dedicaded server|Num players|Explicación|
|-----|--------|-------|-----------|
|No|1|Un solo jugador, actúa como cliente y servidor|
|Sí|1|Un solo jugador, actúa como cliente. El servidor es un proceso en background, no tiene representación gráfica.|
|Sí|2 o más|Dos o más jugadores, todos como clientes. El servidor es un proceso en background, no tiene representación gráfica.|
|No|2 o más|Dos o más jugadores, el primero actúa como cliente y servidor. El resto de jugadores son clientes y se conectan al primero.|

Dos juegos y un código

Hemos dicho que el servidor debe encargarse de la lógica, spawnear enemigos, comprobar si un disparo ha sido certero, ¡incluso si puede disparar!, en definitiva, se encarga de las decisiones importantes.

Por otra parte, el cliente "solo" debe enviar empaquetada las intenciones del usuario y simular el juego basándose en lo que le diga el servidor.

Entonces, ¿debemos escribir dos códigos? ¿uno para el cliente y otro para el servidor? La respuesta es NO. O quizás un sí pero muy pequeño.

Pongamos un ejemplo, usemos la plantilla FirstPersonShooter.

Veamos. Si creamos un proyecto nuevo con la plantilla FirstPersonShooter y le damos a Play con dos jugadores y con la opción Run Dedicaded Server desmarcada tenemos:

Podemos ver varias cosas que van muy mal:

  1. El disparo no está replicado. Cuando un jugador dispara un proyectil el otro no lo ve,
  2. Cuando el disparo, que no está replicado, impacta sobre un actor, tampoco se replica su movimiento, causando que el estado del mapa quede en desincronía entre los clientes y el servidor.
  3. La animación ni el sonido de disparar está replicado. Es decir, cuando un jugador dispara, los otros jugadores no ven su animación ni escuchan el sonido.

Otras que que van regular:

  • Las manos de un jugador no son vistas por el resto.

Las que van genial:

  1. Los jugadores se spawnean correctamente, y si pusiéramos 10 jugadores también funcionarían bien.
  2. El movimiento está replicado y se mantiene actualizado entre todos los clientes.

En cuanto al punto que va regular, el de las manos que no se ven. Edita la clase FirstPersonCharacter si has creado el proyecto con Blueprints ó su correspondiente versión en C++ si has creado el proyecto en C++.

Localiza el componente llamado Mesh2P que es el responsable de mostrar las manos. Puedes localizar un atributo llamado Only Owner See que está marcado a true.

Only Owner See

En C++ es el método:

Mesh1P->SetOnlyOwnerSee(true);

Como su propio nombre indica, solo el "dueño" puede ver ese mesh (las manos). Es una propiedad sumamente útil para juegos multiplayer, junto con la propiedad Owner No See.

Si desmarcas la opción (o pones el valor a false en C++), los jugadores podrán ver las manos de los otros jugadores.

Imagina que, en aras de mejorar el juego, añadimos un nuevo componente adicional al FirstPersonCharacter en concreto un SkeletalMeshComponent. El objetivo de este componente será mostrar el cuerpo completo del personaje.

Para el "dueño", es decir, el jugador que controla el personaje, solo debería ver las manos. Las manos deberían ser visibles al dueño e invisibles al resto, mientras que el cuerpo completo debería ser visible para el resto e invisible para el dueño.

Así que este nuevo componente encargado de mostrar el cuerpo marcaríamos Owner No See a true. Mientras que el SkeletalMeshComponent responsable de mostrar las manos, en nuestro caso llamado Mesh2P, marcaríamos Only Owner See.

En cuanto a la magia negra que hace funcionar lo que va realmente bien y sin necesidad de que nosotros movamos un dedo, hablaremos largo y tendido más abajo porque es un tema de vital importancia.

Pero antes de entrar en ello, vamos a resolver lo que va realmente mal.

El código para dos juegos

Localiza el código responsable de disparar. Vamos usar la plantilla  FirstPersonShooter en C++ ya que para programar el  multiplayer se usa commumente C++. Al final de este apartado repasaremos como quedaría en Blueprint.

En mi caso la función que se use para disparar es:

void AMultiplayerTestCharacter::OnFire()
{
	// DISPARAR EL PROYECTIL
    if (ProjectileClass != NULL)
	{
		UWorld* const World = GetWorld();
		if (World != NULL)
		{
            ...
    ...
    }
    // FIN DE DISPARAR EL PROYECTIL

    // EFECTOS DE SONIDO Y ANIMACIÓN    
    if (FireSound != NULL)
	{
		UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
        ...
    ...
    // FIN DE EFECTOS DE SONIDO Y ANIMACIÓN
}

Bueno aquí podemos ver el problema. El código se ejecuta en el cliente, en local.

Así que cuando spawnea un proyectil, solo lo verá él y reaccionará solo con su entorno local. Cuando spawnea un sonido solo lo escuchará él. Y cuando reproduce una animación solo la verá él. No hay comunicación con el servidor.

Eso no es lo que queremos. El código para disparar el proyectil debe ejecutarlo el servidor. Ya que los clientes intentarán simular el estado del servidor, los clientes también verán el proyectil spawneado por el servidor. Recuerda, el juego se juega en el servidor.

¿Cómo decimos que un trozo de código debe ejecutarse en el servidor?

UE4 lo pone muy fácil, metemos el trozo de código en una función y marcamos la función como "ejecutar en el servidor".

UFUNCTION(Server, Reliable, WithValidation, Category = "Gameplay")
void ShootProjectile();

Los decoradores son:

Server

Esta función se ejecutará en el servidor.

Si la llamas desde el servidor se ejecutará tal cuál.

Si la llamas desde un cliente entonces no se ejecutará en el cliente y, más importante, enviarás una petición al servidor para que se ejecute en el servidor, todo completamente transparente para el desarrollador.

Reliable

Significa que esta función es importante y se garantiza su ejecución.

En redes muy congestionadas con pérdidas de paquetes, el cliente se encargará de reenviar la petición las veces que sean necesarias para garantizar que el servidor la ejecute.

Por contra, Unreliable hace que la función no se garantice su ejecución. O dicho de otro modo, se envía la petición y se olvida. Quizás el paquete se pierda. No hay garantías.

WithValidation

Significa que antes de ejecutar la función se llamará a otra función que devolverá un bool (true o false) que actuará como validador.

Si esta función devuelve true entonces se ejecuta el método si no, no. Perfecto para hacer unas comprobaciones previas y anti-cheat.


Ahora uimplementamos esta función:

void AMultiplayerTestCharacter::ShootProjectile_Implementation()
{
	// Cortar y pegar la parte de disparar el proyectil del método OnFire

    // DISPARAR EL PROYECTIL
    if (ProjectileClass != NULL)
	{
		UWorld* const World = GetWorld();
		if (World != NULL)
		{
            ...
    ...
    }
    // FIN DE DISPARAR EL PROYECTIL

}

bool AMultiplayerTestCharacter::ShootProjectile_Validate()
{
    // En un proyecto real habría que hacerlo mejor
	return true;
}

Nota como se añade como sufijo _Implementation y _Validate.
Ahora el código para el método OnFire queda:

void AMultiplayerTestCharacter::OnFire()
{
	ShootProjectile();

    // EFECTOS DE SONIDO Y ANIMACIÓN    
    if (FireSound != NULL)
	{
		UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
        ...
    ...
    // FIN DE EFECTOS DE SONIDO Y ANIMACIÓN
}

En cuanto a Blueprint, puedes crear tus propios eventos y marcarlos si son ejecutados en el servidor:

Si le das al Play ahora, podrás ver como, efectivamente, aparecen todos los proyectiles. ¡Pero solo en el servidor!

Replicando actores

Si disparas desde el servidor, aparecen los proyectiles en el servidor pero no en el cliente. Si disparas desde el cliente aparecen los proyectiles en el servidor, ¡pero no en el cliente!.

¿Por qué? Una cosa está clara, el código se está ejecutando en el servidor. ¿Pero por qué no se ve en el cliente? ¿No hemos dicho que los clientes intentarán simular lo más exactamente posible el estado del juego del servidor?

Respuesta: porque los proyectiles son actores que no hemos marcado como replicables.

Un actor no replicable si es spawneado se queda allí dónde fue spawneado. Si fue en un cliente en el cliente, si fue en el servidor (que es lo que está pasando en nuestro caso), en el servidor.

Un actor que es replicable, si es spawneado en el cliente se queda SOLO en el cliente, y si es spawneado en el servidor aparece en el servidor y EN TODOS los clientes.

Vamos a hacer que el actor proyectil sea replicable. Edita el código del proyectil y añade al final del constructor:

bReplicates = true;
bReplicateMovement = true;

Compila y asegúrate que en el Blueprint también se actualiza con estos valores.
¡Ahora sí que funciona!

Los cubos que no se mueven

Tenemos el problema de que los cubos no se replican. ¿Adivinas por qué? Efectivamente, no están marcados como replicados.

Dado que los cubos son StaticMeshActor, una clase propia de UE4, no será tan fácil como acceder a su código y modificarlo.

La recomendación es hacer una subclase. Crea un Blueprint (ó C++) que herede de StaticMeshActor y sobreescribe el valor de la propiedad bReplicates y bReplicateMovement a true. O crea una clase nueva y añade el componente StaticMeshComponent, como gustes. Sustituye los cubos por instancias de esta nueva clase. ¡Listo!.

Nos queda la parte de reproducir el sonido y la animación.

Más allá del decorador Server

Queremos que el sonido y la animación se ejecuten en todos los clientes, además del servidor.

Al igual que teníamos el decorador Server para ejecutar una función en el servidor, tenemos el decorador NetMulticast que sirve, precisamente, para ejecutar en todos los clientes.

Más información sobre estos decoradores en la documentación oficial.

Esa es una opción, otra opción es usar variables replicadas.

Replicación de variables

Hasta ahora hemos visto como llamar a funciones del servidor.

Pero, ¿como hacemos para que una variable se mantenga sincronizada entre todos los clientes?

Utiliza el decorador Replicated.

UPROPERTY(Replicated)
float Health;

El actor debe estar marcado como replicado:

bReplicates = true;

Cuando tienes variables replicadas debes, forzosamente, que implementar el método virtual GetLifetimeReplicatedProps para indicarle al engine "cómo" quieres replicar la variable:

void AMultiplayerTestCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    /* Cuando el servidor actualiza el valor, los clientes actualizarán el valor.
    Es el modo para replicar más usual. */
    DOREPLIFETIME(AMultiplayerTestCharacter, Health);

    /* La variable TeamNumber es replicada pero solo
    se actualiza con el valor inicial.
    No se permiten cambios de equipo a lo largo de la partida */
    DOREPLIFETIME(AMultiplayerTestCharacter, TeamNumber, COND_InitialOnly);

    /* Los clientes actualizarán el valor, EXCEPTO el cliente dueño.
    Por ejemplo, porque ya has actualizado el valor
    en el cliente así ahorras ancho de banda  */
    DOREPLIFETIME(AMultiplayerTestCharacter, CosmeticFX, COND_SkipOwner);

    // otras condiciones pueden ser COND_SimulatedOnly,
    // y COND_AutonomousOnly.
}

Recuerda, el servidor es quién manda y los clientes intentan estar lo más sincronizados posibles con el servidor. Por tanto, si modificas la variable en un cliente solo se modificará en el cliente, no será replicada. Si modificas el valor de la varibale en el servidor, entonces el valor será propagado por todos los clientes.

Si quieres saber si estás en el servidor, puedes usar:

// aquí podríamos estar en el cliente o en el servidor

if (Role == ROLE_Authority)
{
    // aquí estamos en el servidor
}

// aquí podríamos estar en el cliente o en el servidor

A veces es interesante tener algún tipo de notificación cuando una variable es actualizada. Para ello puedes usar el decorador ReplicatedUsing. Por ejemplo:

UPROPERTY(ReplicatedUsing=OnRep_TeamColor)
int TeamNumber;

Y como implementación:

void AMultiplayerTestCharacter::OnRep_TeamColor()
{
	/* Se ha modificado el valor TeamNumber.
    Actualizar el color del personaje
     en función del TeamNumber. */
}

Podemos usar como variables replicadas estructuras propias.

Un ejemplo más completo usando todo la anterior podría ser:

USTRUCT()
struct FHitScanTrace
{
	GENERATED_BODY()
public:
	UPROPERTY()
	TEnumAsByte<EPhysicalSurface> SurfaceType;
	UPROPERTY()
	FVector_NetQuantize TraceTo;
};

UCLASS()
class MULTIPLAYEREXAMPLE_API AMyWeapon : public AActor
{
    ...

    UPROPERTY(ReplicatedUsing=OnRep_HitScanTrace)
	FHitScanTrace HitScanTrace;
    UFUNCTION()
	void OnRep_HitScanTrace();

    void Fire();

	UFUNCTION(Server, Reliable, WithValidation)
	void ServerFire();

    ...
};

Fíjate como en la estructura usamos FVector_NetQuantize en vez de FVector. Con FVector_NetQuantize ahorramos mucho ancho de banda ya que cada componente del vector tiene como máximo 20 bits (sin decimales) por 32 bits por componente de FVector.

También tenemos disponibles FVector_NetQuantize10 con 24 bits por cada componente y un decimal. Y FVector_NetQuantize100 con dos decimales y 30 bits por componente.

La implementación podría ser:

// El cliente llama a disparar porque ha pulsado el botón izquierdo del ratón.
void AMyWeapon::Fire()
{
    /* PRIMERA PARTE: Solo si somos el cliente.

    Si somos el cliente, llamamos al servidor para que
     notifique a todos los clientes. */
    if (Role < ROLE_Authority)
	{
		ServerFire();
	}

    /* SEGUNDA PARTE: Común a los clientes y servidor.

    Seguimos la ejecución ya seamos el servidor o el cliente.
     Reproduciremos los efectos cosméticos con esto conseguimos que:
     1) si somos el cliente el usuario obtendrá feedback inmediato porque reproducimos los efectos
     2) si somos el servidor reproducimos en el servidor el efecto para este player */

    CalculateTraceHit();        
    PlayFireEffects(TracerEndPoint);
    PlayImpactEffects(SurfaceType, TracerEndPoint);

    /* TERCERA PARTE: Si somos el servidor.

    Si somos el servidor, actualiza la variable replicada
     para que todos los clientes sean notificados */
    if (Role == ROLE_Authority)
	{
	    HitScanTrace.TraceTo = TracerEndPoint;
		HitScanTrace.SurfaceType = SurfaceType;
	}
}

void AMyWeapon::ServerFire_Implementation()
{
	Fire();
}

bool AMyWeapon::ServerFire_Validate()
{
	return true;
}

// esta función es llamada cuando se actualiza HitScanTrace.
void AMyWeapon::OnRep_HitScanTrace()
{
	/* Aquí todos los clientes que no son el dueño ni el servidor
     tienen la oportunidad de reproducir los efectos */
	PlayFireEffects(HitScanTrace.TraceTo);
	PlayImpactEffects(HitScanTrace.SurfaceType, HitScanTrace.TraceTo);
}

void AMyWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    /* replicamos la variable salvo al dueño, que ya nos encargamos
     nosotros mismos de ejecutar los efectos */
	DOREPLIFETIME_CONDITION(AMyWeapon, HitScanTrace, COND_SkipOwner);
}

Gameframework pensando en Multiplayer

El multiplayer y el gameframework están íntimamente relacionados. Tanto es así, que el gameframework fue construido con el multiplayer en mente.

Si aún no le has echado un vistazo te recomiendo mirar antes el post acerca del gameframework.

La únidad básica para trabajar en multiplayer son los actores. Algunos actores son marcados para ser replicados a lo largo de la red. El estado de los actores (posición, salud, etc.,) se actualiza en el servidor. Recuerda que el servidor es la autoridad. Y los clientes replican esos actores e intentan mantener su estado lo más actualizado posible con respecto al servidor.

Los actores replicados pueden existir o solo en los clientes, o solo en el servidor o en ambos.

Actor Servidor Cliente Descripción
GameMode No Responsable de toda la lógica. También gestiona la conexión de cada jugador. Dónde colocarlos, cambios de equipo, etc.,
GameState Toda la estadística de la partida. Ej: nivel, número de oleada, marcador, tiempo transcurrido, etc.,
PlayerState Info de cada jugador. Ej: número de muertes, equipo, puntos, etc.,
Pawn Actores que son controlados por el jugador o por la IA
PlayerController Responsable de la lógica de cada jugador. Toma la entrada del usuario
HUD No Interfaz de usuario
Otro actor si está marcado como replicado Otro actor cualquiera

Tiene todo bastante lógica. Si el GameMode es el responsable de la lógica del juego y el servidor es el único que toma las decisiones importantes (lo valida todo), tiene sentido que el GameMode solo exista en el servidor. Si desde un cliente intentas acceder al GameMode obtendrás un objeto nulo.

El GameMode actualiza los valores del GameState, y éste es replicado a todos los clientes para que actualicen su interfaz de usuario o respondan de otra manera.
Mención aparte merece los PlayerController.

El PlayerController es responsable de tomar la entrada del usuario y gestionar la lógica del mismo.

El código que gestiona la lógica se ejecuta en el servidor, como no podía ser de otro modo. Por eso el PlayerController existe en el servidor.

Mientras que la entrada del usuario (por teclado, ratón, gamepad o lo que fuera) así como la simulación de lo que ocurra en el servidor, debe ejecutarse en el cliente. Por eso el PlayerController también existe en el cliente, ¡pero solo en el cliente de ese jugador! No tiene sentido que el PlayerController del jugador A exista en el cliente del jugador B. Lo mismo se puede decir para PlayerState.

En cuanto al resto de actores que componen el juego, como puede ser los proyectiles, las plataformas, o cualquier otro actor que sea importante que esté en todos los clientes, debe ser marcado como replicado.

Comprender el single player

Antes de profundizar más en el multiplayer debemos entender como funciona el game framework en single player.

Para ello vamos a:

  1. Crear un proyecto vacío. Sería más fácil usar una plantilla, pero creo que desde un proyecto vacío se ilustra mejor el ejemplo.
  2. Crea una nueva clase (en blueprint o C++, lo que prefieras) de tipo Character, llámalo BP_Hero.
  3. Añade al proyecto el Animation Starter Pack. Puedes encontrar este pack en el marketplace.
  4. Usa el skeletal mesh del Animation Starter Pack como asset para BP_Hero.
  5. Pulsa Play.

Nota: si te has perdido en algún paso quizás te sirva repasar tema de componentes y gameframework.

Así es como pinta la escena antes de dar al play:

Antes de dar al Play

¿Qué ocurre cuando pulsas Play?

En primer lugar no aparece nuestro personaje (BP_Hero) en escena, tiene sentido ya que no lo hemos añadido ni indicado en ningún parte, tan solo lo hemos creado. Pero a pesar de ello, tenemos una cámara y tenemos unos controles (teclas W, A, S, D y ratón) para movernos por el escenario, ¿de dónde sale esa cámara y por qué reacciona a la entrada del usuario, dónde está programado eso?

Añade ahora a la escena el personaje que hemos creado. Puedes arrastrar el blueprint desde el content browser y soltarlo en la escena, con eso crearás una instancia.

Nuestro personaje recién creado es un Character. Los Character heredan de Pawn. Recuerda que un Pawn es un actor que puede ser controlado por un controlador (PlayerController si es el Pawn de un jugador ó AIController si es el Pawn de un NPC).

Así que, de algún modo, tiene sentido que si lo añadimos a la escena, haya algún cambio.

Pulsa de nuevo Play. ¿Qué ocurre ahora?. Pues exactamente lo mismo que antes. Nada ha cambiado. ¿Qué está pasando?

GameMode al rescate

Lo que está ocurriendo es que Unreal, cuando carga cualquier mapa, hace:

1. Busca el GameMode asociado al mapa. Puedes encontrar el GameMode asociado al mapa en la pestaña World Settings.

World Settings GameMode

En nuestro caso como hemos empezado un proyecto vacío, no hemos seteado ninguno.

2. Si no hay ningún GameMode seteado en el mapa entonces UE4 buscará el GameMode por defecto.

Puedes encontrar el GameMode por defecto en Project Settings > Maps & Modes

Tampoco hemos modificado el GameMode por defecto, así que se trata del GameMode base. El GameMode base spawnea un  Pawn vacío llamado DefaultPawn que responde a los controles WASD y ratón. ¡Justo lo que nos ocurría antes y no sabíamos de dónde venían!

Así es como queda la escena cuando pulsamos Play:

Después de darle al Play

Fíjate como UE4 ha spawneado el GameModeBase que encontró en Project Settings > Maps & Modes. Que en dicho GameModeBase tenía como pawn por defecto DefaultPawn y, efectivamente, ese es el que puedes encontrar en la escena.

Si queremos que el PlayerController use nuestro BP_Hero tenemos, principalmente, tres opciones:

  1. Crear un GameMode y le indicamos que el Default Pawn Class es nuestro BP_Hero. De este modo el GameMode spawneará un BP_Hero en vez del DefaultPawn
  2. Indicar en el BP_Hero que puede ser "auto poseído" por el PlayerController
  3. Indicar, mediante código, que el PlayerController posea el BP_Hero. Puedes hacer esto usando el método Possess del PlayerController. Pasándole como argumento el Pawn que quieres poseer.

La opción 3 es un poco matar moscas a cañonazos. Con esta opción estaríamos spawneando el DefaultPawn, el PlayerController lo poseería para seguidamente desposeerlo y poseer el BP_Hero.

El caso 2 es muy simple. Selecciona el BP_Hero que hemos añadido a la escena, en la pestaña Details busca el atributo AutoPossess Player. Se encuentra en Disabled lo que quiere decir que este actor no será autoposeído por ningún PlayerController. Si lo cambias a Player 0 y pulsas Play verás el cambio.

Auto Possess Player

Lo que ha ocurrido es que GameMode ha spawneado el PlayerController, ha detectado que existe al menos un actor con el AutoPossess para Player 0 (nuestro BP_Hero), entonces en vez de spawnear el DefaultPawn ha dejado que el PlayerController posea BP_Hero.

Ahora vamos a crear un nuevo GameMode:

  1. Crea una nueva clase que herede de GameMode. Llámala BP_GameMode.
  2. Edítala y cambia el campo Default Pawn Class a BP_Hero.
  3. Ve a Project Settings > Maps & Modes y cambia el gamemode por defecto al recién creado BP_GameMode.
  4. Recuerda volver a setear AutoPossess Player a Disabled en el BP_Hero colocado en la escena.
  5. Pulsa Play, ¿qué ocurre ahora?

Ahora UE4 busca el GameMode en el mapa, no hay ninguno seteado y busca el gamemode por defecto. En este caso, el que hemos creado, BP_GameMode.

Nuestro BP_GameMode, al heredar de GameModeBase, buscará un Pawn en la escena que pueda ser autoposeído. No encontrará ninguno, así que spawneará el pawn por defecto, ¡que hemos modificado a BP_Hero!. Y, efectivamente, ahí aparece en la escena un nuevo BP_Hero.

BP_GameMode también spawneará un PlayerController por defecto y poseerá nuestro BP_Hero.

¿Y esa cámara? ¿Por qué aparece una cámara si no hemos creado ninguna y justo en la posición de BP_Hero?

Cuando un PlayerController posee un pawn (en nuestro caso, BP_Hero), por defecto busca una cámara entre sus componentes, si la encuentra será la cámara que use. Si no encuentra ninguna, crea una cámara por defecto en su posición. Este comportamiento por defecto es así porque el PlayerController tiene, por defecto, marcado como true su atributo Auto Manage Active Camera Target.

Si quieres que cuando un PlayerController posea algún Pawn no modifique la cámara activa entonces tendrás que crear un PlayerController y desmarcar Auto Manage Active Camera Target. No olvides indicar al GameMode (en nuestro caso BP_GameMode) que ahora el PlayerController que debe spawnear no es el por defecto, si no el que recien has creado.

Desmarcar Auto Manage Active Camera Target es útil, si no imprescindible, si usas una cámara externa que no forma parte como componente del Pawn. De esta forma, cuando el PlayerController posea el Pawn no sobreescribirá la cámara actual.

Si quieres puedes experimentar añadiendo una cámara externa y modificando su atributo Auto Active for Player y jugar con el Auto Manage Active Camera Target del PlayerController.

Hágase el multiplayer

Crea un nuevo proyecto. Utiliza la plantilla Third Person.

Si eres más aventurero puedes partir del proyecto anterior, añadir una component cámara al pawn, añadir los eventos para responder a la entrada del usuario y añadir movimientos.

Ahora pulsa Play pero ¡con dos jugadores!

Set Number of Players

Sin hacer nada más, el GameMode ha spawneado el segundo jugador. Gracias a que tiene indicado en Default Pawn Class el Pawn llamado ThirdPersonCharacter (si usas la plantilla ThirdPerson ó BP_Hero si usas el ejemplo anterior).

No solo eso, si no que el GameMode ha spawneado un jugador en el servidor (recuerda que el GameMode solo se ve en el servidor) y también se ve en el cliente. ¿Por qué? Si te vas a ThirdPersonCharacter verás que tiene marcado Replicates.

No solo eso, si no que si te mueves, ¡el movimiento está replicado!. ¿Por qué? Porque todos los Character, y ThirdPersonCharacter hereda de Character, tienen el componente CharacterMovement.

Component ChracterMovement en la clase Character

El componente CharacterMovement ya tiene toda la lógica para replicar el movimiento a través de la red. Gracias Epic.

Siguientes pasos

¡Aún nos queda mucho por ver!

¿Cómo gestiona GameMode la llegada de nuevos usuarios? ¿y la desconexión? ¿Cómo viajamos entre mapas en un juego multiplayer? ¿Como descubrimos nuevos servidores y conectamos a ellos? ¿Como lidiar con el problema del lag? ¿Cómo gestionar partidas masivas de hasta 100 jugadores como en Fortnite? ¿Cómo hacer matchmaking? ¿Como usar los servidores de Steam?

Todo esto y más, en siguientes tutoriales.

Jorge Moreno Aguilera

Jorge Moreno Aguilera