0
mini_map = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 1, _, 1, 1, _, _, _, _, _, 1, 1, _, _, _, 1],
    [1, _, _, _, _, 1, 1, _, _, _, 1, 1, 1, _, _, _, 1, 1, _, _, _, _, _, 1, 1, 1, _, _, 1, 1, _, 1],
    [1, _, _, _, _, _, 1, _, _, _, _, _, 1, _, _, _, _, _, 1, 1, 1, _, _, _, _, _, 1, _, _, _, _, 1],
    [1, _, 1, _, _, _, _, _, _, _, 1, 1, _, _, _, 1, 1, 1, 1, _, _, _, _, _, 1, 1, _, _, 1, 1, _, 1],
    [1, _, _, 1, 1, _, _, _, 1, 1, _, _, _, 1, 1, _, _, _, 1, _, 1, _, 1, _, 1, 1, 1, _, _, 1, _, 1],
    [1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, 1],
    [1, _, 1, 1, 1, _, _, _, _, 1, 1, 1, _, _, _, _, _, 1, _, _, _, 1, _, _, _, _, 1, _, _, _, _, 1],
    [1, _, _, _, _, _, _, _, _, _, 1, 1, 1, _, _, 1, 1, _, _, _, _, _, _, _, _, 1, _, _, _, 1, _, 1],
    [1, _, _, _, _, _, _, 1, 1, _, _, _, 1, _, _, 1, _, _, _, _, _, 1, 1, _, _, _, _, 1, _, _, _, 1],
    [1, _, _, _, 1, 1, _, _, 1, _, _, _, 1, 1, _, _, _, _, _, 1, 1, _, _, 1, 1, _, _, 1, _, _, _, 1],
    [1, _, _, 1, _, _, _, 1, 1, _, _, _, _, _, _, 1, 1, 1, 1, 1, _, _, _, _, _, _, 1, _, 1, _, _, 1],
    [1, _, _, _, _, _, 1, _, _, _, _, _, 1, _, _, _, _, 1, _, _, _, _, 1, _, _, _, _, 1, _, _, _, 1],
    [1, _, 1, _, _, _, _, _, _, _, 1, 1, _, _, _, 1, _, _, _, _, _, _, _, _, 1, _, _, 1, _, _, _, 1],
    [1, _, _, 1, 1, _, _, _, 1, 1, _, _, _, 1, 1, _, _, 1, 1, _, _, _, _, _, _, 1, 1, _, _, _, _, 1],
    [1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, _, 1, _, _, 1],
    [1, _, _, _, _, _, _, _, 1, 1, _, _, _, 1, 1, 1, _, _, _, 1, 1, _, 1, _, _, _, 1, _, _, _, _, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]
def check_wall(self, x, y):
    return (x, y) not in self.game.map.world_map

def check_wall_collision(self, dx, dy: object):
    if self.check_wall(int(self.x + dx), int(self.y)):
       self.x += dx
    if self.check_wall(int(self.x), int(self.y + dy)):
       self.y += dy

Need the collision system to work with a 32 X 18 mini-map but i am starting to think it's just not compatible. I am trying to make a 3D style game with python utilizing what i've learned thus far as i am new to python, started in june this year. Already worked on a basic project but using raycasting to create a 3D style game in python and it's on a whole other level but definitely fun. I also think the problem might be update related as i am running pygame 2.1.2 and python 3.10.7 and certain syntax's have been altered in one way or another.

