Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
### Misc Changes
- Sped up `arcade.draw_text()`. It no longer flushes OpenGL after every call (an unchanged call went from 76 to 15 µs), and several lines drawn in the same style each keep their own cached label instead of re-laying out one label every call (5 static lines went from 1.24 ms to 0.08 ms per frame).
- Sped up `arcade.Text` updates. Setting a property to the value it already has no longer lays out the text again (for example `font_size`, `bold`, or `width` went from about 165 µs to under 1 µs), and a `with text:` block only lays out the text if something in it needs a new layout. An unchanged `arcade.TextPool` frame with 5 lines went from 760 µs to 6 µs.
- Added a "Drawing Text" page to the programming guide: choosing between `draw_text`, `Text`, `TextPool`, and `create_text_sprite`, what's slow and fast to change, batches, fonts (names, font files, and the bundled Kenney and Liberation fonts), and styles. Updated the text section of the performance tips, and the `draw_text` warning and docstring, which overstated how slow it is.

## 4.0.0.dev8

Expand Down
13 changes: 7 additions & 6 deletions arcade/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,8 +1211,8 @@ def _get_draw_text_label(label_cache, style: tuple, text: str) -> "Text | None":

@warning(
message=(
"draw_text is an extremely slow function for displaying text. "
"Consider using Text objects instead."
"draw_text is much slower than drawing arcade.Text objects, especially "
"for many lines or changing text. Consider using Text objects instead."
),
warning_type=PerformanceWarning,
)
Expand All @@ -1238,10 +1238,11 @@ def draw_text(

.. warning:: Use :py:class:`arcade.Text` objects instead.

This method of drawing text is very slow
and might be removed in the near future.
Text objects can be 10-100 times faster
depending on the use case.
This method of drawing text is slower than ``Text`` objects
and might be removed in the near future. Each call has some
overhead, text that changes is laid out again on every call,
and many ``Text`` objects can be drawn at once in a batch.
See :ref:`text_guide`.

.. warning:: Cameras affect text drawing!

Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ help improve Arcade.
programming_guide/input/index
programming_guide/sound
programming_guide/textures
programming_guide/text
programming_guide/event_loop
programming_guide/camera
programming_guide/sections
Expand Down
18 changes: 15 additions & 3 deletions doc/programming_guide/performance_tips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,27 @@ They also lack many of the features you may be used to in Arcade.
Text drawing performance
------------------------

The slowest thing aside from disk access is :meth:`arcade.draw_text`.
Laying out text is slow, so the fastest text is text that isn't laid out
again every frame.

To improve performance:

#. Use :py:class:`arcade.Text` instead
#. (Optional) Pass a pyglet :py:class:`~pyglet.graphics.Batch` object at creation
#. Use :py:class:`arcade.Text` objects instead of :py:func:`arcade.draw_text`.
Create them once, and only change their text when it changes.
#. Change position, color, and rotation freely. They don't lay out the
text again, but changing the text, font, size, or style does.
#. To change several layout properties at once, do it in a
``with text:`` block, so the text is laid out once.
#. Add many ``Text`` objects to a pyglet :py:class:`~pyglet.graphics.Batch`
and draw them with one call.
#. If you prefer ``draw_text`` style code, use :py:class:`arcade.TextPool`.
#. For text that doesn't change but moves, rotates, or scales, use
:py:func:`arcade.create_text_sprite` and draw it in a
:py:class:`~arcade.SpriteList`.

See the following to learn more:

* :ref:`text_guide`
* :ref:`drawing_text_objects`
* :ref:`drawing_text_objects_batch`

Expand Down
284 changes: 284 additions & 0 deletions doc/programming_guide/text.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
.. _text_guide:

Drawing Text
============

Arcade has four ways to draw text:

.. list-table::
:header-rows: 1
:widths: 25 40 35

* - Way
- Best for
- Cost
* - :py:func:`arcade.draw_text`
- Quick prototypes, and a few lines
- Slowest per line drawn
* - :py:class:`arcade.Text`
- Most games: scores, menus, labels
- Fast to draw, slow to create or change
* - :py:class:`arcade.TextPool`
- Many lines that change, without managing ``Text`` objects
- Like ``Text``
* - :py:func:`arcade.create_text_sprite`
- Text that moves, rotates, or scales like a sprite
- Slow to create, very fast to draw

All of them use the same arguments for the font, size, color and layout.

.. note::

Text is drawn with the current camera, just like sprites. If you
scroll the game world with a :py:class:`~arcade.Camera2D`, use a second
camera for text that should stay in place, such as a score. See
:ref:`sprite_move_scrolling`.

The numbers on this page were measured on one desktop computer.
Your numbers will differ, but the comparisons between them should hold.

draw_text
---------

:py:func:`arcade.draw_text` draws text in one call, which is handy while
you're getting something working:

.. code-block:: python

def on_draw(self):
self.clear()
arcade.draw_text("Hello, World!", 100, 300, arcade.color.WHITE, 24)

Laying out text is slow, so ``draw_text`` keeps the text it laid out and
reuses it when you draw the same text again. Even so, each call has
overhead, and text that changes every frame, such as a timer, has to be
laid out again every frame. Drawing many lines with ``draw_text`` gets
slow quickly. For anything more than a few lines, use
:py:class:`arcade.Text`. ``draw_text`` reminds you of this with a
:py:class:`~arcade.exceptions.PerformanceWarning`.

Text objects
------------

An :py:class:`arcade.Text` object lays out its text once, when it's
created, and then draws it as often as you like. Create it once, for
example in ``__init__`` or a ``setup`` method, and draw it in ``on_draw``:

.. code-block:: python

class MyGame(arcade.Window):
def __init__(self):
super().__init__()
self.score = 0
self.score_text = arcade.Text("Score: 0", 10, 10, arcade.color.WHITE, 16)

def on_update(self, delta_time):
self.score_text.text = f"Score: {self.score}"

def on_draw(self):
self.clear()
self.score_text.draw()

What costs what:

* **Creating** a ``Text`` takes around half a millisecond. Don't create
them every frame.
* **Drawing** one takes a few tens of microseconds.
* **Moving**, **recoloring**, **rotating**, or hiding it is almost free.
Use :py:attr:`~arcade.Text.position` to change ``x`` and ``y`` together.
* **Changing the text**, font, size, bold, italic, width, or alignment
lays the text out again, which takes about as long as drawing it
several times. Setting a property to the value it already has costs nothing,
so code like the example above only pays when the score changes.

To change several of these at once, use a ``with`` block. The text is
laid out once, at the end of the block, instead of once for each change:

.. code-block:: python

with self.title:
self.title.text = "Game Over"
self.title.font_size = 48
self.title.color = arcade.color.RED

See :ref:`drawing_text_objects` for an example.

Batches
~~~~~~~

Drawing many ``Text`` objects one at a time adds up. If you add them to a
pyglet :py:class:`~pyglet.graphics.Batch`, one call draws all of them:

.. code-block:: python

import pyglet

self.batch = pyglet.graphics.Batch()
self.lines = [
arcade.Text(f"Line {i}", 10, 20 * i, batch=self.batch)
for i in range(20)
]

def on_draw(self):
self.clear()
self.batch.draw()

In one measurement, drawing 20 ``Text`` objects one at a time took about
570 µs, and drawing them in one batch took about 30 µs.

You can still change texts in a batch. Set ``text.batch = None`` to take
one out. See :ref:`drawing_text_objects_batch` for an example.

TextPool
--------

If you'd rather write ``draw_text`` style code but want the speed of
``Text`` objects, use a :py:class:`arcade.TextPool`. You give each line
a name, and the pool keeps one ``Text`` object per name, updating it
only when something changes:

.. code-block:: python

self.pool = arcade.TextPool()

def on_draw(self):
self.clear()
self.pool.draw("score", f"Score: {self.score}", 10, 580, font_size=16)
self.pool.draw("lives", f"Lives: {self.lives}", 10, 560, font_size=16)

Text sprites
------------

:py:func:`arcade.create_text_sprite` draws text into a texture once and
returns an :py:class:`arcade.Sprite` that shows it. Creating one takes a
few milliseconds, so it's for text that doesn't change, but after that it
draws as fast as any other sprite, and can be added to a
:py:class:`~arcade.SpriteList`, moved, rotated, and scaled:

.. code-block:: python

self.sprites = arcade.SpriteList()
label = arcade.create_text_sprite("Bonus!", color=arcade.color.YELLOW, font_size=20)
label.position = 400, 300
self.sprites.append(label)

To change the text, create a new sprite. Scaling a text sprite up makes
it blurry, so create it at the size it will be drawn.

Fonts
-----

``font_name`` can be:

* **The name of a font** installed on the computer, such as ``"arial"``.
* **A tuple or list of names.** The first one that's found is used,
which helps when different computers have different fonts. The default
is ``("calibri", "arial")``.
* **The path to a font file**, such as ``"fonts/MyFont.ttf"``, or a
:ref:`resource handle <resource_handles>`, such as
``":resources:fonts/ttf/MyFont.ttf"``. The file is loaded the first
time it's used.

If no font is found, the platform's default font is used instead, without
an error. If text shows up in the wrong font, check the name.

Fonts that come with your game
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To use a font file by its name, load it first with
:py:func:`arcade.load_font`, and then use the font's name, not the
file name:

.. code-block:: python

arcade.load_font("fonts/MyFont.ttf")
text = arcade.Text("Hello", 10, 10, font_name="My Font")

The name is set inside the font file. Your operating system's font
viewer shows it.

Fonts that come with Arcade
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Arcade includes the Kenney and Liberation fonts. They aren't loaded
automatically, so load them before using them:

.. code-block:: python

arcade.resources.load_kenney_fonts()
arcade.resources.load_liberation_fonts()

text = arcade.Text("Hello", 10, 10, font_name="Kenney Future")

The Kenney fonts are ``"Kenney Blocks"``, ``"Kenney Future"``,
``"Kenney Future Narrow"``, ``"Kenney High"``, ``"Kenney High Square"``,
``"Kenney Mini"``, ``"Kenney Mini Square"``, ``"Kenney Pixel"``,
``"Kenney Pixel Square"``, ``"Kenney Rocket"`` and
``"Kenney Rocket Square"``. The Liberation fonts are
``"Liberation Sans"``, ``"Liberation Serif"`` and ``"Liberation Mono"``,
which can stand in for Arial, Times New Roman and Courier New.

.. note::

Windows treats ``"Kenney Future Narrow"`` as a narrower width of
``"Kenney Future"``, so it can't be found by its own name there. On
Windows, use ``font_name="Kenney Future"`` with
``stretch="semicondensed"`` instead.

See :ref:`drawing_text` for an example with these fonts.

Style
-----

``bold``
``True`` or ``False``, or a font weight such as ``"thin"``,
``"light"``, ``"medium"``, ``"semibold"``, ``"extrabold"`` or
``"black"``. Not every font has every weight. If a font doesn't have
the one you ask for, a nearby weight is used instead.

``italic``
``True`` or ``False``, or ``"oblique"``.

``color``
Any Arcade color, such as ``arcade.color.WHITE`` or
``(255, 128, 0, 200)``. The alpha value makes the text partly
transparent. For a gradient from the left edge of the text to the
right, use a :py:class:`pyglet.text.LinearGradient`, which takes two
RGBA colors:

.. code-block:: python

from pyglet.text import LinearGradient

title = arcade.Text(
"Arcade", 100, 300, font_size=48,
color=LinearGradient((255, 0, 0, 255), (0, 0, 255, 255)),
)

Layout
------

By default, ``x`` and ``y`` are the left end of the text's baseline, the
line most letters sit on. ``anchor_x`` (``"left"``, ``"center"``,
``"right"``) and ``anchor_y`` (``"baseline"``, ``"bottom"``,
``"center"``, ``"top"``) change which point of the text ``x`` and ``y``
refer to. For example, to center a title on the screen:

.. code-block:: python

title = arcade.Text(
"My Game", window.width / 2, window.height / 2, font_size=48,
anchor_x="center", anchor_y="center",
)

For text that wraps across several lines, set ``multiline=True`` and a
``width`` in pixels. ``align`` (``"left"``, ``"center"``, ``"right"``)
then aligns each line within that width.

To find out how big the text is, use
:py:attr:`~arcade.Text.content_width` and
:py:attr:`~arcade.Text.content_height`, or
:py:attr:`~arcade.Text.left`, :py:attr:`~arcade.Text.right`,
:py:attr:`~arcade.Text.top` and :py:attr:`~arcade.Text.bottom`.

For more options, see the :py:class:`arcade.Text` API documentation.
Loading