如何用Python的Kivy库制作带六边形碰撞盒的六边形按钮
Hex游戏:实现六边形按钮与六边形碰撞盒
要实现六边形形状的按钮并让其碰撞盒也为六边形,核心是自定义Kivy组件,重绘按钮外观并修改碰撞检测逻辑。以下是具体实现方案:
1. 自定义HexButton组件
继承Kivy的Button类,在canvas中绘制六边形,并通过重写collide_point方法实现六边形区域的碰撞判断。
from kivy.graphics import Color, Line, Rectangle from kivy.uix.button import Button from kivy.metrics import dp import math class HexButton(Button): def __init__(self, **kwargs): super(HexButton, self).__init__(**kwargs) # 设置按钮默认尺寸,适配正六边形比例 self.size_hint = (None, None) self.size = (dp(80), dp(92)) # 高度=宽度*√3,匹配正六边形外接圆尺寸 self.bind(pos=self.update_hex, size=self.update_hex) self.update_hex() def update_hex(self, *args): self.canvas.before.clear() self.canvas.after.clear() with self.canvas.before: # 绘制六边形背景 Color(*self.background_color) self.hex_points = self.get_hex_points() Line(points=self.hex_points, close=True, width=dp(2)) Rectangle(points=self.hex_points) with self.canvas.after: # 绘制六边形黑色边框 Color(0,0,0,1) Line(points=self.hex_points, close=True, width=dp(2)) def get_hex_points(self): # 计算正六边形的六个顶点坐标(从顶部开始顺时针排列) x, y = self.pos width, height = self.size radius = width / 2 angles = [math.pi/2, math.pi/6, -math.pi/6, -math.pi/2, -5*math.pi/6, 5*math.pi/6] points = [] for angle in angles: px = x + radius + radius * math.cos(angle) py = y + radius + radius * math.sin(angle) points.extend([px, py]) return points def collide_point(self, x, y): # 射线法判断点是否在六边形内部 points = self.get_hex_points() n = len(points) // 2 inside = False p1x, p1y = points[0], points[1] for i in range(n + 1): p2x, p2y = points[(i%n)*2], points[(i%n)*2 + 1] if y > min(p1y, p2y): if y <= max(p1y, p2y): if x <= max(p1x, p2x): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside
2. 替换原有按钮并调整棋盘布局
在Game类中,将默认矩形按钮替换为自定义的HexButton,并改用FloatLayout实现六边形棋盘的交错排列:
self.list_w = [[None for _ in range(1, self.longeur - 1)] for _ in range(1, self.longeur - 1)] # 清空原有GridLayout内容,改用FloatLayout实现六边形布局 self.clear_widgets() self.layout = FloatLayout() self.add_widget(self.layout) hex_size = dp(80) hex_height = dp(92) offset_x = hex_size * 0.75 # 六边形横向间距 offset_y = hex_height * 0.5 # 六边形纵向间距 for i in range(len(self.list_w)): for j in range(len(self.list_w[i])): btn = HexButton(text=f"{str(i+1)};{str(j+1)}") # 奇数行偏移半个六边形宽度,实现交错排列 x_pos = j * offset_x + (hex_size * 0.375 if i % 2 == 1 else 0) # 翻转y轴,让第一行显示在顶部 y_pos = (len(self.list_w)-1 - i) * offset_y btn.pos = (x_pos, y_pos) btn.bind(on_press=lambda instance, x=i+1, y=j+1: self.play(instance, (x, y))) self.list_w[i][j] = btn self.layout.add_widget(btn)
3. 完整修改后的代码
将上述组件整合到原有代码中,完整可运行代码如下:
# -*- coding: utf-8 -*- """ Created on Mon Jan 15 16:16:24 2024 @author: shamiiow """ from kivy.app import App from kivy.uix.gridlayout import GridLayout from kivy.uix.floatlayout import FloatLayout from kivy.uix.button import Button from kivy.graphics import Color, Line, Rectangle from kivy.metrics import dp import math class HexButton(Button): def __init__(self, **kwargs): super(HexButton, self).__init__(**kwargs) self.size_hint = (None, None) self.size = (dp(80), dp(92)) self.bind(pos=self.update_hex, size=self.update_hex) self.update_hex() def update_hex(self, *args): self.canvas.before.clear() self.canvas.after.clear() with self.canvas.before: Color(*self.background_color) self.hex_points = self.get_hex_points() Line(points=self.hex_points, close=True, width=dp(2)) Rectangle(points=self.hex_points) with self.canvas.after: Color(0,0,0,1) Line(points=self.hex_points, close=True, width=dp(2)) def get_hex_points(self): x, y = self.pos width, height = self.size radius = width / 2 angles = [math.pi/2, math.pi/6, -math.pi/6, -math.pi/2, -5*math.pi/6, 5*math.pi/6] points = [] for angle in angles: px = x + radius + radius * math.cos(angle) py = y + radius + radius * math.sin(angle) points.extend([px, py]) return points def collide_point(self, x, y): points = self.get_hex_points() n = len(points) // 2 inside = False p1x, p1y = points[0], points[1] for i in range(n + 1): p2x, p2y = points[(i%n)*2], points[(i%n)*2 + 1] if y > min(p1y, p2y): if y <= max(p1y, p2y): if x <= max(p1x, p2x): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside class Game(GridLayout): def __init__(self, **kwargs): #0 : case vide #1 : case bleu #2 : case rouge self.player = False self.couleur = ["white","blue", "red"] self.winner = 0 self.recap = [] self.longeur = 4 + 2 self.grid_p = [[0 for _ in range(self.longeur)] for _ in range(self.longeur)] self.grid_w = [[0 for _ in range(self.longeur)] for _ in range(self.longeur)] super(Game, self).__init__(**kwargs) self.cols = 1 for i in range(self.longeur): for j in range(self.longeur): if (i == 0) or (j == 0) or (i == self.longeur-1) or (j == self.longeur-1): self.grid_p[i][j] = 9 self.list_w = [[None for _ in range(1, self.longeur - 1)] for _ in range(1, self.longeur - 1)] self.layout = FloatLayout() self.add_widget(self.layout) hex_size = dp(80) hex_height = dp(92) offset_x = hex_size * 0.75 offset_y = hex_height * 0.5 for i in range(len(self.list_w)): for j in range(len(self.list_w[i])): btn = HexButton(text=f"{str(i+1)};{str(j+1)}") x_pos = j * offset_x + (hex_size * 0.375 if i % 2 == 1 else 0) y_pos = (len(self.list_w)-1 - i) * offset_y btn.pos = (x_pos, y_pos) btn.bind(on_press=lambda instance, x=i+1, y=j+1: self.play(instance, (x, y))) self.list_w[i][j] = btn self.layout.add_widget(btn) def play(self, instance, c): print(self.winner) if self.winner != 0: self.update() return if self.grid_p[c[0]][c[1]] != 0: return self.grid_w = [[0 for _ in range(self.longeur)] for _ in range(self.longeur)] self.grid_p[c[0]][c[1]] = 1 + int(self.player) self.recap.append(c) self.update() self.win() self.player = self.player ^ True def win(self): self.grid_w = [[0 for _ in range(self.longeur)] for _ in range(self.longeur)] for i in range(1,self.longeur-1): if self.winner == 0: self.path((1 ,i), 1) self.grid_w = [[0 for _ in range(self.longeur)] for _ in range(self.longeur)] for i in range(1,self.longeur-1): if self.winner == 0: self.path((i, 1), 2) def path(self, c, p): x, y = c[0], c[1] if self.grid_p[x][y] != p: return for i in [-1, 0, 1]: for j in [-1, 0, 1]: if [i, j] != [0, 0] and [i, j] != [-1, -1]and [i, j] != [1, 1]: if self.grid_p[x+i][y+j] == p and self.grid_w[x+i][y+j] == 0: self.grid_w[x+i][y+j] = 1 self.path((x+i, y+j), p) if (1 in self.grid_w[self.longeur-2] and p == 1) or (1 in [self.grid_w[i][self.longeur-2] for i in range(self.longeur)] and p == 2): self.winner = 1 + int(self.player) def update(self): if self.winner != 0: pass #commande pour afficher la victoire for i in range(self.longeur): for j in range(self.longeur): if 1<=i<=self.longeur-2 and 1<=j<=self.longeur-2: if self.grid_p[i][j] == 0: self.list_w[i-1][j-1].background_color = (1,1,1,1) if self.grid_p[i][j] == 1: self.list_w[i-1][j-1].background_color = (0,0,1,1) if self.grid_p[i][j] == 2: self.list_w[i-1][j-1].background_color = (1,0,0,1) def affiche(self): for i in range(self.longeur): print(self.grid_p[i], self.grid_w[i]) class Hex(App): def build(self): return Game() if __name__ == "__main__": Hex().run()
关键说明
- 六边形绘制:通过三角函数计算正六边形的六个顶点,在canvas中绘制边框和填充色,实现六边形外观。
- 碰撞检测:重写
collide_point方法,使用射线法判断点击点是否在六边形区域内,确保只有点击六边形内部才触发按钮事件。 - 布局调整:使用FloatLayout替代GridLayout,通过计算每行的偏移量,实现Hex游戏特有的交错六边形棋盘布局。
内容的提问来源于stack exchange,提问作者Ishihlanzo gluttony
相关产品推荐
相关产品推荐

