wxPython和pycairo练习记录14
继续之前的坦克游戏。碰撞检测技术像什么四叉树、凸包,看不懂啊,所以先从成熟的 pygame 框架里抠出几个能用到的。
pygame 介绍
pygame 是一个利用 SDL(c语言编写)库的写的一个游戏库。
那么游戏的基本流程是什么,说白了就是下面这个事情:
- 检查玩家输入(事件)
- 判断元素间有无冲突(比如子弹碰撞、碰到补给、碰到怪物等)
- 根据信息绘制屏幕上的元素
- 重复1、2、3步骤
以上内容出自https://www.bilibili.com/read/cv23670475/ 作者:教娃学编程
从上面 pygame 的介绍来看,和 wxPython 工作流程基本是一样的。
抠代码,pygame 中的碰撞检测
文档:
https://www.pygame.org/docs/ref/sprite.html#pygame.sprite.collide_rect
源码:
https://github.com/pygame/pygame/blob/main/src_py/sprite.py
https://github.com/pygame/pygame/blob/main/src_c/rect.c
之前写坦克游戏并没有明确组合多个 Sprite 对象,还是先只考虑独立的 Sprite 吧。这样,需要用到和改写的方法只有 collide_rect、collide_rect_ratio、collide_circle、collide_circle_ratio、collide_mask
。
观察了一下,发现这些方法其实可以直接把待检测 Sprite 对象作为参数,都可以独立出来,而不需要和 Sprite 类绑定。
collide_rect、collide_rect_ratio
这部分较简单,比较根据 Sprite 对象坐标和尺寸生成的矩形的四个顶点坐标,判断是否有重合。缩放是相对 Sprite 对象中心点进行缩放,得到新的操作矩形,然后判断重合。
class Collision:
@staticmethod
def inflate(rect, x, y):
return wx.Rect(int(rect.X - x / 2), int(rect.Y - y / 2), int(rect.Width + x), int(rect.Height + y))
@staticmethod
def colliderect(A, B):
if A.Width == 0 or A.Height == 0 or B.Width == 0 or B.Height == 0:
return False
return (min(A.X, A.X + A.Width) < max(B.X, B.X + B.Width) and
min(A.Y, A.Y + A.Height) < max(B.Y, B.Y + B.Height) and
max(A.X, A.X + A.Width) > min(B.X, B.X + B.Width) and
max(A.Y, A.Y + A.Height) > min(B.Y, B.Y + B.Height))
@staticmethod
def collide_rect(left, right):
return Collision.colliderect(left.GetRect(), right.GetRect())
@staticmethod
def collide_rect_ratio(left, right, ratio=1.0):
leftrect = left.GetRect()
width = leftrect.Width
height = leftrect.Height
leftrect = Collision.inflate(leftrect, width * ratio - width, height * ratio - height)
rightrect = right.GetRect()
width = rightrect.Width
height = rightrect.Height
rightrect = Collision.inflate(rightrect, width * ratio - width, height * ratio - height)
return Collision.colliderect(leftrect, rightrect)
这里的矩形 wx.Rect 类似图形处理程序里选中图像后的调节边框,但它是横平竖直的。如果 Sprite 对象的 surface 是菱形或者其他不规则图形,collide_rect
明显就不适用了。
collide_circle、collide_circle_ratio
简单通过勾股定理,先计算两个 Sprite 对象的圆心距离,然后通过对象的 wx.Rect 操作矩形计算圆半径,两个对象半径之和与之前的圆心距离比较。可以看到操作矩形是圆的外切正方形,圆半径与外切正方形对角线一半的比值为1:2^0.5,似乎没必要计算,直接等于矩形宽度的一半。如果不是正圆,这两个碰撞检测方法应该也不适用。
pygame 官方代码中,如果不指定半径,就直接用外切正方形对角线一半作为半径,明显不对啊。这里不是重点,所以就不去验证了。
https://github.com/pygame/pygame/blob/main/src_py/sprite.py
@staticmethod
def center(rect):
return rect.X + rect.Width / 2, rect.Y + rect.Height / 2
@staticmethod
def collide_circle(left, right):
leftrect = left.GetRect()
rightrect = right.GetRect()
leftcenterx, leftcentery = Collision.center(leftrect)
rightcenterx, rightcentery = Collision.center(rightrect)
xdistance = leftcenterx - rightcenterx
ydistance = leftcentery - rightcentery
distancesquared = xdistance ** 2 + ydistance ** 2
leftradiussquared = (leftrect.Width ** 2 + leftrect.Height ** 2) / 4
rightradiussquared = (rightrect.Width ** 2 + rightrect.Height ** 2) / 4
return distancesquared < leftradiussquared + rightradiussquared
@staticmethod
def collide_circle_ratio(left, right, ratio=1.0):
leftrect = left.GetRect()
rightrect = right.GetRect()
leftcenterx, leftcentery = Collision.center(leftrect)
rightcenterx, rightcentery = Collision.center(rightrect)
xdistance = leftcenterx - rightcenterx
ydistance = leftcentery - rightcentery
distancesquared = xdistance ** 2 + ydistance ** 2
ratio = ratio ** 2 / 4.0
leftradiussquared = (leftrect.Width ** 2 + leftrect.Height ** 2) * ratio
rightradiussquared = (rightrect.Width ** 2 + rightrect.Height ** 2) * ratio
return distancesquared < leftradiussquared + rightradiussquared
单元测试
collide_mask
还没改完,先把前面两个方法的测试代码抠过来。
https://github.com/pygame/pygame/blob/main/test/rect_test.py
https://github.com/pygame/pygame/blob/main/test/sprite_test.py
# -*- coding: utf-8 -*-
import unittest
from wx import Rect
from cairo import ImageSurface, FORMAT_ARGB32
from collision import Collision
from display import Sprite
class CollisionTest(unittest.TestCase):
def setUp(self):
self.s1 = Sprite(0, 0, ImageSurface(FORMAT_ARGB32, 50, 10))
self.s2 = Sprite(40, 0, ImageSurface(FORMAT_ARGB32, 10, 10))
self.s3 = Sprite(100, 100, ImageSurface(FORMAT_ARGB32, 10, 10))
def test_inflate__larger(self):
"""The inflate method inflates around the center of the rectangle"""
r = Rect(2, 4, 6, 8)
r2 = Collision.inflate(r, 4, 6)
self.assertEqual(Collision.center(r), Collision.center(r2))
self.assertEqual(r.left - 2, r2.left)
self.assertEqual(r.top - 3, r2.top)
self.assertEqual(r.right + 2, r2.right)
self.assertEqual(r.bottom + 3, r2.bottom)
self.assertEqual(r.width + 4, r2.width)
self.assertEqual(r.height + 6, r2.height)
def test_inflate__smaller(self):
"""The inflate method inflates around the center of the rectangle"""
r = Rect(2, 4, 6, 8)
r2 = Collision.inflate(r, -4, -6)
self.assertEqual(Collision.center(r), Collision.center(r2))
self.assertEqual(r.left + 2, r2.left)
self.assertEqual(r.top + 3, r2.top)
self.assertEqual(r.right - 2, r2.right)
self.assertEqual(r.bottom - 3, r2.bottom)
self.assertEqual(r.width - 4, r2.width)
self.assertEqual(r.height - 6, r2.height)
def test_colliderect(self):
r1 = Rect(1, 2, 3, 4)
self.assertTrue(
Collision.colliderect(r1, Rect(0, 0, 2, 3)),
"r1 does not collide with Rect(0, 0, 2, 3)",
)
self.assertFalse(
Collision.colliderect(r1, Rect(0, 0, 1, 2)), "r1 collides with Rect(0, 0, 1, 2)"
)
self.assertTrue(
Collision.colliderect(r1, Rect(r1.right, r1.bottom, 2, 2)),
"r1 does not collide with Rect(r1.right, r1.bottom, 2, 2)",
)
self.assertTrue(
Collision.colliderect(r1, Rect(r1.left + 1, r1.top + 1, r1.width - 2, r1.height - 2)),
"r1 does not collide with Rect(r1.left + 1, r1.top + 1, "
+ "r1.width - 2, r1.height - 2)",
)
self.assertTrue(r1.Intersects(Rect(r1.left + 1, r1.top + 1, r1.width - 2, r1.height - 2)))
self.assertTrue(
Collision.colliderect(r1, Rect(r1.left - 1, r1.top - 1, r1.width + 2, r1.height + 2)),
"r1 does not collide with Rect(r1.left - 1, r1.top - 1, "
+ "r1.width + 2, r1.height + 2)",
)
self.assertTrue(
Collision.colliderect(r1, Rect(r1)), "r1 does not collide with an identical rect"
)
self.assertFalse(
Collision.colliderect(r1, Rect(r1.right, r1.bottom, 0, 0)),
"r1 collides with Rect(r1.right, r1.bottom, 0, 0)",
)
self.assertTrue(
Collision.colliderect(r1, Rect(r1.right, r1.bottom, 1, 1)),
"r1 does not collide with Rect(r1.right, r1.bottom, 1, 1)",
)
self.assertTrue(r1.Intersects(Rect(r1.right, r1.bottom, 1, 1)))
def test_collide_rect(self):
# Test colliding - some edges touching
self.assertTrue(Collision.collide_rect(self.s1, self.s2))
self.assertTrue(Collision.collide_rect(self.s2, self.s1))
# Test colliding - all edges touching
self.s2.SetX(self.s3.GetX())
self.s2.SetY(self.s3.GetY())
self.assertTrue(Collision.collide_rect(self.s2, self.s3))
self.assertTrue(Collision.collide_rect(self.s3, self.s2))
# Test colliding - no edges touching
self.s2.SetRect(Collision.inflate(self.s2.GetRect(), 10, 10))
self.assertTrue(Collision.collide_rect(self.s2, self.s3))
self.assertTrue(Collision.collide_rect(self.s3, self.s2))
# Test colliding - some edges intersecting
self.s2.SetX(self.s1.GetRect().right - self.s2.GetWidth() / 2)
self.s2.SetY(self.s1.GetRect().bottom - self.s2.GetHeight() / 2)
self.assertTrue(Collision.collide_rect(self.s1, self.s2))
self.assertTrue(Collision.collide_rect(self.s2, self.s1))
# Test not colliding
self.assertFalse(Collision.collide_rect(self.s1, self.s3))
self.assertFalse(Collision.collide_rect(self.s3, self.s1))
def test_collide_rect_ratio__ratio_of_one_like_default(self):
# collide_rect_ratio should behave the same as default at a 1.0 ratio.
self.assertTrue(
Collision.collide_rect_ratio(self.s1, self.s2, 1.0)
)
self.assertFalse(
Collision.collide_rect_ratio(self.s1, self.s3, 1.0)
)
def test_collide_rect_ratio__collides_all_at_ratio_of_twenty(self):
# collide_rect_ratio should collide all at a 20.0 ratio.
self.assertTrue(
Collision.collide_rect_ratio(self.s1, self.s2, 20.0)
)
self.assertTrue(
Collision.collide_rect_ratio(self.s1, self.s3, 20.0)
)
def test_collide_circle__no_ratio_set(self):
# collide_circle with no ratio set.
self.assertTrue(
Collision.collide_circle(self.s1, self.s2)
)
self.assertFalse(
Collision.collide_circle(self.s1, self.s3)
)
def test_collide_circle_ratio__no_radius_and_ratio_of_one(self):
# collide_circle_ratio with no radius set, at a 1.0 ratio.
self.assertTrue(
Collision.collide_circle_ratio(self.s1, self.s2, 1.0)
)
self.assertFalse(
Collision.collide_circle_ratio(self.s1, self.s3, 1.0)
)
def test_collide_circle_ratio__no_radius_and_ratio_of_twenty(self):
# collide_circle_ratio with no radius set, at a 20.0 ratio.
self.assertTrue(
Collision.collide_circle_ratio(self.s1, self.s2, 20.0)
)
self.assertTrue(
Collision.collide_circle_ratio(self.s1, self.s3, 20.0)
)
if __name__ == "__main__":
unittest.main(verbosity=2)
后续改完再发。