Up to date

This page is up to date for Godot 4.3. If you still find outdated information, please open an issue.

Dessin personnalisé en 2D

Introduction

Godot a des nœuds pour dessiner des sprites, des polygones, des particules, du texte, et beaucoup d'autres besoins communs de développement de jeux. Cependant, si vous avez besoin de quelque chose de spécifique non couvert avec les nœuds standards, vous pouvez faire que n'importe quel nœud 2D (par exemple, basé sur Control ou Node2D) dessine sur l'écran en utilisant des commandes personnalisées.

Le dessin personnalisé dans un nœud 2D est vraiment utile. Voici quelques cas d'utilisation :

  • Dessiner des formes ou une logique que les nœuds existants ne peuvent pas faire, comme une image avec des traînées ou un polygone animé spécial.

  • Dessiner un grand nombre d'objets simples, comme une grille ou une planche pour un jeu 2d. Le dessin personnalisé évite l'utilisation d'un grand nombre de nœuds, réduisant éventuellement l'utilisation de la mémoire et améliorant les performances.

  • Création d'un contrôle d'interface utilisateur personnalisé. Il existe de nombreux contrôles disponibles, mais si vous avez des besoins inhabituels, vous aurez probablement besoin d'un contrôle personnalisé.

Dessin

Ajoutez un script à n'importe quel nœud dérivé de CanvasItem, comme Control ou Node2D. Puis remplacez la fonction _draw().

extends Node2D

func _draw():
    pass  # Your draw commands here.

Les commandes de dessin sont décrites dans la référence de classe CanvasItem. Elles sont nombreuses et nous allons voir quelques exemples de celles-ci en dessous.

Mise à jour

La fonction _draw n'est appelée qu'une fois, puis les commandes de dessin sont mises en mémoire tampon et mémorisées, de sorte que les appels ultérieurs sont inutiles.

Si redessiner est nécessaire parce qu'une variable ou autre chose a changé, appelez CanvasItem.queue_redraw dans ce même nœud et un nouvel appel _draw() se produira.

Voici un exemple un peu plus complexe, où nous avons une variable de texture qui peut être modifiée à tout moment, et à l'aide d'un setter, il force un redessin de la texture lorsqu'elle est modifiée :

extends Node2D

@export var texture : Texture2D:
    set(value):
        texture = value
        queue_redraw()

func _draw():
    draw_texture(texture, Vector2())

Pour le voir en action, vous pouvez définir la texture en l'icône Godot sur l'éditeur en glissant-déposant l’icône par défaut icon.svg de l'onglet FileSystem à la propriété Texture sur l'onglet Inspector. En changeant la valeur de la propriété Texture pendant que le script précédent s'exécute, la texture change aussi automatiquement.

Dans certains cas, il peut être souhaitable de dessiner chaque image. Pour cela, appelez simplement queue_redraw, comme ceci :

extends Node2D

func _draw():
    pass  # Your draw commands here.

func _process(_delta):
    queue_redraw()

Coordinates and line width alignment

L'API de dessin utilise le système de coordonnées du CanvasItem, pas nécessairement les coordonnées de pixels. Cela signifie que _draw() utilise l'espace de coordonnées créé après avoir appliqué la transformation du CanvasItem. En outre, vous pouvez appliquer une transformation personnalisée en plus de celle-ci en utilisant draw_set_transform ou draw_set_transform_matrix.

Lors de l'utilisation de draw_line, vous devriez considérer la largeur de la ligne. Lorsqu'il s'agit d'une largeur impaire, la position des points de début et de fin devrait être décalée de 0.5 pour garder la ligne au centre, comme montré ci-dessous.

../../_images/draw_line.png
func _draw():
    draw_line(Vector2(1.5, 1.0), Vector2(1.5, 4.0), Color.GREEN, 1.0)
    draw_line(Vector2(4.0, 1.0), Vector2(4.0, 4.0), Color.GREEN, 2.0)
    draw_line(Vector2(7.5, 1.0), Vector2(7.5, 4.0), Color.GREEN, 3.0)

Il en va de même pour la méthode draw_rect avec filled = false.

