diff --git a/arcade/application.py b/arcade/application.py index 29865b2ed..fef04a98b 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -1186,11 +1186,13 @@ def show_view(self, new_view: View) -> None: """ Set the currently active view. - This will hide the current view - and show the new view in the next frame. + Before returning, this calls :py:meth:`~arcade.View.on_hide_view` + on the view that was showing, if any, then + :py:meth:`~arcade.View.on_show_view` on the new view. From then on, + the new view gets the window's events, and the next frame is + drawn by it. - This is not a blocking call. It will simply point to the new view - and return immediately. + See :ref:`windows_and_views` to learn more. Calling this function is the same as setting the :py:attr:`arcade.Window.current_view` attribute. @@ -1536,14 +1538,23 @@ def on_draw(self) -> bool | None: pass def on_show_view(self) -> None: - """Called once when the view is shown. + """Called each time the view is shown by :py:meth:`arcade.Window.show_view`. + + A view can be shown more than once, so put one-time setup in + ``__init__``, and things that should happen each time the view + appears here, such as starting music. .. seealso:: :py:meth:`~arcade.View.on_hide_view` """ pass def on_hide_view(self) -> None: - """Called once when this view is hidden.""" + """Called each time the view stops being shown. + + This happens when the window shows another view, or + :py:meth:`arcade.Window.hide_view` is called. Use it to undo what + :py:meth:`on_show_view` did, such as stopping music. + """ pass def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> bool | None: diff --git a/doc/index.rst b/doc/index.rst index 74765346d..08eb803b6 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -105,6 +105,7 @@ help improve Arcade. programming_guide/sound programming_guide/textures programming_guide/text + programming_guide/windows_and_views programming_guide/event_loop programming_guide/camera programming_guide/sections diff --git a/doc/programming_guide/windows_and_views.rst b/doc/programming_guide/windows_and_views.rst new file mode 100644 index 000000000..e9b97b464 --- /dev/null +++ b/doc/programming_guide/windows_and_views.rst @@ -0,0 +1,208 @@ +.. _windows_and_views: + +Windows and Views +================= + +Every Arcade game has one :py:class:`arcade.Window`. It's the window on +your screen, and it receives events such as key presses, mouse movement, +and the regular calls to update and draw. + +A :py:class:`arcade.View` is one screen of your game, such as a menu, +the game itself, a pause screen or a game over screen. The window shows +one view at a time and passes its events to that view. + +The Window +---------- + +Create one window when your program starts: + +.. code-block:: python + + window = arcade.Window(1280, 720, "My Game") + +Some useful arguments and methods: + +* ``fullscreen=True`` opens the window full screen. + :py:meth:`~arcade.Window.set_fullscreen` switches while the game runs. + See the :ref:`full_screen_example` example. +* ``resizable=True`` lets the player resize the window. + See the :ref:`resizable_window` example. +* ``update_rate`` and ``draw_rate`` set how often ``on_update`` and + ``on_draw`` are called, 60 times a second by default. + :py:meth:`~arcade.Window.set_update_rate` and + :py:meth:`~arcade.Window.set_draw_rate` change them later. +* :py:attr:`~arcade.Window.background_color` is the color + :py:meth:`~arcade.Window.clear` fills the window with. + +For how often each event is called and how time is measured, see +:doc:`event_loop`. + +You can put a whole game in a :py:class:`~arcade.Window` subclass by +overriding its ``on_draw``, ``on_update`` and input methods. That's fine +for a small program with one screen. + +Why Use Views? +-------------- + +With several screens in one window, ``on_draw`` and every input method +need to check which screen is showing: + +.. code-block:: python + + def on_draw(self): + self.clear() + if self.state == "menu": + ... + elif self.state == "playing": + ... + elif self.state == "game_over": + ... + +A view keeps each screen in its own class instead. A view has the same +event methods as a window, such as ``on_draw``, ``on_update``, +``on_key_press`` and ``on_mouse_press``, and only the view being shown +gets them: + +.. code-block:: python + + class MenuView(arcade.View): + def on_draw(self): + self.clear() + arcade.draw_text("Press any key to start", 100, 100) + + def on_key_press(self, symbol, modifiers): + self.window.show_view(GameView()) + + + class GameView(arcade.View): + def on_draw(self): + self.clear() + ... # Draw the game + + + window = arcade.Window(1280, 720, "My Game") + window.show_view(MenuView()) + arcade.run() + +Inside a view, ``self.window`` is the window showing it. + +Showing Views +------------- + +:py:meth:`window.show_view(view) ` switches to +a view. Before it returns, it: + +#. Calls :py:meth:`~arcade.View.on_hide_view` on the view that was + showing, if there was one, and stops sending it events. +#. Calls :py:meth:`~arcade.View.on_show_view` on the new view, and starts + sending it events. + +The next frame is drawn by the new view. +:py:attr:`window.current_view ` is the view +being shown, and :py:meth:`~arcade.Window.hide_view` hides it without +showing another. + +A view can be shown, hidden, and shown again. ``__init__`` runs once, +when you create the view, but ``on_show_view`` runs **every time** it's +shown. So: + +* Put things that should happen once in ``__init__``, such as loading + sprites and sounds. +* Put things that should happen each time the player arrives in + ``on_show_view``, such as starting music, or resetting a menu's + selection. +* Undo them in ``on_hide_view``, such as stopping music. + +For example, a pause screen can keep the game view and show it again, so +the game continues where it left off: + +.. code-block:: python + + class PauseView(arcade.View): + def __init__(self, game_view): + super().__init__() + self.game_view = game_view + + def on_key_press(self, symbol, modifiers): + if symbol == arcade.key.ESCAPE: + # The same GameView object, so nothing is reset + self.window.show_view(self.game_view) + +To start a level over instead, show a new view: ``GameView()``. + +How Events Reach a View +----------------------- + +When the window gets an event, it calls the view's method first, then the +window's own method with the same name. + +If you subclass :py:class:`~arcade.Window` and also use views, the +window's methods still run while a view is showing. For example, a +window ``on_draw`` that draws a score draws it on top of every view. +To stop the window's method from running for one event, return ``True`` +from the view's method: + +.. code-block:: python + + class GameView(arcade.View): + def on_key_press(self, symbol, modifiers): + if symbol == arcade.key.SPACE: + self.player.jump() + return True # The window doesn't get this key press + +Most games that use views use :py:class:`~arcade.Window` itself, without +subclassing it, so this doesn't come up. + +Resizing +-------- + +Only the view being shown gets ``on_resize``. If the window is resized +while a view is hidden, the view isn't told. Cameras that match the +window size should be updated when the view is shown again: + +.. code-block:: python + + def on_show_view(self): + self.camera.match_window() + + def on_resize(self, width, height): + self.camera.match_window() + +See :doc:`camera` for more on cameras. + +Background Color +---------------- + +Each view can have its own background color. +:py:meth:`View.clear() ` uses the view's +:py:attr:`~arcade.View.background_color` if it has one, and the window's +otherwise: + +.. code-block:: python + + class MenuView(arcade.View): + def __init__(self): + super().__init__(background_color=arcade.color.DARK_BLUE) + + def on_draw(self): + self.clear() # Fills with dark blue + +GUI Views +--------- + +:py:class:`arcade.gui.UIView` is a view with a +:py:class:`~arcade.gui.UIManager` in ``self.ui``. It turns the GUI on in +``on_show_view`` and off in ``on_hide_view``, so buttons only respond +while the view is showing. If you override either method, call +``super().on_show_view()`` or ``super().on_hide_view()`` so this still +happens. See :ref:`gui` to learn more. + +Learn More +---------- + +* :ref:`view-tutorial`: a tutorial adding instruction and game over + screens to a game +* :ref:`view_screens_minimal`, :ref:`view_pause_screen` and + :ref:`view_instructions_and_game_over`: examples +* :ref:`sections`: splitting one view into areas that each get their own + events