跳转到主要内容

一个方便的二维向量类

项目描述

vec

一个合理、性能良好的2D向量对象,适用于游戏

版权所有 © 2019-2024 Larry Hastings

概述

vec是一个模块,目前发布了一个类:Vector2,这是一个为游戏开发设计的二维向量对象。

特性

  • Vector2对象是不可变的。
  • Vector2支持所有常见的向量对象功能,包括操作符重载。
  • 尽可能懒加载Vector2对象的属性。
  • Vector2对象轻松支持笛卡尔和极坐标。

vec支持Python 3.6+,并且通过了100%覆盖率的单元测试套件。

为什么还需要另一个向量类?

我参加了四次PyWeek游戏挑战。两次,在比赛中期,出于极度沮丧,我编写了自己的向量类。

大多数Python向量对象的最大问题是它们是可变的。坦白说,这种方法很疯狂。向量对象应该是不可变的——从API的角度来看,这很有意义。如果你将某个游戏引擎中的棋子的位置设置为特定的向量对象,然后修改该向量对象,那么棋子应该自动更新其位置——如果是这样,它将如何知道值已更改?

类似地,一些向量类使用度数而不是弧度来表示极坐标。同样,这种方法很疯狂。Python的math模块中的三角函数在弧度域内操作,而跟踪某个域在哪里——并来回转换——是一个无谓的概念复杂性。你还有一个游戏要写!

(一些向量类支持极坐标的弧度度数。这是糟糕的API设计——它加倍了API的表面积,增加了无谓的复杂性,并增加了维护和测试开销。拥抱弧度,朋友们。)

关于相关话题,许多矢量类将极坐标视为二等公民。大多数矢量类只存储笛卡尔坐标系中的矢量,因此程序员必须在矢量对象外部执行所有极坐标操作,或者每次操作都承受转换为极坐标和再转换回来的开销和累积误差。

vec的Vector2避免了所有这些问题

  • Vector2对象是不可变的
  • 它们使极坐标和笛卡尔坐标都是一等公民,并且
  • 它们严格使用弧度作为极坐标。

概念模型

Vector2对象从概念上表示一个矢量。它们可以使用笛卡尔坐标或极坐标来定义,并且任何Vector2都可以查询其笛卡尔坐标和极坐标。

大多数游戏中的矢量对象都使用笛卡尔坐标定义。Vector2使这变得容易,支持创建一个矢量所需的任何数量的调用。离散参数、可迭代对象以及支持xy属性的对象都可以正常工作

Vector2(0, 1)
Vector2(x=0, y=1)
Vector2((0, 1))
Vector2([0, 1])
Vector2(iter([0, 1]))
Vector2({'x':0, 'y':1})
Vector2(types.SimpleNamespace(x=0, y=1))

所有这些定义了相同的矢量。最后一个例子是为了说明Vector2可以根据具有xy属性的对象创建矢量。

每个Vector2对象都支持笛卡尔坐标和极坐标。您可以使用笛卡尔坐标定义一个Vector2,然后检查其极坐标。这

v = vec.Vector2(0, 1)
print(v.theta, v.r)

打印出1.5707963267948966 1.0。第一个数字是π/2(大约)。

相反,您可以使用极坐标定义一个Vector2对象,然后检查其笛卡尔坐标

v2 = vec.Vector2(r=1, theta=math.pi/2)
print(v2.x, v2.y)

这打印出6.123233995736766e-17 1.0。从概念上讲,这应该打印出0.0, 1.0,但math.pi只是一个近似值,这意味着我们的结果有一个无穷小的误差。

实现细节

为了定义一个有效的Vector2对象,您必须有一个完整的笛卡尔或极坐标集——两者都足够。所有其他属性将在需要时按需懒加载。

Vector2对象使用槽位,并依赖于__getattr__来实现这种懒加载。在创建时只设置矢量的已知值。如果用户引用尚未计算的属性,Python将调用Vector2.__getattr__(),它计算、缓存并返回该值。对该属性的后续引用将跳过此机制,并简单地返回缓存的值,这仅相当于在常规对象上查找属性的开销。

Vector2对象的操作使用最经济的途径来计算结果。如果您有一个使用极坐标定义的Vector2对象,并且您对其调用.rotate().scale(),所有数学运算都在极域内完成。另一方面,添加矢量通常在笛卡尔域内完成,因此如果您将极坐标矢量添加到任何其他矢量,其笛卡尔坐标可能会被计算——并且结果矢量始终使用笛卡尔坐标定义。

