Blueprint vs C++ : Comment bien architecturer vos projets avec Unreal Engine

Au démarrage d’un projet Unreal Engine, on a le choix entre le faire avec un langage de scripting visuel, le Blueprint, ou alors avec un langage plus bas niveau, le C++. Les deux solutions ont leurs avantages et leurs inconvénients, mais il y a une chose qui est sûr, c’est qu’il ne faut pas miser uniquement sur l’un ou sur l’autre ! La grande force de ce système est d’exploiter les deux en même temps. Le C++ pour développer les briques bas niveau et les Blueprints pour le développement haut niveau. Par exemple dans un FPS, le contrôle et la physique du personnage seront gérés en C++, le Blueprint permettra d’enrichir cette logique avec des éléments visuels ou sonores, comme des modèles 3D, des sons, etc.. Cela dit.. parfois, on peut aussi tout faire en Blueprint pour des raisons de simplicité/rapidité/connaissances.. Dans ce cas, comment se fixer des limites et être le plus efficace dans son architecture ? C’est ce que nous allons voir !

Le Blueprint qu’est ce que c’est ?

C’est un langage de script visuel, mais aussi une sorte de package similaire à un prefab sur Unity. Dans ce package il y a du code, des éléments visuels, des sons, du gameplay, etc.. Tout se fait généralement avec la souris. On branche des boites ensemble pour créer une logique. En mode code, on peut créer des variables (locales ou globales suivant le contexte), des fonctions, des macros et modifier tout un tas de paramètres relatifs à la classe dont dérive ce Blueprint. D’un point de vue logique, un Blueprint dérivera toujours d’un objet C++. De fait, il est possible de créer un Blueprint qui dérive d’un autre Blueprint. Il n’est cependant pas possible de dériver une classe C++ d’un Blueprint.

Exemple de scripting Blueprint

Les plus

  • Très rapide à mettre en place
  • Très facile d’accès, même pour des non programmeurs
  • Editeur très complet et agréable à utiliser
  • Accès aux différentes ressources/références en un clic
  • Beaucoup de fonctionnalités disponibles
  • Beaucoup de plugins disponibles
  • Débogage facile
  • Les null check ne font pas crasher votre jeu

Les moins

  • Les null check ne font pas crasher votre jeu (oui car c’est pas si bien que ça en fait hein :p )
  • Les performances de la boucle Tick
  • Le multiplateforme compliqué à gérer correctement
  • Le code spaghetti !
  • Refactoring compliqué quand la base de code est importante
  • Sérialisation binaire, donc pas possible de travailler à deux sur le même fichier facilement :'(

Les deux gros points noirs pour moi viennent du versionning et du côté code spaghetti, que l’on peut éviter en découpant sont code en fonction. Mais là aussi on se retrouve avec une collection incroyable de fonctions.. Il faut donc plutôt créer ces fonctions en C++ et les appeler en Blueprint.

Dans quel cas utiliser du code Blueprint ?

  • Menu
  • Interface utilisateur
  • Animation
  • Gameplay haut niveau (déclencher une action via un trigger par exemple)

Un exemple concret d’utilisation de Blueprint avec un Pawn

Blueprint dérivant d’une classe C++

Sur la capture d’écran ci-dessus on peut voir que ce Blueprint dérive d’une classe C++ Quadcopter, qui dérive elle même de la classe APawn. La classe C++ Quadcopter définie la structure d’un drone, sa hiérarchie d’objets, gère la physique, récupère les inputs Joystick via du code natif et réalise d’autres actions spécifiques.

Le Blueprint permet de sélectionner le bon modèle 3D à utiliser, et à surcharger les paramètres de simulation relatif à ce modèle en particulier. D’un point de vue équipe, le programmeur reste sur son code C++ et le Game Designer (ou la personne qui travaille avec lui) peut changer les valeurs sur le Blueprint, sans impacter le code. Le Sound Designer peut aussi changer les différents sons. Si une fonction spécifique est nécessaire, elle peut être ajoutée en C++ et sera accessible à l’ensemble des Blueprint qui dérivent de ce code C++. Il est aussi possible d’ajouter du code Blueprint spécifique à un modèle. Par exemple pour bouger une caméra sur les drones équipés d’une caméra de prise de vue !

Un exemple concret d’utilisation de Blueprint avec une UI

Le Blueprint est aussi parfait dans le cas des interfaces utilisateur (Widget Blueprint). Vous définissez votre interface avec les outils graphiques et basculez sur l’onglet Graph pour gérer les logique qui va avec.

Le C++ : C’est quoi et pourquoi on l’utilise ?

Le langage C++ est un « vieux » langage compilé, qui est dit performant, mais pas forcément simple à appréhender quand on vient du monde des langages dynamiques ou interprétés. En C++ il faut généralement gérer la mémoire soit même sous peine d’avoir une erreur d’exécution, je pense à la très célèbre erreur de segmentation 🙂 Mais est-ce que c’est dur de faire du C++ avec Unreal ? Par chance pas trop. Unreal avec son système de Macro (UFUNCTION & UPROPERTY) facilite bien la tâche aux développeurs que nous sommes. Finalement avec les bons outils (l’éditeur de code Rider par exemple), on arrive à être assez performant et à prendre du plaisir à écrire du code. C’est cependant plus lourd que de faire du scripting C# avec Unity.

