Alien Destroyer - Sidescroller Game

Game development specific discussions.
Post Reply
ixiz9
Posts: 14
Joined: Jan 01, 2023 4:50

Alien Destroyer - Sidescroller Game

Post by ixiz9 »

I have created a new side scroller action game inspired by Defender and its ilk.

You can find it on github at:
https://github.com/ixiz9/freebasic-game ... descroller

Or on itch.io at:
https://ixiz9.itch.io/alien-destroyer-sidescroller-game

Enjoy and looking forward to your comments!
paul doe
Moderator
Posts: 1732
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Alien Destroyer - Sidescroller Game

Post by paul doe »

My my what do we have here? Seems like a collection of classic games, each of them really well done!

I was getting uneven performance in Pacman, so I went to check what was happening:

Code: Select all

  ...
  next
  
  draw_pman(maze_x, maze_y)
  screenunlock
  frame_counter += 1
  frames_per_sec = frame_counter / (timer() - start_t)
  sleep 1
  if pman.state = DEAD then
  ...
Aha! The sleep statement sleeps the thread for the specified time, but it turns out it can be interrupted when you press a key. To prevent that (and thus preventing the sleep time to be uneven), call it like this:

Code: Select all

sleep 1, 1
The second '1' tells the command to not interrupt the sleeping when you press a key. After the change, the game runs like a charm.

I like the idea of encoding the graphics/assets in the code itself. It can be further refined to have a more efficient representation, of course; but then you'll lose the ability to quickly make changes to them if needed (there's a discussion of an encoder for such a thing on another thread, meant to be able to post proof of concept code without the need to attach any extra assets :idea: )

Very nice work indeed :D
ixiz9
Posts: 14
Joined: Jan 01, 2023 4:50

Re: Alien Destroyer - Sidescroller Game

Post by ixiz9 »

Thanks for the nice comments. Much appreciated!

Thanks also for looking at the code and identifying the issue with sleep. I had been treating sleep 1 as a kind of thread_yield(). I'm surprised it makes such a big difference since it's supposed to be measured in milliseconds. I'd thought the difference between a sleep 1 that completes and one that is interrupted would be at most 1 msec, well below what humans can detect, but that's clearly not always the case. I wonder if this is a Windows thing. Anyway, what you describe matches reports I had from another user which I couldn't reproduce and couldn't fix, so I'm very grateful for the correction. I will update the code.
paul doe
Moderator
Posts: 1732
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Alien Destroyer - Sidescroller Game

Post by paul doe »

ixiz9 wrote: Apr 04, 2023 4:23 ...
I'd thought the difference between a sleep 1 that completes and one that is interrupted would be at most 1 msec, well below what humans can detect, but that's clearly not always the case. I wonder if this is a Windows thing.
...
Exactly. Turns out that (if you don't specify otherwise), the default resolution for Windows is 15ms (you can't sleep below that). There are other topics here that explore this very same issue and how to change the resolution, so it might be well worth checking those.

I also like to code games (it was the reason why I started coding when I was a toddler), but I have lost most of my work on a HDD crash a while back. If you're interested, I made a simple Asteroids clone as a tutorial for a member of the Discord server. You can find it in my repo:

https://github.com/glasyalabolas/fb-asteroids

There are also lots of other stuff in there so feel free to have a look at them. Have fun :)
ixiz9
Posts: 14
Joined: Jan 01, 2023 4:50

Re: Alien Destroyer - Sidescroller Game

Post by ixiz9 »

Thanks. I've been reading through the asteroids code and found it very useful to see how you approached the problem. I like how cleanly the code is organized.

I noticed that you, like me, implemented your own keyboard handler. However, for my upcoming project I was thinking of maybe dropping that and just using Multikey(). I'm wondering if for a game where I want to react to users' keypresses that would work. Specifically I'm thinking if it's possible to miss a keypress if the user presses down and then quickly releases the key between calls to multikey(), and if it can happen, how big of an issue that is.
Do you have any thoughts?
paul doe
Moderator
Posts: 1732
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: Alien Destroyer - Sidescroller Game

Post by paul doe »

ixiz9 wrote: Apr 07, 2023 18:31 ...
I'm wondering if for a game where I want to react to users' keypresses that would work. Specifically I'm thinking if it's possible to miss a keypress if the user presses down and then quickly releases the key between calls to multikey(), and if it can happen, how big of an issue that is.
...
Yes, it would work, and yes, it could 'miss a beat' every now and then (especially if the framerate is too low). Generally speaking it won't happen, and if it does happen how big of an issue it is depends on your game of course. In any case, working with multikey is a bit different than working with events, so write some test code so you can see how both differ. For comparison, this is the keyboard handler for multikey:

Code: Select all

/'
  Represents the input received from the keyboard.
  
  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( sc )
    released( sc )
    held( sc )
    
    or
    
    held( sc )
    released( sc )
    pressed( sc )
    
    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.
'/
type KeyboardInput
  public:
    declare constructor()
    declare constructor( as integer )
    declare destructor()
    
    declare sub lock()
    declare sub unlock()
    
    declare function pressed( as long ) as boolean
    declare function released( as long ) as boolean
    declare function held( as long, as double = 0.0 ) as boolean
    declare function repeated( as long, as double = 0.0 ) as boolean
    
  private:
    enum KeyState
      None
      Pressed               = 1 shl 0
      AlreadyPressed        = 1 shl 1
      Released              = 1 shl 2
      ReleasedInitialized   = 1 shl 3
      Held                  = 1 shl 4
      HeldInitialized       = 1 shl 5
      Repeated              = 1 shl 6
      RepeatedInitialized   = 1 shl 7
    end enum
    
    '' These will store the bitflags for the key states
    as ubyte _
      _state( any ), _
      _prevState( any )
    
    '' 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
  
  '' Set and clear flags
  #define SETF( _c_, _f_ ) _c_ or= ( _f_ )
  #define CLRF( _c_, _f_ ) _c_ = _c_ and not ( _f_ )
  
  '' Check flags
  #define ISSET( _c_, _f_ ) cbool( _c_ and ( _f_ ) )