sam60420
  • 1
  • 1
  • With 32 x 18, the number of columns and rows is not equal. Have you accidentally swapped rows and columns somewhere? – Rabbid76 Oct 31 '22 at 21:15
  • @Rabbid76 Oh so it has to be equal, that explains it bro. Thanks, you just fixed my problem. I thought it didn't matter if it were equal or not – sam60420 Oct 31 '22 at 21:17
  • collision system still doesn't work, will try to figure it out – sam60420 Oct 31 '22 at 21:25
  • How much does `dx` change by? Say `dx` is 2, that makes it possible to "jump" the wall, because you need to test the *path* through to the destination, and stop the movement at the point where it's blocked. Ah, I'll just write an answer. – Kingsley Oct 31 '22 at 22:12
  • collision system still doesn't work – sam60420 Oct 31 '22 at 22:13
  • @Kingsley ah yes please do lol but lemme guess you saying there might be an error with the x and y axis and it's variable. I was thinking the same thing but honestly still not that good at defining functions. – sam60420 Oct 31 '22 at 22:18

1 Answers1

0

No you can have any size map, limited only by your system memory.

Looking at your code. The collision algorithm only checks the destination is clear. If your ray moves more than 1-unit at a time, the collision routine ignores everything in between.

One way to implement this is to determine the set of steps between the current location, and the target location. Then check each position-step in-turn, stopping when a collision is found.

An easy-ish way to implement this is to use Bresenman's Line Algorithm to generate the list of points between two map cell co-ordinates:

def getLinePathTo( start_pos, end_pos ):
    """ Using Bresenham's Line algorithm, get a list of all the points
        along a straight-line path from the start to the end (inclusive).
        Note that this includes diagonal movement """
    x0, y0 = start_pos
    x1, y1 = end_pos
    points = []

    # ref: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#Algorithm_for_integer_arithmetic
    dx = abs(x1 - x0)
    if x0 < x1:
        sx = 1
    else:
        sx = -1
    dy = -abs(y1 - y0)
    if y0 < y1:
        sy = 1
    else:
        sy = -1
    error = dx + dy

    while True:
        points.append( ( x0,y0 ) )  # remember the point 
        if x0 == x1 and y0 == y1:
            break
        e2 = 2 * error
        if e2 >= dy:
            if x0 == x1:
                break
            error = error + dy
            x0 = x0 + sx
        if e2 <= dx:
            if y0 == y1:
                break
            error = error + dx
            y0 = y0 + sy

    if ( ( x0, y0 ) > ( x1, y1 ) ):
        points.reverse()   # ensure the path goes away from the player at (x0,y0)
    return points

If you used this function on your mini_map from 6,6 to 13,9 (through a wall), it gives the set of cells [ (6,6), (6,7), (7,7), (7,8), (8,8) ... (13,9) ].

We then iterate through the list of points, checking and moving:

movement_path = getLinePathTo( ray_start_position, target_position )

for point in movement_path:
    map_x, map_y = point
    if ( mini_map[map_y][map_x] != '_' ):
        break   # not OK to move into this map-cell, stop moving
    else:
        player_position = point   # empty cell, player can move

When the iterator gets to the list item (8,8), this is a 1 cell, so the continuation clause fails, exiting the loop. The final position stays at (7,8), one step before the "wall".

Working Example Code:

import pygame

# Window size
WINDOW_WIDTH  = 320 * 4
WINDOW_HEIGHT = 180 * 4
GRID_WIDTH    = 9   # changed on map-load
GRID_HEIGHT   = 12

BLACK    = (  0,   0,   0)
GREY     = (200, 200, 200)
BROWN    = (135,  69,  31)
WHITE    = (255, 255, 255)
RED      = (220,   0,   0)



def getLinePathTo( start_pos, end_pos ):
    """ Using Bresenham's Line algorithm, get a list of all the points
        along a straight-line path from the start to the end (inclusive).
        Note that this includes diagonal movement """
    x0, y0 = start_pos
    x1, y1 = end_pos
    points = []

    # ref: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#Algorithm_for_integer_arithmetic
    dx = abs(x1 - x0)
    if x0 < x1:
        sx = 1
    else:
        sx = -1
    dy = -abs(y1 - y0)
    if y0 < y1:
        sy = 1
    else:
        sy = -1
    error = dx + dy
    
    while True:
        points.append( ( x0,y0 ) )
        if x0 == x1 and y0 == y1:
            break
        e2 = 2 * error
        if e2 >= dy:
            if x0 == x1:
                break
            error = error + dy
            x0 = x0 + sx
        if e2 <= dx:
            if y0 == y1:
                break
            error = error + dx
            y0 = y0 + sy

    if ( ( x0, y0 ) > ( x1, y1 ) ):
        points.reverse()   # ensure the path goes away from the player (x0,y0)
    return points



