Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
28 changes: 14 additions & 14 deletions arcade/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class Clock:
only certain elements rather than everything.

Args:
initial_elapsed (float, optional): The amount of time the clock should assume has
initial_elapsed: The amount of time the clock should assume has
already occurred. Defaults to 0.0
initial_tick (int, optional): The number of ticks the clock should assume has already
initial_tick: The number of ticks the clock should assume has already
occurred. Defaults to 0.
tick_speed (float, optional): A multiplier on how the 'speed' of time.
tick_speed: A multiplier on how the 'speed' of time.
i.e. a value of 0.5 means time elapsed half as fast for this clock. Defaults to 1.0.
"""

Expand All @@ -40,7 +40,7 @@ def tick(self, delta_time: float):
Update the clock with the time that has passed since the last tick.

Args:
delta_time (float): The amount of time that has passed since the last tick.
delta_time: The amount of time that has passed since the last tick.
"""
self._tick_delta_time = delta_time * self._tick_speed
self._elapsed_time += self._tick_delta_time
Expand All @@ -51,7 +51,7 @@ def set_tick_speed(self, new_tick_speed: float):
Set the speed of time for this clock.

Args:
new_tick_speed (float): A multiplier on the 'speed' of time.
new_tick_speed: A multiplier on the 'speed' of time.
i.e. a value of 0.5 means time elapsed half as fast for this clock.
"""
self._tick_speed = new_tick_speed
Expand All @@ -61,7 +61,7 @@ def time_since(self, time: float) -> float:
Calculate the amount of time that has passed since the given time.

Args:
time (float): The time to compare against.
time: The time to compare against.
"""
return self._elapsed_time - time

Expand All @@ -70,7 +70,7 @@ def ticks_since(self, tick: int) -> int:
Calculate the number of ticks that have occurred since the given tick.

Args:
tick (int): The tick to compare against.
tick: The tick to compare against.
"""
return self._tick - tick

Expand Down Expand Up @@ -123,7 +123,7 @@ class FixedClock(Clock):
Arcade provides a global fixed clock which is automatically ticked every update

Args:
sibling (Clock): The unfixed clock which this clock will sync with.
sibling: The unfixed clock which this clock will sync with.
fixed_tick_rate (float, optional): The fixed number of seconds that pass
for this clock every tick. Defaults to ``1.0 / 60.0``.
"""
Expand All @@ -138,7 +138,7 @@ def set_tick_speed(self, new_tick_speed: float):
Set the speed of time for this clock.

