If you haven't already, read the Code of the Ninja: Introduction
Welcome back, Code Ninjas!
I apologise for not posting in such a long time (I know ninjas are supposed to be silent, but not that silent), but I've been working on a couple of projects that I hope will soon suprise and delight.
Right now, though, I want to talk about an improvement to my earlier Joypad code. I've been interacting a lot recently with disassemblies of Sonic the Hedgehog, and it's a great learning experience. Regardless of what might have happened to the Sonic series over the years, Yuji Naka's programming remains an inspiration to me. Studying his code has taught me plenty of little tricks, not least because the Genesis is very limited by today's standards and it took a lot of skill to squeeze great results out of it.
Anyway, last time I had described a system that updates a variable called JoyCurrent each step with the current state of the joypad, with each bit representing one button. There was also a second variable called JoyPrevious in which JoyCurrent is stored right before JoyCurrent gets updated. And finally, a third variable called JoyPast, only there to smooth out problems with cheap joypads that occasionally glitch up.
There were three scripts: joy() for checking if a button is down; joy_pressed() for checking if a button is down now but not one step ago; and joy_released() for checking if a button is up now but not one step ago (or even two steps ago, in the case of the aforementioned glitchy controllers).
But I've since discovered a serious deficiency with the joy_pressed() and joy_released() scripts. They can't check for more than one button at a time without causing problems. Let me explain.
Since JoyCurrent and its related variables contain bits that are either on or off to represent the state of buttons on the joypad, checking the state of a button is as easy as testing any given bit with code like this:
Code:
return ( JoyCurrent & argument0 );
where argument0 is a value such as 1 (for testing the first bit), 2 (for testing the second bit), 4 (for testing the third bit), and so on. It's best to define these values as constants so that they can be sensibly named after the buttons, like A, B, LEFT, or START.
Anyone paying enough attention can see that you can test for more than one button simultaneously just by passing a value as argument0 that has more than 1 bit on. For instance, you could pass a value like 65535 to test if any button was down, or you could use OR to test any combination such as LEFT|RIGHT.
Now, the way the script was written it will return true if any bit of argument0 matches up with one in JoyCurrent. If you want to be sure all the bits match, then you'd have to write something like this:
Code:
if (joy(LEFT|RIGHT)==LEFT|RIGHT) then ...
This is all very well and good, but it falls apart when we get to joy_pressed(). This is how it was written:
joy_pressed()
return ( JoyCurrent & argument0 ) and !( JoyPrevious & argument0 );
Now suddenly, because of that boolean "and", the value being returned is degraded - it's now only useful as true or false and doesn't give us as much information. Worse, the following happens:
Imagine you want to check whether A, B, or C are pressed, like in the old Sonic games where any of the three buttons makes him jump or Spin Dash. You don't care if one of the buttons is already down when another is pressed - you still want to detect the new press. The way my code was written, this is impossible with only one call to joy_pressed() because if any of the bits is on in JoyPrevious, the new press won't be detected. The only solution would be to make multiple calls something like this:
Code:
if (joy_pressed(A) or joy_pressed(B) or joy_pressed(C)) then ...
which is just tacky and consumes more processor time. It would be far better to be able to type:
Code:
if (joy_pressed(A|B|C) then...
and have it be done with. (Of course, A|B|C could be a constant called JUMPBUTTON or something, too, to make it even nicer.)
Well, then, how can we change the code so that this is possible? I'm glad you asked that.
At the end of the script joy_step() (the one that updates JoyCurrent and JoyPrevious), we need to update two new variables, JoyPressed and JoyReleased (not to be confused with the scripts that have similar names!) These should be global variables, declared in joy_init().
These variables are destined to behave just like JoyCurrent, only for pressed and released. Just like how you can test to see if buttons are down by checking JoyCurrent as joy() does:
joy()
return ( JoyCurrent & argument0 );
you'll be able to check which buttons are newly down or up in one simple comparison by rewriting joy_pressed like so:
joy_pressed()
return ( JoyPressed & argument0 );
and joy_released() like so:
joy_released()
return ( JoyReleased & argument0 );
(At this point these scripts are all so simple you might not even want to make them scripts at all, but merely type JoyPressed&BUTTON anywhere you would have typed joy_pressed(BUTTON), but it's up to you.)
This sounds great, and it will solve all of the problems I mentioned above, but I haven't told you yet how to update JoyPressed and JoyReleased at the end of joy_step(). It requires a little bit of explanation, though, so we can understand the underlying principles. Otherwise, it would get confusing and complex if you ever need to expand upon it.
First, let's look at a visual representation of our variables. I'm assuming only 8 buttons for convenience. Here's a state with no buttons down:
JoyPrevious: - - - - - - - - JoyCurrent: - - - - - - - -
Let's press the first button (we'll call it A).
JoyPrevious: - - - - - - - - JoyCurrent: - - - - - - - A
Now let's, without advancing a step yet, add a third variable to this visual guide, temp. It's contents will be JoyPrevious AND JoyCurrent (i.e. "temp = JoyPrevious&JoyCurrent;").
JoyPrevious: - - - - - - - - JoyCurrent: - - - - - - - A temp: - - - - - - - -
As far as temp is concerned, nothing has happened! But what happens when we do advance one step, without letting go of A?
JoyPrevious: - - - - - - - A JoyCurrent: - - - - - - - A temp: - - - - - - - A
JoyPrevious becomes JoyCurrent, JoyCurrent remains the same, and temp finally notices what's going on. Clearly, temp is no good for checking buttons that are newly down, because for one temp has only detected the new press one step late, and for two if we continue to hold A temp will not revert to 0. Merely using bitwise AND (&) isn't enough. We need to do one more calculation, XOR (^). Let's go back to our previous step:
JoyPrevious: - - - - - - - - JoyCurrent: - - - - - - - A temp: - - - - - - - -
and add a fourth variable, called JoyPressed. It's contents will be temp ^ JoyCurrent.
JoyPrevious: - - - - - - - - JoyCurrent: - - - - - - - A temp: - - - - - - - - JoyPressed: - - - - - - - A
By XORing temp and JoyCurrent, JoyPressed contains only bits that are different between them. In the next step, the magic happens:
JoyPrevious: - - - - - - - A JoyCurrent: - - - - - - - A temp: - - - - - - - A JoyPressed: - - - - - - - -
Now JoyPressed has reverted to 0, meaning it accurately represents buttons pressed - bits will only trigger for one frame when their corresponding button is pressed. The same thing will happen even if A is released instead of held down:
JoyPrevious: - - - - - - - A JoyCurrent: - - - - - - - - temp: - - - - - - - - JoyPressed: - - - - - - - -
And, if a new button is pressed while another is held down, it will still be detected as a new press:
JoyPrevious: - - - - - - - A JoyCurrent: - - - - - - B A temp: - - - - - - - A JoyPressed: - - - - - - B -
Fantastic! Let's add another variable, JoyReleased, that is temp ^ JoyPrevious (instead of JoyCurrent) and advance one step while releasing B (but not A).
JoyPrevious: - - - - - - B A JoyCurrent: - - - - - - - A temp: - - - - - - - A JoyPressed: - - - - - - - - JoyReleased: - - - - - - B -
The same principle operates as with JoyPressed. We just solved the problem. Hooray! The actual code at the end of joy_step() would look something like this:
JoyPressed = ( JoyPrevious & JoyCurrent ) ^ JoyCurrent; JoyReleased = ( JoyPrevious & JoyCurrent ) ^ JoyPrevious;
Really the only thing to be done now is make sure that cheap joypads don't cause false press and release events simply because the signal is interrupted for a step once in a while. This is easily done by ORing JoyPrevious and JoyPast together to create a sort of "buffered" previous state when checking for presses, and ORing JoyCurrent and JoyPrevious together for a buffered current state (and using JoyPast in place of JoyPrevious where it used to appear in the line) when checking for releases. For example:
joy_step()
JoyPressed = ( ( JoyPrevious | JoyPast ) & JoyCurrent ) ^ JoyCurrent;
JoyReleased = ( ( JoyCurrent | JoyPrevious ) & JoyPast ) ^ JoyPast;
Conceivably you could also, instead of doing everything in 2 lines, store more information like so:
joy_step()
JoyCurrentBuffered = JoyCurrent | JoyPrevious;
JoyPreviousBuffered = JoyPrevious | JoyPast;
JoyDown = JoyPreviousBuffered & JoyCurrent;
JoyUp = JoyCurrentBuffered & JoyPast;
JoyPressed = JoyDown ^ JoyCurrent;
JoyReleased = JoyUp ^ JoyPast;
This way you could check JoyDown or JoyUp to see whether a button is down but not pressed, or up but not released, which might be useful. Hey, you never know.
That takes care of today's subject. I'll be posting again soon. Until then, happy coding!
If you use my code or scripts in your game or engine, no credit is necessary. But I'd love to hear about your project if you do! Just drop me a comment below, or e-mail me at us.mercurysilver@gmail.com
No comments:
Post a Comment