You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 03:50:01