../../_images/draw_rect.png
func _draw():
    draw_rect(Rect2(1.0, 1.0, 3.0, 3.0), Color.GREEN)
    draw_rect(Rect2(5.5, 1.5, 2.0, 2.0), Color.GREEN, false, 1.0)
    draw_rect(Rect2(9.0, 1.0, 5.0, 5.0), Color.GREEN)
    draw_rect(Rect2(16.0, 2.0, 3.0, 3.0), Color.GREEN, false, 2.0)

Dessin avec anticrénelage

Godot offre des paramètres de méthode dans draw_line pour activer l'antialiasing, mais certaines méthodes de dessin personnalisé n'offrent pas ce paramètre antialiased.

Pour les méthodes de dessin personnalisées qui ne fournissent pas un paramètre antialiased, vous pouvez activer la MSAA 2D, qui affecte le rendu dans l'ensemble de la fenêtre d'affichage. Cela fournit un anticrénelage de haute qualité, mais un coût de performance plus élevé et seulement sur des éléments spécifiques. Voir Anticrénelage 2D pour plus d'informations.

Voici une comparaison d'une ligne de largeur minimale (width=-1) tracée avec antialiased=false, antialiased=true, et antialiased=false avec la MSAA 2D 2x, 4x et 8x activés.

../../_images/draw_antialiasing_options.webp

Outils

Dessiner vos propres nœuds peut également être désiré lors de leur exécution dans l'éditeur. Cela peut être utilisé comme aperçu ou visualisation de certaines fonctionnalités ou comportements.

Pour ce faire, vous pouvez utiliser l'annotation tool aussi bien avec GDScript qu'avec C#. Consultez l'exemple ci-dessous et Exécuter le code dans l'éditeur pour plus d'informations.

Exemple 1 : Dessiner un forme personnalisée

Nous allons maintenant utiliser la fonctionnalité de dessin personnalisé du moteur Godot pour dessiner quelque chose pour lequel Godot ne fournit pas de fonctions. Nous allons recréer le logo de Godot mais avec du code, seulement en utilisant des fonctions de dessin.

Vous devrez coder une fonction pour réaliser cela et dessiner le logo vous-même.

Note

The following instructions use a fixed set of coordinates that could be too small for high resolution screens (larger than 1080p). If that is your case, and the drawing is too small consider increasing your window scale in the project setting Display > Window > Stretch > Scale to adjust the project to a higher resolution (a 2 or 4 scale tends to work well).

Dessiner une forme de polygone personnalisée

Bien qu'il y ait un nœud dédié à dessiner des polygones personnalisés ( Polygon2D), nous utiliserons dans ce cas exclusivement des fonctions de dessin de plus bas niveau pour les combiner sur le même nœud et être en mesure de créer des formes plus complexes plus tard.

D'abord, nous définirons un ensemble de points -ou coordonnées X et Y - qui formeront la base de notre forme :

extends Node2D

var coords_head : Array = [
    [ 22.952, 83.271 ],  [ 28.385, 98.623 ],
    [ 53.168, 107.647 ], [ 72.998, 107.647 ],
    [ 99.546, 98.623 ],  [ 105.048, 83.271 ],
    [ 105.029, 55.237 ], [ 110.740, 47.082 ],
    [ 102.364, 36.104 ], [ 94.050, 40.940 ],
    [ 85.189, 34.445 ],  [ 85.963, 24.194 ],
    [ 73.507, 19.930 ],  [ 68.883, 28.936 ],
    [ 59.118, 28.936 ],  [ 54.494, 19.930 ],
    [ 42.039, 24.194 ],  [ 42.814, 34.445 ],
    [ 33.951, 40.940 ],  [ 25.637, 36.104 ],
    [ 17.262, 47.082 ],  [ 22.973, 55.237 ]
]

Ce format, bien que compact, n'est pas celui que Godot comprend pour dessiner un polygone. Dans un scénario différent, nous pourrions devoir charger ces coordonnées à partir d'un fichier ou calculer les positions pendant que l'application fonctionne, et alors une transformation pourrait être nécessaire.

