Here is your code with a suggested fix, along with several other advice for improvement.
First things first: what was wrong is that you never checked if the horizontal movement would make the blocks collide. You had only one check in place, and it actually only did anything after any collision had already happened. And in this case, it would prevent "gravity" from doing anything. Since you were updating your player pixel by pixel, the fact the collision had already happened went by mostly unnoticed. The fix is to create a check that verifies if the moving block can move to a certain place, and only then allowing it. And do it for all of the movement, not only for gravity movement.
So, here I will explain the improvements that can allow this sample code to evolve into a complete game.
Eliminate "floating code" on the module level and put everything inside functions. This is essential for any game that will have even something as simple as a starting screen (and possibly a "play again" screen) in the future. I made two functions, one that creates setup
your game environment and creates your objects, and the main game function. You should separate the "setup" stage further in the future,
for being able to create more game objects, or different game levels.
Reuse your code. You had the lines in your __init__
method mostly duplicated between your two classes, but for initial position and image-name constants. So you'd actually need just one class, and pass different values for these attributes. Since you hint you will have two distinct game types of objects by using the groups and such, I kept the two classes,but factored out the common code.
Frame delay: your code would simply move "as fast as possible" - which would create different game speeds in different computers, and use 100% of CPU - causing possible machine slowdowns and excessive power usage. I included a hardcoded pause of 30ms - which will allow your game to move at close to 30FPS, keep the speed stable across different devices. The other change needed is that your hardcoded "1" values for gravity and speed, that would make the player move 1px at a time had to be changed to larger values.
Last but not least, the "can I move there" logic: it can actually seem extensive - but perceive that the plain English description of the checking of the "can this object move to that position" is lengthy. The method essentially does: "Store the current position". "Fake a position on with the given x and y displacements". "check if I am colliding with something". "if not, restore the previous position, and say the movement is allowed". "otherwise, restore the y position, check if there is a collision in the X direction, if so, restrict horizontal movement". "Restore the x position, move the y position by desired amount, check if collision happens. If so, restrict movement in the y direction". "If movement is not restricted in either x or y, it is just the combined movement that causes the collision: restrict movement in both directions to be fair". "Restore original position, return allowed changes to x and y positions, and the object we'd collide with". All this logic is put inside the "living" objects class itself, so it can be used at any time, with a single line of code in the main game logic.
code:
import pygame
class GameObject(pygame.sprite.Sprite):
def __init__(self, x=0, y=0, s=100, image=""):
super(GameObject, self).__init__()
if not image:
image = self.image
self.image = pygame.transform.scale(pygame.image.load(image), (s, s))
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
class Player(GameObject):
image = "player.png"
def can_move(self, xCh, yCh, group):
old_pos = self.rect.x, self.rect.y
self.rect.x += xCh
self.rect.y += yCh
collided_with = pygame.sprite.spritecollideany(self, group)
if not collided_with:
# No Collisions at all - allow movement
self.rect.x = old_pos[0]
self.rect.y = old_pos[1]
return True, xCh, yCh, None
# Check if the colision was due to horizontal movement:
self.rect.y = old_pos[1]
if pygame.sprite.spritecollideany(self, group):
# Yes, then indicate horizontal movement should be 0
xCh = 0
# check if collision was due to vertical movement:
self.rect.y += yCh
self.rect.x = old_pos[0]
if pygame.sprite.spritecollideany(self, group):
# Yes - indicate vertical movemnt should be 0
yCh = 0
# if only diagonal movement causes collision, then
# cancel movrment in both axes:
# (i.e. just the diagonal movement would hit the
# obstacle )
if not xCh == 0 and not yCh == 0:
xCh = 0
yCh = 0
self.rect.x = old_pos[0]
self.rect.y = old_pos[1]
return False, xCh, yCh, collided_with
class Block(GameObject):
image = "wall.png"
def setup():
global display, player, living, noliving, gravity, speed, sprites
pygame.init()
display = pygame.display.set_mode((800,600))
player = Player()
block = Block(y=500)
sprites = pygame.sprite.Group()
living = pygame.sprite.Group()
noliving = pygame.sprite.Group()
sprites.add(player)
sprites.add(block)
living.add(player)
noliving.add(block)
gravity = 10
speed = 10
def main():
xCh = 0
yCh = 0
while True:
display.fill((159, 159, 159))
for event in pygame.event.get():
if(event.type == pygame.KEYDOWN):
if(event.key == pygame.K_ESCAPE):
quit()
elif(event.key == pygame.K_a or event.key == pygame.K_LEFT):
xCh = -speed
elif(event.key == pygame.K_d or event.key == pygame.K_RIGHT):
xCh = speed
elif(event.type == pygame.KEYUP):
xCh = 0
yCh = 0
elif(event.type == pygame.QUIT):
quit()
yCh = gravity
for liv in living:
if liv == player:
check_x_ch = xCh
else:
check_x_ch = 0
can_move, xCh, yCh, obstacle = liv.can_move(xCh, yCh, noliving)
liv.rect.x += xCh
liv.rect.y += yCh
# Do other actions if "can_move" indicates a block was hit...
sprites.draw(display)
pygame.display.update()
pygame.time.delay(30)
setup()
main()