一个方便的二维向量类
项目描述
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
使这变得容易,支持创建一个矢量所需的任何数量的调用。离散参数、可迭代对象以及支持x
和y
属性的对象都可以正常工作
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
可以根据具有x
和y
属性的对象创建矢量。
每个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
对象。您可以选择传入任意多个或少量这些参数;然而,您必须传入要么同时传入x
和y
,要么同时传入r
和theta
。在构造时未传入的任何属性将在评估时按需懒加载。您还可以传入一个单个对象来初始化矢量。支持的对象包括
- 现有的
Vector2
对象(将返回该对象), - 具有
.x
和.y
属性的对象, - 一个具有两个键(
'x'
和'y'
)的映射对象,以及 - 一个具有两个元素的有序可迭代对象,这两个元素将被分别用作
'x'
和'y'
。
Vector2
仅对其参数进行一些验证。它确保r
和theta
已归一化。然而,它不会检查(x, y)
和(r, theta)
是否描述了相同的向量。如果你传入x
和y
,并且也传入一个不匹配的theta
和r
,你将得到你请求的Vector2
。祝你好运! - 现有的
属性
Vector2
对象支持五个属性:x
、y
、r
、theta
和r_squared
。无论对象是用笛卡尔坐标还是极坐标定义的,它们都会正常工作。
r_squared
等价于r*r
。但是如果你有一个用笛卡尔坐标定义的Vector2
对象,计算r_squared
比计算r
要便宜得多。而且有很多使用场景中,r_squared
的效果和r
一样好。
例如,考虑游戏中的碰撞检测。决定两个物体是否碰撞的一种方法是通过测量它们之间的距离——如果距离小于某个特定的距离R
,则两个物体发生碰撞。但是计算实际距离是非常昂贵的——它需要耗时平方根。计算两点之间距离的平方要便宜得多。如果那个值小于R^2
,则两个物体发生碰撞。
运算符和协议
Vector2
对象支持迭代协议。对Vector2
对象执行len()
将始终返回2。你还可以遍历一个Vector2
对象,它将按顺序产生x
和y
属性。
Vector2
对象支持序列协议。你可以使用索引访问它们,就像Vector2
对象是一个包含x
和y
属性的长为2的元组一样。
Vector2
对象支持布尔协议;你可以使用布尔运算符,并且可以在它们上调用bool()
。在布尔上下文中使用时,零向量评估为False
,所有其他向量评估为True
。
Vector2
对象是可哈希的,但它们不是有序的。(你不能询问一个向量是否小于另一个向量。)
Vector2
对象支持以下运算符
v1 + v2
将两个向量相加。v1 - v2
从左向量减去右向量。v1 * scalar
将向量乘以一个标量量,相当于v1.scale(scalar)
。v1 / scalar
将向量除以一个标量量。+v1
与v1
完全相同。-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)
-
从极坐标
r
和theta
构造一个Vector2
对象。您也可以传入一个对象来初始化向量。支持的对象包括
- 现有的
Vector2
对象(将返回该对象), - 具有
.r
和.theta
属性的对象, - 具有恰好两个键的映射对象,
'r'
和'theta'
,和 - 具有恰好两个元素的有序可迭代对象,将按顺序解释为
'r'
和'theta'
。
如果
r
为0
,则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)
-
返回“点积”
self
•other
。该结果是一个标量值,而不是向量。
Vector2.cross(other)
返回“叉积”
self
⨯other
。该结果是一个标量值,而不是向量。注意:从技术上讲,二维向量没有定义“叉积”。这实际上返回两个向量的“垂直点积”,因为当人们询问两个 2D 向量的“叉积”时,通常意味着这个。
Vector2.polar()
-
返回一个二元组
(self.r, self.theta)
。
Vector2.lerp(other, ratio)
-
返回一个表示在
self
和other
之间进行线性插值的向量,根据标量比率ratio
。ratio
应该是在 (包括)0
和1
之间的值。如果ratio
是0
,则返回self
。如果ratio
是1
,则返回other
。如果ratio
在 (不包括)0
和1
之间,则返回由两个端点self
和other
定义的线段上的点,该点在self
和other
之间,比例为ratio
。例如,如果ratio
是0.4
,则返回(self * 0.6) + (other * 0.4)
。请注意,指定小于
0
或大于1
的ratio
不会产生错误,并且ratio
不会被限制在这个范围内。
Vector2.slerp(other, ratio, *, reflected=None, epsilon=1e-6)
-
返回一个表示在
self
和other
之间进行球面插值的向量,根据标量比率ratio
。ratio
应该是在 (包括)0
和1
之间的值。如果ratio
是0
,则返回self
。如果ratio
是1
,则返回other
。请注意,指定小于
0
或大于1
的ratio
不会产生错误,并且ratio
不会被限制在这个范围内。为了与 pygame 兼容,默认情况下
slerp
产生与pygame.math.Vector2.slerp
相同的结果。这包括以下行为:当ratio
小于0
时,slerp
取消ratio
的符号,并在self
和other
之间绘制另一条路径。这使得无法使用实际的负值作为ratio
。如果您想使用实际的负比率,请指定
reflected
参数。这既指定了要跟踪的路径,又禁用了在0
周围的反射。reflected=False
选择正比率通常跟踪的路径,而reflected=True
选择负比率通常跟踪的路径。(
slerp
的先前实现产生了不同的结果;它仍然可用,新名称为Vector2.vec_slerp
。它不接受epsilon
参数。)
Vector2.nlerp(other, ratio)
-
返回一个表示
self
和other
之间按标量比率ratio
进行归一化线性插值的向量。ratio
应该是一个介于(包括)0
和1
之间的值。如果ratio
是0
,则返回self
。如果ratio
是1
,则返回other
。请注意,指定小于
0
或大于1
的ratio
不会出错,且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
对其输入进行一些验证。坐标(x
、y
、r
、theta
)必须是 int
或 float
。技术上,theta
也可以是 None
。)这最好地服务于 vec
作为 Python 游戏编程 2D 向量库的预期用途。
如果您想尝试将 vec
用于其他用例,您可能希望 vec
允许其他类型作为有效坐标。 vec
提供了一种简单机制来实现这一点。只需在创建向量之前调用
vec.permit_coordinate_type(T)
,传递您想要用作坐标的类型作为 T
,然后 vec
现在将接受该类型的对象作为坐标。
请注意,您以这种方式扩展 vec
的类型应该表现得像数字类型,例如 int
和 float
。
变更日志
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
。如果x
和y
都设置了,并且它们都是整数,那么只显示这些值会更愉快,即使其他一些/所有值都缓存在那里。这使得repr(vector2_zero)
更容易阅读。 - 更新了版权年份至2024。
0.6.3 2023/10/26
-
添加了三个新的预定义向量
vector2_0_1
是Vector2(0, 1)
vector2_1_0
是Vector2(1, 0)
vector2_1_1
是Vector2(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
构造函数:现在它也接受映射。映射必须恰好有两个元素,x
和y
。 - 增强了
Vector2.from_polar
。它现在接受与Vector2
构造函数相同的所有内容:Vector2
对象、命名空间、映射和可迭代对象。在检查名称(属性、键)时,它自然使用r
和theta
而不是x
和y
。
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的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | afec651b457dba6afae810f409315f587305310e5297af5d7326f8191df24d17 |
|
MD5 | a5e5bff5db180b817dbe7acd58dd0b22 |
|
BLAKE2b-256 | 1b4b0390380ed6f0ee3ad20b89cd0c45c34d6b92202903a3c57ec15513883f30 |