Skip to content

Instantly share code, notes, and snippets.

@Marsgames
Last active December 23, 2024 12:25
Show Gist options
  • Save Marsgames/c9c20d50d2a0e3c81f369a4bcc6b53b3 to your computer and use it in GitHub Desktop.
Save Marsgames/c9c20d50d2a0e3c81f369a4bcc6b53b3 to your computer and use it in GitHub Desktop.
Ce document présente les good practices que nous respectons au sein de Headcrab.

Programmation Guideline

Ce document présente les good practices que nous respectons au sein de Headcrab.

Sommaire

Conseils généraux

Langue :

Sauf projet spécifique, la langue du projet est l'anglais.
Les fichiers, les classes, les variables, les fonctions, les commentaires... doivent être en anglais.

Gestion des accolades :

Sauf spécification du langages (js, swift, ...), les accolades commencent sur la ligne du dessous.
Les accolades d'un if sont TOUJOURS requises. Quoi qu'il arrive.

private void MyFunction()
{
    if (myVariable)
    {
        // Multiple instructions  
    }
    else
    {
        // One unique instruction
    }
}

Le fait de commencer les accolades sur la ligne du dessous permet d'avoir les accolades de début et de fin alignées, et donc de voir d'un simple coup d'oeil le bloc de code concerné.

Les accolades sont requises même lorsque un if n'a qu'une instruction pour :

  • faciliter la lisibilité;
  • faciliter le rajout d'instructions, si nécessaire;
  • garder de la cohérence.

if imbriqués :

Évitez au maximum les if imbriqués s'ils ne sont pas nécessaires.
On préfère sortir de la condition si elle n'est pas remplie, plutôt que de rajouter une condition supplémentaire.

Ce qu'il ne faut pas faire :

private void GoToNextLevel()
{
    if (score > 100)
    {
        // On fait tout un tas de tests
        // On set quelques variables
        // Quelques commentaires pour expliquer

        if (playerSpeed > 5)
        {
            // On refait quelques autres tests
            // On assigne une variable
        }

        // On load enfin le prochain niveau
    }
}

Ce qu'il faut privilégier :

private void GoToNextLevel()
{
    // On sait directement que si score <= 100 on sort de la fonction
    if (score <= 100)
    {
        return;
    }

    // On fait toutes les actions nécessaires

    if (playerSpeed > 5)
    {
        // Some code ...
    }

    // Load next level
}

Il est souvent plus lisible et plus facile de sortir directement de la fonction quand la condition n'est pas remplie plutôt que de devoir scroller X lignes avant de se rendre compte qu'il n'y avait pas de else après le if (score > 100).


Nommage :

Toutes les variables utilisées doivent avoir un nom compréhensible, en rapport avec leur usage.
(On accepte les i pour les boucles for, "nous ne sommes pas des sauvages".)

Cette fonction est difficilement compréhensible

var s = 10;

public void Process()
{
    if (100 == s)
    {
        // Some code ...
    }
}

Cette fonction est bien plus lisible

int score = 10;

public void CheckScore()
{
    if (100 == score)
    {
        // Some code ...
    }
}

Les interfaces doivent commencer par la lettre I majuscule, suivi du PascalCase.
Elles représentent une relation "can do", ce qu'une classe "peut faire".
ICanMove, ICanDie, ICanFight, ICanCollect...

La convention à tendance à dire qu'une interface doit avoir un suffixe -able / -or / -er...
Serializable, Printable, ScanHelper, IEnumerator...

Il y a un débat quant à cette convention, ne serait-ce que pour savoir s'il faut garder le I hérité de l'Hungarian Notation qui a fait son temps.
Comment reconnaitre si dans class Apple: Fruit Fruit est une classe de base, ou une interface ?
A-t-on seulement besoin de le savoir ?

J'ai tendance à préférer utiliser ICan... car cela représente bien la relation can do. Cependant, nous accepterons d'autres notations, du moment que c'est COHÉRANT avec le reste du code !