Pour transformer ces coordonnées dans le bon format, nous allons créer une nouvelle méthode float_array_to_Vector2Array(). Ensuite, nous redéfinirons la fonction _ready(), que Godot n'appellera qu'une seule fois - au début de l'exécution - pour charger ces coordonnées dans une variable :

var head : PackedVector2Array

func float_array_to_Vector2Array(coords : Array) -> PackedVector2Array:
    # Convert the array of floats into a PackedVector2Array.
    var array : PackedVector2Array = []
    for coord in coords:
        array.append(Vector2(coord[0], coord[1]))
    return array

func _ready():
    head = float_array_to_Vector2Array(coords_head);

Pour enfin dessiner notre première forme, nous utiliserons la méthode draw_polygon et y passons les points (comme un tableau de coordonnées Vector2) et sa couleur, comme ceci :

func _draw():
    # We are going to paint with this color.
    var godot_blue : Color = Color("478cbf")
    # We pass the PackedVector2Array to draw the shape.
    draw_polygon(head, [ godot_blue ])

Quand de l'exécution, vous devriez voir quelque chose comme ça :

../../_images/draw_godot_logo_polygon.webp

Notez que la partie inférieure du logo semble segmentée - c'est parce qu'une faible quantité de points ont été utilisés pour définir cette partie. Pour simuler une courbe lisse, nous pourrions ajouter plus de points à notre tableau, ou peut-être utiliser une fonction mathématique pour interpoler une courbe et créer une forme lisse à partir du code (voir example 2).

Les polygones vont toujours connecter leur dernier point défini à leur premier afin d'avoir une forme fermée.

Dessiner des lignes connectées

Dessiner une séquence de lignes connectées qui ne se rejoignent pas pour former un polygone est très semblable à la méthode précédente. Nous utiliserons un ensemble de lignes connectées pour dessiner la bouche du logo de Godot.

D'abord, nous définirons la liste des coordonnées qui forment la forme de la bouche, comme ceci :

var coords_mouth = [
    [ 22.817, 81.100 ], [ 38.522, 82.740 ],
    [ 39.001, 90.887 ], [ 54.465, 92.204 ],
    [ 55.641, 84.260 ], [ 72.418, 84.177 ],
    [ 73.629, 92.158 ], [ 88.895, 90.923 ],
    [ 89.556, 82.673 ], [ 105.005, 81.100 ]
]

Nous allons charger ces coordonnées dans une variable et définir une variable supplémentaire pour une épaisseur de ligne configurable :

var mouth : PackedVector2Array
var _mouth_width : float = 4.4

func _ready():
    head = float_array_to_Vector2Array(coords_head);
    mouth = float_array_to_Vector2Array(coords_mouth);

Et finalement nous utiliserons la méthode draw_polyline afin de dessiner la ligne, comme ceci :

func _draw():
    # We will use white to draw the line.
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")

    draw_polygon(head, [ godot_blue ])

    # We draw the while line on top of the previous shape.
    draw_polyline(mouth, white, _mouth_width)

Vous devriez obtenir la sortie suivante :

../../_images/draw_godot_logo_polyline.webp

Unlike draw_polygon(), polylines can only have a single unique color for all its points (the second argument). This method has 2 additional arguments: the width of the line (which is as small as possible by default) and enabling or disabling the antialiasing (it is disabled by default).

L'ordre des appels de _draw est important- comme avec les positions des Node dans la hiérarchie en arbre, les différentes formes seront dessinées de haut en bas, entraînant les dernières formes à cacher les précédentes si elles se chevauchent. Dans ce cas, nous voulons que la bouche soit dessinée sur la tête, donc nous l'avons mis après.

Remarquez comment nous pouvons définir les couleurs de différentes manières, soit avec un code hexadécimal ou un nom de couleur prédéfini. Regardez la classe Color pour d'autres constantes et de moyens de définir des Colors.

Dessiner des cercles

Pour créer les yeux, nous allons ajouter 4 appels supplémentaires pour dessiner les formes des yeux, dans différentes tailles, couleurs et positions.

