Code: Select all
#include once "fbgfx.bi"
#define fmod( n, d ) _
( cdbl( n ) - int( ( n ) / ( d ) ) * cdbl( d ) )
#define min( a, b ) _
iif( ( a ) < ( b ), ( a ), ( b ) )
#define max( a, b ) _
iif( ( a ) > ( b ), ( a ), ( b ) )
#define clamp( v, a, b ) _
max( a, min( v, b ) )
#define wrap( v, a, b ) _
( ( ( ( v ) - ( a ) ) mod ( ( b ) - ( a ) ) ) + _
( ( b ) - ( a ) ) ) mod ( ( b ) - ( a ) ) + ( a )
#define fwrap( v, a, b ) _
fmod( ( _
fmod( ( ( v ) - ( a ) ), _
( ( b ) - ( a ) ) ) + ( ( b ) - ( a ) ) ), _
( ( b ) - ( a ) ) + ( a ) )
/'
Represents the input received from the keyboard. It uses FreeBasic's
'event' queue instead of 'multiKey()'.
There are four states that can be queried: pressed, released,
held and repeated. All of them are:
* Independent
You don't need to query one to get the report for another. That is to
say, you don't need to query 'pressed()' before querying 'released()':
they will report their correct status when you query them individually.
* Order-invariant
Doesn't matter the order in which you place their queries in the code:
pressed( scanCode )
released( scanCode )
held( scanCode )
or
held( scanCode )
released( scanCode )
pressed( scanCode )
will net the same results, in the order you would expect. Said order,
should you query all of them for the same key, will also be invariant:
pressed
{ held
repeated } -> If you specify intervals; otherwise they'll get
released reported in the order in which you query them
See the comments on the method definitions for specificities about each
one.
TODO:
- Implement key state pushing/popping from an internal stack (to make
the abstraction self-contained).
'/
type _
PolledKeyboardInput
public:
declare constructor()
declare constructor( _
byval as integer )
declare destructor()
declare sub _
lock()
declare sub _
unlock()
declare sub _
onEvent( _
byval as Fb.Event ptr )
declare function _
pressed( _
byval as long ) _
as boolean
declare function _
released( _
byval as long ) _
as boolean
declare function _
held( _
byval as long, _
byval as double => 0.0 ) _
as boolean
declare function _
repeated( _
byval as long, _
byval as double => 0.0 ) _
as boolean
private:
enum _
KeyState
None
Pressed => 1
AlreadyPressed => 2
Released => 4
AlreadyReleased => 8
Held => 16
HeldInitialized => 32
Repeated => 64
RepeatedInitialized => 128
end enum
/'
Caches when a key started being held/repeated
'/
as double _
_heldStartTime( any ), _
_repeatedStartTime( any )
/'
For using the abstraction in a multithread engine
'/
as any ptr _
_mutex
'' These will store the bitflags for the key states
as ubyte _
_state( any )
end type
constructor _
PolledKeyboardInput()
this.constructor( 128 )
end constructor
constructor _
PolledKeyboardInput( _
byval aNumberOfKeys as integer )
dim as integer _
keys => iif( aNumberOfKeys < 128, _
128, aNumberOfKeys )
redim _
_state( 0 to keys - 1 )
redim _
_heldStartTime( 0 to keys - 1 )
redim _
_repeatedStartTime( 0 to keys - 1 )
_mutex => mutexCreate()
end constructor
destructor _
PolledKeyboardInput()
mutexDestroy( _mutex )
end destructor
/'
If you are using this in a multithreaded engine, you'll probably have to
acquire the lock before the keyboard is handled, and release it after the
handling with this instance has ended. This will prevent some very nasty
hangs, depending on what method you use to render the screen.
'/
sub _
PolledKeyboardInput.lock()
mutexLock( _mutex )
end sub
sub _
PolledKeyboardInput.unlock()
mutexUnlock( _mutex )
end sub
/'
The 'onEvent()' method MUST be called before querying any of the four
states. This is needed because we can't simply empty the event queue
to set the states appropriately, since there could be other parts of
the code that also want to handle some of them (or even the very same
events but differently).
'/
sub _
PolledKeyboardInput.onEvent( _
byval e as Fb.Event ptr )
if( e->type = Fb.EVENT_KEY_PRESS ) then
_state( e->scanCode ) or=> _
( KeyState.Pressed or KeyState.Held or KeyState.Repeated )
_state( e->scanCode ) => _
_state( e->scanCode ) and not KeyState.AlreadyPressed
end if
if( e->type = Fb.EVENT_KEY_RELEASE ) then
_state( e->scanCode ) or=> KeyState.Released
_state( e->scanCode ) => _
_state( e->scanCode ) and not KeyState.AlreadyReleased
_state( e->scanCode ) => _state( e->scanCode ) and not _
( KeyState.Held or KeyState.HeldInitialized or _
KeyState.Repeated or KeyState.RepeatedInitialized )
end if
end sub
/'
Returns whether or not a key was pressed.
'Pressed' in this context means that the method will return 'true'
*once* upon a key press. If you press and hold the key, it will
not report 'true' until you release the key and press it again.
'/
function _
PolledKeyboardInput.pressed( _
byval scanCode as long ) _
as boolean
dim as boolean _
isPressed
if( _
cbool( _state( scanCode ) and KeyState.Pressed ) andAlso _
not cbool( _state( scanCode ) and KeyState.AlreadyPressed ) ) then
isPressed => true
_state( scanCode ) or=> KeyState.AlreadyPressed
end if
return( isPressed )
end function
/'
Returns whether or not a key was released.
'Released' means that a key has to be pressed and then released for
this method to return 'true' once, just like the 'pressed()' method
above.
'/
function _
PolledKeyboardInput.released( _
byval scanCode as long ) _
as boolean
dim as boolean _
isReleased
if( _
cbool( _state( scanCode ) and KeyState.Released ) andAlso _
not cbool( _state( scanCode ) and KeyState.AlreadyReleased ) ) then
isReleased => true
_state( scanCode ) or=> KeyState.AlreadyReleased
end if
return( isReleased )
end function
/'
Returns whether or not a key is being held.
'Held' means that the key was pressed and is being held pressed, so the
method behaves pretty much like a call to 'multiKey()', if the 'interval'
parameter is unspecified.
If an interval is indeed specified, then the method will report the 'held'
status up to the specified interval, then it will stop reporting 'true'
until the key is released and held again.
Both this and the 'released()' method expect their intervals to be expressed
in milliseconds.
'/
function _
PolledKeyboardInput.held( _
byval scanCode as long, _
byval interval as double => 0.0 ) _
as boolean
dim as boolean _
isHeld
if( cbool( _state( scanCode ) and KeyState.Held ) ) then
isHeld => true
if( cbool( interval > 0.0 ) ) then
if( not cbool( _state( scanCode ) and KeyState.HeldInitialized ) ) then
_state( scanCode ) or=> KeyState.HeldInitialized
_heldStartTime( scanCode ) => timer()
else
dim as double _
elapsed => ( timer() - _heldStartTime( scanCode ) ) * 1000.0
if( elapsed >= interval ) then
isHeld => false
_state( scanCode ) => _
_state( scanCode ) and not KeyState.Held
end if
end if
end if
end if
return( isHeld )
end function
/'
Returns whether or not a key is being repeated.
'Repeated' means that the method will intermittently report the 'true'
status once 'interval' milliseconds have passed. It can be understood
as the autofire functionality of some game controllers: you specify the
speed of the repetition using the 'interval' parameter.
Bear in mind, however, that the *first* repetition will be reported
AFTER one interval has elapsed. In other words, the reported pattern is
[pause] [repeat] [pause] instead of [repeat] [pause] [repeat].
If no interval is specified, the method behaves like a call to
'multiKey()'.
'/
function _
PolledKeyboardInput.repeated( _
byval scanCode as long, _
byval interval as double => 0.0 ) _
as boolean
dim as boolean _
isRepeated
if( cbool( _state( scanCode ) and KeyState.Repeated ) ) then
if( cbool( interval > 0.0 ) ) then
if( not cbool( _state( scanCode ) and KeyState.RepeatedInitialized ) ) then
_repeatedStartTime( scanCode ) => timer()
_state( scanCode ) or=> KeyState.RepeatedInitialized
else
dim as double _
elapsed => ( timer() - _repeatedStartTime( scanCode ) ) * 1000.0
if( elapsed >= interval ) then
isRepeated => true
_state( scanCode ) => _
_state( scanCode ) and not KeyState.RepeatedInitialized
end if
end if
else
isRepeated => true
end if
end if
return( isRepeated )
end function
/'
Test code
'/
dim as integer _
screenWidth => 800, _
screenHeight => 600
screenRes( _
screenWidth, screenHeight, 32 )
dim as integer _
x => 400, _
y => 300, _
radius => 10, _
speed => 3
/'
Some simple test code.
Use the arrow keys to move the white ball. Each key is handled
by a different method:
up
repeated
left down right
released pressed held
In the context of a platformer, these can be very handy to
implement several things in a straightforward manner. Here are
some ideas:
Firing:
Use the 'pressed()' method to begin firing. Afterwards, if the
weapon is continuous, use the 'held()' method. Otherwise, the
'repeated()' method can trigger firing at the desired rate.
Jumping:
At the start of the jump, use the 'pressed()' method to initiate
it. Afterwards, if you want to implement variable-height jumping
ala Mario, use the 'held()' method to add a small value to the
vertical acceleration. The interval will thus determine the
maximum height of a full jump (tapping/helding the jump key for
shorter periods will result in lower jumps).
Charging (ala Mega Buster):
Use the 'pressed()' method for the shot button to initiate the
charge (cache the starting time), and the 'released()' method to
trigger the release of the charged shot. The elapsed time between
both methods determines the power of the shot.
Running (ala River City Ransom/Double Dragon/Countless others)
Running is frequently implemented as a double tap of the directional
keys. For this, use two 'pressed()' calls: the first will start
caching the time between presses, and the second will determine the
elapsed time between both. If it is below some threshold, initiate
running and handle it with the 'held()' method.
Tapping/Double tapping
Same as above, but also with the 'pressed()' and 'released()' methods
(for tapping).
Some games (notably Beyond Oasis for the MegaDrive) used a tap/hold
control scheme: if you tap the C button you'll jump, if you hold it
you'll crouch. Makes for some quite interesting control schemes that
discourage button mashing gameplay.
Special powers ala Denjin Makai II (MAME)/Spawn(SNES):
One of the coolest and funniest side-scrolling Beat'em Up that I've
ever played was Guardians/Denjin Makai II (you can play it nowadays
thanks to MAME).
This game boasted a whooping 8 different characters to play as, each
with its own set of special powers. These were performed simply by
helding the attack/special button, making a (very simple) sequence
of moves, and then releasing the button. The character then unleashed
the attack which, thanks to this scheme, could easily be comboed
with normal attacks. Highly recommended game.
Implementing this is easy enough: use the 'pressed()' method to signal
the start of a special move. Then, as long as 'held()' is 'true',
handle 'pressed()' methods to collect the moves that are part of the
special. Upon 'released()', if the collected moves match some special
power, unleash!
Capcom-style specials
As the above, but simply collect the moves as the keys are 'pressed()'.
'Collecting moves' is simple enough to do with bitflags (and a long can
represent up to 32 sequential moves so it should be plenty) and activate
the appropriate power depending on the bitflags.
And lots of others...
'/
var _
aKey => PolledKeyboardInput()
dim as Fb.Event _
e
dim as boolean _
done
dim as integer _
prevRow, _
prevCol
do
/'
Notice how we need poll the events queue to correctly set the states
we will query afterwards.
Now, while the abstraction still retains all the properties of the
non-event based one (order invariability and state independency), we
need this extra step because we can't simply put this code inside the
'onEvent()' method, for it will effectively empty the event queue and,
if there is other code that needs to handle some of them, they will not
have anything to handle (since the event queue will be empty) and will
thus be completely ignored.
Strictly speaking, they are called 'events' but are, in fact, messages,
implemented using a 'Pull model': the underlying system pushes all
relevant messages into a queue, and it's the client's responsibility to
pull the info they may require from it (which has some consequences as
described above).
Again, mutexing is only needed if you're using this on a multithreaded
environment. I left it here just for reference.
'/
aKey.lock()
do while( screenEvent( @e ) )
aKey.onEvent( @e )
loop
'' Handle some keypresses
if( aKey.repeated( Fb.SC_UP, 200.0 ) ) then
y => max( 0, y - 50 )
end if
if( aKey.pressed( Fb.SC_DOWN ) ) then
y => min( screenHeight, y + 50 )
end if
if( aKey.released( Fb.SC_LEFT ) ) then
x => fwrap( x - 50, 0, screenWidth )
end if
if( aKey.held( Fb.SC_RIGHT, 500.0 ) ) then
x => fwrap( x + 1, 0, screenWidth )
end if
aKey.unlock()
'' Render frame
screenLock()
cls()
circle _
( x, y ), _
radius, rgb( 255, 255, 255 ), , , , f
screenUnlock()
sleep( 1, 1 )
loop until( aKey.pressed( Fb.SC_ESCAPE ) )
Pay attention to the comments. When you handle events, you have to pass unhandled events to other functions that may want to process them. This example shows what I mean (it uses your implementation of
As you can see, the responsiveness isn't quite the same, and changes depending on which handler you call first (since you're essentially 'eating' unhandled events). It doesn't happen in your code, naturally, since you're handling keyboard and mouse using different methods. Just be aware of this fact, and be nice and handle events inclusively =D