1

I'm trying to let the user press and hold the right mouse button to make squares, then move them around with the mouse by left clicking. I was able to make a sprite that could be moved around with left click and to make new sprites when the user right clicks:

    while True:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            pg.quit()
        elif event.type == pg.KEYDOWN:
            if event.key == pg.K_ESCAPE:
                quit()
        elif event.type == pg.MOUSEBUTTONDOWN:
            if event.button == 1:
                if x <= m_x <= x + tilesize_w and y <= m_y <= y + tilesize_h:
                    m_xrom = m_x - x
                    m_yrom = m_y - y
                    down = True
            elif event.button == 3:
                m_xrom = m_x
                m_yrom = m_y
        elif event.type == pg.MOUSEBUTTONUP:
            if event.button == 1:
                down = False
            elif event.button == 3:
                tiles.add(Player((m_xrom,m_yrom), 876567, abs(m_x - m_xrom), abs(m_y - m_yrom))
m_x,m_y = pg.mouse.get_pos()

    if down:
        x = m_x - m_xrom
        y = m_y - m_yrom  
    
    color = 279348
    lvl = Level((x,y), color, tilesize_w, tilesize_h)
    lvl.run()

However I don't know how to the move the sprites that the user makes. I would like to be able to make several different user-made sprite that can move independently.

Jan Wilamowski
  • 3,308
  • 2
  • 10
  • 23

1 Answers1

0

This is a somewhat simple process once the code "saves" the newly drawn boxes somehow. I think a good way to do this is to create a PyGame sprite object for each new box, and keep them in a sprite Group.

At the end of a "mouse drag" operation, we have a start-position - the mouse-xy of the MOUSEBUTTONDOWN event, and the end-position is the mouse-xy of the MOUSEBUTTONUP event. With a bit of fiddling to handle negative-ranges, we calculate a PyGame rectangle object (Rect), using it as the basis for a BoxSprite:

class BoxSprite( pygame.sprite.Sprite ):
    """ Rectangular, coloured box-sprite """
    def __init__( self, drag_rect ):
        super().__init__()
        self.image = pygame.Surface( ( drag_rect.width, drag_rect.height ) )
        self.rect  = drag_rect.copy()
        self.image.fill( GREEN )

Looking at the BoxSprite, it's really not much more than a coloured Surface, sized and positioned over the original drag_rect.

Once we have a Sprite to hold the box, PyGame has the super-useful Sprite Group class. This can be used to hold all the BoxSprites, and provides easy drawing, membership and collision functions.

So now the code can "remember" every box, how can we determine if the user clicked on one? An easy way to do this is to test if the click mouse-position is inside any of the group members. A simple approach would be to iterate through every sprite in the group, checking each sprite's Rect. Or if we can somehow treat the mouse-click as a sprite-object, we can use the existing Sprite Group's "Sprite Vs Sprite-Group" collision-test function. This operation returns a handy list of collided sprites from the group. Nice.

Once you have this list, how you treat them is up to you. In the example below I implemented a drag-existing sprite, so clicking on an existing Box "grabs" it until the mouse button is released.

demo video

import pygame

# window sizing
WIDTH   = 500
HEIGHT  = 500
MAX_FPS = 60

BLACK = (  0,   0,  0)
RED   = (255,   0,  0)
GREEN = ( 20, 200, 20)
YELLOW= ( 255,255,  0)

pygame.init()
window = pygame.display.set_mode( ( WIDTH, HEIGHT ) )
pygame.display.set_caption( "Box Dragging Demo" )

                
###
### This is a sprite we use to hold each rectangular box the User creates.   
### Once the user completes an on-screen draw operation, the box gets stored in 
### an instance of a BoxSprite
### 
class BoxSprite( pygame.sprite.Sprite ):
    """ Rectangular, coloured box-sprite """
    def __init__( self, drag_rect ):
        super().__init__()
        self.image = pygame.Surface( ( drag_rect.width, drag_rect.height ) )
        self.rect  = drag_rect.copy()
        self.image.fill( GREEN )

    def moveBy( self, dx, dy ):
        """ Reposition the sprite """
        self.rect.x += dx
        self.rect.y += dy

    def setColour( self, c ):
        self.image.fill( c )


###
### Sprite Groups have no function to test collision against a point
### So this object holds a position as a "dummy" 1x1 sprite, which
### can be used efficiently in collision functions.
###
class CollidePoint( pygame.sprite.Sprite ):
    """ Simple single-point Sprite for easy collisions """
    def __init__( self, point=(-1,-1) ):
        super().__init__()
        self.image = None  # this never gets drawn
        self.rect  = pygame.Rect( point, (1,1) )

    def moveTo( self, point ):
        """ Reposition the point """
        self.rect.topleft = point


# Sprite Group to hold all the user-created sprites
user_boxes_group = pygame.sprite.Group()    # initially empty

rect_start = ( -1, -1 ) # start position of drawn rectangle
drag_start = ( -1, -1 ) # start position of a mouse-drag

mouse_click = CollidePoint()  # create a sprite around the mouse-click for easier collisions
clicked_boxes = []            # the list of sprites being clicked and/or dragged

clock = pygame.time.Clock()   # used to govern the max MAX_FPS

### MAIN
exiting = False
while not exiting:

    # Handle events
    mouse_pos = pygame.mouse.get_pos()
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            exiting = True

        elif ( event.type == pygame.MOUSEBUTTONDOWN ):
            # MouseButton-down signals beginning a drag or a draw

            # Did the user click on an existing sprite?
            mouse_click.moveTo( mouse_pos )
            clicked_boxes = pygame.sprite.spritecollide( mouse_click, user_boxes_group, False )  # get the list of boxes clicked (if any)

            # was anything clicked?
            if ( len( clicked_boxes ) > 0 ):
                drag_start = mouse_pos    # yes, begin a drag operation
                for box in clicked_boxes:
                    box.setColour( YELLOW )
            else: 
                # Nothing clicked, not already drawing, start new rectangle-draw
                rect_start = pygame.mouse.get_pos()

        elif ( event.type == pygame.MOUSEBUTTONUP ):            
            # MouseButton-up signals both drag is complete, and draw is complete
            # Was the user dragging any sprites?
            if ( len( clicked_boxes ) > 0 ):
                # drag is over, drop anything we were dragging
                for box in clicked_boxes:
                    box.setColour( GREEN )
                drag_start = ( -1, -1 )      
                clicked_boxes = []

            # Was the user drawing a rectangle?
            elif ( rect_start != ( -1, -1 ) ):
                # Rects are always defined from a top-left point
                # So swap the points if the user dragged up
                if ( rect_start > mouse_pos ):
                    swapper = ( mouse_pos[0], mouse_pos[1] )
                    mouse_pos = rect_start
                    rect_start = swapper

                # create a new sprite from the drag
                new_width  = abs( mouse_pos[0] - rect_start[0] )
                new_height = abs( mouse_pos[1] - rect_start[1] )
                if ( new_width > 0 and new_height > 0 ):
                    new_sprite = BoxSprite( pygame.Rect( rect_start, ( new_width, new_height ) ) )
                    user_boxes_group.add( new_sprite )
                rect_start = ( -1, -1 )  # done drawing
                
        elif ( event.type == pygame.MOUSEMOTION ):
            # The mouse is moving, so move anything we're dragging along with it
            # Note that we're moving the boxes bit-by-bit, not a start->final move
            if ( len( clicked_boxes ) > 0 ):
                x_delta = mouse_pos[0] - drag_start[0]
                y_delta = mouse_pos[1] - drag_start[1]
                for box in clicked_boxes:
                    box.moveBy( x_delta, y_delta )
                drag_start = mouse_pos


    # Handle keys
    keys = pygame.key.get_pressed()
    if ( keys[pygame.K_ESCAPE] ):
        exiting = True

    # paint the window
    window.fill( BLACK )             # paint background
    user_boxes_group.draw( window )  # paint the sprites

    # If we're already dragging a box, paint a target-rectangle
    if ( rect_start != ( -1, -1 ) ):
        # Use a lines instead of a Rect, so we don't have to handle width/height co-ordinate position issues 
        box = [ rect_start, ( mouse_pos[0], rect_start[1] ), mouse_pos, ( rect_start[0], mouse_pos[1] ) ]
        pygame.draw.lines( window, RED, True, box, 1 )

    pygame.display.flip()
    clock.tick( MAX_FPS )

pygame.quit()
Kingsley
  • 14,398
  • 5
  • 31
  • 53
  • Thank you, I wouldn't have thought to make a sprite for the mouse that collides with the sprites. It works very well, however I'm confused why the display.flip() function is necessary. – Elijah Peck Jun 09 '22 at 02:19
  • @ElijahPeck - Maybe the `.flip()` isn't necessary, and you could call `.update()` instead - https://stackoverflow.com/questions/29314987/difference-between-pygame-display-update-and-pygame-display-flip# It depends on your circumstances. – Kingsley Jun 09 '22 at 02:28
  • Your right, it works the same calling update instead, thanks – Elijah Peck Jun 09 '22 at 04:24
  • Now that I can add new sprites, how do I call the attributes of the sprite so you can manipulate them. Like getting the position, height, width, and color. – Elijah Peck Jun 09 '22 at 16:37
  • @ElijahPeck - Well you can add your own member functions to the `BoxSprite` class, that's the most correct way. For example, see the `BoxSprite.MoveTo()` function. The Sprite Group behaves a lot like a python list too, so the code can iterate and find on this too. But if you just want quick and dodgy access, you can just use the `BoxSprite.image` and `BoxSprite.rect` directly. But doing so will eventually cause unwanted complexity in the long-term. – Kingsley Jun 09 '22 at 23:21