Pour dessiner un cercle, vous le positionnez en fonction de son centre à l'aide de la méthode draw_circle. Le premier paramètre est un Vector2 avec les coordonnées de son centre, le second est son rayon, et le troisième est sa couleur :

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var grey : Color = Color("414042")

    draw_polygon(head, [ godot_blue ])
    draw_polyline(mouth, white, _mouth_width)

    # Four circles for the 2 eyes: 2 white, 2 grey.
    draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
    draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
    draw_circle(Vector2(43.423, 65.92), 6.246, grey)
    draw_circle(Vector2(84.626, 66.008), 6.246, grey)

Lors de l'exécution, vous devriez avoir quelque chose comme ça :

../../_images/draw_godot_logo_circle.webp

Pour les arcs de cercle non remplis (portions d'une forme de cercle entre certains angles arbitraires), vous pouvez utiliser la méthode draw_arc.

Dessiner des lignes

Pour dessiner la forme finale (le nez) nous utiliserons une ligne pour l'approximer.

draw_line peut être utilisé pour dessiner un seul segment en fournissant ses coordonnées de début et de fin comme arguments, comme ceci :

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var grey : Color = Color("414042")

    draw_polygon(head, [ godot_blue ])
    draw_polyline(mouth, white, _mouth_width)
    draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
    draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
    draw_circle(Vector2(43.423, 65.92), 6.246, grey)
    draw_circle(Vector2(84.626, 66.008), 6.246, grey)

    # Draw a short but thick white vertical line for the nose.
    draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8)

Vous devriez maintenant être capable de voir la forme suivante à l'écran :

../../_images/draw_godot_logo_line.webp

Notez que si de multiples lignes non connectées vont être dessinées en même temps, vous pouvez obtenir une meilleure performance en les dessinant tous en un seul appel, en utilisant la méthode draw_multiline.

Dessiner du texte

While using the Label Node is the most common way to add text to your application, the low level _draw function includes functionality to add text to your custom Node drawing. We will use it to add the name "GODOT" under the robot head.

Nous utiliserons la méthode draw_string pour le faire, comme ceci :

var default_font : Font = ThemeDB.fallback_font;

func _draw():
    var white : Color = Color.WHITE
    var godot_blue : Color = Color("478cbf")
    var grey : Color = Color("414042")

    draw_polygon(head, [ godot_blue ])
    draw_polyline(mouth, white, _mouth_width)
    draw_circle(Vector2(42.479, 65.4825), 9.3905, white)
    draw_circle(Vector2(85.524, 65.4825), 9.3905, white)
    draw_circle(Vector2(43.423, 65.92), 6.246, grey)
    draw_circle(Vector2(84.626, 66.008), 6.246, grey)
    draw_line(Vector2(64.273, 60.564), Vector2(64.273, 74.349), white, 5.8)

    # Draw GODOT text below the logo with the default font, size 22.
    draw_string(default_font, Vector2(20, 130), "GODOT",
                HORIZONTAL_ALIGNMENT_CENTER, 90, 22)

Ici, nous chargeons d'abord dans la variable defaultFont la police par défaut configurée (une police personnalisée peut être définie à la place), puis nous passons les paramètres suivants : police, position, texte, alignement horizontal, largeur et taille de police.

Vous devriez voir ce qui suit sur votre écran :

../../_images/draw_godot_logo_text.webp

Des paramètres supplémentaires ainsi que d'autres méthodes liées au texte et aux caractères peuvent être trouvés sur la référence de classe de CanvasItem.

Afficher le dessin lors de l'édition

Alors que le code est jusqu'à présent capable de dessiner le logo sur une fenêtre d'exécution, il ne s'affichera pas sur la vue 2D de l'éditeur. Dans certains cas, vous pouvez souhaiter également montrer votre contrôle ou Node2D personnalisé dans l'éditeur, pour le positionner et l'élargir correctement, comme la plupart des autres nœuds font.

Pour afficher le logo directement dans l'éditeur (sans l'exécuter), vous pouvez utiliser l'annotation @tool pour demander que le dessin personnalisé du nœud apparaisse également pendant l'édition, comme ceci :

@tool
extends Node2D

