Jump to content
  • Advertisement
Sign in to follow this  
benreed

Pygame: Adjust character speed upon landing from a jump?

This topic is 1097 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hey all,

I am working on a crude 2D platformer-brawler (think Smash Bros.) prototype in Pygame. I have mostly been following tutorials at programarcadegames.com and referencing the Pygame docs more and more as I get a better idea of what I'm doing. Right now I'm just working on a very basic physics/input handling sandbox while I learn what constitutes good architecture (proper encapsulation, best practices, avoiding short-term fixes that will provide errors and/or poor performance in a more complex project, etc). I know that this is a fairly ambitious project for a beginning game developer, let alone as a solo project, so I'm not trying to rush development -- I'm introducing features very, very slowly so that I can clearly see what works and what doesn't work when I try to add a desired feature. Movement, input, and animation are pretty much my only focuses for the present.

So far I have been able to make the test character collide with/land on platforms, run right and left on platforms, jump, air jump, and come to a stop without trouble. However, I'm having trouble implementing what I thought would be a simple movement mechanic. Here's what I'm trying to do (relevant code will follow):

When the character jumps, they can move left and right in the air, but at a lower speed than if they were standing on a platform. They also can't change their directional facing (left or right) while in the air. (This would be an odd design choice for a single-player/co-op platformer, but makes more sense for a fighting game -- jumping needs to be a bigger commitment for the player, with less room to "change your mind" about where you are and where/when you'll land.) Upon landing on a platform, I want the character's left/right movement speed to be immediately re-adjusted to its (higher) ground value, and I also want the character to be able to change directional facing on the 1st frame possible if the player is holding the opposite direction as they land (land and immediately run full speed in that direction, a la Castlevania, Mega Man, etc). I thought this would be as easy as just changing member variable values whenever a platform landing collision is detected in the character's update() method, but apparently I was wrong.

What's happening instead is this: if I move backwards in the air (relative to the direction I was facing when I jumped), my slower air movement speed is properly applied, but it's never readjusted when I land. This doesn't happen if I hold forward when I jump, or if I release forward and press it again during a jump that's already moving forward. So if I move backwards in the air from a forward jump or a neutral jump (jump while not holding left or right), when my character lands, it moves slowly (air steer speed) and cannot change its directional facing. If I release left/right to trigger my keyup event handling, the character's stop() method gets called, which resets deltaX to 0, and the speed/facing variables are assigned the new values that I wanted.

Here's my input handling in the main loop:

if event.type == pygame.KEYDOWN:

   # Keydown left: Move character left
   if event.key == pygame.K_LEFT:
      player.go_left()

   # Keydown right: Move character right
   if event.key == pygame.K_RIGHT:
      player.go_right()

   # Keydown up: Jump / air jump
   if event.key == pygame.K_UP:
      if player.airborne:
         player.air_jump()
      else:
         player.jump()

if event.type == pygame.KEYUP:
   # Stop character if left is released while moving
   #   left
   if event.key == pygame.K_LEFT and player.deltaX < 0:
      player.stop()

   # Stop character if right is released while moving
   #   right
   if event.key == pygame.K_RIGHT and player.deltaX > 0:
      player.stop()

Here are the movement methods called from the character class:

 

   # Player-controlled movement:
   def go_left(self):
      """ Called when the user hits the left arrow. """
      self.deltaX = -1 * self.movement_speed
      if not self.airborne:
         self.direction = "L"

   def go_right(self):
      """ Called when the user hits the right arrow. """
      self.deltaX = self.movement_speed
      if not self.airborne:
         self.direction = "R"

   def jump(self):
      """ Called when user hits 'jump' button. """

      # move down a bit and see if there is a platform below us.
      # Move down 2 pixels because it doesn't work well if we only move down
      # 1 when working with a platform moving down.
      self.rect.y += 2
      platform_hit_list = pygame.sprite.spritecollide(self, self.level.platform_list, False)
      self.rect.y -= 2

      # If it is ok to jump, apply jump force and
      #   declare the character airborne
      if len(platform_hit_list) > 0 or self.rect.bottom >= constants.SCREEN_HEIGHT:
         self.deltaY = self.jump_force
         self.airborne = True
         self.movement_speed = self.air_steer_speed

   def air_jump(self):
      """ Called when user hits 'jump' button while airborne """
      if self.airborne == True and self.air_jumped == False:
         self.air_jumped = True
         self.deltaY = self.air_jump_force

   def stop(self):
      """ Called when the user releases left or right """
      self.deltaX = 0

   def land(self):
      """ Called when player lands on a platform """
      # Stop player's vertical movement and declare that
      #   character is not airborne
      self.deltaY = 0

      # Set horizontal movement speed back to run speed
      self.movement_speed = self.run_speed

      # Reset jumping state values
      self.airborne = False
      self.air_jumped = False