Les plus

  • Versionning texte, on peut travailler à plusieurs sur le même fichier et gérer le merge facilement
  • Accès bas niveau à toutes les couches du moteur (avec un grand pouvoir vient de grandes responsabilités)
  • Code multiplateforme possible avec les pré-processeurs adaptés (PLATFORM_ANDROID, WITH_EDITOR, etc…)
  • Possibilité de spawner des acteurs blueprint depuis du code C++ (dans le cas d’un plugin Blueprint qui ne fonctionnerait que sur une plateforme)
  • La macro UPROPERTY pour gérer certains types d’objets sans se prendre la tête avec la gestion mémoire
  • Refactoring beaucoup plus simple
  • Compilation incrémentale
  • Une plus grande liberté dans l’architecture du projet
  • Travail en équipe facilité

Les moins

  • Débogage plus compliqué (il faut redémarrer l’éditeur en mode debug)
  • Temps de compilation non négligeables quand on a beaucoup de plugins
  • Beaucoup de Macro
  • Visual Studio à la ramasse totale avec les Macro (Rider est vraiment ce qu’il faut utiliser à la place, ou VS Code si vous savez ce que vous faites)
  • L’application va planté au moindre problème de mémoire (On a pas trop cette habitude avec des langages managés)

Dans quel cas utiliser du C++ ?

Lorsque vous voulez créer un projet mulitplateforme, il est nécessaire d’avoir recours à des pré-procésseurs pour ne compiler que certaines portions de code. Cela permet par exemple de bloquer certaines fonctionnalités.

void AFSPlayer::SetupLocalPlayer()
{
 #if PLATFORM_ANDROID
        DisableFancyEffects();
        OptimizeLevelStructure();
#endif

#if PLATFORM_WINDOWS
        if (!bVREnabled)
                EnableRayTracing();
#endif
}

De même dans certains cas, les performances sont primordiales, il est ainsi plus simple d’écrire ce code en C++ pour pouvoir l’appeler en Blueprint par la suite (ou pas).

void AAircraft::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (IsLocallyControlled() && FSPlayerController != nullptr)
	{
		FSPlayerController->ReadAllAxis(RawThrottle, RawYaw, RawPitch, RawRoll, bForceMidPointThrottle);
		UpdateInput(RawThrottle, RawYaw, RawPitch, RawRoll);
	}

        if (HasAuthority())
	        UpdatePhysics(DeltaTime);
}

Voici un exemple typique de l’utilisation du C++ : La fonction Tick, qui est appelée plusieurs fois par seconde. Dans mon cas, je récupère les inputs utilisateur et je mets à jour la physique. C’est beaucoup plus efficace de le faire ici en C++ qu’en Blueprint.

Architecture de projet à base de Blueprint, C++ et Plugins

Lorsque vous développez une application, il y a plusieurs types de fonctionnalités :

  • Unique au projet : Par exemple la logique de certaines interfaces utilisateur, des Gameplay spécifiques, etc…
  • Externalisable : Un module de simulation, un gestionnaire d’input, etc…
  • Un peu des deux : Ça arrive plus souvent qu’on ne le pense ! Dois-je créer un composant générique et faire un peu d’over-engineering à côté ? ou alors est ce que je fais un composant interne avec un gros copier/coller. La réponse n’est pas forcement simple. J’aurais tendance à dire que l’over-engineering est à éviter. La bonne réponse serait donc, si je n’ai pas trop d’adaptation à faire, alors oui, va pour du générique.

L’idée est donc de découper le projet en briques logiques de haut niveau et vérifier les dépendances entre les briques. Ensuite, ce qui peut être externalisé le sera sous forme de plugin. Le reste fera parti du projet, c’est aussi simple 🙂

Je vous recommande de créer tous vos types de base en C++, ainsi que les différentes énumérations et structures. Cela vous permettra de partager facilement vos données entre C++ et Blueprint. Évidemment une énumération qui n’est utilisée que dans un Blueprint n’a pas besoin d’être exposée en C++, mais toutes les données qui peuvent être exploitées dans les 2 sens, doivent être déclarées en C++.

Les plugins

Le plugin va comporter du code « générique » et idéalement (s-il vous plait!) qui compile sur toutes les plateformes. Compiler sur toutes les plateformes ne veut pas dire que votre plugin fonctionnera partout, mais qu’il sera silencieux en build et ne cassera pas la compilation. Pour les décideurs, sachez que cela coûte plus cher de mal faire un plugin, que de bien le faire sur le moyen/long terme.

Un plugin peut comporter du code C++, des blueprints et des même tout types d’assets ! Profitez en pour y ajouter un dossier de documentation, un README et le nécessaire pour le versionning si c’est utile.

Alors blueprint ou C++ ?

Comme cela a été démontré plus haut, la réponse n’est pas si simple et va dépendre de votre équipe, la taille du projet, de l’aspect multi-plateforme, etc… Pour moi il est évident qu’un nouveau projet doit être commencé en C++ avec tous les types de base natif. Cela permettra par la suite d’échanger facilement des données (structures, énumérations) et d’être efficace lors du travail en équipe. Le versionning est un point noir sur Unreal et l’utilisation du C++ permet de palier grandement à ce problème.

Enfin les plugins sont une séparation physique et logique d’une fonctionnalité haut niveau, là encore le versionning est facilité.

Pour les adorateurs du code, sachez quand même qu’on peut réaliser des gros jeux, très complexes, rien qu’avec des blueprints.

Et vous, comment travaillez vous avec Unreal ?