例外是什么?对于具有相同theta的两个极坐标矢量的加法有一个特殊情况:只需将它们的r值相加。这种方法比转换为笛卡尔坐标更便宜,也更精确,返回一个使用极坐标定义的矢量!Vector2利用了许多这样的意外,尽可能以最便宜和最准确的方式执行矢量数学。

API

Vector2(x=None, y=None, *, r=None, theta=None, r_squared=None)

构造一个Vector2对象。您可以选择传入任意多个或少量这些参数;然而,您必须传入要么同时传入xy要么同时传入rtheta。在构造时未传入的任何属性将在评估时按需懒加载。

您还可以传入一个单个对象来初始化矢量。支持的对象包括

  • 现有的Vector2对象(将返回该对象),
  • 具有.x.y属性的对象,
  • 一个具有两个键('x''y')的映射对象,以及
  • 一个具有两个元素的有序可迭代对象,这两个元素将被分别用作'x''y'

Vector2 仅对其参数进行一些验证。它确保rtheta已归一化。然而,它不会检查(x, y)(r, theta)是否描述了相同的向量。如果你传入xy,并且也传入一个不匹配的thetar,你将得到你请求的Vector2。祝你好运!

属性

Vector2对象支持五个属性:xyrthetar_squared。无论对象是用笛卡尔坐标还是极坐标定义的,它们都会正常工作。

r_squared等价于r*r。但是如果你有一个用笛卡尔坐标定义的Vector2对象,计算r_squared比计算r要便宜得多。而且有很多使用场景中,r_squared的效果和r一样好。

例如,考虑游戏中的碰撞检测。决定两个物体是否碰撞的一种方法是通过测量它们之间的距离——如果距离小于某个特定的距离R,则两个物体发生碰撞。但是计算实际距离是非常昂贵的——它需要耗时平方根。计算两点之间距离的平方要便宜得多。如果那个值小于R^2,则两个物体发生碰撞。

运算符和协议

Vector2对象支持迭代协议。Vector2对象执行len()将始终返回2。你还可以遍历一个Vector2对象,它将按顺序产生xy属性。

Vector2对象支持序列协议。你可以使用索引访问它们,就像Vector2对象是一个包含xy属性的长为2的元组一样。

Vector2对象支持布尔协议;你可以使用布尔运算符,并且可以在它们上调用bool()。在布尔上下文中使用时,零向量评估为False,所有其他向量评估为True

Vector2对象是可哈希的,但它们不是有序的。(你不能询问一个向量是否小于另一个向量。)

Vector2对象支持以下运算符

  • v1 + v2将两个向量相加。
  • v1 - v2从左向量减去右向量。
  • v1 * scalar将向量乘以一个标量量,相当于v1.scale(scalar)
  • v1 / scalar将向量除以一个标量量。
  • +v1v1完全相同。
  • -v1返回v1的相反数,使得v1 + (-v1)应该是零向量。(这可能不会总是如此,因为浮点数的不精确性。)
  • v1 == v2如果两个向量完全相同,则为True,否则为False。为了保持一致性,这仅比较笛卡尔坐标。请注意,浮点数的不精确性可能导致两个应该相同的向量在==检查中失败。考虑使用almost_equal方法,它允许比较中有一些不精确性。
  • v1 != v2如果两个向量完全相同,则为False,否则为True。为了保持一致性,这仅比较笛卡尔坐标。请注意,浮点数的不精确性可能导致两个应该相同的向量在!=检查中通过。再次,考虑使用almost_equal方法并取其结果的反义。
  • v[0]v.x计算出的结果是相同的。
  • v[1]v.y计算出的结果是相同的。
  • list(v)[v.x, v.y] 是相同的。

类方法

vec.from_polar(r, theta)

从极坐标 rtheta 构造一个 Vector2 对象。

您也可以传入一个对象来初始化向量。支持的对象包括

  • 现有的Vector2对象(将返回该对象),
  • 具有 .r.theta 属性的对象,
  • 具有恰好两个键的映射对象,'r''theta',和
  • 具有恰好两个元素的有序可迭代对象,将按顺序解释为 'r''theta'

如果 r0,则 theta 必须为 None,并且 from_polar 将返回零向量。如果 r 不为 0,则 theta 不能为 None