class Player:
    """ Implements a little 'i' shaped icon at a known map location """
    def __init__( self, start_pos, colour=BROWN ):
        self.location   = start_pos  # map-cell (x,y)
        self.colour     = colour
        self.derring_do = 70
        self.alacrity   = 80

    def getPosition( self ):
        return self.location

    def moveTo( self, position ):
        self.location = position

    def draw( self, window ):
        # Player is basically an 'i', really badly drawn
        pixel_x = self.location[0] * GRID_WIDTH
        pixel_y = self.location[1] * GRID_HEIGHT
        centre_x = pixel_x + ( GRID_WIDTH // 2 )  # centre of character cell
        third_y  = GRID_HEIGHT / 3
        # TODO render to internal surface (once)
        # head
        head_centre = ( centre_x, int( pixel_y + ( third_y / 2 ) ) )     # head fills top third of cell (chibi!)
        pygame.draw.circle( window, self.colour, head_centre, int( GRID_WIDTH/3 ) )
        # body
        body_rect = [ pixel_x+ GRID_WIDTH//4, pixel_y + GRID_HEIGHT//2, GRID_WIDTH//2, GRID_HEIGHT//2 ]
        pygame.draw.rect( window, self.colour, body_rect )



class Map:
    """ A class to convert a mini-map to a drawable object """
    def __init__( self, cells, ground=GREY, wall=BLACK ):
        self.mini_map      = cells
        self.colour_ground = ground
        self.colour_wall   = wall
        self.render()   # create the map surface


    def render( self ):
        """ Create a surface containing the map.
            If the mini_map is bad, default to a single red cell """
        # Make the surface from the given mini_map
        global GRID_WIDTH, GRID_HEIGHT
        had_error = False
        try:
            map_width  = len( self.mini_map[0] ) 
            map_height = len( self.mini_map )     
            GRID_WIDTH = WINDOW_WIDTH // map_width    # resize the map grid to fill the window
            GRID_HEIGHT= WINDOW_HEIGHT // map_height
            print( "Map is %dx%d, cells %dx%d" % ( map_width, map_height, GRID_WIDTH, GRID_HEIGHT ) )
            self.surface = pygame.Surface( ( map_width * GRID_WIDTH, map_height * GRID_HEIGHT ), pygame.SRCALPHA )
        except:
            # fail-safe
            self.surface = pygame.Surface( ( GRID_WIDTH, GRID_HEIGHT ), pygame.SRCALPHA )
            self.surface.fill( RED )
            sys.stderr.write( "Bad map; empty?\n" )
            had_error = True
        self.rect = self.surface.get_rect( topleft=(0,0) )

        # Dictionary to map colours to map-tokens
        colours = { '1':self.colour_wall, '_':self.colour_ground }

        # Walk over the map, plotting rectangles for map-cells
        # It might be more efficient to fill the entire map with "ground" first
        for y in range( map_height ):
            pixel_y = y * GRID_HEIGHT
            for x in range( map_width ):
                pixel_x = x * GRID_WIDTH
                cell_rect = [ pixel_x, pixel_y, GRID_WIDTH, GRID_HEIGHT ]
                pygame.draw.rect( self.surface, colours[ self.mini_map[y][x] ], cell_rect )

    def getPixelPosition( self, pixel_pos ):
        """ Given a pixel position inside the map, return the map-grid coordinates.
            Returns ( -1, -1 ) if the position out-of-bounds """
        cell_x = -1
        cell_y = -1
        pixel_x, pixel_y = pixel_pos
        if ( pixel_x < self.rect.width ):
            cell_x = pixel_x // GRID_WIDTH
            if ( pixel_y < self.rect.height ):
                cell_y = pixel_y // GRID_HEIGHT
            else:
                cell_x = -1   # if one fails, both fail
        return ( cell_x, cell_y )
                
            

    def getPath( self, start_pos, end_pos ):
        """ Given a map cell-position, trace the up-to and obstruction.
            Returns a list of map-cell coordinates along the path       """
        map_cells = getLinePathTo( start_pos, end_pos )   # list of cells along path
        longest_path = []
        for xy in map_cells:
            cell_x, cell_y = xy
            #print( "MAP CELL[%d][%d] is [%s]" % ( cell_y, cell_x, self.mini_map[cell_y][cell_x] ) )
            if ( self.mini_map[cell_y][cell_x] != '_' ):  # '_' means "empty"
                break  # reached as far as we can go
            else:
                longest_path.append( [ cell_x, cell_y ] )
        return longest_path


    def highlightPath( self, window, path ):
        """ Draw borders around the cells in the given path """
        for xy in path:
            cell_x, cell_y = xy
            pixel_x = cell_x * GRID_WIDTH
            pixel_y = cell_y * GRID_HEIGHT
            cell_rect = [ pixel_x,pixel_y, GRID_WIDTH,GRID_HEIGHT ]
            pygame.draw.rect( window, RED, cell_rect )


    def draw( self, window ):
        """ Paint the generated map to the window """
        window.blit( self.surface, self.rect )
        
        



mini_map = [
    '11111111111111111111111111111111',
    '1________________1_11_____11___1',
    '1____11___111___11_____111__11_1',
    '1_____1_____1_____111_____1____1',
    '1_1_______11___1111_____11__11_1',
    '1__11___11___11___1_1_1_111__1_1',
    '1___1___1___1___1___1___1___1__1',
    '1_111____111_____1___1____1____1',
    '1_________111__11________1___1_1',
    '1______11___1__1_____11____1___1',
    '1___11__1___11_____11__11__1___1',
    '1__1___11______11111______1_1__1',
    '1_____1_____1____1____1____1___1',
    '1_1_______11___1________1__1___1',
    '1__11___11___11__11______11____1',
    '1___1___1___1___1___1___1___1__1',
    '1_______11___111___11_1___1____1',
    '11111111111111111111111111111111',
]
        
player = Player( ( 6, 6 ) )
world  = Map( mini_map )



###
### MAIN
###
pygame.init()
window  = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE )
pygame.display.set_caption("Map Movement")

# Main loop
walk_path = []
next_move_time = 0
clock = pygame.time.Clock()
running = True
while running:
    time_now = pygame.time.get_ticks()

    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            running = False
        elif ( event.type == pygame.MOUSEBUTTONUP ):
            mouse_pos = pygame.mouse.get_pos()
            cell_x, cell_y = world.getPixelPosition( mouse_pos )
            walk_path      = world.getPath( player.getPosition(), ( cell_x, cell_y ) )
            #print( "PATH IS "+str( walk_path ) )

    # Paint the window
    window.fill( WHITE )
    world.draw( window )
    player.draw( window )

    # Draw the walking path to the most recent mouse-click (if any)
    world.highlightPath( window, walk_path )        

    pygame.display.flip()


    # If there's a walking path, move the player along it
    if ( len( walk_path ) > 0 and time_now > next_move_time ):
        player.moveTo( walk_path[0] )
        walk_path = walk_path[1:]  # consume the step
        next_move_time = time_now + 300


    # Clamp FPS
    clock.tick(60)

pygame.quit()
Kingsley
  • 14,398
  • 5
  • 31
  • 53