python 戰棋遊戲代碼實現(2):六邊形地圖尋路和顯示

六邊形地圖介紹

之前的文章 生物行走和攻擊選擇 實現了簡單的方格地圖,戰棋遊戲一般是使用六邊形地圖,六邊形地圖的顯示和尋路會更加複雜些,所以這邊自己嘗試增加了六邊形地圖的實現。
圖1 六邊形
六邊形地圖有2種形式,這裏採用上面圖1的形式。

地圖的座標還是採用二維數組,如下面圖2所示,六邊形裏面的(x, y) 表示在地圖數組中的座標。
注意一點:奇數行的六邊形數會比偶數行少一,比如圖2中第0行有10個六邊形,第1行有9個六邊形。
圖2 六邊形座標

生物的行走範圍如圖3所示,圖3 中的步兵行走範圍屬性值是4,表示可以走4個六邊形,圖中背景是深藍色的方格就是該步兵可以走的格子,可以一步走到相鄰的6個格子。
圖3 行走範圍

遊戲截圖如下:
圖4遊戲截圖
圖4中,目前輪到行動的生物是我方的左下角背景爲淺藍色的步兵,可以看到背景爲深藍色的六邊形爲步兵可以行走的範圍。背景爲綠色的六邊形爲目前選定要行走到得方格。鼠標指向敵方生物,如果敵方生物背景六邊形顏色變成黃色,表示可以攻擊。圖中還有石塊,表示不能移動到的格子。

代碼介紹

地圖六邊形顯示

看下如何在代碼中實現圖1的六邊形顯示,這邊使用的pygame的繪製多邊形的函數pygame.draw.polygon() 來設置六邊形的背景色,繪製連續線段的函數pygame.draw.lines() 來畫六邊形的黑色邊。

pygame.draw.polygon()
draw a polygon
polygon(surface, color, points) -> Rect
    points (tuple(coordinate) or list(coordinate)):
       -- a sequence of 3 or more (x, y) 

pygame.draw.lines()
draw multiple contiguous straight line segments
lines(surface, color, closed, points) -> Rect

points的使用可以看下面drawBackgroundHex函數中的例子,points 列表保存了六邊形六個頂點的座標。
圖5 六邊形
如圖5所示,紅色長方形裏面的六邊形,先根據該六邊形的座標計算出左上角紅點的值(base_x, base_y),HEX_X_SIZE 和 HEX_Y_SIZE 分別表示紅色長方形的寬度和長度。
getHexMapPos函數計算出給定座標的左上角紅點的值,可以看到奇數行相對偶數行的x值會有X_LEN的偏移(即長方形的寬度的一半)。計算y值時也是按照奇數行或偶數行分別計算。