方法

Vector2 对象支持以下方法

Vector2.almost_equal(other, places)

如果向量和 other 在小数点后 places 位上是相同的,则返回 True。与 Vector2 类对 == 运算符的支持一样,比较只使用笛卡尔坐标进行,以保持一致性。

Vector2.scaled(scalar)

返回一个新 Vector2 对象,相当于原始向量乘以该标量。

Vector2.scaled_to_length(r)

返回一个新 Vector2 对象,相当于原始向量,其长度设置为 r

Vector2.normalized()

返回一个新 Vector2 对象,相当于原始向量按长度缩放至 1。

Vector2.rotated(theta)

返回一个新 Vector2 对象,等于原始向量旋转 theta 弧度。

Vector2.dot(other)

返回“点积”selfother。该结果是一个标量值,而不是向量。

Vector2.cross(other)

返回“叉积”selfother。该结果是一个标量值,而不是向量。

注意:从技术上讲,二维向量没有定义“叉积”。这实际上返回两个向量的“垂直点积”,因为当人们询问两个 2D 向量的“叉积”时,通常意味着这个。

Vector2.polar()

返回一个二元组 (self.r, self.theta)

Vector2.lerp(other, ratio)

返回一个表示在 selfother 之间进行线性插值的向量,根据标量比率 ratioratio 应该是在 (包括) 01 之间的值。如果 ratio0,则返回 self。如果 ratio1,则返回 other。如果 ratio 在 (不包括) 01 之间,则返回由两个端点 selfother 定义的线段上的点,该点在 selfother 之间,比例为 ratio。例如,如果 ratio0.4,则返回 (self * 0.6) + (other * 0.4)

请注意,指定小于 0 或大于 1ratio 不会产生错误,并且 ratio 不会被限制在这个范围内。

Vector2.slerp(other, ratio, *, reflected=None, epsilon=1e-6)

返回一个表示在 selfother 之间进行球面插值的向量,根据标量比率 ratioratio 应该是在 (包括) 01 之间的值。如果 ratio0,则返回 self。如果 ratio1,则返回 other

请注意,指定小于 0 或大于 1ratio 不会产生错误,并且 ratio 不会被限制在这个范围内。

为了与 pygame 兼容,默认情况下 slerp 产生与 pygame.math.Vector2.slerp 相同的结果。这包括以下行为:当 ratio 小于 0 时,slerp 取消 ratio 的符号,并在 selfother 之间绘制另一条路径。这使得无法使用实际的负值作为 ratio

如果您想使用实际的负比率,请指定 reflected 参数。这既指定了要跟踪的路径,又禁用了在 0 周围的反射。reflected=False 选择正比率通常跟踪的路径,而 reflected=True 选择负比率通常跟踪的路径。

slerp 的先前实现产生了不同的结果;它仍然可用,新名称为 Vector2.vec_slerp。它不接受 epsilon 参数。)

Vector2.nlerp(other, ratio)

返回一个表示 selfother 之间按标量比率 ratio 进行归一化线性插值的向量。 ratio 应该是一个介于(包括)01 之间的值。如果 ratio0,则返回 self。如果 ratio1,则返回 other

请注意,指定小于 0 或大于 1ratio 不会出错,且 ratio 不会被限制在这个范围内。

常量

vector2_zero

"零" Vector2 向量对象。 vec 保证每个零向量都是对该对象的引用

>>> v = vec.Vector2(0, 0)
>>> v is vec.vector2_zero
True

从数学的角度讲,当用极坐标表示时,零向量没有定义的角度。因此 vec 将其零向量定义为具有 None 角度。零向量必须将 r 设置为零,将 theta 设置为 None,而任何其他向量都必须具有非零的 r 和将 theta 设置为数值。

vector2_1_0

预定义的 Vector2 向量对象,相当于 Vector2(1, 0)。当构造一个与该向量完全等价的 Vector2 对象时,Vector2 构造函数将始终返回对该向量的引用

>>> v = vec.Vector2(1, 0)
>>> v is vec.vector2_1_0
True
>>> v2 = vec.Vector2(r=1, theta=0)
>>> v2 is vec.vector2_1_0
True

vector2_0_1

预定义的 Vector2 向量对象,相当于 Vector2(0, 1)。当构造一个与该向量完全等价的 Vector2 对象时,Vector2 构造函数将始终返回对该向量的引用