Une classe dédie a l'héritage doit avoir le mot "Base" comme suffixe.
L'héritage représente une relation "is a", "c'est un / une".
UnitBase -> peut avoir des héritiers FlightUnit, GroundUnit... (Ce sont des units)
CollectibleBase-> peut avoir des héritiers Coin, Ammo, LifePack... (Ce sont des collectibles)


Taille des fonctions :

Il est toujours compliqué de définir la taille idéale d'une fonction...
Afin de se faciliter la tâche, on peut se donner une limite arbitraire : une fonction ne doit pas être plus grande que la taille d'un écran. Elle ne doit pas nécessiter de scroller pour être lue.

⚠️ --> J'ai un écran de 16" 1080p landscape <-- ⚠️

Privilégiez les sous-fonctions plutôt que des fonctions trop longues.


Gestion des espaces :

Sauf spécification du langage :

  • placez un espace entre des opérateurs ;
  • placez un espace après une virgule, un if, un while, un for, un switch, un catch...;
  • placez un espace après l'ouverture et avant la fermeture d'une accolade dans le cas d'une lambda ;
  • ne placez pas d'espace avant un point-virgule ;
  • ne placez pas d'espace après l'ouverture, ou avant la fermeture d'une parenthèse, d'un crochet ;
  • ne placez pas d'espace entre un appel de fonction et ses paramètres.

C#

private void MyFunction(int param1, int param2)
{
    if (param1 > param2)
    {
        // ...
    }

    for (int i = 0; i < 10; i++)
    {
        // ...
    }

    var myList = new List<int> 
    { 
        1, 
        2, 
        3, 
        4, 
        5 
    };

    var newList = myList.Where(x => x > 2);
}

Swift

var list = [
    1,
    2,
    3,
    4,
    5
]

var newList = list.filter({ $0 > 2 })

Scope :

Toutes les fonctions ou variables qui n'ont pas besoin d'être public doivent être private.
Toutes les classes qui n'ont pas pour objet d'être héritées doivent vérouilles l'héritage (sealed en C#, final en Swift...)


Booléens :

Préférez toujours des booléens positifs.

private bool canShoot = false; // booléen "positif"
private bool cannotFly = true; // booléen "négatif"

private void Start()
{
    // Préférez
    if (!canShoot)
    {
        // Action if you cannot shoot;
    }

    // Plutôt que
    if (!cannotFly)
    {
        // Action if you cannot not fly 😵
        // Which should be `if (canFly)` instead
    }
}

Headers :

Vos fonctions doivent posséder un header clair et concis.
Visual Studio le génère automatiquement en faissant ///* au dessus de la fonction.
Xcode le génère automatiquement en faisant clique droit sur la fonction, puis Add documentation.

Visual Studio

/// <summary>
/// Check if the player is still alive
/// </summary>
/// <param name="lifePoints">Actual life points of the player</param>
/// <returns>True if the player is alive, otherwise false</returns>
private bool CheckAlive(int lifePoints)
{
    // Some code ...
}

Commentaires :

Les commentaires sont importants, mais ne doivent pas être omniprésents.
Un code clair et concis se passe de commentaires.

Il est inutile de commenter chaque ligne de code, cependant, les autres utilisateurs doivent pouvoir comprendre votre code.

local function GetActiveTemplate()
    local returnValue

    -- Can occurs if you select CustomTemplate but you did not set a template
    if (nil == weightTemplate or "" == weightTemplate) then
        weightTemplate = "NOX"
    end

    if weightTemplate == "NOX" then
        local currentSpec = GetSpecializationInfo(GetSpecialization())
        if templates[currentSpec]["NOX"] == nil then
            error(ExceptionMissingNoxTemplate)
        end

        returnValue = templates[currentSpec]["NOX"]
    else
        if (nil == CW[weightTemplate]) then
            error(ExceptionMissingCustomTemplate)
        end

        returnValue = CW[weightTemplate]
    end

    return returnValue
end