HEX_Y_SIZE = 56
HEX_X_SIZE = 48
def getHexMapPos(x, y):
    X_LEN = c.HEX_X_SIZE // 2
    Y_LEN = c.HEX_Y_SIZE // 2
    if y % 2 == 0:
        base_x = X_LEN * 2 * x
        base_y = Y_LEN * 3 * (y//2)
    else:
        base_x = X_LEN * 2 * x + X_LEN
        base_y = Y_LEN * 3 * (y//2) + Y_LEN//2 + Y_LEN
    return (base_x, base_y)

drawBackgroundHex 函數計算出六邊形所在長方形的左上角頂點值 (base_x, base_y) 後,就可以根據固定的偏移量,計算出六邊形六個頂點的值, 具體的偏移量可以按照圖5上的示例算出來。

    def drawBackgroundHex(self, surface):
        Y_LEN = c.HEX_Y_SIZE // 2
        X_LEN = c.HEX_X_SIZE // 2

        pg.draw.rect(surface, c.LIGHTYELLOW, pg.Rect(0, 0, c.MAP_WIDTH, c.MAP_HEIGHT))

        for y in range(self.height):
            for x in range(self.width):
                if self.bg_map[y][x] == c.BG_EMPTY:
                    color = c.LIGHTYELLOW
                elif self.bg_map[y][x] == c.BG_ACTIVE:
                    color = c.SKY_BLUE
                elif self.bg_map[y][x] == c.BG_RANGE:
                    color = c.NAVYBLUE
                elif self.bg_map[y][x] == c.BG_SELECT:
                    color = c.GREEN
                elif self.bg_map[y][x] == c.BG_ATTACK:
                    color = c.GOLD

                base_x, base_y = tool.getHexMapPos(x, y)
                points = [(base_x, base_y + Y_LEN//2 + Y_LEN), (base_x, base_y + Y_LEN//2),
                          (base_x + X_LEN, base_y), (base_x + X_LEN * 2, base_y + Y_LEN//2),
                          (base_x + X_LEN * 2, base_y + Y_LEN//2 + Y_LEN), (base_x + X_LEN, base_y + Y_LEN*2)]
                pg.draw.polygon(surface, color, points)

        surface.blit(self.map_image, self.rect)

        for y in range(self.height):
            for x in range(self.width):
                if y % 2 == 1 and x == self.width - 1:
                    continue
                base_x, base_y = tool.getHexMapPos(x, y)
                points = [(base_x, base_y + Y_LEN//2 + Y_LEN), (base_x, base_y + Y_LEN//2),
                          (base_x + X_LEN, base_y), (base_x + X_LEN * 2, base_y + Y_LEN//2),
                          (base_x + X_LEN * 2, base_y + Y_LEN//2 + Y_LEN), (base_x + X_LEN, base_y + Y_LEN*2)]
                pg.draw.lines(surface, c.BLACK, True, points)

A*算法的六邊形尋路修改

A* 算法的代碼實現可以看我之前的一篇文章 A*算法實現
針對六邊形地圖的實現修改比較簡單,就修改2個函數。
getMovePositions函數獲取當前位置可以移動的格子,從 圖2 可以看到奇數行和偶數行格子的相鄰座標是不一樣的。

 def getMovePositions(x, y):
    if c.MAP_HEXAGON:
        if y % 2 == 0:
            offsets = [(-1, 0), (-1, -1), (0, -1), (1, 0), (-1, 1), (0, 1)]
        else:
            offsets = [(-1, 0), (0, -1), (1, -1), (1, 0), (0, 1), (1, 1)]

calHeuristicDistance函數用來估計兩個座標之前的位置,注意兩個座標間X軸的距離計算:如果X軸距離小於Y軸距離的一半,就可以忽略,否則需要減去Y軸距離一半的值。

   def calHeuristicDistance(self, x1, y1, x2, y2):
       if c.MAP_HEXAGON:
           dis_y = abs(y1 - y2)
           dis_x = abs(x1 - x2)
           half_y = dis_y // 2
           if dis_y >= dis_x:
               dis_x = 0
           else:
               dis_x -= half_y
           return (dis_y + dis_x)

如圖6所示舉兩個計算距離的例子,

  • 從座標A(0,0) 到座標B(1,3) 的距離, 在圖中可以看到按照紅色線移動的距離是3 。兩點間X軸距離是1,Y軸距離是3,X軸距離小於Y軸距離的一半,所以X軸距離可以忽略。
  • 從座標A(0,0) 到座標C(3,3) 的距離,在圖中可以看到是兩條紅色線條相加,距離是5。兩點間X軸距離是3,Y軸距離是3,X軸距離大於Y軸距離的一半(3/2=1),所以X軸距離修正爲2(3 - 1)。
    圖6 距離

判斷某個點在哪個六邊形中

如何切分地圖的六邊形,可以有多種方式,圖7中紅色長方形是我採用的切分方式,長方形的寬度是六邊形的寬度 (HEX_X_SIZE),長方形的長度是六邊形長度的1.5倍(HEX_Y_SIZE/2 * 3)
圖7 在這裏插入圖片描述
從圖7出看到紅色長方形裏面有分成9個小的三角形或長方形區域。假設 編號3,4,5所在六邊形的座標爲(map_x, map_y), 那麼按照圖2上的座標可以算出:

  • 編號1所在六邊形的座標爲(map_x-1, map_y-1)
  • 編號2所在六邊形的座標爲(map_x, map_y-1)
  • 編號6,8所在六邊形的座標爲(map_x-1, map_y+1)
  • 編號7,9所在六邊形的座標爲(map_x, map_y+1)

getHexMapIndex函數就是具體的實現,根據Y軸的offset先分成三個區域(編號1,2,3, 4), (編號5,6,7)和(編號8,9)。先判斷在哪個區域內,如果區域內有三角形,就調用isInTriangle函數判斷是否在某個三角形中。

def getHexMapIndex(x, y):
    X_LEN = c.HEX_X_SIZE // 2
    Y_LEN = c.HEX_Y_SIZE // 2
    tmp_x, offset_x = divmod(x, c.HEX_X_SIZE)
    tmp_y, offset_y = divmod(y, Y_LEN * 3)
    map_x, map_y = 0, 0
    if offset_y <= (Y_LEN + Y_LEN//2):
        if offset_y >= Y_LEN//2:
            map_x, map_y = tmp_x, tmp_y * 2
        else:
            triangle_list = [(0, 0, 0, Y_LEN//2, X_LEN, 0),
                             (0, Y_LEN//2, X_LEN, 0, c.HEX_X_SIZE, Y_LEN//2),
                             (X_LEN, 0, c.HEX_X_SIZE, 0, c.HEX_X_SIZE, Y_LEN//2)]
            map_list = [(tmp_x - 1, tmp_y * 2 -1), (tmp_x, tmp_y * 2), (tmp_x, tmp_y * 2 -1)]
            for i, data in enumerate(triangle_list):
                if isInTriangle(*data, offset_x, offset_y):
                    map_x, map_y = map_list[i]
                    break
    elif offset_y >= c.HEX_Y_SIZE:
        if offset_x <= X_LEN:
            map_x, map_y = tmp_x - 1, tmp_y * 2 + 1
        else:
            map_x, map_y = tmp_x, tmp_y *2 + 1
    else:
        triangle_list = [(0, Y_LEN + Y_LEN//2, 0, c.HEX_Y_SIZE, X_LEN, c.HEX_Y_SIZE),
                         (0, Y_LEN + Y_LEN//2, X_LEN, c.HEX_Y_SIZE, c.HEX_X_SIZE, Y_LEN + Y_LEN//2),
                         (X_LEN, c.HEX_Y_SIZE, c.HEX_X_SIZE, Y_LEN + Y_LEN//2, c.HEX_X_SIZE, c.HEX_Y_SIZE)]
        map_list = [(tmp_x - 1, tmp_y * 2 + 1), (tmp_x, tmp_y * 2), (tmp_x, tmp_y *2 + 1)]
        for i, data in enumerate(triangle_list):
            if isInTriangle(*data, offset_x, offset_y):
                map_x, map_y = map_list[i]
                break
    if map_x == 0 and map_y == 0:
        print('pos[%d, %d](%d, %d) base[%d, %d] off[%d, %d] ' % (map_x, map_y, x, y, tmp_x, tmp_y, offset_x, offset_y))
    return (map_x, map_y)

isInTriangle函數的參數是三角形的三個頂點座標和目標座標,通過計算頂點和目標座標的向量乘積來判斷某個點是否在三角形中。

class Vector2d():
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def minus(self, vec):
        return Vector2d(self.x - vec.x, self.y - vec.y)

    def crossProduct(self, vec):
        return (self.x * vec.y - self.y * vec.x)

def isInTriangle(x1, y1, x2, y2, x3, y3, x, y):
    A = Vector2d(x1, y1)
    B = Vector2d(x2, y2)
    C = Vector2d(x3, y3)
    P = Vector2d(x, y)
    PA = A.minus(P)
    PB = B.minus(P)
    PC = C.minus(P)
    t1 = PA.crossProduct(PB)
    t2 = PB.crossProduct(PC)
    t3 = PC.crossProduct(PA)
    if (t1 * t2 >= 0) and (t1 * t3 >= 0):
        return True
    return False

完整代碼

遊戲默認是使用方塊地圖,如果要改成六邊形地圖,需要修改 source\constants.py 中的參數MAP_HEXAGON 爲True

MAP_HEXAGON = True

遊戲實現代碼的github鏈接 戰棋遊戲
這邊是csdn的下載鏈接 六邊形戰棋遊戲

編譯運行

1.編譯環境
python3.7 + pygame1.9
2.運行
直接運行根目錄下的 main.py
$ python main.py

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章