And here is the order of operations in the character's update() method:

def update(self):

   # Calculate and apply gravity
   self.calc_grav()

   # Move left/right (apply deltaX)
   self.rect.x += self.deltaX
   pos = self.rect.x + self.level.world_shift

   # Set running frame based on direction, screen position,
   #   and frame rate
   if self.direction == "R":
      frame = (pos // 30) % len(self.running_frames_R)
      self.image = self.running_frames_R[frame]
   else:
      frame = (pos // 30) % len(self.running_frames_L)
      self.image = self.running_frames_L[frame]

   # Check for platform collisions (x-axis)
   block_hit_list = pygame.sprite.spritecollide(self, self.level.platform_list, False)
   for block in block_hit_list:
      # If we are moving right,
      # set our right side to the left side of the item we hit
      if self.deltaX > 0:
         self.rect.right = block.rect.left
      elif self.deltaY < 0:
         # Otherwise if we are moving left, do the opposite.
         self.rect.left = block.rect.right

   # Move up/down (apply deltaY)
   self.rect.y += self.deltaY

   # Check for platform collisions (y-axis)
   block_hit_list = pygame.sprite.spritecollide(self, self.level.platform_list, False)
   for block in block_hit_list:

      # Reset rect position based on the top/bottom of the object.
      if self.deltaY > 0:
         self.rect.bottom = block.rect.top
      elif self.deltaY < 0:
         self.rect.top = block.rect.bottom

      # Call platform landing method
      self.land()

I used print statements to see exactly when and how frequently these methods were called. I know that for every left/right KEYDOWN event, go_left() and go_right() are only called once. I know I won't get a fresh call unless I release the key (for a KEYUP event that triggers a stop() call) and then press again (for a new KEYDOWN event and thus a new go_right() or go_left() call).

I am not entirely sure how Pygame event handling works. I'm still going through tutorials and poring over the docs to understand where they come from, what members they have, what they can responsibly be used to accomplish in one's project, those kinds of things. So one suspicion I have about my problem is that I don't have enough information (or current enough information) about the key state passed to my character's methods -- I need more/better info in order to keep the data state updated. My impression is that in update(), when land() is called (as it is every frame while the character bottom collides with the top of a platform, logically reaffirming that it is standing on a platform), I need to somehow verify if a key is still pressedor perhaps has not been released, since the last time go_left() or go_right() was called. That may give me enough information to drive an if block that adjusts the character's speed and direction on the 1st frame the character is determined to have landed. (I am confused, however, as to why my boolean members for air jump and airborne get set properly when land() is called in update(), but not my movement speed and directional facing members.)

How correct or incorrect are any of these assumptions I make? If you need to see any more of my code, let me know and I'll edit this post. Thank you in advance for any help you can provide.

Share this post


Link to post
Share on other sites
Advertisement

You don't re-adjust the player direction nor deltaX when he lands, so, if you jump facing right, then press left, when the player lands the facing variable will still be 'R' and deltaX will still be the slow left since it too hasn't changed when he lands.

 

You need a check in Land to see if airborne == true, and, if so, you may need to adjust the facing and deltaX variables.

Edited by BeerNutts

Share this post


Link to post
Share on other sites

Hey BeerNutts,

Thank you very much for your help. I had to think about it a bit, but I realized what I was doing wrong in this case. Here is my new code in the character's stop() method:

 

def land(self):
   """ Called when player lands on a platform """
   # Stop player's vertical movement and declare that
   #   character is not airborne
   self.deltaY = 0

   # Reset jumping state values
   self.airborne = False
   self.air_jumped = False

   # Re-adjust movement speed
   self.movement_speed = self.run_speed

   # Re-adjust deltaX to ground movement speed,
   #   based on directional facing
   if self.deltaX < 0 and self.direction != "L":
      self.deltaX = -1 * self.movement_speed
      self.direction = "L"
   elif self.deltaX < 0 and self.direction == "L":
      self.deltaX = -1 * self.movement_speed

   if self.deltaX > 0 and self.direction != "R":
      self.deltaX = self.movement_speed
      self.direction = "R"
   elif self.deltaX > 0 and self.direction == "R":
      self.deltaX = self.movement_speed

This corrects both the movement speed issue and the character's unfortunate tendency to "moonwalk" upon landing after a backward air steer while holding that direction. 

The problem I had, I assume, was that I was treating a lot of the landing logic as implicit from update(), when I needed to be more explicit about boolean tests in land() in order to make sure values got adjusted that very same frame upon landing.

I was surprised that this problem turned out to be so subtle. Definitely something to think about as I move forward. I think I'm going to review a bit more about event handling and animation next, since I feel my grasp on how those work in Pygame is still pretty tenuous. 

Thank you again for your help!

Share this post


Link to post
Share on other sites

I wonder why you call "self.land()" for every block in the block_hit_list in your first post.

 

In your second post, the code block at lines 16 to 20 can be simplified (and 22 to 26 too).

If you compare 16 vs 19, the only difference is the self.direction test. Lines 17 and 20 are the same, and then line 18 you correct self.direction if it is not what it should be.

 

Since assigning a value to a variable that it already has is not a problem, you can remove the self.direction test, and simply always assign, as in

if self.deltaX < 0:
    self.deltaX = -self.movement_speed
    self.direction = "L"

It does not matter what "self.direction" old value was, afterwards, it's always "L"

 

I also replaced "-1 * self.movement_speed" by "-self.movement_speed". Both are equivalent, just use what you like best.

Share this post


Link to post
Share on other sites

Alberth,

Apologies for the late reply. Busy at work as of late and I stopped to review and clean up my code a bit before I replied to this post.

I agree with you about self.land(), I was calling it too generically. I changed the call to only happen if the player collides with a platform along the y-axis and their deltaY is greater than 0. This should hopefully make self.update() a little less busy, although I realize the collision logic is still a bit broad (more on that below). Here's the new condition under which I call self.land():

def update(self):

   # (more code above)

   # Check for collisions (y-axis)
   block_hit_list = pygame.sprite.spritecollide(self, self.level.platform_list, False)
   for block in block_hit_list:

      # Reset rect position based on the top/bottom of the object.
      if self.deltaY > 0:
         self.rect.bottom = block.rect.top
         # DEBUG: Land only if character was falling when
         #   collision occurred
         self.land()
      elif self.deltaY < 0:
         self.rect.top = block.rect.bottom
         # Cause character to start falling if they bump
         #   their head on the bottom of a platform
         self.stop_rising()

def stop_rising(self):
   # Apply half of jump force in the opposite
   #   y direction to deltaY to accelerate
   #   the effect of gravity, resulting in
   #   a short jump or a fall after bumping
   #   against the bottom of a platform
   self.deltaY += -0.5 * self.jump_force

stop_rising() was a method I wrote for use with my short jump logic, which was a surprisingly straightforward implementation -- I call player.jump(), count frames in main loop since jump keydown event happened, and call player.stop_rising() if jump keyup event gets polled within a certain number of frames elapsed since the keydown event(I semi-arbitrarily chose 10). I noticed my jump against the underside of low-height platforms was unintentionally floaty, so I added a stop_rising() call in the top->bottom collision logic to cause a sharp drop when the player "bumps their head".

I also changed the syntax in self.land() like you suggested. I was in a hurry the night I fixed it and wasn't sure how much of my logic was being redundant until I looked at it with fresh eyes. For adjusting the movement speed, I also think your syntax (-foo instead of -1 * foo) is better, because for switching between left and right movement, I will always just be adjusting that value by 1*foo or -1*foo instead of a different multiplier (like when I apply or adjust a proportion of my character's jump force to deltaY in self.stop_rising()). Closer to plain human language that way.

Much of the collision detection logic is a holdover from the tutorial code I was following (specifically, this tutorial by Paul Vincent Craven at http://programarcadegames.com/python_examples/en/sprite_sheets/). I'm not sure how/if I can improve on the for block in block_hit_list method (having gotten block_hit_list from a spritecollide() on the player and the level member's list of Sprite-extending platforms) he uses. The most obvious problem I can see at a glance is that colliding with overlapping/tiled platforms could result in a bunch of redundant (and expensive?) self.land() calls. However, I'm not sure yet how/if I might change the collision logic to limit self.land() calls beyond what I already have in place. I'll have to take another pass through collision logic to see exactly what I need and what I  don't need for my purposes.

 

If I have more questions about different problems with my project (not directly related to the topic in the top post), is it better to post in this same thread, or make a new thread for a new problem? I want to avoid forum spam as much as possible. (And of course I'll do a forum + Google search for similar problems before I make my own thread.)

Thank you again for your help.

EDIT: I see a potential solution to redundant collision generally suggested in the MDN collision detection tutorial here:
https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection

I'll start looking at how to accomplish broad vs narrow phases in Pygame.

Edited by benreed

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!