diff --git a/content/courses/programmation/01-bases/05-boucles.md b/content/courses/programmation/01-bases/05-boucles.md index b4afb7e..8e3ac9f 100644 --- a/content/courses/programmation/01-bases/05-boucles.md +++ b/content/courses/programmation/01-bases/05-boucles.md @@ -77,5 +77,3 @@ let fundingTotal = donations.reduce((a, b) => a + b) Ici, `fundingTotal` vaut 63. Notre *reducer* a d'abord réalisé le calcul `3 + 50`, puis `53 + 10`, et aurait continué de la sorte si le tableau contenait d'autres valeurs. Il s'agit d'un exemple très simple, mais on pourrait imaginer un *reducer* plus complexe qui récupèrerait notre liste d'ingrédients pour en faire une salade ! Le principe est toujours de transformer un ensemble de valeurs en une valeur unique. - -> Le premier chapitre de cette formation touche à sa fin et les suivants sont en cours d'écriture. Revenez bientôt ! diff --git a/content/courses/programmation/02-logique-et-design/01-complexite.md b/content/courses/programmation/02-logique-et-design/01-complexite.md new file mode 100644 index 0000000..1cca844 --- /dev/null +++ b/content/courses/programmation/02-logique-et-design/01-complexite.md @@ -0,0 +1,40 @@ +--- +title: "La complexité d'un algorithme" +--- + +Dans le chapitre précédent, nous avons vu les opérateurs, les types, les fonctions et les structures de données. La base de la programmation, c'est d'utiliser tous ces outils pour construire un algorithme : un programme fait pour résoudre un problème. + +Imaginons que l'on souhaite récupérer uniquement les chiens dans la liste des animaux de notre refuge. C'est un algorithme très simple, mais qui peut être écrit de plusieurs façons différentes ! On peut filtrer la liste avec une boucle *for in* ou bien avec une fonction de premier ordre. + +```js +let dogs = [] +for (animal in animals) { + if (animal.race == "chien") { + dogs.insert(animal) + } +} +``` + +```js +let dogs = animals.filter(animal => animal.race == "chien") +``` + +Pour des problèmes plus complexes, il peut exister des approches radicalement différentes et plus ou moins intéressantes. Par exemple, pour ranger tous nos chiens dans l'ordre alphabétique de leur nom, il nous faudrait un algorithme de tri. Cela peut paraître simple, mais il existe des centaines de manières différentes d'implémenter un tel tri ! + +La méthode du [tri par sélection](https://fr.wikipedia.org/wiki/Tri_par_s%C3%A9lection) passe en revue tous les éléments, trouve le plus petit et le place au début de la liste, avant de recommencer avec le reste de la liste. Plus stupide, le [bogosort](https://fr.wikipedia.org/wiki/Tri_stupide) mélange tous les éléments au hasard en espérant tomber sur le bon ordre. C'est un algorithme si inefficace que si vous avez beaucoup d'éléments à trier, vous pourriez bien ne jamais tomber sur la bonne solution ! D'autres tris sont bien plus performants, mais seraient plus difficiles à résumer en quelques lignes... + +Pour juger de la performance d'un algorithme, on peut étudier sa **complexité**, aussi appelée temps de calcul. Dans le cas le plus simple, un algorithme est noté O(*n*), nous indiquant que son temps de résolution augmente linéairement avec le nombre d'éléments étudiés. + +![Courbe évoluant linéairement selon deux axes, le temps de résolution et le nombre d'éléments.](./complexite.png) + +Vous verrez également des algorithmes être notés O(log *n*), indiquant qu'ils sont performants sur de très nombreux éléments, ou à l'inverse, O(*n* log *n*) qui indique que le temps explose avec le nombre d'éléments. Un tel algorithme ne devrait être utilisé que sur de petites listes ! + +![Deux courbes au même format que l'image précédente. A gauche, la courbe évolue avec une réduction progressive du temps de résolution par élément étudié. A droite, une courbe dont le temps de résolution augmente exponentiellement avec le nombre d'éléments étudiés.](./complexite2.png) + +:::profremi +La complexité d'un algorithme peut être difficile à comprendre, mais ce qui est important à retenir, c'est que l'on juge de la performance d'un algorithme au temps qu'il met à résoudre un problème avec plus ou moins d'éléments. +::: + +En programmation, il est important d'éviter de réinventer la roue. Pas besoin de recréer un algorithme de tri quand votre langage en embarque déjà un qui sera compris par tous les autres programmeurs et qui sera mis à jour. + +De la même manière, si vous êtes confronté·e à un problème qui vous semble courant, il existe peut-être une manière standardisée d'y répondre. C'est ce que nous allons voir dans la section suivante. diff --git a/content/courses/programmation/02-logique-et-design/02-design-patterns.md b/content/courses/programmation/02-logique-et-design/02-design-patterns.md new file mode 100644 index 0000000..fa54048 --- /dev/null +++ b/content/courses/programmation/02-logique-et-design/02-design-patterns.md @@ -0,0 +1,49 @@ +--- +title: "Les design patterns" +--- + +Les design patterns sont des bouts de code qui permettent de résoudre des problèmes courants en programmation, mais qui ne sont pas automatisés par le langage que vous utilisez. Ces solutions standardisées sont partagées sous la forme d'un code à recopier dans votre programme et à adapter légèrement. + +Les design patterns ont deux intérêts : le premier est d'avoir sans doute été pensé par quelqu'un d'expérimenté, le second est d'être connu, et donc compris, par une grande partie des programmeurs de votre langage. + +:::remi +L'exemple qui va suivre peut sembler curieux, mais pas de panique. Le but est simplement de comprendre le concept, non pas ce design pattern spécifiquement. +::: + +Prenons un exemple avec un pattern Factory pour créer des animaux. Tous les animaux auront la même structure : un objet contenant un attribut nom et une méthode sound pour leur cri. Plutôt que de réécrire plusieurs fois le même code, on va créer une `AnimalFactory` qui, en fonction du type de l'animal, va créer l'objet adapté. + +```js +let newCat = (name) => { + let cat = { + name: name + } + return cat +} +``` + +Dans notre exemple, toutes les parties "communes" aux chats et aux chiens (c'est à dire avoir un nom et renvoyer un objet) sont dans la factory `createAnimal`, et seuls les parties qui diffèrent (dans notre cas, la méthode `sound`) est dans les fonctions `Dog` et `Cat`. + +Une fois cela fait nous pouvons, en une seule ligne, créer un chat dont le nom serait Potimarron et donc la méthode `.sound()` afficherait "miaou" dans la console. + +```js +let nouvelAnimal = AnimalFactory.createAnimal("cat", "Potimarron") +Console.print(nouvelAnimal.sound()) // affiche miaou +``` + +Il n'est pas utile de s'éterniser sur l'explication ligne par ligne, car la manière de faire diffère énormément d'un langage à l'autre. Si le sujet vous intéresse, il existe de nombreux sites référençant les design patterns et vous expliquant comment les mettre en place, tels que [Refactoring Guru](https://refactoring.guru/fr/design-patterns) et [Dofactory](https://www.dofactory.com/javascript/design-patterns/). Le site [Game Programming Patterns](https://gameprogrammingpatterns.com/) référence les patterns spécifiquement liés à la création de jeux. + +:::marvin +Super ! Je peux régler tous mes problèmes en copiant-collant des patterns d'internet ! +::: + +:::notlikethisremi +Pas si vite ! Il ne faut pas utiliser les design patterns à tout va... +::: + +:::winkastride +Comme le dit l'adage, quand on a un marteau, tout ressemble à un clou ! Attention à ne pas utiliser un pattern complexe pour régler un problème simple. +::: + +Les design patterns existent avant tout pour combler un manque d'outils dans certains langages (on peut parler d'un manque d'**abstraction**). De la même manière que les fonctions de premier ordre sont plus sûres et lisibles que des boucles for, une abstraction fournie par votre langage serait plus expressive et plus simple qu'un design pattern. + +Utiliser un pattern copié d'Internet et dont vous ne comprenez pas le fonctionnement pourrait rendre le débogage plus compliqué lorsque votre code ne fonctionnera plus. Ainsi, il ne faut pas voir les design patterns comme des solutions magiques, mais toujours se demander s'il existe une autre solution plus adaptée à votre problème ! diff --git a/content/courses/programmation/02-logique-et-design/03-paradigmes-oriente-objet-fonctionnel.md b/content/courses/programmation/02-logique-et-design/03-paradigmes-oriente-objet-fonctionnel.md new file mode 100644 index 0000000..1130888 --- /dev/null +++ b/content/courses/programmation/02-logique-et-design/03-paradigmes-oriente-objet-fonctionnel.md @@ -0,0 +1,132 @@ +--- +title: "Les paradigmes orienté objet et fonctionnel" +--- + +Précédemment, nous avons vu que pour régler certains problèmes, il existe des solutions standardisées, que tout le monde utilise. Plus largement, il existe même plusieurs manières de penser l'organisation de son programme, qu'on appelle des **paradigmes**. + +Le paradigme le plus ancien et le plus simple, c'est la **programmation impérative**. Il consiste simplement à penser un programme comme une suite d'instructions que la machine exécute de haut en bas pour arriver à la solution. Bien sûr, il existe des paradigmes plus complexes ! Nous aborderons les deux les plus populaires : + +- la **programmation orientée objet**, largement majoritaire dans le développement de jeux ; +- la **programmation fonctionnelle**, moins présente dans les jeux, mais qui nous permettra de comprendre comment on peut penser les programmes différemment. + +### L'orienté objet + +Comme son nom l'indique, l'orienté objet pense les programmes comme un assemblage de briques logiques que l'on appelle des objets. Un objet contient : + +- des variables qui le définissent, que l'on appelle des attributs ou des propriétés ; +- des fonctions qui lui sont propres, que l'on appelle des méthodes. + +Afin de faciliter la création de ces objets, on utilise des classes, qui servent de modèle. Une classe définit les propriétés et méthodes que peut avoir un certain type d'objet, et fournit une méthode pour créer des objets héritant de cette classe. + +Par exemple, on peut créer une classe Voiture avec un poids et une vitesse fixe. La couleur de chaque voiture pourra être différente. La classe propose aussi une méthode `drive()` qui fait avancer la voiture en fonction de sa vitesse. + +```ts +class Car { + weight = 1500 + speed = 60 + position = (0, 0) + color: string + + drive = () => { + position.x += speed * 0.1 + } +} +``` + +Les classes proposent également un constructeur : une méthode pour créer un objet héritant de la classe. Dans notre exemple, le constructeur de `Car` permet de choisir la couleur de la voiture que l'on crée. + + +```ts +let redCar = new Car(color = "red") +let blueCar = new Car(color = "blue") +let greenCar = new Car(color = "green") +``` + +On appelle ces objets des **instances** de la classe `Car`. Chacun d'entre eux peut se déplacer individuellement en faisant appel à leur méthode. + +```ts +redCar.drive() +blueCar.drive() +greenCar.drive() +``` + +#### L'héritage + +Une classe peut hériter d'une autre classe. Dans ce cas, la classe enfant possède les mêmes attributs et méthodes que sa classe parente, mais également des attributs et méthodes supplémentaires. Par exemple, on peut créer une classe Camion avec une méthode supplémentaire, Charger, lui permettant de prendre des colis. + +```ts +class Truck : Car { + trunk = list{} // coffre vide + + load = (item) => { + trunk.add(item) + } +} +``` + +```ts +let blueTruck = new Truck(color = "blue") +blueTruck.load("cargaison") +blueTruck.drive() +``` + +Une classe enfant peut également remplacer des attributs et des fonctions de la classe parent. Par exemple, on pourrait créer une classe Voiture de course, avec une vitesse plus élevée. + +```ts +class RaceCar : Car { + speed = 180 +} +``` + +```ts +let redCar = new Car(color = "red") +let redRaceCar = new RaceCar(color = "red") +redCar.drive() // avance de 6 +redRaceCar.drive() // avance de 18 +``` + +:::marvin +En fait, tous les composants de mon jeu seront des objets ! +::: + +:::profremi +Exactement. La programmation orientée objet considère les programmes comme étant des interactions entre différents objets. +::: + +:::astride +C'est pour cela que ce paradigme est populaire dans les jeux vidéo. Tout l'enjeu est de prévoir comment ces objets vont interagir. Par exemple, si deux véhicules entrent en collision, ils se suppriment ! +::: + +L'orienté objet présente beaucoup d'avantages mais a aussi quelques défauts, notamment des problèmes de maintenabilité. Si l'on change d'avis sur la manière de penser nos classes ou si l'on souhaite corriger un bug, le système d'héritage a tendance à nous faire réécrire de nombreuses classes pour arriver à nos fins, ce qui peut mener à du code spaghetti. C'est ainsi que l'on appelle un code devenu si fouilli et incohérent que l'on a du mal à le comprendre et à remonter à la source d'un problème. Il est donc intéressant de se pencher sur la programmation fonctionnelle, qui aide à contrer ce phénomène. + +### La programmation fonctionnelle + +Pour rappel, une fonction est un outil qui, pour des paramètres donnés, retourne un résultat. La programmation fonctionnelle considère un programme comme étant un ensemble de fonctions mathématiques permettant d'arriver à un résultat final. Pour ce faire, deux problèmes sont importants à comprendre : les effets de bord et la mutabilité des structures de données. + +#### Éviter les effets de bord + +Une des choses qui peut causer un code spaghetti est un programme avec des fonctions ou des méthodes qui ont trop d'effets de bord, autrement dit des effets secondaires qui ne sont pas la valeur de retour de la fonction. + +Imaginons qu'une fonction prenne un nombre et me renvoie un nombre modifié, mais qu'entre-temps, elle effectue aussi d'autres actions, par exemple modifier une autre variable et afficher une image. Ce sont des effets de bord ! Si cela arrive trop fréquemment dans votre programme, il vous sera difficile de réécrire votre code, car vous pourriez accidentellement briser d'autres parties du programme qui dépendaient des effets de bord d'anciennes fonctions. + +On dit qu'une fonction est pure si elle est transparente référentiellement : la fonction n'a aucun effet de bord, et donc on pourrait remplacer l'appel de la fonction par son résultat. En d'autres termes, pour un paramètre donné, la fonction renvoie toujours le même résultat. + +:::oofmarvin +Mais il y a forcément des effets de bord dans mon programme, sinon je ne pourrais pas afficher mes graphismes par exemple... +::: + +:::astride +C'est vrai ! Mais il faut essayer de les isoler dans quelques fonctions qui sont uniquement dédiées à cela. +::: + +#### La mutabilité des données + +Un second problème à résoudre est la mutabilité des structures de données. Si plusieurs processus tournent dans mon programme, chacun modifiant les mêmes valeurs, cela peut créer des bugs ou casser la logique d'autres processus qui ne se retrouvent pas devant les valeurs qu'ils attendaient. + +Reprenons l'exemple d'une liste d'ingrédients. J'ai une fonction qui me permet de faire un sandwich en découpant ces ingrédients en tranches. Cependant, ailleurs dans mon programme, une fonction a besoin des ingrédients d'origine. Si cette fonction se retrouve devant les ingrédients coupés en rondelles, cela peut l'empêcher de fonctionner correctement ! + +En programmation fonctionnelle, les structures de données sont immuables et les variables sont quasiment toutes des constantes. La liste d'ingrédients ne pourrait donc pas changer, et je devrai en faire une copie pour créer une liste d'ingrédients découpés ! + +En évitant les effets de bord et en protégeant les données, un programme écrit de manière fonctionnelle est donc un ensemble de fonctions que l'on peut facilement refactorer (réécrire, remplacer) sans avoir peur de briser quelque chose à l'autre bout du programme. Chaque processus peut librement agir sur les données sans avoir peur d'empêcher le travail d'un autre. + +Ce n'est pas pour autant que vous devriez forcément utiliser ce paradigme : il est peu courant dans le jeu vidéo et vous auriez du mal à trouver des outils qui le supportent convenablement. En pratique, dans la création d'un jeu, vous pourriez écrire une structure générale en orienté objet mais penser certaines parties du code dans la logique fonctionnelle pour vous aider à le rendre plus maintenable. diff --git a/content/courses/programmation/02-logique-et-design/chapter.md b/content/courses/programmation/02-logique-et-design/chapter.md new file mode 100644 index 0000000..2022882 --- /dev/null +++ b/content/courses/programmation/02-logique-et-design/chapter.md @@ -0,0 +1,3 @@ +--- +title: "Logique et design" +--- diff --git a/content/courses/programmation/02-logique-et-design/complexite.png b/content/courses/programmation/02-logique-et-design/complexite.png new file mode 100644 index 0000000..a3b6384 Binary files /dev/null and b/content/courses/programmation/02-logique-et-design/complexite.png differ diff --git a/content/courses/programmation/02-logique-et-design/complexite2.png b/content/courses/programmation/02-logique-et-design/complexite2.png new file mode 100644 index 0000000..4a52445 Binary files /dev/null and b/content/courses/programmation/02-logique-et-design/complexite2.png differ diff --git a/content/courses/programmation/course.md b/content/courses/programmation/course.md index d6e450e..d0db81c 100644 --- a/content/courses/programmation/course.md +++ b/content/courses/programmation/course.md @@ -1,10 +1,10 @@ --- type: SKILL title: "Les bases de la programmation" -short_title: "Programmation" +short_title: "Mieux programmer" description: "Aspects théoriques de la programmation pour comprendre tous les termes techniques et devenir autonome dans la création de jeux et de logiciels." -date: "2021-08-02" -author: "Nev, Aurélien Dos Santos" +date: "2022-12-14" +author: "Goulven Clec'h, Aurélien Dos Santos" medal: SILVER medal_message: "Cette formation est en cours de rédaction : seul le chapitre 1 est finalisé. [Voir l'avancement.](https://github.com/gamedevalliance/fairedesjeux.fr/issues/39)" video: "https://www.youtube.com/playlist?list=PLHKUrXMrDS5v1I6RCFObboACa2PtEfTmA"