Subsystems

Uno de los modos más elegantes para extender UE4 con tus propias clases es el uso de subsytems. Con subsystem tus funciones quedarán expuestas fácilmente en blueprint y sus ciclos de vida son autogestionados por el propio motor.

Este post es un resumen del video Programming Subsystems, diseccionado y ampliado con apuntes tomados de la documentación oficial.

¿Qué son los Subsystems?

Los subsystems son clases automáticamente instanciadas cuyo ciclo de vida es gestionado por el propio motor.

Automáticamente instanciadas quiere decir que las instancias de las clases son creadas para ti, no tendrás que escribir código para instanciarlas.

El ciclo de vida es autogestionado por el propio motor. Deberás elegir que ciclo de vida corresponde a tu subsystem. Entre las distintas posibilidades están:

  • Game Instance (UGameInstanceSubsystem)
  • Local Player (ULocalPlayerSubsystem)
  • World (UWorldSubsystem)
  • Engine (UEngineSubsystem)
  • Editor (UEEditorSubsystem)

Tu punto de entrada debe ser heredar de una de estas clases en función del ciclo de vida que desees.

Tu clase, que debe heredar de una de las clases USubsystem anteriores, tiene la oportunidad de sobreescribir alguno de estos métodos que serán llamados cuando se crea una instancia y cuando se destruye, respectivamente:

virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;

Ejemplo con GameInstance

Vamos a añadir una lógica básica para mantener la puntuación de un juego.

Lo primero que debemos preguntarnos es ¿en qué clase almacenamos la puntuación del jugador? ¿Podríamos almacenarla en el Level Blueprint? ¿Como un atributo del Pawn? ¿Quizás en el Player Controller?

La respuesta es DEPENDE. Depende de la complejidad del proyecto, de si es multijugador, si la puntuación es persistente a lo largo de todas las escenas, etc.,

En la inmensa mayoría de ocasiones, y salvo que el juego sea muy sencillo, las clases candidatas para este tipo de cosas, las sospechosas habituales, son GameState, PlayerState y GameInstance.

Aquí un resumen del game framework de Unreal por si necesitas repasar conceptos.

Si queremos que la puntuación sea una información que perdure durante todo el juego un candidato sería GameInstance.

Así que manos a la obra, crearíamos una subclase de GameInstance y le añadiríamos la lógica para mantener una puntuación. Algo así:

UCLASS()
class STREAMLABS_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Delta);

	UFUNCTION(BlueprintCallable, Category = "Score")
	void Clear();

	UFUNCTION(BlueprintPure, Category = "Score")
	FORCEINLINE int32 GetScore() const { return Score; }

	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FScoreDelegate, int32, Score);

	UPROPERTY(BlueprintAssignable, Category = "Score")
	FScoreDelegate OnScore;

private:
	int32 Score;
};

No olvidar setear este GameInstance en Project Settings > Maps & Modes.

Al ser un objeto global que pervive durante todo el juego, tiene sentido que sea en esta pestaña de settings.

Set Game Instance

Motivación, ¿por qué usar Subsystem?

La clase GameInstance es firme candidata a convertirse en un auténtico monstruo con miles de líneas de código.

Le hemos añadido en el ejemplo una lógica básica de puntuación. Pero otro compañero, o nosotros mismos, podemos añadir en el futuro más y más lógica.

Piensa en la clase PlayerState, aquí añadimos lógica sobre el estado del jugador (y solo del jugador). Con GameState solo del estado del juego (del mapa que se esté jugando, por ejemplo, los ranking de kills). Pero ¿y con GameInstance?

Pues todo lo que perdure durante todo el juego, que potencialmente puede ser cualquier cosa.

Mientras que con PlayerState y GameState están claramente separados y acotados los roles, con GameInstance no ocurre.

Es por ello que GameInstance es una clase perfecta para usar con Subsystem.

Con Subsystem es como si partieras la clase GameInstance en pequeñas "clases parciales " cada uno con su propia responsabilidad. Otro modo de verlo es como si tuvieras varios GameInstances.

Para crear un Subsystem de GameInstance:

UCLASS()
class STREAMLABS_API UScoreGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:

	virtual void Initialize(FSubsystemCollectionBase& Collection) override;

	virtual void Deinitialize() override;

	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Delta);

	UFUNCTION(BlueprintCallable, Category = "Score")
	void Clear();

	UFUNCTION(BlueprintPure, Category = "Score")
	FORCEINLINE int32 GetScore() const { return Score; }

	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FScoreDelegate, int32, Score);
	
	UPROPERTY(BlueprintAssignable, Category = "Score")
	FScoreDelegate OnScore;

private:

	int32 Score;

};

Y ahora, desde cualquier parte (Blueprint incluido) podemos usarlo tal que así:

Subsystem Blueprint

En C++:

// Puedes usar UGameplayStatics para obtener GameInstance
UGameInstance* GameInstance = ...;

auto ScoreSubsystem = GameInstance->GetSubsystem<UScoreGameInstanceSubsystem>();

¿Cuando usar Subsystem?

Algunos usos interesantes de subsystem:

  • Debes añadir código no específico del juego. Por ejemplo, estadísticas. En vez de añadirlo a GameInstance mejor usar un subsystem y tenerlo aparte.
  • Cuando más grande sea tu clase GameInstance más motivos para desacoplar y usar subsystem.
  • No tienes una clase GameInstance y el código que debes añadirle es "demasiado" simple como para justificar tener que crearla.
  • GameInstance ofrece varias funcionalidades claramente diferenciadas (por ej. puntuación, estadísticas, recursos, etc.,) y cada miembro de tu equipo trabaja en un aspecto. En vez de que todos editeis el mismo archivo, separar en subsystem.

Nota: Uso GameInstance para ejemplificar pero vale para cualquier otra: Local Player, World, Engine, Editor