custom_drawing_in_2d.rst 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. .. _doc_custom_drawing_in_2d:
  2. Custom drawing in 2D
  3. ====================
  4. Why?
  5. ----
  6. Godot has nodes to draw sprites, polygons, particles, and all sorts of
  7. stuff. For most cases this is enough but not always. Before crying in fear,
  8. angst, and rage because a node to draw that specific *something* does not exist...
  9. it would be good to know that it is possible to easily make any 2D node (be it
  10. :ref:`Control <class_Control>` or :ref:`Node2D <class_Node2D>`
  11. based) draw custom commands. It is *really* easy to do it too.
  12. But...
  13. ------
  14. Custom drawing manually in a node is *really* useful. Here are some
  15. examples why:
  16. - Drawing shapes or logic that is not handled by nodes (example: making
  17. a node that draws a circle, an image with trails, a special kind of
  18. animated polygon, etc).
  19. - Visualizations that are not that compatible with nodes: (example: a
  20. tetris board). The tetris example uses a custom draw function to draw
  21. the blocks.
  22. - Managing drawing logic of a large amount of simple objects (in the
  23. hundreds of thousands). Using a thousand nodes is probably not nearly
  24. as efficient as drawing, but a thousand of draw calls are cheap.
  25. Check the "Shower of Bullets" demo as example.
  26. - Making a custom UI control. There are plenty of controls available,
  27. but it's easy to run into the need to make a new, custom one.
  28. OK, how?
  29. --------
  30. Add a script to any :ref:`CanvasItem <class_CanvasItem>`
  31. derived node, like :ref:`Control <class_Control>` or
  32. :ref:`Node2D <class_Node2D>`. Then override the _draw() function.
  33. .. tabs::
  34. .. code-tab:: gdscript GDScript
  35. extends Node2D
  36. func _draw():
  37. # Your draw commands here
  38. pass
  39. .. code-tab:: csharp
  40. public override void _Draw()
  41. {
  42. // Your draw commands here
  43. }
  44. Draw commands are described in the :ref:`CanvasItem <class_CanvasItem>`
  45. class reference. There are plenty of them.
  46. Updating
  47. --------
  48. The _draw() function is only called once, and then the draw commands
  49. are cached and remembered, so further calls are unnecessary.
  50. If re-drawing is required because a state or something else changed,
  51. simply call :ref:`CanvasItem.update() <class_CanvasItem_update>`
  52. in that same node and a new _draw() call will happen.
  53. Here is a little more complex example. A texture variable that will be
  54. redrawn if modified:
  55. .. tabs::
  56. .. code-tab:: gdscript GDScript
  57. extends Node2D
  58. export var texture setget _set_texture
  59. func _set_texture(value):
  60. # if the texture variable is modified externally,
  61. # this callback is called.
  62. texture = value #texture was changed
  63. update() # update the node
  64. func _draw():
  65. draw_texture(texture, Vector2())
  66. .. code-tab:: csharp
  67. public class CustomNode2D : Node2D
  68. {
  69. private Texture _texture;
  70. public Texture Texture
  71. {
  72. get
  73. {
  74. return _texture;
  75. }
  76. set
  77. {
  78. _texture = value;
  79. Update();
  80. }
  81. }
  82. public override void _Draw()
  83. {
  84. DrawTexture(_texture, new Vector2());
  85. }
  86. }
  87. In some cases, it may be desired to draw every frame. For this, just
  88. call update() from the _process() callback, like this:
  89. .. tabs::
  90. .. code-tab:: gdscript GDScript
  91. extends Node2D
  92. func _draw():
  93. # Your draw commands here
  94. pass
  95. func _process(delta):
  96. update()
  97. .. code-tab:: csharp
  98. public class CustomNode2D : Node2D
  99. {
  100. public override _Draw()
  101. {
  102. // Your draw commands here
  103. }
  104. public override _Process(delta)
  105. {
  106. Update();
  107. }
  108. }
  109. An example: drawing circular arcs
  110. ----------------------------------
  111. We will now use the custom drawing functionality of the Godot Engine to draw
  112. something that Godot doesn't provide functions for. As an example, Godot provides
  113. a draw_circle() function that draws a whole circle. However, what about drawing a
  114. portion of a circle? You will have to code a function to perform this and draw it yourself.
  115. Arc function
  116. ^^^^^^^^^^^^
  117. An arc is defined by its support circle parameters. That is: the center position
  118. and the radius. The arc itself is then defined by the angle it starts from
  119. and the angle at which it stops. These are the 4 parameters that we have to provide to our drawing.
  120. We'll also provide the color value, so we can draw the arc in different colors if we wish.
  121. Basically, drawing a shape on screen requires it to be decomposed into a certain number of points
  122. linked from one to the following one. As you can imagine, the more points your shape is made of,
  123. the smoother it will appear, but the heavier it will also be in terms of processing cost. In general,
  124. if your shape is huge (or in 3D, close to the camera), it will require more points to be drawn without
  125. it being angular-looking. On the contrary, if your shape is small (or in 3D, far from the camera),
  126. you may reduce its number of points to save processing costs. This is called *Level of Detail (LoD)*.
  127. In our example, we will simply use a fixed number of points, no matter the radius.
  128. .. tabs::
  129. .. code-tab:: gdscript GDScript
  130. func draw_circle_arc(center, radius, angle_from, angle_to, color):
  131. var nb_points = 32
  132. var points_arc = PoolVector2Array()
  133. for i in range(nb_points+1):
  134. var angle_point = deg2rad(angle_from + i * (angle_to-angle_from) / nb_points - 90)
  135. points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
  136. for index_point in range(nb_points):
  137. draw_line(points_arc[index_point], points_arc[index_point + 1], color)
  138. .. code-tab:: csharp
  139. public void DrawCircleArc(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
  140. {
  141. int nbPoints = 32;
  142. var pointsArc = new Vector2[nbPoints];
  143. for (int i = 0; i < nbPoints; ++i)
  144. {
  145. float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90f);
  146. pointsArc[i] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
  147. }
  148. for (int i = 0; i < nbPoints - 1; ++i)
  149. DrawLine(pointsArc[i], pointsArc[i + 1], color);
  150. }
  151. Remember the number of points our shape has to be decomposed into? We fixed this
  152. number in the nb_points variable to a value of 32. Then, we initialize an empty
  153. PoolVector2Array, which is simply an array of Vector2.
  154. The next step consists of computing the actual positions of these 32 points that
  155. compose an arc. This is done in the first for-loop: we iterate over the number of
  156. points for which we want to compute the positions, plus one to include the last point.
  157. We first determine the angle of each point, between the starting and ending angles.
  158. The reason why each angle is reduced by 90° is that we will compute 2D positions
  159. out of each angle using trigonometry (you know, cosine and sine stuff...). However,
  160. to be simple, cos() and sin() use radians, not degrees. The angle of 0° (0 radian)
  161. starts at 3 o'clock although we want to start counting at 12 o'clock. So we reduce
  162. each angle by 90° in order to start counting from 12 o'clock.
  163. The actual position of a point located on a circle at angle 'angle' (in radians)
  164. is given by Vector2(cos(angle), sin(angle)). Since cos() and sin() return values
  165. between -1 and 1, the position is located on a circle of radius 1. To have this
  166. position on our support circle, which has a radius of 'radius', we simply need to
  167. multiply the position by 'radius'. Finally, we need to position our support circle
  168. at the 'center' position, which is performed by adding it to our Vector2 value.
  169. Finally, we insert the point in the PoolVector2Array which was previously defined.
  170. Now, we need to actually draw our points. As you can imagine, we will not simply
  171. draw our 32 points: we need to draw everything that is between each of them.
  172. We could have computed every point ourselves using the previous method, and drew
  173. it one by one. But this is too complicated and inefficient (except if explicitly needed).
  174. So, we simply draw lines between each pair of points. Unless the radius of our
  175. support circle is big, the length of each line between a pair of points will
  176. never be long enough to see them. If this happens, we simply would need to
  177. increase the number of points.
  178. Draw the arc on screen
  179. ^^^^^^^^^^^^^^^^^^^^^^
  180. We now have a function that draws stuff on the screen:
  181. It is time to call in the _draw() function.
  182. .. tabs::
  183. .. code-tab:: gdscript GDScript
  184. func _draw():
  185. var center = Vector2(200, 200)
  186. var radius = 80
  187. var angle_from = 75
  188. var angle_to = 195
  189. var color = Color(1.0, 0.0, 0.0)
  190. draw_circle_arc(center, radius, angle_from, angle_to, color)
  191. .. code-tab:: csharp
  192. public override void _Draw()
  193. {
  194. var center = new Vector2(200, 200);
  195. float radius = 80;
  196. float angleFrom = 75;
  197. float angleTo = 195;
  198. var color = new Color(1, 0, 0);
  199. DrawCircleArc(center, radius, angleFrom, angleTo, color);
  200. }
  201. Result:
  202. .. image:: img/result_drawarc.png
  203. Arc polygon function
  204. ^^^^^^^^^^^^^^^^^^^^
  205. We can take this a step further and not only write a function that draws the plain
  206. portion of the disc defined by the arc, but also its shape. The method is exactly
  207. the same as previously, except that we draw a polygon instead of lines:
  208. .. tabs::
  209. .. code-tab:: gdscript GDScript
  210. func draw_circle_arc_poly(center, radius, angle_from, angle_to, color):
  211. var nb_points = 32
  212. var points_arc = PoolVector2Array()
  213. points_arc.push_back(center)
  214. var colors = PoolColorArray([color])
  215. for i in range(nb_points+1):
  216. var angle_point = deg2rad(angle_from + i * (angle_to - angle_from) / nb_points - 90)
  217. points_arc.push_back(center + Vector2(cos(angle_point), sin(angle_point)) * radius)
  218. draw_polygon(points_arc, colors)
  219. .. code-tab:: csharp
  220. public void DrawCircleArcPoly(Vector2 center, float radius, float angleFrom, float angleTo, Color color)
  221. {
  222. int nbPoints = 32;
  223. var pointsArc = new Vector2[nbPoints + 1];
  224. pointsArc[0] = center;
  225. var colors = new Color[] { color };
  226. for (int i = 0; i < nbPoints; ++i)
  227. {
  228. float anglePoint = Mathf.Deg2Rad(angleFrom + i * (angleTo - angleFrom) / nbPoints - 90);
  229. pointsArc[i + 1] = center + new Vector2(Mathf.Cos(anglePoint), Mathf.Sin(anglePoint)) * radius;
  230. }
  231. DrawPolygon(pointsArc, colors);
  232. }
  233. .. image:: img/result_drawarc_poly.png
  234. Dynamic custom drawing
  235. ^^^^^^^^^^^^^^^^^^^^^^
  236. Alright, we are now able to draw custom stuff on screen. However, it is static:
  237. Let's make this shape turn around the center. The solution to do this is simply
  238. to change the angle_from and angle_to values over time. For our example,
  239. we will simply increment them by 50. This increment value has to remain
  240. constant or else the rotation speed will change accordingly.
  241. First, we have to make both angle_from and angle_to variables global at the top
  242. of our script. Also note that you can store them in other nodes and access them
  243. using get_node().
  244. .. tabs::
  245. .. code-tab:: gdscript GDScript
  246. extends Node2D
  247. var rotation_angle = 50
  248. var angle_from = 75
  249. var angle_to = 195
  250. .. code-tab:: csharp
  251. public class CustomNode2D : Node2D
  252. {
  253. private float _rotationAngle = 50;
  254. private float _angleFrom = 75;
  255. private float _angleTo = 195;
  256. }
  257. We make these values change in the _process(delta) function.
  258. We also increment our angle_from and angle_to values here. However, we must not
  259. forget to wrap() the resulting values between 0 and 360°! That is, if the angle
  260. is 361°, then it is actually 1°. If you don't wrap these values, the script will
  261. work correctly, but the angle values will grow bigger and bigger over time until
  262. they reach the maximum integer value Godot can manage (2^31 - 1).
  263. When this happens, Godot may crash or produce unexpected behavior.
  264. Since Godot doesn't provide a wrap() function, we'll create it here, as
  265. it is relatively simple.
  266. Finally, we must not forget to call the update() function, which automatically
  267. calls _draw(). This way, you can control when you want to refresh the frame.
  268. .. tabs::
  269. .. code-tab:: gdscript GDScript
  270. func wrap(value, min_val, max_val):
  271. var f1 = value - min_val
  272. var f2 = max_val - min_val
  273. return fmod(f1, f2) + min_val
  274. func _process(delta):
  275. angle_from += rotation_ang
  276. angle_to += rotation_ang
  277. # We only wrap angles if both of them are bigger than 360
  278. if angle_from > 360 and angle_to > 360:
  279. angle_from = wrap(angle_from, 0, 360)
  280. angle_to = wrap(angle_to, 0, 360)
  281. update()
  282. .. code-tab:: csharp
  283. private float Wrap(float value, float minVal, float maxVal)
  284. {
  285. float f1 = value - minVal;
  286. float f2 = maxVal - minVal;
  287. return (f1 % f2) + minVal;
  288. }
  289. public override void _Process(float delta)
  290. {
  291. _angleFrom += _rotationAngle;
  292. _angleTo += _rotationAngle;
  293. // We only wrap angles if both of them are bigger than 360
  294. if (_angleFrom > 360 && _angleTo > 360)
  295. {
  296. _angleFrom = Wrap(_angleFrom, 0, 360);
  297. _angleTo = Wrap(_angleTo, 0, 360);
  298. }
  299. Update();
  300. }
  301. Also, don't forget to modify the _draw() function to make use of these variables:
  302. .. tabs::
  303. .. code-tab:: gdscript GDScript
  304. func _draw():
  305. var center = Vector2(200, 200)
  306. var radius = 80
  307. var color = Color(1.0, 0.0, 0.0)
  308. draw_circle_arc( center, radius, angle_from, angle_to, color )
  309. .. code-tab:: csharp
  310. public override void _Draw()
  311. {
  312. var center = new Vector2(200, 200);
  313. float radius = 80;
  314. var color = new Color(1, 0, 0);
  315. DrawCircleArc(center, radius, _angleFrom, _angleTo, color);
  316. }
  317. Let's run!
  318. It works, but the arc is rotating insanely fast! What's wrong?
  319. The reason is that your GPU is actually displaying the frames as fast as it can.
  320. We need to "normalize" the drawing by this speed. To achieve, we have to make
  321. use of the 'delta' parameter of the _process() function. 'delta' contains the
  322. time elapsed between the two last rendered frames. It is generally small
  323. (about 0.0003 seconds, but this depends on your hardware). So, using 'delta' to
  324. control your drawing ensures that your program runs at the same speed on
  325. everybody's hardware.
  326. In our case, we simply need to multiply our 'rotation_ang' variable by 'delta'
  327. in the _process() function. This way, our 2 angles will be increased by a much
  328. smaller value, which directly depends on the rendering speed.
  329. .. tabs::
  330. .. code-tab:: gdscript GDScript
  331. func _process(delta):
  332. angle_from += rotation_ang * delta
  333. angle_to += rotation_ang * delta
  334. # we only wrap angles if both of them are bigger than 360
  335. if angle_from > 360 and angle_to > 360:
  336. angle_from = wrap(angle_from, 0, 360)
  337. angle_to = wrap(angle_to, 0, 360)
  338. update()
  339. .. code-tab:: csharp
  340. public override void _Process(float delta)
  341. {
  342. _angleFrom += _rotationAngle * delta;
  343. _angleTo += _rotationAngle * delta;
  344. // We only wrap angles if both of them are bigger than 360
  345. if (_angleFrom > 360 && _angleTo > 360)
  346. {
  347. _angleFrom = Wrap(_angleFrom, 0, 360);
  348. _angleTo = Wrap(_angleTo, 0, 360);
  349. }
  350. Update();
  351. }
  352. Let's run again! This time, the rotation displays fine!
  353. Tools
  354. -----
  355. Drawing your own nodes might also be desired while running them in the
  356. editor to use as a preview or visualization of some feature or
  357. behavior.
  358. Remember to use the "tool" keyword at the top of the script
  359. (check the :ref:`doc_gdscript` reference if you forgot what this does).