Yet Another Platformer

Game development specific discussions.
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Yet Another Platformer

Post by paul doe »

You'll understand better with a simple example:

Code: Select all

#include once "fbgfx.bi"
#include once "thread.bi"

type as sub( _
  byval as any ptr ) _
  ThreadedSub

type _
  MouseContext
  
  as integer _
    x, y, button
end type

/'
  Mutexing the call to 'getMouse()' is needed for it to correctly work if
  multithreaded. This is so regardless of having compiled the code with the
  '-mt' compiler switch.
'/
sub _
  pollMouse( _
    byval parent as Thread ptr )
  
  parent->lock()
    var _
      c => cptr( _
        MouseContext ptr, _
        parent->context )
      
    getMouse( _
      c->x, _
      c->y, , _
      c->button )
    
  parent->unlock()
end sub

/'
  Implementing it like this causes a nasty hang when acquiring the
  lock for the screen, even when compiling with '-mt'.
'/
sub _
  hangingPollMouse( _
    byval parent as Thread ptr )
    
  var _
    c => cptr( _
      MouseContext ptr, _
      parent->context )
    
  getMouse( _
    c->x, _
    c->y, , _
    c->button )
end sub

screenRes( 800, 600, 32 )

dim as MouseContext _
  myContext

'' This one won't work
var _
  mouseThread => Thread( _
    cptr( _
      ThreadedSub, _
      @hangingPollMouse ), _
    @myContext )

'' This one will
'var _
'  mouseThread => Thread( _
'    cptr( _
'      ThreadedSub, _
'      @pollMouse ), _
'    @myContext )

mouseThread.start()

do
  mouseThread.lock()
  
  screenLock()
    cls()
    
    line _
      ( myContext.x - 50, myContext.y - 50 ) - _
      ( myContext.x + 50, myContext.y + 50 ), _
      rgba( 255, 0, 0, 255 ), bf
  screenUnlock()
  
  mouseThread.unlock()
  
  sleep( 1, 1 )
loop until( inkey() <> "" )
thread.bi

Code: Select all

type as sub( _
  byval as any ptr ) _
  ThreadedSub

/'
  Minimal abstraction that represents a thread of execution.
'/
type _
  Thread
  
  public:
    declare constructor( _
      byval as ThreadedSub, _
      byval as any ptr )
    
    declare destructor()
    
    declare property _
      context() as any ptr
    
    declare sub _
      lock()
    declare sub _
      unlock()
    
    declare sub _
      start()
    declare sub _
      pause()
    declare sub _
      resume()
    
  private:
    declare constructor()
    
    declare static sub run( _
      byval as Thread ptr )
    
    declare sub _
      stop()
    
    as any ptr _
      _thread, _
      _threadMutex
    as any ptr ptr _
      _context
    as ThreadedSub _
      _callback
    as boolean _
      _running, _
      _paused
end type

constructor _
  Thread()
end constructor

constructor _
  Thread( _
    byval aCallback as ThreadedSub, _
    byval aContext as any ptr )
  
  _callback => aCallback
  _context => aContext
  
  _threadMutex => mutexCreate()
  _thread => threadCreate( _
    cptr( _
      ThreadedSub, _
      @run ), _
    @this )
end constructor

destructor _
  Thread()
  
  this.stop()
  
  threadWait( _thread )
  mutexDestroy( _threadMutex )
end destructor

property _
  Thread.context() _
  as any ptr
  
  return( _context )
end property

sub _
  Thread.lock()
  
  mutexLock( _threadMutex )
end sub

sub _
  Thread.unlock()
  
  mutexUnlock( _threadMutex )
end sub

sub _
  Thread.start()
  
  _running => true 
end sub

sub _
  Thread.stop()
  
  _running => false
end sub

sub _
  Thread.pause()
  
  _paused => true
end sub

sub _
  Thread.resume()
  
  _paused => false
end sub

/'
  This will call the sub specified in the constructor continuously
  (unless the thread is paused), until the instance is destroyed.
'/
sub _
  Thread.run( _
    byval instance as Thread ptr )
  
  do while( instance->_running )
    if( not instance->_paused ) then
      instance->_callBack( instance )
    end if
    
    sleep( 1, 1 )
  loop
end sub
As you can see, the process deadlocks after a while (the time varies). The reason is that the input isn't mutexed (the same thing happens with multiKey(), in case you were wondering). Pass the thread a pointer to the pollMouse() sub (which is mutexed) and you won't get any hangs.
Last edited by paul doe on Nov 17, 2019 11:20, edited 1 time in total.
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Yet Another Platformer

Post by paul doe »

This is essentially the same abstraction but using FreeBasic 'events':

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 UPDATE_KEYBOARD():

Code: Select all

#include once "fbgfx.bi"

function UPDATE_KEYBOARD() _
  as boolean
  
  dim as Fb.Event _
    E
  
  while screenevent(@E)
    if E.TYPE = 1 then 'EVENT_KEY_PRESS
      ? "A key has been pressed"
      
      if( E.SCANCODE = Fb.SC_ESCAPE ) then
        return( true )
      end if
    elseif E.TYPE = 2 then 'EVENT_KEY_RELEASE
      ? "A key has been released"
    end if
  wend
  
  return( false )
end function

sub updateMouse()
  dim as Fb.Event _
    e
  
  do while( screenEvent( @e ) )
    if( e.type = Fb.EVENT_MOUSE_MOVE ) then
      ? "x: " & e.x & ", y: " & e.y
    end if
  loop
end sub

screenRes( 800, 600, 32 )

dim as boolean _
  done

do
  done => UPDATE_KEYBOARD()
  updateMouse()
  
  /'
    Whatever other updates needed
    
    UPDATE_OBJECTS()
    UPDATE_MOUSE()
    UPDATE_CAMERA()
  '/
loop until( done )
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
sero
Posts: 59
Joined: Mar 06, 2018 13:26
Location: USA

Re: Yet Another Platformer

Post by sero »

paul doe wrote:@sero: You might find this abstraction useful
Thank you for your responses. I'm still tinkering away at this platformer thing but the past two semesters stole much of my free time. I have a light summer class load and am anxious to get back to this. I've updated my code to use your keyboard handler. The top post has the link to the github page where the newest version can be downloaded. The demonstration now include an alpha blend by pressing the shift or control keys to create differing motion blur amounts.
paul doe
Moderator
Posts: 1733
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Yet Another Platformer

Post by paul doe »

I'll have a look at it, thanks. Currently, I'm coding some tools for an engine like this, and I plan to use Raylib for the rendering. We'll see...
Post Reply