One of my favorite features within Twisted — but also one of the least known —
is
LoopingCall.withCount
,
which can be used in applications where you have some real-time thing
happening, which needs to keep happening at a smooth rate regardless of any
concurrent activity or pauses in the main loop. Originally designed for
playing audio samples from a softphone without introducing a desync delay over
time, it can also be used to play animations while keeping track of their
appropriate frame.
LoopingCall
is all around a fun tool to build little game features with.
I’ve built a quick little demo to showcase some discoveries I’ve made over a
few years of small hobby projects (none of which are ready for an open-source
release) over here: DrawSnek.
This little demo responds to 3 key-presses:
q
quits. Always a useful thing for full-screen apps which don’t always play nice with C-c :).s
spawns an additional snek. Have fun, make many sneks.h
introduces a random “hiccup” of up to 1 full second so you can see what happens visually when the loop is overburdened or stuck.
Unfortunately a fully-functioning demo is a bit lengthy to go over line by line in a blog post, so I’ll just focus on a couple of important features for stutter- and tearing-resistant animation & drawing with PyGame & Twisted.
For starters, you’ll want to use a very recent prerelease of PyGame
2, which recently added
support
for vertical
sync
even without OpenGL mode; then, pass the vsync=1
argument to set_mode
:
1 2 3 4 5 |
|
To allow for as much wall-clock time as possible to handle non-drawing work, such as AI and input handling, I also use this trick:
1 2 3 4 5 6 7 |
|
By deferring pygame.display.flip
to a thread1, the main loop can continue
processing AI timers, animation, network input, and user input while blocking
and waiting for the vertical blank. Since the time-to-vblank can easily be up
to 1/120th of a second, this is a significant amount of time! We know that the
draw
won’t overlap with flip
, because LoopingCall
respects Deferred
s
returned from its callable and won’t re-invoke you until the Deferred
fires.
Drawing doesn’t use withCount
, because it just needs to repeat about once
every refresh interval (on most displays, about 1/60th of a second); the vblank
timing is what makes sure it lines up.
However, animation looks like this:
1 2 3 |
|
We move the index forward by however many frames it’s been, then be sure it wraps around by modding it by the number of frames.
Similarly, the core2 of movement looks like this:
1 2 3 |
|
Rather than moving based on the number of times we’ve been called, which can result in slowed-down movement when the framerate isn’t keeping up, we jump forward by however many frames we should have been called at this point in time.
One of these days, maybe I’ll make an actual game, but in the meanwhile I hope you all enjoy playing with these fun little basic techniques for using Twisted in your game engine.