Les commentaires ne SERVENT PAS à garder un historique de vos modifications. Vous utilisez un gestionnaire de version pour cela.
Supprimez ce code que vous avez gardé commenté, au cas où... Git est à vos côtés.


Imports :

Les imports non utilisés doivent être supprimés.
Les imports doivent être triés par ordre alphabétique.
Cela permet de diminuer le temps de compilation.
(Visual Studio le fait automatiquement en appuyant sur Ctrl + R, Ctrl + G)


Programmation défensive :

Quand vous utilisez un opérateur de comparaison, mettez la constante en premier.

if (100 == score)
{
    // Do some code
}

Certains langages comme le C++ acceptent le code suivant, et une faute de frappe est si vite arrivée :

if (score = 100)
{

}

Pire, il assigne la valeur 100 à la variable, et retourne true (donc passe aussi dans le if, quoi qu'il arrive)

Quel que soit le langage, 100 = score ne compile pas, et vous évite de nombreux problèmes.



Voyez l'exemple si dessous.
Exemple C++ (Il est toujours bon de prendre de bonnes habitudes afin de les avoir et de ne pas se faire piéger quand on change de langage.)



N'oubliez pas de faire des null checks sur vos objets pour vous éviter des erreurs.

public User GetUserDetails(Address address)
{
    if (null == address || null == address.User)
    {
        return null;
    }

    return address.User;
}

Strings :

  • Utilisez au maximum les string interpolation plutôt que les concaténations.
    Cela permet une meilleure gestion de la mémoire : StackOverflow
int year = 2020;
string helloWorld = $"Hey we are in {year}";
  • A moins que vous aillez un besoin particulier, en cas de comparaison de string utilisez ToUpper(), ToLower(), ou une comparaison CaseIncensitive.
  • Pour comparer des string vides, préférez .IsEmpty(), .IsNullOrEmpty(), .IsNullOrWhiteSpace(), ... plutôt que str != ""
  • Utilisez des fichiers de ressources plutôt que des strings en dur dans votre code (identifiants BDD, chaines de connexion, ...).

Flags de compilation :

Quand vous démarrez un nouveau projet, assurez vous que le projet est configuré pour que les warnings soient traités comme des erreurs.


C#

Cette partie présentes les spécificités du langage C#.

Sommaire


Convention de nommage

  • Les noms des classes sont en PascalCase --> class MyClass
  • Les classes de base servant à l'héritage doivent avoir comme suffixe Base --> class PlayerBase
  • Les variables membres sont en camelCase --> private var myPrivateVariable
  • Les variables public sont en PascalCase --> public var MyPublicVariable
  • Les constantes commencent par K_ puis sont en UPPER_SNAKE_CASE --> static K_MY_CONSTANT
  • Les interfaces commencent par un i majuscule puis sont en PascalCase --> interface ICanDoSomething
  • Les enumérations commencent par un e majuscule puis sont en PascalCase --> enum EType
  • Les valeurs des enum sont en PascalCase
  • Les lists, array, map... qui contiennent plusieurs éléments, sont au pluriel --> List scores; / int[,] positions;
  • Les fonctions sont en PascalCase --> void MyFunction()

Getters et Setters

Le rôle des getters et setters est d'accèder à un membre ou lui assigner une valeur. Faire du traitement complémentaire dans les getters / setters d'un membre est considéré comme une mauvaise pratique car le traitement est caché à l'utilisateur. Si l'on veut faire du traitement complémentaire sur un membre on définit une fonction dont c'est le rôle, nommée correctement et avec un header pour faciliter la compréhension.

C'est pour cela que l'on préfèrera utiliser les auto properties de C# pour définir les getters / setters dont les rôle est d'uniquement assigner ou retourner une valeur :

public int Test { get; private set; }

Si l'on souhaite faire un traitement supplémentaire sur la valeur avant l'assignation dans ce cas on utilisera un getter classique et une fonction dédié à l'assignation :

public int Test { get; private set; }

void AssignAndClamp(int value) 
{
    Test = value > 100 ? 20 : value; 
}

Pour rappel, les getters / setters ne sont pas automatique, par défaut la visibilité des membres doit être privé


Variables

var peut être utilisé si le type est explicité lors de l'initialisation.
Dans le cas contraire, le type doit être explicité.

var score = 10;

var name = "John";

var player = new Player();

List<Vehicle> cars = driver.GetCars();

foreach (Vehicle car in cars){};

Unity

Quand vous clonez un projet Unity OU que vous créez un nouveau projet Unity, vérifiez que le fichier setup.py est présent et exécutez le avant toute chose.


Général

De manière général quand on travail avec Unity, il faut utiliser == plutôt que .Equal() ou même is.
En C# "normal" je recommanderais d'utiliser myVar is null mais quand on travail avec Unity, il faut toujours utiliser myVar == null. Unity override l'opérateur == (ce n'est pas possible avec is, et je ne sais pas (il faudrait se renseigner pour mettre à jour cette section) si celà a été fait avec la fonction .Equal()) pour prendre en compte les gameobject inactifs, détruits, etc...
Vous risquez donc de vous retrouver avec des comportements qui ne sont pas ceux souhaités, si vous n'utilisez pas ==.

En revanche, quand on test une logique entièrement basée sur le code, on peut totalement utilser les autres opérateurs.

public bool OnCollide(BaseEntity other)
{
    if (other is ICanRedirect redirector)
    {
        redirector.SetNewTarget(this);
        return true;
    }

    if (other is ICanFight fighter)
    {
        fighter.InflictDamage(this);
        return true;
    }

    if (other is Nexus nexus)
    {
        nexus.TakeDamage(1);
        Destroy(gameObject);
        return true;
    }

    return false;
}

Template

N'oubliez pas de télécharger le template de script que nous utilisons
(N'oubliez pas de changer la partie Author)
Pour changer le template par défaut, remplacer le script 81-C# par celui que vous venez de récupérer.

  • Windows: C:\Program Files\Unity\Editor\Data\Resources\ScriptTemplates
  • Mac: /Applications/Unity/Unity.app/Contents/Resources/ScriptTemplates

Inspecteur

Les variables devant être affichées dans l'inspecteur sont [SerializeField] (NB: les variables public sont automatiquement sérializées, donc il n'y a pas de différence de performances avec une variable privée + sérializée. Pourquoi faire du privé + sérializé ? Parce qu'on ne laisse JAMAIS rien de public inutilement. On ne veut pas que n'importe qui accède à n'importe quoi !)



Même si par défaut tout est private, on indique toujours le mot clé private pour plus de lisibilité.



N'oubliez pas l'existence de [Tooltip()] pour afficher des informations dans l'éditeur

[Tooltip("Durée en seconde à attendre avant de récupérer sa vie\nValeur par défaut : 3")] float restTime = 3;

Exemple tooltip

N'oubliez pas l'existence de [Range(min, max)] pour créer un slider dans l'éditeur.
(Valable pour les int, float et double)

[Range(1, 10)] public int LifePoints = 3;

Exemple range


Git et Unity

⚠️ Les *.meta doivent TOUJOURS être versionnés par git. Faites attention à ne pas les ignorer dans le .gitignore
(Ce gitignore devrait faire l'affaire pour la plupart des projets Unity)
(Il est bon de savoir que le site gitignore.io existe)



Vous devez toujours créer une nouvelle branche quand vous travaillez sur une nouvelle tâche.
Vous ne pouvez de toute façon rien pousser sur develop ou master sans être passés par une code review.



Une branche ne doit pas servir à faire l'entièreté du projet. Personne n'a envie de review 100 000 lignes de code en une fois.



Préfixez vos branches en fonction de ce que vous faites dessus :

  • Une feature : feature/add-speed-bonus
  • Un fix fix/#XX (référencez le numéro de l'issue git)
  • ...

Cela permet de savoir ce qu'il se passe sur cette branche.



Les noms et descriptions des commits doivent être le plus simple MAIS explicite possible.



Si vous avez une pull request ouverte, mais que vous n'avez pas fini de travailler dessus, vous devez OBLIGATOIREMENT la renommer pour la préfixer avec le tag WIP



N'hésitez pas à jeter un oeil à ce lien pour vous familiariser avec Git




CSC.RSP

TOUJOURS compiler en considérant les warnings comme des erreurs !
Il est possible d'avoir un fonctionnement warnings as errors pour Unity, en créant un fichier csc.rsp.



Ajoutez un fichier csc.rsp dans votre dossier Assets, contenant la ligne suivante :
-warnaserror+



Pour éviter les erreurs de type :
"error CS0649: Field 'm_myVariable' is never assigned to, and will always have its default value 0"
Une variable privée et sérializée doit toujours être initialisée à sa déclaration. (null, 0, "", ce que vous voulez...)



Si vous avez une erreur due à un warning que vous ne pouvez pas corriger (ex: l'utilisation de fonctions obsolète parce que vous devez vous servir de Unet, warning dans une dépendance...), vous pouvez supprimer l'erreur localement.
Ex : si vous avez une error CS0612 : function 'is obsolete" dans un fichier, ouvre le fichier et ajouter un #pragma warning disable CS0XXX (où XXX représente le code de l'erreur)

#pragma warning disable CS0612 // CS0612 is the code for obsolete warning
    [SerializeField, Obsolete] private int myVariable = 0;
#pragma warning restore

N'oubliez pas de faire un restore après.
(Le numéro de l'erreur s'affiche dans la console de Unity en même temps que l'erreur)


AutoSave.cs

Il est très fortement recommandé d'ajouter un fichier de type AutoSave dans votre dossier Assets/Editor



Ce script permet de sauvegarder votre scène automatiquement à chaque fois qu'on appuie sur Play.
(Très utile pour éviter les pertes de données en cas de crash d'Unity (une boucle infinie est si vite arrivée...))


Autres

2 scripts GameManager peuvent être trouvés en suivant ce lien, le premier est le script singleton classique avec une variable Instance public. Le deuxième ajoute le minimum requis pour avoir des pubs en jeu.
Libre à vous de les utiliser ou non, à condition que vos scripts perso respectent ce qui est demandé dans ce document.



⚠️ Tous les scripts contenant un using UnityEditor; (sauf s'il y à un #if UNITY_EDITOR pour le gérer) doivent être placés dans le dossier Assets/Editor, sinon le cloud build ne fonctionne pas !



Les opérateurs '??', '?.' et '??=' ne doivent JAMAIS être utilisés avec des MonoBehaviour. En effet, Unity utilise sa propre implémentation de l'opérateur '==', or les opérateurs mentionnés précedemment utilisent l'opérateur '==' traditionnel. Le résultat ne sera donc pas toujours celui que vous retrouveriez en faisant le test vous même.


Swift

Sommaire


Convention de nommage

  • Les noms des classes sont en PascalCase --> class MyClass
  • Les classes de base servant à l'héritage doivent avoir comme suffixe Base --> class PlayerBase
  • Les variables membres sont en camelCase --> private var myPrivateVariable
  • Les variables public sont en PascalCase --> public var MyPublicVariable
  • Les constantes sont en camelCase--> let myConstant
  • Les interfaces commencent par un i majuscule puis sont en PascalCase --> interface ICanDoSomething
  • Les enumérations commencent par un e majuscule puis sont en PascalCase --> enum EType
  • Les valeurs des enum sont en camelCase
  • Les lists, array, map... qui contiennent plusieurs éléments, sont au pluriel --> List scores; / int[,] positions;
  • Les fonctions sont en camelCase --> func myFunction()

Discord

Nous utilisons Discord pour communiquer au sein de la société.
Pour rejoindre le serveur, utilisez ce lien
Attention ce lien fait de vous un membre provisoire. Pensez à nous demander de vous attribuer un rôle, sinon vous serez expulsé du serveur à votre déconnexion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment