[Python] Keeping Segments Spacing Consistent and Fixing Rogue Segments

Discussão em 'Python' iniciado por Stack, Março 28, 2025.

  1. Stack

    Stack Membro Participativo

    So my issue is with the centipede class where the first segment is the head and then subsequent segments are the body. The subsequent segments take the position of the one in front so you get the basic look of a snake game. Problem is that they don't stay consistent distance apart (SEGMENT SIZE), and tend to overlap or space a bit too much and sometimes second segment is on top of head segment about half way down the screen.

    The other issue is when the centipede is hit by a bullet (in the Game events section). It splits into a front and back section where the front continues on as normal, just shorter, and the back segment becomes its own centipede with new head. Problem is it mostly works, but I get rogue segments that are not following the rest of the centipede properly or don't turn into a head if the only segment left (you can see from the debug red lines of segment pieces suppose to be together). Any suggestion on how to fix or make moving and splitting more concise?

    import pygame as pg
    import os
    import sys
    import random

    SCREEN_WIDTH = 600
    SCREEN_HEIGHT = 800
    SEGMENT_SIZE = 20


    class Player(pg.sprite.Sprite):
    def __init__(self, game, level, pos):
    super().__init__()
    self.game = game
    self.level = level
    self.image = pg.Surface((SEGMENT_SIZE, SEGMENT_SIZE), pg.SRCALPHA)
    self.image.fill((255, 0, 0))
    self.rect = self.image.get_rect(center=pos)
    self.speed = 300 # Adjusted speed to work with delta time (in pixels per second)
    self.can_shoot = True # Track whether the player can shoot a bullet
    self.mask = pg.mask.from_surface(self.image)

    def update(self, delta_time):
    # Initialize movement vector
    movement = pg.math.Vector2(0, 0)

    # Handle keyboard input
    keys = pg.key.get_pressed()
    if keys[pg.K_LEFT] and self.rect.left > 5:
    movement.x -= 1
    if keys[pg.K_RIGHT] and self.rect.right <= (SCREEN_WIDTH - 5):
    movement.x += 1
    if keys[pg.K_UP] and self.rect.top > (SCREEN_HEIGHT - (SEGMENT_SIZE * 6)):
    movement.y -= 1
    if keys[pg.K_DOWN] and self.rect.bottom < (SCREEN_HEIGHT - SEGMENT_SIZE):
    movement.y += 1
    if keys[pg.K_SPACE] and self.can_shoot:
    self.can_shoot = False
    self.game.bullets.add(Bullet(self.game, self.rect.midtop))

    # Handle joystick input
    for joystick in self.game.joysticks:
    hat_pos = joystick.get_hat(0)
    movement.x += hat_pos[0]
    movement.y -= hat_pos[1]

    for joystick in self.game.joysticks:
    if joystick.get_button(0):
    if self.can_shoot:
    self.game.bullets.add(Bullet(self, self.rect.midtop))
    self.can_shoot = False

    # Normalize diagonal movement
    if movement.length() > 0:
    movement = movement.normalize()

    # Apply movement
    self.rect.x += int(movement.x * self.speed * delta_time)
    self.rect.y += int(movement.y * self.speed * delta_time)

    # Enforce screen boundaries
    if self.rect.left < 5:
    self.rect.left = 5
    if self.rect.right > (SCREEN_WIDTH - 5):
    self.rect.right = (SCREEN_WIDTH - 5)
    if self.rect.y > (SCREEN_HEIGHT - SEGMENT_SIZE):
    self.rect.y = (SCREEN_HEIGHT - SEGMENT_SIZE)
    if self.rect.y < (SCREEN_HEIGHT - (SEGMENT_SIZE * 6)):
    self.rect.y = (SCREEN_HEIGHT - (SEGMENT_SIZE * 6))

    def draw(self, screen):
    screen.blit(self.image, self.rect)


    class Bullet(pg.sprite.Sprite):
    def __init__(self, game, pos):
    super().__init__()
    self.game = game
    self.image = pg.Surface((4, 10))
    self.image.fill((25, 255, 255))
    self.rect = self.image.get_rect(center=pos)
    self.speed = 1250 # Pixels per second

    def update(self, delta_time):
    self.rect.y -= int(self.speed * delta_time)


    class Centipede(pg.sprite.Sprite):
    def __init__(self, game, level, x, y, num_segments, base_speed):
    super().__init__()
    self.game = game
    self.level = level
    self.base_speed = base_speed
    self.segments = [] # Tracks the individual segments of the centipede for movement and collision
    self.new_centipede_created = False
    self.surface = pg.Surface((20, 20), pg.SRCALPHA)

    # Create all centipede segments
    for i in range(num_segments):
    segment_x = x - i * SEGMENT_SIZE
    is_head = (i == 0)
    segment = {
    "rect": pg.Rect(segment_x, y, SEGMENT_SIZE, SEGMENT_SIZE),
    "speed": base_speed,
    "x_direction": 1,
    "y_direction": SEGMENT_SIZE, # Controls vertical movement
    "move_down_next_frame": False,
    "move_up_next_frame": False, # Added for upward movement logic
    "poisoned": False,
    "is_head": is_head,
    "current_frame": 0,
    "current_frame_poisoned": 0,
    "last_update_time": pg.time.get_ticks(),
    "down_time": pg.time.get_ticks()
    }
    self.segments.append(segment)

    def update_speed(self):
    """Updates the speed of the centipede based on the number of segments."""
    if len(self.segments) == 1:
    speed = self.base_speed + 2
    elif 2 <= len(self.segments) < 6:
    speed = self.base_speed + 1
    else:
    speed = self.base_speed

    for segment in self.segments:
    segment["speed"] = speed

    def update(self, mushrooms, delta_time):
    """Updates the movement and behavior of the centipede."""
    self.update_speed()

    for i, segment in enumerate(self.segments):
    now = pg.time.get_ticks()

    # Manage animation frames
    if not segment["poisoned"]:
    if segment["move_down_next_frame"]:
    segment["rect"].y += segment["y_direction"]
    segment["move_down_next_frame"] = False
    else:
    segment["rect"].y += 5

    # Reverse y_direction logic
    if segment["y_direction"] < 0: # Moving up
    segment["poisoned"] = False # Reset poisoned status when moving up
    if segment["rect"].y <= SCREEN_HEIGHT - 120:
    for mushroom in mushrooms:
    if segment["rect"].colliderect(mushroom.rect):
    segment["y_direction"] *= -1 # Reverse to move down when hitting a mushroom
    segment["move_up_next_frame"] = True
    break
    if segment["rect"].left <= 0 or segment["rect"].right >= SCREEN_WIDTH:
    segment["y_direction"] *= -1 # Reverse to move down when hitting screen edges
    segment["move_up_next_frame"] = True

    elif segment["rect"].y < 0: # Prevent going off-screen vertically
    segment["y_direction"] *= -1

    elif segment["rect"].y >= SCREEN_HEIGHT - SEGMENT_SIZE * 2: # At the bottom edge
    segment["y_direction"] *= -1 # Reverse to upward movement

    # **Per-Frame Downward Movement ("move_down_next_frame")**
    if segment["move_down_next_frame"]: # Move downwards based on this flag
    segment["rect"].y += segment["y_direction"]
    segment["move_down_next_frame"] = False

    # Horizontal movement logic
    if not segment["poisoned"]:
    segment["rect"].x += segment["speed"] * segment["x_direction"]
    else:
    down_now = pg.time.get_ticks()
    if down_now - segment["down_time"] > 40: # Reverse horizontal direction at intervals while poisoned
    segment["down_time"] = down_now
    segment["x_direction"] *= -1 # Flip direction
    segment["rect"].x += segment["speed"] * segment["x_direction"]

    # Screen edge collisions
    if segment["rect"].left <= 0 and segment["x_direction"] == -1:
    segment["x_direction"] = 1
    segment["move_down_next_frame"] = True
    elif segment["rect"].right >= SCREEN_WIDTH and segment["x_direction"] == 1:
    segment["x_direction"] = -1
    segment["move_down_next_frame"] = True

    # Maintain spacing between segments
    if i > 0:
    prev_segment = self.segments[i - 1]
    if segment["x_direction"] == prev_segment["x_direction"]:
    if segment["x_direction"] == 1:
    segment["rect"].x = max(segment["rect"].x, prev_segment["rect"].x - SEGMENT_SIZE)
    else:
    segment["rect"].x = min(segment["rect"].x, prev_segment["rect"].x + SEGMENT_SIZE)

    def draw(self, screen):
    """Draws each centipede segment on the screen."""
    for segment in self.segments:
    is_head = segment["is_head"]
    color = (255, 255, 255) if is_head else (255, 255, 0)
    self.surface.fill((0, 0, 0, 0)) # Clear surface
    pg.draw.circle(self.surface, color, (10, 10), 10) # Draw circle on surface
    screen.blit(self.surface, segment["rect"].topleft)

    # debuging, checking body vs head alignment
    for i in range(1, len(self.segments)):
    pg.draw.line(screen, (255, 0, 0), self.segments[i - 1]["rect"].center,
    self.segments["rect"].center,2)


    class Mushroom(pg.sprite.Sprite):
    def __init__(self, game, level, x, y):
    super().__init__()
    self.game = game
    self.level = level
    self.full_height = SEGMENT_SIZE
    self.image = pg.Surface((20, 20))
    self.image.fill((0, 255, 0))
    self.health = 0 # Mushrooms take 3 hits to destroy (start at 0 and move up to 3)
    self.rect = self.image.get_rect(topleft=(x, y))
    self.is_poisoned = False
    self.mask = pg.mask.from_surface(self.image)

    def update_mushroom(self):
    if not self.is_poisoned:
    self.image.fill((0, 255, 0))
    else:
    self.image.fill((201, 137, 50))

    def take_hit(self):
    # Increase health when hit and update mushroom sprite
    self.health += 1
    if self.health < 4:
    self.update_mushroom()
    else:
    self.kill()


    class Game:
    def __init__(self):
    self.round = 0
    self.music_round = 0
    self.base_centipede_speed = 2
    self.total_centipedes = 1
    self.length = 13
    self.loners = -1
    self.life = 2
    self.previous_score = 0
    self.sprite_level = 0
    pg.init()
    pg.joystick.init()
    self.joysticks = [pg.joystick.Joystick(x) for x in range(pg.joystick.get_count())]
    pg.key.set_repeat(10, 50)
    pg.display.set_caption("CENTIPEDE")
    os.environ['SDL_VIDEO_WINDOW_POS'] = '350, 40'

    self.VIRTUAL_WIDTH = 600
    self.VIRTUAL_HEIGHT = 800

    # Get display resolution (e.g., full screen or windowed)
    self.SCREEN_WIDTH, self.SCREEN_HEIGHT = 660, 880 # Replace with desired resolution
    self.scale_x = self.SCREEN_WIDTH / self.VIRTUAL_WIDTH
    self.scale_y = self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT

    # Initialize display and virtual surfaces
    self.screen = pg.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pg.RESIZABLE)
    self.virtual_surface = pg.Surface((self.VIRTUAL_WIDTH, self.VIRTUAL_HEIGHT))

    self.clock = pg.time.Clock()
    self.font = pg.font.Font(None, 36)
    self.paused = False
    self.start_of_game = True
    self.last_update = pg.time.get_ticks()

    self.load_data()

    def is_position_valid(self, x, y, mushrooms):
    """Check if the position (x, y) is valid (not overlapping/touching existing mushrooms)."""
    for mushroom in mushrooms:
    if mushroom.rect.colliderect(pg.Rect(x, y, SEGMENT_SIZE, SEGMENT_SIZE)):
    return False
    return True

    def advance_level(self):
    self.round += 1
    self.sprite_level += 1
    if self.sprite_level > 14:
    self.sprite_level = 1
    self.sprite_round = "level{}".format(self.sprite_level)

    if self.round > 1:
    for mushroom in self.mushrooms:
    mushroom.kill()
    self.mushrooms.add(Mushroom(self, self.sprite_round, mushroom.rect.x, mushroom.rect.y))
    mushroom.update_mushroom()

    self.base_centipede_speed += 1
    if self.base_centipede_speed > 4:
    self.base_centipede_speed = 4

    self.length -= 1
    self.loners += 1
    if self.length < 0:
    self.length = 12
    self.loners = 0

    def lose_life(self):
    self.life -= 1
    if self.life < 0:
    self.quit()
    # self.game_over_screen()
    else:
    self.round -= 1
    self.length += 1
    self.loners -= 1
    self.base_centipede_speed -= 1
    self.sprite_level -= 1
    self.end_round_mushroom_cleanup()

    def load_data(self):
    # load sprite images

    self.dim_screen = pg.Surface(self.screen.get_size()).convert_alpha()
    self.dim_screen.fill((0, 0, 0, 120))

    def adjusted_mouse_position(self, mouse_pos):
    """Scale mouse position based on the virtual resolution."""
    scaled_x = mouse_pos[0] / self.scale_x
    scaled_y = mouse_pos[1] / self.scale_y
    return scaled_x, scaled_y

    def new(self):
    self.bullets = pg.sprite.Group()
    self.centipedes = pg.sprite.Group()

    # if first round
    if self.start_of_game:
    self.mushrooms = pg.sprite.Group()

    # Adding a few initial mushrooms ensuring no overlaps
    while len(self.mushrooms) < 40:
    x = random.randint(20, SCREEN_WIDTH - SEGMENT_SIZE)
    if x % 20 != 0:
    x = 20 * round(x/20)
    y = random.randint(40, SCREEN_HEIGHT - (SEGMENT_SIZE * 8))
    if y % 20 != 0:
    y = 20 * round(y/20)
    if self.is_position_valid(x, y, self.mushrooms):
    self.mushrooms.add(Mushroom(self, 'level1', x, y))

    self.advance_level()
    self.last_update_time = pg.time.get_ticks()

    self.player = Player(self, self.sprite_round, (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50))

    # Centipede initialization
    if self.round % 5 == 0:
    self.total_centipedes += 1
    if self.total_centipedes > 4:
    self.total_centipedes = 1
    self.base_centipede_speed += 1
    for i in range(self.total_centipedes):
    x = random.randrange(SCREEN_WIDTH // 2, SCREEN_WIDTH - SEGMENT_SIZE, 20)
    y = random.randrange(-60, -40, 20)
    self.centipedes.add(Centipede(self, self.sprite_round, x, y, self.length, self.base_centipede_speed))
    if self.loners > 0:
    for _ in range(self.loners):
    x = random.randrange(SCREEN_WIDTH // 2, SCREEN_WIDTH - SEGMENT_SIZE, 20)
    y = random.randrange(-60, -40, 20)
    self.centipedes.add(Centipede(self, self.sprite_round, x, y, 1, self.base_centipede_speed))

    self.start_of_game = False

    self.run()

    def run(self):
    self.playing = True
    while self.playing:
    self.mx, self.my = pg.mouse.get_pos()
    self.dt = self.clock.tick(60) / 1000
    self.events()
    self.update()
    self.draw()

    def quit(self):
    pg.quit()
    sys.exit()

    def update(self):
    if not self.paused:
    if not getattr(self, "enemies_paused", False):
    # Update player
    self.player.update(self.dt)
    for mushroom in self.mushrooms:
    if self.player.rect.colliderect(mushroom.rect):
    # Adjust player position to prevent overlap
    if self.player.rect.right > mushroom.rect.left and self.player.rect.left < mushroom.rect.left:
    self.player.rect.right = mushroom.rect.left
    elif self.player.rect.left < mushroom.rect.right and self.player.rect.right > mushroom.rect.right:
    self.player.rect.left = mushroom.rect.right
    if self.player.rect.bottom > mushroom.rect.top and self.player.rect.top < mushroom.rect.top:
    self.player.rect.bottom = mushroom.rect.top
    elif self.player.rect.top < mushroom.rect.bottom and self.player.rect.bottom > mushroom.rect.bottom:
    self.player.rect.top = mushroom.rect.bottom
    # Update bullets with delta time
    for bullet in self.bullets:
    bullet.update(self.dt)
    # Update centipedes, mushrooms
    self.mushrooms.update()
    self.centipedes.update(self.mushrooms, self.dt)

    def draw(self):
    pg.display.set_caption(f"FPS: {self.clock.get_fps():.2f}" + " " + f"Delta: {self.dt * 1000:.2f}")

    # Clear virtual surface
    self.virtual_surface.fill((0, 0, 0))

    # Draw everything on the virtual surface
    self.player.draw(self.virtual_surface)
    self.bullets.draw(self.virtual_surface)
    self.mushrooms.draw(self.virtual_surface)
    for centipede in self.centipedes:
    centipede.draw(self.virtual_surface)

    # Game pause overlay
    if self.paused:
    self.virtual_surface.blit(self.dim_screen, (0, 0))

    # Scale virtual surface to the actual screen size
    scaled_surface = pg.transform.scale(self.virtual_surface, (self.SCREEN_WIDTH, self.SCREEN_HEIGHT))
    self.screen.blit(scaled_surface, (0, 0))

    # Calculate the largest fit while maintaining aspect ratio
    scale = min(self.SCREEN_WIDTH / self.VIRTUAL_WIDTH, self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT)
    scaled_width = int(self.VIRTUAL_WIDTH * scale)
    scaled_height = int(self.VIRTUAL_HEIGHT * scale)
    offset_x = (self.SCREEN_WIDTH - scaled_width) // 2
    offset_y = (self.SCREEN_HEIGHT - scaled_height) // 2

    # Scale virtual surface and add black bars
    scaled_surface = pg.transform.scale(self.virtual_surface, (scaled_width, scaled_height))
    self.screen.fill((0, 0, 0)) # Black bars
    self.screen.blit(scaled_surface, (offset_x, offset_y))

    pg.display.flip()

    def events(self):
    for event in pg.event.get():
    if event.type == pg.QUIT:
    self.quit()

    if event.type == pg.KEYDOWN:
    if event.key == pg.K_ESCAPE:
    self.quit()

    if event.type == pg.VIDEORESIZE:
    # Adjust physical screen dimensions
    self.SCREEN_WIDTH, self.SCREEN_HEIGHT = event.size
    self.scale_x = self.SCREEN_WIDTH / self.VIRTUAL_WIDTH
    self.scale_y = self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT
    self.screen = pg.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pg.RESIZABLE)

    if event.type == pg. KEYUP:
    if event.key == pg.K_p:
    self.paused = not self.paused

    if event.type == pg.JOYBUTTONDOWN: # 0=A, 1=B, 2=X, 3=Y, 4=FTL, 5=FTR, 6=Back, 7=Start
    if event.button == 7:
    self.paused = not self.paused

    # Check if mouse touches the sprite
    mouse_pos = pg.mouse.get_pos()
    mouse_scaled = self.adjusted_mouse_position(mouse_pos)
    for mushroom in self.mushrooms:
    if mushroom.rect.collidepoint(mouse_scaled):
    mushroom.is_poisoned = True
    mushroom.update_mushroom()

    if self.playing and not self.centipedes:
    self.end_round_mushroom_cleanup()

    # Check for bullet going off-screen
    for bullet in self.bullets:
    if bullet.rect.bottom < 0:
    bullet.kill()
    self.player.can_shoot = True
    break

    # Check for bullet collisions with mushrooms
    for bullet in self.bullets:
    for mushroom in self.mushrooms:
    if bullet.rect.colliderect(mushroom.rect):
    mushroom.take_hit()
    bullet.kill()
    self.player.can_shoot = True
    break

    # Check for bullet collision with centipede
    for bullet in self.bullets:
    for centipede in self.centipedes: # Iterate safely over centipedes
    for i, segment in enumerate(centipede.segments[:]): # Track index for referencing front segment
    if bullet.rect.colliderect(segment["rect"]):
    # Remove the bullet after collision
    bullet.kill()

    if segment["x_direction"] == 1:
    if self.is_position_valid(segment["rect"].x + 20, segment["rect"].y, self.mushrooms):
    self.mushrooms.add(Mushroom(self, self.sprite_round, segment["rect"].x + 20, segment["rect"].y))
    else:
    if self.is_position_valid(segment["rect"].x - 20, segment["rect"].y, self.mushrooms):
    self.mushrooms.add(Mushroom(self, self.sprite_round, segment["rect"].x - 20, segment["rect"].y))

    # Determine the index where the segment was hit
    hit_index = centipede.segments.index(segment)

    # Split the centipede into two groups
    front_segments = centipede.segments[:hit_index] # Segments before the hit
    back_segments = centipede.segments[hit_index + 1:] # Segments after the hit

    # Remove the hit segment completely
    centipede.segments.pop(hit_index) # Avoid rogue behavior

    # Handle the front segments
    if front_segments:
    # Assign the first remaining segment as the head
    front_segments[0]["is_head"] = True
    # Ensure it retains the correct direction
    front_segments[0]["x_direction"] = segment["x_direction"]
    front_segments[0]["y_direction"] = segment["y_direction"]
    centipede.segments = front_segments # Update the original centipede
    else:
    # Remove the centipede if there are no front segments
    self.centipedes.remove(centipede)

    # Handle the back segments
    if back_segments:
    # Assign the new head for the back segment group
    back_segments[0]["is_head"] = True
    # Ensure it retains the correct direction
    back_segments[0]["x_direction"] = segment["x_direction"]
    back_segments[0]["y_direction"] = segment["y_direction"]

    # Create a new centipede for the back group
    new_centipede = Centipede(
    self,
    self.sprite_round,
    back_segments[0]["rect"].x,
    back_segments[0]["rect"].y,
    len(back_segments),
    centipede.base_speed # Retain the original speed
    )
    new_centipede.segments = back_segments # Assign new split segments
    self.centipedes.add(new_centipede)

    # Allow player shoot again
    self.player.can_shoot = True

    break

    # Check for player collision with centipede segments
    for centipede in self.centipedes:
    for segment in centipede.segments:
    if self.player.rect.colliderect(segment["rect"]):
    self.lose_life() # Reduce player's life

    # Handle collisions between centipede and mushrooms
    for centipede in self.centipedes:
    for segment in centipede.segments:
    for mushroom in self.mushrooms:
    if segment["rect"].colliderect(mushroom.rect):
    if not mushroom.is_poisoned:
    if not segment["poisoned"]:
    if segment["x_direction"] == 1 and segment["rect"].right >= mushroom.rect.left:
    segment["x_direction"] = -1
    segment["move_down_next_frame"] = True
    elif segment["x_direction"] == -1 and segment["rect"].left <= mushroom.rect.right:
    segment["x_direction"] = 1
    segment["move_down_next_frame"] = True
    else:
    segment["poisoned"] = True

    def end_round_mushroom_cleanup(self):

    # Step 1: Freeze enemy updates
    self.enemies_paused = True

    # Step 2: Gather all objects to remove (poisoned mushrooms and enemies)
    poisoned_mushrooms = [m for m in self.mushrooms if m.is_poisoned]

    # Combine all elements into a single list for cleanup
    objects_to_remove = poisoned_mushrooms

    # Step 3: Process each object with a delay
    for obj in objects_to_remove:
    if isinstance(obj, pg.sprite.Sprite):
    obj.kill()

    self.draw()
    pg.display.flip()
    pg.time.delay(200) # delay between removing

    # Restore hit mushrooms back to full
    hit_mushrooms = [m for m in self.mushrooms if m.health > 0]
    for mushroom in hit_mushrooms:
    mushroom.health = 0
    mushroom.update_mushroom()
    self.draw()
    pg.display.flip()
    pg.time.delay(200) # delay between restoring

    self.enemies_paused = False

    self.new()


    if __name__ == '__main__':
    g = Game()
    g.new()

    Continue reading...

Compartilhe esta Página