Vous devrez sauvegarder votre scène, reconstruire votre projet (uniquement pour C#) et recharger manuellement la scène actuelle via l'option de menu Scene > Reload Saved Scene pour actualiser le nœud actuel dans la vue 2D la première fois que vous ajoutez ou supprimez l'annotation @tool.

Animation

Si nous voulions faire en sorte que la forme personnalisée change au moment de l'exécution, nous pourrions modifier les méthodes appelées ou leurs arguments au moment de l'exécution, ou appliquer une transformation.

Par exemple, si nous voulons que la forme personnalisée que nous venons de concevoir tourne, nous pourrions ajouter la variable et le code suivants aux méthodes _ready et _process :

extends Node2D

@export var rotation_speed : float = 1  # In radians per second.

func _ready():
    rotation = 0
    ...

func _process(delta: float):
    rotation -= rotation_speed * delta

Le problème avec le code ci-dessus est que, puisque nous avons créé les points approximativement sur un rectangle commençant à partir du coin supérieur gauche, la coordonnée (0, 0) et s'étendant vers la droite et vers le bas, nous voyons que la rotation est effectuée en utilisant le coin supérieur gauche comme pivot. Un changement de transformation de position sur le nœud ne nous aidera pas ici, car la transformation de rotation est appliquée en premier.

Bien que nous pourrions réécrire toutes les coordonnées des points pour être centré autour de (0, 0), y compris les coordonnées négatives, ce serait beaucoup de travail.

Une façon possible de contourner ce problème est d'utiliser la méthode de bas niveau draw_set_transform pour résoudre ce problème, en translatant tous les points dans le référentiel du CanvasItem, puis en le replaçant à sa place d'origine avec une transformation de noeud standard, soit dans l'éditeur, soit dans le code, comme ceci :

func _ready():
    rotation = 0
    position = Vector2(60, 60)
    ...

func _draw():
    draw_set_transform(Vector2(-60, -60))
    ...

Voilà le résultat, tournant autour d'un pivot placé en (60, 60) :

../../_images/draw_godot_rotation.webp

Si ce que nous voulions animer était une propriété à l'intérieur de l'appel _draw(), nous devons nous souvenir d'appeler queue_redraw() pour forcer un rafraîchissement, car sinon il ne serait pas mis à jour sur l'écran.

Par exemple, c'est ainsi que nous pouvons faire que le robot ait l'air d'ouvrir et fermer sa bouche, en changeant la largeur de la ligne de sa bouche en suivant un courbe sinusoïdale (sin) :

var _mouth_width : float = 4.4
var _max_width : float = 7
var _time : float = 0

func _process(delta : float):
    _time += delta
    _mouth_width = abs(sin(_time) * _max_width)
    queue_redraw()

func _draw():
    ...
    draw_polyline(mouth, white, _mouth_width)
    ...

Il ressemblera quelque peu à ceci lors de l’exécution :

../../_images/draw_godot_mouth_animation.webp

Veuillez noter que _mouth_width est une propriété définie par l'utilisateur comme n'importe quelle autre et qu'elle ou tout autre propriété utilisée comme argument de dessin peut être animée en utilisant des méthodes standard de plus haut niveau comme un Tween ou un nœud AnimationPlayer. La seule différence est qu'un appel de queue_redraw() est nécessaire pour appliquer ces changements afin qu'ils soient affichés à l'écran.

Exemple 2 : dessiner une ligne dynamique

L'exemple précédent était utile pour apprendre à dessiner et modifier des nœuds avec des formes et des animations personnalisées. Cela pourrait avoir certains avantages, comme l'utilisation de coordonnées et de vecteurs exacts pour le dessin, plutôt que des bitmaps, ce qui signifie qu'ils s'adapteront bien lorsqu'ils seront transformés à l'écran. Dans certains cas, des résultats similaires pourraient être obtenus en composant des fonctionnalités de plus haut niveau avec des nœuds tels que sprites ou AnimatedSprites chargeant des ressources SVG (qui sont également des images définies avec des vecteurs) et le nœud AnimationPlayer.

Dans d'autres cas, ce ne sera pas possible parce que nous ne saurons pas ce que la représentation graphique résultante sera avant d'exécuter le code. Ici, nous verrons comment dessiner une ligne dynamique dont les coordonnées ne sont pas connues à l'avance, et sont affectées par l'entrée de l'utilisateur.

Dessiner une ligne droite entre 2 points

Supposons que nous voulions dessiner une ligne droite entre deux points : le premier sera fixé dans le coin supérieur gauche (0, 0) et le second sera défini par la position du curseur à l'écran.

Nous pourrions dessiner une ligne dynamique entre ces deux points comme ceci :

extends Node2D

var point1 : Vector2 = Vector2(0, 0)
var width : int = 10
var color : Color = Color.GREEN

var _point2 : Vector2

func _process(_delta):
    var mouse_position = get_viewport().get_mouse_position()
    if mouse_position != _point2:
        _point2 = mouse_position
        queue_redraw()

func _draw():
    draw_line(point1, _point2, color, width)

Dans cet exemple, nous obtenons la position de la souris dans la vue par défaut à chaque frame avec la méthode get_mouse_position. Si la position a changé depuis la dernière requête draw (une petite optimisation pour éviter de redessiner à chaque frame), nous planifierons un nouveau draw. Notre méthode _draw() ne contient qu'une seule ligne : draw d'une ligne verte d'une largeur de 10 pixels entre le coin supérieur gauche et la position obtenue.

La largeur, la couleur et la position du point de départ peuvent être configurées avec les propriétés correspondantes.

Il devrait ressembler à ça une fois exécuté :

../../_images/draw_line_between_2_points.webp

Dessiner un arc entre 2 points

L'exemple ci-dessus fonctionne, mais nous pourrions vouloir relier ces deux points avec une forme ou une fonction différente, autre qu'une ligne droite.

Essayons maintenant de créer un arc (une portion de circonférence) entre les deux points.

Exporter le point de départ de la ligne, les segments, la largeur, la couleur et l'anticrénelage nous permettra de modifier ces propriétés très facilement directement depuis le panneau de l'inspecteur de l'éditeur :

extends Node2D

@export var point1 : Vector2 = Vector2(0, 0)
@export_range(1, 1000) var segments : int = 100
@export var width : int = 10
@export var color : Color = Color.GREEN
@export var antialiasing : bool = false

var _point2 : Vector2
../../_images/draw_dynamic_exported_properties.webp

Pour dessiner l'arc, nous pouvons utiliser la méthode :ref:draw_arc<class_CanvasItem_method_draw_arc>. Il existe de nombreux arcs qui passent par deux points, donc pour cet exemple, nous choisirons le demi-cercle dont le centre se trouve au point médian entre les deux points initiaux.

Le calcul de cet arc de cercle sera plus complexe que dans le cas de la ligne :

func _draw():
    # Calculate the arc parameters.
    var center : Vector2 = Vector2((_point2.x - point1.x) / 2,
                                   (_point2.y - point1.y) / 2)
    var radius : float = point1.distance_to(_point2) / 2
    var start_angle : float = (_point2 - point1).angle()
    var end_angle : float = (point1 - _point2).angle()
    if end_angle < 0:  # end_angle is likely negative, normalize it.
        end_angle += TAU

    # Finally, draw the arc.
    draw_arc(center, radius, start_angle, end_angle, segments, color,
             width, antialiasing)

Le centre du demi-cercle sera le point médian entre les deux points. Le rayon sera la moitié de la distance entre les deux points. Les angles de début et de fin seront les angles du vecteur de point1 à point2 et vice-versa. Notez que nous avons dû normaliser le end_angle dans des valeurs positives parce que si end_angle est inférieur à start_angle, l'arc sera dessiné en sens inverse des aiguilles d'une montre, ce que nous ne voulons pas dans ce cas (l'arc serait à l'envers).

Le résultat devrait être quelque chose comme ça, avec l'arc descendant entre les points :

../../_images/draw_arc_between_2_points.webp

N'hésitez pas à jouer avec les paramètres de l'inspecteur pour obtenir différents résultats : changer la couleur, la largeur, l'anticrénelage, et augmenter le nombre de segments pour améliorer le lissage de la courbe, au coût de moins bonnes performances.