[Microbit-Python] Game API objects I want
Larry Hastings
larry at hastings.org
Wed Oct 7 10:26:44 CEST 2015
Having written two games on the micro:bit--one of which actually
works!--and having some previous experience writing video games, I'd
like to get up on my soap box and hold forth.
There are a couple of technologies that /nearly //every//game/ will
use. (One that /literally every game/ will use!) And given that
they're mildly complicated to do properly, and can consume a good chunk
of the micro:bit's precious RAM to implement, I strongly suggest they be
written in C++ and provided with MicroPython on the micro:bit. I don't
know if I'm up to writing (and debugging!) them on the micro:bit
itself. Maybe I could make a first pass in normal C++ on Linux, and
someone else could port them to the micro:bit?
I can name three such technologies, as follows.
*1: A debounced accelerometer scaling object*
My slalom game simply reads the current accelerometer value, restricts
it to a specific range, then scales it down to the range I want to use
and uses the resulting value, like so:
[-1024 1023] <- accelerometer
[ 0 | 1 | 2 | 3 | 4 ] <- slalom column
Values lower than -1024 are ceilinged to -1024, values greater than 1023
are floored to 1023. I then add 1024 to the value, multiply by 5,
divide by 2048, and call int(). I get a number from 0 to 4
(inclusive). Simple, done.
The problem is that it kind of sucks. If you hold the micro:bit /just
so/, on the border between two values (say 2 and 3) it quickly and
seemingly-randomly bounces back and forth between the two. This can
make playing slalom kind of tricky.
Most of the code in the scrolly image demo and whack-a-mole game went
into writing a debounced accelerometer position reader class. The idea
is simple enough: instead of simply taking the range of the
accelerometer you're interested in, and scaling it to the range you
want, you also establish "dead zones" between regions like so:
[-512 511] <- accelerometer
[ 0 |a| 1 |b| 2 |c| 3 |d| 4 ] <- range with dead zones
Here I've labeled the dead zones with lowercase letters (a, b, c, d).
Dead zones magnified for illustrative purposes; in practice they're much
smaller.
The idea is, the value reported when you're in a "dead zone" depends on
the previous value. The dead zones establish dynamic low-water and
high-water marks for how far you'd have to go to cross over into another
zone.
For example, if on frame N you were clearly in zone 1:
[ 0 |a| 1 |b| 2 |c| 3 |d| 4 ]
*
And on frame N+1 you were in zone b:
[ 0 |a| 1 |b| 2 |c| 3 |d| 4 ]
*
the debounced reader would then report that you're still in zone 1. You
have to force it all the way past the dead zone for it to report you as
being in zone 2. If, on a subsequent frame P, you were definitely in
zone 2:
[ 0 |a| 1 |b| 2 |c| 3 |d| 4 ]
*
And on frame P+1 you were in zone b:
[ 0 |a| 1 |b| 2 |c| 3 |d| 4 ]
*
the debounced reader would then report that you're (still) in zone 2.
One way to think of it is that the dead zones prefer the zone nearest to
the previous accelerometer reading. So if the accelerometer is
currently in the spot marked with an asterisk, we can pretend that the
zones map out as follows:
*
[ 0 | 1 | 2 | 3 | 4 ]
*
[ 0 | 1 | 2 | 3 | 4 ]
*
[ 0 | 1 | 2 | 3 | 4 ]
*
[ 0 | 1 | 2 | 3 | 4 ]
*
[ 0 | 1 | 2 | 3 | 4 ]
Long story short, the reason the scrolly image demo is so pleasant to
use is because of this debounced accelerometer class. If I didn't have
that, it'd have the same bouncing-between-two-values problem that the
slalom game has. I suggest that most games on the micro:bit will use
the accelerometer, and if they simply scale it they'll have the bouncing
problem.
Of course, my debounced accelerometer class appears to consume most of
the RAM of the micro:bit. And that's /after/ I hard-coded it to some of
the behavior I wanted. A general-purpose debounced accelerometer class
might not even fit!
I could write a simple class that does this in C++, and let somebody
else who understands MicroPython could port it to the runtime and get it
working on the micro:bit.
I think the API would look something like this:
class AccelerometerScalingDebouncer:
def __init__(self, fn, input_min, input_max, output_min,
output_max, dead_zone=0.08):
...
def read(self):
...
fn would be the function to call to do a reading (like
microbit.accelerometer.get_x). input_min and input_max would establish
the range we'd clip the input to. output_min and output_max would
establish the range of values returned (inclusive). dead_zone would
establish how big the dead zone around each value would be, and we could
hide this from the students (or handwave it away, "don't worry about it,
it does the right thing for you").
read() would read the current value, clip it, debounce it, scale it, and
return the resulting value.
Again, you can see a working example of a class like this in my "scrolly
image" demo. I've attached it here so you don't have to go hunting for
it. It runs on the micro:bit.
*2: A debounced reader for all the buttons*
A moment's thought suggests: we want a debounced button class, too. Both
my games used the buttons, and I had to write debouncing code for both.
The basic idea: if the user presses the button, you often want to
process the button press exactly once. So code like this doesn't work:
next_frame = 0
while True:
delta = next_frame - microbit.time()
if delta > 0:
microbit.sleep(delta)
continue
next_frame = microbit.time() + 50
if microbit.button_a.is_pressed():
do_thing()
The problem: if the user holds down the button, you'll call do_thing()
every 50ms.
I've seen people mention that there's actually an event queue in the
DAL. Maybe just exposing this somehow is sufficient?
I think what I want is an object like this:
class ButtonDebouncer:
def __init__(self, a=True, b=True, both=False, delay=100):
...
def poll(self):
...
def a_pressed(self):
...
def b_pressed(self):
...
def both_pressed(self):
...
You pass in to the constructor booleans indicating which buttons you
care about: a, b, or both. If both=True and either a=True or b=True,
"delay" indicates how long to wait after one button is pressed before
giving up and assuming we're not getting both buttons pressed.
If you press and hold button A, after the next time you call poll(),
a_pressed() would return True /once/. After that first time it would
return False until you released A, called poll(), then pressed A and
called poll().//
The object would internally use microbit.time() for time. __init__
would call poll().
I tried to write an example of this in Python for the micro:bit. However
my first try hit a MemoryError. (Argh! Double-argh!) So I've attached
my attempt, which you can consider a non-working mockup.
*3: A simple scheduler*
/Every/ realtime game would want this. And I doubt people will write
non-realtime games on the micro:bit.
The basic idea: we want to do something at future time x. And we also
might want to do something every y milliseconds.
Python actually ships with a module to do this, "sched". The
sched.scheduler class is powerful and general-purpose. But it's way
more general than we need, and also it provides an inconvenient
callback-based API. ("I don't like callback-based APIs, I always get
them wrong." --GvR)
How about this:
class Scheduler:
def __init__(self):
...
def schedule(self, event, delta, repeat=False):
...
def clear(self):
...
def run(self):
...
schedule() would add an event to the scheduler. "event" is any object,
"delta" is how many ms in the future we want the event to happen, and if
repeat is True the event will recur every "delta" ms thereafter.
clear() would clear the current events from the schedule.
run() would sleep until the next scheduled event. It would return the
"event" object associated with the event. If you run() and there are no
currently scheduled events, it throws an exception.
I mocked this up in normal Python, attached. I also made it easy to
port to the micro:bit by simulating the microbit module. To run on the
microbit, just return the first couple paragraphs of stuff (it's marked)
and add "import microbit" to the top. Works fine.
What do you think? Anyone interested enough to work on the micro:bit
implementations of these?
//arry/
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0001.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: button.debouncer.mockup.py
Type: text/x-python
Size: 2794 bytes
Desc: not available
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0002.py>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: simple.scheduler.py
Type: text/x-python
Size: 3657 bytes
Desc: not available
URL: <https://mail.python.org/mailman/private/microbit/attachments/20151007/5a369e90/attachment-0003.py>
More information about the Microbit
mailing list