end type

constructor KeyboardInput()
  constructor( 128 )
end constructor

constructor KeyboardInput( aNumberOfKeys as integer )
  dim as integer keys = iif( aNumberOfKeys < 128, 128, aNumberOfKeys )
  
  redim _state( 0 to keys - 1 )
  redim _prevState( 0 to keys - 1 )
  redim _heldStartTime( 0 to keys - 1 )
  redim _repeatedStartTime( 0 to keys - 1 )
  
  _mutex = mutexCreate()
end constructor

destructor KeyboardInput()
  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 KeyboardInput.lock()
  mutexLock( _mutex )
end sub

sub KeyboardInput.unlock()
  mutexUnlock( _mutex )
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 KeyboardInput.pressed( sc as long ) as boolean
  if( multiKey( sc ) ) then
    _prevState( sc ) = _state( sc )
    
    if( cbool( _prevState( sc ) = KeyState.None ) orElse _
      not ISSET( _prevState( sc ), KeyState.Pressed ) ) then
      
      SETF( _state( sc ), KeyState.Pressed )
    end if
  else
    CLRF( _state( sc ), KeyState.Pressed )
  end if
  
  return( not ISSET( _prevState( sc ), KeyState.Pressed ) andAlso _
    ISSET( _state( sc ), KeyState.Pressed ) )
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 KeyboardInput.released( sc as long ) as boolean
  if( not multiKey( sc ) ) then
    _prevState( sc ) = _state( sc )
    
    if( not ISSET( _prevState( sc ), KeyState.ReleasedInitialized ) andAlso _
      not ISSET( _state( sc ), KeyState.ReleasedInitialized ) ) then
      
      SETF( _state( sc ), KeyState.ReleasedInitialized )
    end if
    
    SETF( _state( sc ), KeyState.Released )
  else
    CLRF( _state( sc ), KeyState.Released )
  end if
  
  return( ISSET( _state( sc ), KeyState.Released ) andAlso _
    not ISSET( _prevState( sc ), KeyState.Released ) andAlso _
    ISSET( _state( sc ), KeyState.ReleasedInitialized ) andAlso _
    ISSET( _prevState( sc ), KeyState.ReleasedInitialized ) )
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 KeyboardInput.held( sc as long, interval as double = 0.0 ) as boolean
  if( multiKey( sc ) ) then
    _prevState( sc ) = _state( sc )
  
    if( not ISSET( _prevState( sc ), KeyState.HeldInitialized ) andAlso _
      not ISSET( _state( sc ), KeyState.HeldInitialized ) ) then
      
      SETF( _state( sc ), KeyState.HeldInitialized )
      _heldStartTime( sc ) = timer()
    end if
    
    if( ISSET( _prevState( sc ), KeyState.Held ) orElse _
      ISSET( _prevState( sc ), KeyState.HeldInitialized ) ) then
      
      SETF( _state( sc ), KeyState.Held )
      
      dim as double _
        elapsed = ( timer() - _heldStartTime( sc ) ) * 1000.0
      
      if( interval > 0.0 andAlso elapsed >= interval ) then
        CLRF( _state( sc ), KeyState.Held )
      end if
    end if
  else
    CLRF( _state( sc ), KeyState.Held )
    CLRF( _state( sc ), KeyState.HeldInitialized )
  end if
  
  return( ISSET( _prevState( sc ), KeyState.Held ) andAlso _
    ISSET( _state( sc ), KeyState.Held ) )
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 KeyboardInput.repeated( sc as long, interval as double = 0.0 ) as boolean  
  dim as boolean isPressed = multiKey( sc )
  
  if( isPressed ) then
    _prevState( sc ) = _state( sc )
  
    if( not ISSET( _prevState( sc ), KeyState.RepeatedInitialized ) andAlso _
      not ISSET( _state( sc ), KeyState.RepeatedInitialized ) ) then
      
      SETF( _state( sc ), KeyState.RepeatedInitialized )
      _repeatedStartTime( sc ) = timer()
    end if
    
    if( ISSET( _prevState( sc ), KeyState.Repeated ) orElse _
      ISSET( _prevState( sc ), KeyState.RepeatedInitialized ) ) then
      
      CLRF( _state( sc ), KeyState.Repeated )
      
      dim as double _
        elapsed = ( timer() - _repeatedStartTime( sc ) ) * 1000.0
      
      if( interval > 0.0 ) then
        if( elapsed >= interval ) then
          SETF( _state( sc ), KeyState.Repeated )
          CLRF( _state( sc ), KeyState.RepeatedInitialized )
        end if
      else
        return( isPressed )
      end if
    end if
  else
    CLRF( _state( sc ), KeyState.Repeated )
    CLRF( _state( sc ), KeyState.RepeatedInitialized )
  end if
  
  return( not ISSET( _prevState( sc ), KeyState.Repeated ) andAlso _
    ISSET( _state( sc ), KeyState.Repeated ) )
end function
Post Reply