Args:
new_tick_speed (float): A multiplier on the 'speed' of time.
new_tick_speed: A multiplier on the 'speed' of time.
i.e. a value of 0.5 means time elapsed half as fast for this clock
"""
raise ValueError(
Expand All @@ -150,7 +150,7 @@ def tick(self, delta_time: float):
Update the clock with the time that has passed since the last tick.

Args:
delta_time (float): The amount of time that has passed since the last tick.
delta_time: The amount of time that has passed since the last tick.
"""
if delta_time != self._fixed_rate:
raise ValueError(
Expand Down Expand Up @@ -184,11 +184,11 @@ def _setup_clock(initial_elapsed: float = 0.0, initial_tick: int = 0, tick_speed
Private method used by the arcade window to setup the global clock post initialization.

Args:
initial_elapsed (float, optional): The amount of time the clock should assume
initial_elapsed: The amount of time the clock should assume
has already occurred. Defaults to 0.0
initial_tick (int, optional): The number of ticks the clock should assume has
initial_tick: The number of ticks the clock should assume has
already occurred. Defaults to 0.
tick_speed (float, optional): A multiplier on the 'speed' of time.
tick_speed: A multiplier on the 'speed' of time.
i.e. a value of 0.5 means time elapsed half as fast for this clock.
Defaults to 1.0.
"""
Expand All @@ -203,7 +203,7 @@ def _setup_fixed_clock(fixed_tick_rate: float = 1.0 / 60.0):
post initialization.

Args:
fixed_tick_rate (float, optional): The fixed number of seconds that pass
fixed_tick_rate: The fixed number of seconds that pass
for this clock every tick. Defaults to 1.0 / 60.0
"""
GLOBAL_FIXED_CLOCK._elapsed_time = GLOBAL_CLOCK.time # noqa: SLF001
Expand Down
2 changes: 1 addition & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ Arcade is developed by volunteers.
programming_guide/sound
programming_guide/textures
programming_guide/event_loop
programming_guide/camera
programming_guide/sections
programming_guide/gui/index
programming_guide/texture_atlas
programming_guide/resource_handlers
programming_guide/opengl_notes
programming_guide/performance_tips
programming_guide/headless
programming_guide/vsync

.. toctree::
:hidden:
Expand Down
2 changes: 2 additions & 0 deletions doc/programming_guide/camera.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Camera
======
164 changes: 142 additions & 22 deletions doc/programming_guide/event_loop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,154 @@ Introduction
------------

Python Arcade provides a simple three event loop to build off.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
All three methods are exposed to be overridden in :py:class:`arcade.Window`
and :py:class:`arcade.View`. You may also register your own handlers
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
to these events using :py:func:`arcade.Window.push_handlers`, but this is
not recommended for beginners.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated

:py:func:`on_draw`
is provided to render to the window. After the ``on_draw`` event the
window will present itself to the screen. By default this attempts
to occur every 1/60 seconds or around 16.7 milliseconds. This can be
changed when initialising your :py:class:`arcade.Window` with the
``draw_rate`` argument. Setting the draw rate to a value above a
screens refresh rate can cause tearing unless the ``vsync`` argument
is set to true. It is recommended to keep your ``draw_rate`` around
the refresh rate of the screen as it does not prevent the other events
from occuring.
^^^^^^^^^^^^^^^^^^
is provided to render to the window. After the ``on_draw`` event, the window
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
will draw to the screen. By default, this attempts to occur every 1/60 seconds
or once every 16.7 milliseconds. It can be changed when initializing your
:py:class:`arcade.Window` with the ``draw_rate`` argument. Setting the draw rate
to a value above a screen's refresh rate can cause tearing unless you set the
``vsync`` argument to true. We recommend keeping your ``draw_rate`` around the
screen's refresh rate. After every draw event camera state will be reset.
This means that non-default cameras must be reused on every draw event.

:py:func:`on_update`
is provided to update state which needs to happen at a roughly regular interval.
The update event is not strictly paired to the draw event, but they share the same
thread. This can cause a bottle-neck if one is significantly slower than the other.
The event also provides a ``delta_time`` argument which is the time elapsed since the
last ``on_update`` event.
^^^^^^^^^^^^^^^^^^^^
is provided to update state which needs to happen at a roughly regular interval.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
The update event is not strictly paired to the draw event, but they share the same
thread. This can cause a bottle-neck if one is significantly slower than the other.
The event also provides a ``delta_time`` argument which is the time elapsed since the
last ``on_update`` event. You can change the rate at which ``on_update`` is called with
the ``update_rate`` argument when initialising your :py:class:`arcade.Window`.

:py:func:`on_fixed_update`
is provided to update state which must happen with an exactly regular interval.
^^^^^^^^^^^^^^^^^^^^^^^^^^
is provided to update state which must happen with an exactly regular interval.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
Because Arcade can't ensure the event is actually fired regularly it stores how
much time has passed since the last update, and once enough time has passed it
releases an ``on_fixed_update`` call. The fixed update always provides the same
``delta_time`` argument. You can change the rate at which ``on__fixed_update`` is
called with the ``fixed_rate`` argument when initialising your :py:class:`arcade.Window`.

**TODO**: add note about camera state resetting once that's in
Time
----
While pyglet does provide a clock for scheduling events it is closely tied
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
to the window's own events. For simple time keeping arcade provides global
clock objects. Both clocks can be imported from ``arcade.clock`` as
``GLOBAL_CLOCK`` and ``GLOBAL_FIXED_CLOCK``

All three methods are exposed to be overridden in :py:class:`arcade.Window`
and :py:class:`arcade.View`. You may also register your own handlers
to these events using :py:func:`arcade.Window.push_handlers`, but this is
not recommended for beginners.
:py:class:`arcade.Clock`
^^^^^^^^^^^^^^^^^^^^^^^^
The base arcade clock tracks the elapsed time in seconds, the total number
of clock ticks, and the amount of time that elapsed since the last tick.
The currently active window automatically ticks the ``GLOBAL_CLOCK`` every ``on_update``.
This means there is no reason to manually tick it. If you need more
clocks, possibly ticking at a different rate, an :py:class:`arcade.Clock`
can be created on the fly.

Time
====
:py:class:`arcade.FixedClock`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The fixed clock tracks the same values as the normal clock, but has two special features.
Firstly it inforces that the ``delta_time`` passed into its ``tick`` method is always the same.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
This is because advanced physics engines require consistent time. Secondly the fixed clock
requires a sibling regular clock. It uses this clock to track how offset from the true time it is.
Like the regular clock you may make a new :py:class:`arcade.FixedClock` at any time,
but ensure they have a sibling.

Up Coming
^^^^^^^^^
In future versions of arcade there will be a :py:class:`arcade.SubClock`. The sub clock will
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
provide many useful features currently missing from regular and fixed clocks. Sub clocks are
ticked by their parent clock. This means you won't need to manually tick clocks created at runtime.
Sub clocks will also allow for time manipulation by changing how fast they see time flow. This
allows you to easily create slow-down effects without any advanced maths or changing the number of
updates per frame. To gain access to a draft :py:class:`arcade.SubClock` you can find it in
``arcade.future.sub_clock``. This version of the sub clock is not final. If you find any bugs do
not hesitate to raise an issue on the github.

More on Fixed update
--------------------
the ``on_fixed_update`` event can be an extremelly powerful tool, but it has many complications
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
that should be accounted for. If used imporperly the event can grind a game to a halt.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated

Death Spiral
^^^^^^^^^^^^
A fixed update represnts a very specific amount of time. If all of the computations take
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
longer than the fixed update represents than the ammount of time accumulated between update
events will grow. If this happens for multiple frames the game will begin to spiral. The
first few frames of the spiral will lead to one update cycle requiring two fixed update
calls. This will increase the extra time accumulated until three fixed updates must occur at once.
This will continue to happen until either: the fixed updates start taking less time, or the game
crashes.

There are a few solutions to this issue. The simplist method, which works best when there may be spikes in
computation time that quickly settle, is to clamp the max number of fixed updates that can occur in a single
frame. In arcade this is done by setting the ``fixed_frame_cap`` argument when initialising your
:py:class:`arcade.Window`. The second method is to slow-down time temporarily. By changing the
``_tick_speed`` of arcade's ``GLOBAL_CLOCK`` is is possible to slow down the accumulation of time.
For example setting ``GLOBAL_CLOCK._tick_speed = 0.5`` would allow the fixed update twice as many frames
to calculate for.

Update Interpolation
^^^^^^^^^^^^^^^^^^^^
Because fixed updates work on the accumulation of time this may not sync with the perfectly with
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
the ``on_draw`` or ``on_update`` events. In extreme cases this can cause a visible stuttering to
objects moved within ``on_fixed_update``. This is where the ``accumulated`` and ``fraction`` properties
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated
on ``GLOBAL_FIXED_CLOCK`` come into play. By storing the last frame's position information it is possible
to use ``fraction`` to interpolate towards the next calculated positions. For a visual representation of
this effect look at the ``arcade.examples.fixed_update_interpolation``. Paired with slowing down time when
computations get expensive allows for a much smoother game experience at the cost of code complexity.
Comment thread
DragonMoffon marked this conversation as resolved.
Outdated

Vertical Synchronization
------------------------

What is vertical sync?
^^^^^^^^^^^^^^^^^^^^^^

Vertical synchronization (vsync) is a window option in which the
video card is prevented from doing anything visible to the display
memory until after the monitor finishes its current refresh cycle.

To enable vsync in Arcade::

# On window creation
arcade.Window(800, 600, "Window Title", vsync=True)

# While the application is running
window.set_vsync(True)

This have advantages and disadvantages depending on the situation.

Most windows are what we call "double buffered". This means
the window actually has two surfaces. A visible surface and a
hidden surface. All drawing commands will end up in the
hidden surface. When we're done drawing our frame the hidden
and visible surfaces swap places and the new frame is revealed
to the user.

If this "dance" of swapping surfaces is not timed correctly
with your monitor you might experience small hiccups in movement
or `screen tearing <https://en.wikipedia.org/wiki/Screen_tearing>`_.

Vertical sync disabled as a default
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The Arcade window is by default created with vertical sync
disabled. This is a much safer default for
a number of reasons.

* In some environments vertical sync is capped to 30 fps.
This can make the game run at half the speed if ``delta_time``
is not accounted for. We don't expect beginners take
``delta_time`` into consideration in their projects.
* If threads are used all threads will stall while the
application is waiting for vertical sync

We cannot guarantee that vertical sync is disabled if
this is enforced on driver level. The vast amount of
driver defaults lets the application control this.