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