Search code examples
pythonpygamechess

Is there an effiecient way of making a function to drag and drop multiple png's?


I'm making a chess game, but I'm completely stuck on the drag and drop element, there's a few guides out there but they're all either dragging shapes, or only dragging one image.

I've tried several variants of code, but all were 50+ lines just to move one .png and most were incredibly inefficient

pygame.init()

pygame.display.set_caption("Python Chess")

clock = pygame.time.Clock()
red = (213,43,67)
chew = pygame.image.load("chew.png")

gameDisplay.fill(red)
gameDisplay.blit(chew, (400, 400))
pygame.display.update()

drag = 0
if pygame.MOUSEBUTTONDOWN:
    drag = 1
if pygame.MOUSEBUTTONUP:
    drag = 0

gameExit = False

while not gameExit:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            gameExit = True

Image simply doesn't drag.


Solution

  • Let's walk through this step by step.

    Step 1: Let's start with a basic skeleton of every pygame game:

    import pygame
    
    def main():
        screen = pygame.display.set_mode((640, 480))
        clock = pygame.time.Clock()
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
            screen.fill(pygame.Color('grey'))
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    We create a window and then start a loop to listen for events and drawing the window.

    enter image description here

    So far, so good. Nothing to see here, let's move on.


    Step 2: a chess board

    So, we want a chess game. So we need a board. We create a list of lists to represent our board, and we create a Surface that draws our board on the screen. We want to always seperate our game's state from the actual drawing functions, so we create a board variable and a board_surf.

    import pygame
    
    TILESIZE = 32
    
    def create_board_surf():
        board_surf = pygame.Surface((TILESIZE*8, TILESIZE*8))
        dark = False
        for y in range(8):
            for x in range(8):
                rect = pygame.Rect(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(board_surf, pygame.Color('black' if dark else 'white'), rect)
                dark = not dark
            dark = not dark
        return board_surf
    
    def create_board():
        board = []
        for y in range(8):
            board.append([])
            for x in range(8):
                board[y].append(None)
        return board
    
    def main():
        screen = pygame.display.set_mode((640, 480))
        board = create_board()
        board_surf = create_board_surf()
        clock = pygame.time.Clock()
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
            screen.fill(pygame.Color('grey'))
            screen.blit(board_surf, (0, 0))
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    enter image description here


    Step 3: Where's the mouse?

    We need to know which piece we want to select, so we have to translate the screen coordinates (where's the mouse relative to the window?) to the world coordinates (which square of the board is the mouse pointing to?).

    So if the board is not located at the origin (the position (0, 0)), we also have to take this offset into account.

    Basically, we have to substract that offset (which is the position of the board on the screen) from the mouse position (so we have the mouse position relative to the board), and divide by the size of the squares.

    To see if this works, let's draw a red rectangle on the selected square.

    import pygame
    
    TILESIZE = 32
    BOARD_POS = (10, 10)
    
    def create_board_surf():
        board_surf = pygame.Surface((TILESIZE*8, TILESIZE*8))
        dark = False
        for y in range(8):
            for x in range(8):
                rect = pygame.Rect(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(board_surf, pygame.Color('black' if dark else 'white'), rect)
                dark = not dark
            dark = not dark
        return board_surf
    
    def create_board():
        board = []
        for y in range(8):
            board.append([])
            for x in range(8):
                board[y].append(None)
        return board
    
    def get_square_under_mouse(board):
        mouse_pos = pygame.Vector2(pygame.mouse.get_pos()) - BOARD_POS
        x, y = [int(v // TILESIZE) for v in mouse_pos]
        try: 
            if x >= 0 and y >= 0: return (board[y][x], x, y)
        except IndexError: pass
        return None, None, None
    
    def main():
        screen = pygame.display.set_mode((640, 480))
        board = create_board()
        board_surf = create_board_surf()
        clock = pygame.time.Clock()
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
    
            piece, x, y = get_square_under_mouse(board)
    
            screen.fill(pygame.Color('grey'))
            screen.blit(board_surf, BOARD_POS)
    
            if x != None:
                rect = (BOARD_POS[0] + x * TILESIZE, BOARD_POS[1] + y * TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(screen, (255, 0, 0, 50), rect, 2)
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    enter image description here


    Step 4: Let's draw some pieces

    Chess is boring without some pieces to move around, so let's create some pieces.

    I just use a SysFont to draw some text instead of using real images, so everyone can just copy/paste the code and run it immediately.

    We store a tuple (color, type) in the nested board list. Also, let's use some other colors for our board.

    import pygame
    
    TILESIZE = 32
    BOARD_POS = (10, 10)
    
    def create_board_surf():
        board_surf = pygame.Surface((TILESIZE*8, TILESIZE*8))
        dark = False
        for y in range(8):
            for x in range(8):
                rect = pygame.Rect(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(board_surf, pygame.Color('darkgrey' if dark else 'beige'), rect)
                dark = not dark
            dark = not dark
        return board_surf
    
    def get_square_under_mouse(board):
        mouse_pos = pygame.Vector2(pygame.mouse.get_pos()) - BOARD_POS
        x, y = [int(v // TILESIZE) for v in mouse_pos]
        try: 
            if x >= 0 and y >= 0: return (board[y][x], x, y)
        except IndexError: pass
        return None, None, None
    
    def create_board():
        board = []
        for y in range(8):
            board.append([])
            for x in range(8):
                board[y].append(None)
    
        for x in range(0, 8):
            board[1][x] = ('black', 'pawn')
        for x in range(0, 8):
            board[6][x] = ('white', 'pawn') 
    
        return board
    
    def draw_pieces(screen, board, font):
        for y in range(8):
            for x in range(8): 
                piece = board[y][x]
                if piece:
                    color, type = piece
                    s1 = font.render(type[0], True, pygame.Color(color))
                    s2 = font.render(type[0], True, pygame.Color('darkgrey'))
                    pos = pygame.Rect(BOARD_POS[0] + x * TILESIZE+1, BOARD_POS[1] + y * TILESIZE + 1, TILESIZE, TILESIZE)
                    screen.blit(s2, s2.get_rect(center=pos.center).move(1, 1))
                    screen.blit(s1, s1.get_rect(center=pos.center))
    
    def draw_selector(screen, piece, x, y):
        if piece != None:
            rect = (BOARD_POS[0] + x * TILESIZE, BOARD_POS[1] + y * TILESIZE, TILESIZE, TILESIZE)
            pygame.draw.rect(screen, (255, 0, 0, 50), rect, 2)
    
    def main():
        pygame.init()
        font = pygame.font.SysFont('', 32)
        screen = pygame.display.set_mode((640, 480))
        board = create_board()
        board_surf = create_board_surf()
        clock = pygame.time.Clock()
        while True:
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
    
            piece, x, y = get_square_under_mouse(board)
    
            screen.fill(pygame.Color('grey'))
            screen.blit(board_surf, BOARD_POS)
            draw_pieces(screen, board, font)
            draw_selector(screen, piece, x, y)
    
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    enter image description here


    Step 5: Drag'n'Drop

    For drag and drop we need two things:

    • we have to change the state of your game (going into the "drag-mode")
    • eventhandling to enter and leave the "drag-mode"

    It's actually not that complicated. To enter the "drag-mode", we just set a variable (selected_piece) when the MOUSEBUTTONDOWN event occurs. Since we already have the get_square_under_mouse function, it's easy to know if there's actually a piece under the mouse cursor.

    if selected_piece is set, we draw a line and the piece under the mouse cursor, and we keep track of the current square under the cursor in case the MOUSEBUTTONUP event occurs. If that's the case, we swap the position of the piece in our board.

    import pygame
    
    TILESIZE = 32
    BOARD_POS = (10, 10)
    
    def create_board_surf():
        board_surf = pygame.Surface((TILESIZE*8, TILESIZE*8))
        dark = False
        for y in range(8):
            for x in range(8):
                rect = pygame.Rect(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(board_surf, pygame.Color('darkgrey' if dark else 'beige'), rect)
                dark = not dark
            dark = not dark
        return board_surf
    
    def get_square_under_mouse(board):
        mouse_pos = pygame.Vector2(pygame.mouse.get_pos()) - BOARD_POS
        x, y = [int(v // TILESIZE) for v in mouse_pos]
        try: 
            if x >= 0 and y >= 0: return (board[y][x], x, y)
        except IndexError: pass
        return None, None, None
    
    def create_board():
        board = []
        for y in range(8):
            board.append([])
            for x in range(8):
                board[y].append(None)
    
        for x in range(0, 8):
            board[1][x] = ('black', 'pawn')
        for x in range(0, 8):
            board[6][x] = ('white', 'pawn') 
    
        return board
    
    def draw_pieces(screen, board, font, selected_piece):
        sx, sy = None, None
        if selected_piece:
            piece, sx, sy = selected_piece
    
        for y in range(8):
            for x in range(8): 
                piece = board[y][x]
                if piece:
                    selected = x == sx and y == sy
                    color, type = piece
                    s1 = font.render(type[0], True, pygame.Color('red' if selected else color))
                    s2 = font.render(type[0], True, pygame.Color('darkgrey'))
                    pos = pygame.Rect(BOARD_POS[0] + x * TILESIZE+1, BOARD_POS[1] + y * TILESIZE + 1, TILESIZE, TILESIZE)
                    screen.blit(s2, s2.get_rect(center=pos.center).move(1, 1))
                    screen.blit(s1, s1.get_rect(center=pos.center))
    
    def draw_selector(screen, piece, x, y):
        if piece != None:
            rect = (BOARD_POS[0] + x * TILESIZE, BOARD_POS[1] + y * TILESIZE, TILESIZE, TILESIZE)
            pygame.draw.rect(screen, (255, 0, 0, 50), rect, 2)
    
    def draw_drag(screen, board, selected_piece, font):
        if selected_piece:
            piece, x, y = get_square_under_mouse(board)
            if x != None:
                rect = (BOARD_POS[0] + x * TILESIZE, BOARD_POS[1] + y * TILESIZE, TILESIZE, TILESIZE)
                pygame.draw.rect(screen, (0, 255, 0, 50), rect, 2)
    
            color, type = selected_piece[0]
            s1 = font.render(type[0], True, pygame.Color(color))
            s2 = font.render(type[0], True, pygame.Color('darkgrey'))
            pos = pygame.Vector2(pygame.mouse.get_pos())
            screen.blit(s2, s2.get_rect(center=pos + (1, 1)))
            screen.blit(s1, s1.get_rect(center=pos))
            selected_rect = pygame.Rect(BOARD_POS[0] + selected_piece[1] * TILESIZE, BOARD_POS[1] + selected_piece[2] * TILESIZE, TILESIZE, TILESIZE)
            pygame.draw.line(screen, pygame.Color('red'), selected_rect.center, pos)
            return (x, y)
    
    def main():
        pygame.init()
        font = pygame.font.SysFont('', 32)
        screen = pygame.display.set_mode((640, 480))
        board = create_board()
        board_surf = create_board_surf()
        clock = pygame.time.Clock()
        selected_piece = None
        drop_pos = None
        while True:
            piece, x, y = get_square_under_mouse(board)
            events = pygame.event.get()
            for e in events:
                if e.type == pygame.QUIT:
                    return
                if e.type == pygame.MOUSEBUTTONDOWN:
                    if piece != None:
                        selected_piece = piece, x, y
                if e.type == pygame.MOUSEBUTTONUP:
                    if drop_pos:
                        piece, old_x, old_y = selected_piece
                        board[old_y][old_x] = 0
                        new_x, new_y = drop_pos
                        board[new_y][new_x] = piece
                    selected_piece = None
                    drop_pos = None
    
            screen.fill(pygame.Color('grey'))
            screen.blit(board_surf, BOARD_POS)
            draw_pieces(screen, board, font, selected_piece)
            draw_selector(screen, piece, x, y)
            drop_pos = draw_drag(screen, board, selected_piece, font)
    
            pygame.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
    

    enter image description here

    Of course there's a lot that can be improved (like using better datatypes than tuples, extract common logic into functions etc), but this should give you a good start on how to implement such things.

    Always keep in mind:

    • write a single game loop that handles events, game logic, and drawing
    • make sure to only call pygame.display.flip once per frame
    • seperate your game state from your drawing functions
    • never call time.sleep or pygame.time.wait
    • use the build-in classes like Vector2 and Rect, they'll make your live easier (I didn't the Sprite class in this code, but it's also very usefull)
    • use functions to clean up your code
    • avoid global variables, except for constants