>>> v = vec.Vector2(0, 1)
>>> v is vec.vector2_0_1
True
>>> v2 = vec.Vector2(r=1, theta=pi/2)
>>> v2 is vec.vector2_0_1
True

vector2_1_1

预定义的 Vector2 向量对象,相当于 Vector2(1, 1)。当构造一个与该向量完全等价的 Vector2 对象时,Vector2 构造函数将始终返回对该向量的引用

>>> v = vec.Vector2(1, 1)
>>> v is vec.vector2_1_1
True
>>> v2 = vec.Vector2(r=2 ** 0.5, theta=pi/4)
>>> v2 is vec.vector2_1_1
True

扩展 vec 以处理其他类型

vec 对其输入进行一些验证。坐标(xyrtheta)必须是 intfloat。技术上,theta 也可以是 None。)这最好地服务于 vec 作为 Python 游戏编程 2D 向量库的预期用途。

如果您想尝试将 vec 用于其他用例,您可能希望 vec 允许其他类型作为有效坐标。 vec 提供了一种简单机制来实现这一点。只需在创建向量之前调用

    vec.permit_coordinate_type(T)

,传递您想要用作坐标的类型作为 T,然后 vec 现在将接受该类型的对象作为坐标。

请注意,您以这种方式扩展 vec 的类型应该表现得像数字类型,例如 intfloat

变更日志

0.7.1 2024/09/15

  • Vector2.slerp 添加了 reflected 参数,允许您渲染实际的负比率。为了与 pygame.math.Vector2.slerp 兼容,负比率通常使用对称单位圆跟踪备用路径。指定 reflected 的布尔值将禁用此行为,让您明确选择您想要的路径。

0.7 2024/09/15

  • 破坏性更改:重新设计了对 slerp(球面线性插值)的支持,以提高跨库兼容性
    • vec 的旧实现已重命名为 vec_slerp
    • 有一个新方法,pygame_slerp,它产生与 pygame.math.Vector2.slerp 实质上相同的结果。
    • Vector2.slerp 方法默认为 pygame_slerp
    • vec_slerp现在在ratio类型错误时抛出TypeError(而不是ValueError)。(这很有道理,对吧?)
  • 修改了Vector2对象的repr。如果xy都设置了,并且它们都是整数,那么只显示这些值会更愉快,即使其他一些/所有值都缓存在那里。这使得repr(vector2_zero)更容易阅读。
  • 更新了版权年份至2024。

0.6.3 2023/10/26

  • 添加了三个新的预定义向量

    • vector2_0_1Vector2(0, 1)
    • vector2_1_0Vector2(1, 0)
    • vector2_1_1Vector2(1, 1)

    任何结果等于这些向量的表达式都将返回预定义的向量。例如,Vector2(1, 0) is vector2_1_0评估为True

0.6.2 2023/06/14

  • 添加了Vector2.almost_equal,它支持测试略有不精确的相等性。

0.6.1 2023/06/14

  • 增强了Vector2构造函数:现在它也接受映射。映射必须恰好有两个元素,xy
  • 增强了Vector2.from_polar。它现在接受与Vector2构造函数相同的所有内容:Vector2对象、命名空间、映射和可迭代对象。在检查名称(属性、键)时,它自然使用rtheta而不是xy

0.6 2023/06/14

这是一个重大改进!

  • vec现在有一个合适的测试套件。

  • vec现在通过了100%覆盖率的测试套件。

  • vec明确支持Python 3.6+。

  • 添加了更多的快捷优化,例如旋转笛卡尔向量以pi/2的倍数。

  • 大大简化了元类的__call__逻辑。

  • 实现了Vector2.slerp,并添加了Vector2.nlerp

  • 允许vec.permit_coordinate_type,以允许扩展坐标的允许类型集。

  • 内部细节

    • 现在内部缓存_cartesian_hash以及_polar。(一个向量可以有一个完整的笛卡尔和极坐标集,所以知道所有可用的信息是很好的——这可以使某些操作更快。)
  • 错误修复:Vector2.dot()是错误的,它应该相乘而不是相加。修复了#3。

0.5 2021/03/21

初始版本。

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分布

vec-0.7.1.tar.gz (33.0 kB 查看哈希值)

上传时间:

构建分布

vec-0.7.1-py3-none-any.whl (17.7 kB 查看哈希值)

上传时间: Python 3

支持