A 2-dimensional vector class designed to be convenient
Project description
vec
A reasonable, performant 2D vector object for games
Copyright 2019-2024 by Larry Hastings
Overview
vec
is a module currently publishing one class: Vector2
, a 2D
vector object designed for game development.
Features:
Vector2
objects are immutable.Vector2
support all the usual vector object features, including operator overloading.- Attributes of
Vector2
objects are lazily-computed where possible. Vector2
objects effortlessly support both cartesian and polar coordinates.
vec
supports Python 3.6+, and passes its unit test suite with 100% coverage.
Why Another Vector Class?
I've participated in four PyWeek gaming challenges. And twice, mid-week, I wrote my own vector class out of sheer frustration.
The biggest problem with most Python vector objects in is that they're mutable. Frankly this way lies madness. Vector objects should be immutable--it just makes sense from an API perspective. What if you set the position of some game-engine pawn to be a particular vector object, then modify that vector object? Should the pawn update its position automatically--and if so, how would it know the value changed?
Similarly, some vector classes use degrees for polar coordinates
instead of radians.
Again this way lies madness. The trigonometric functions in Python's
math
module operate in the radians domain, and having to keep track
of which domain something is in--and translate back and forth--is
a needless conceptual complication. You've got a game to write!
(Some vector classes support both radians and degrees for polar coordinates. This is bad API design--it doubles the surface area of your API, adding needless complexity and increasing maintenance and testing overhead. Embrace the radian, folks.)
On a related note, many vector classes make polar coordinates second-class citizens. Most vector classes only store vectors in cartesian coordinates, so either the programmer must perform all polar operations externally to the vector objects, or they incur the overhead and cumulative error of translating to polar and back again with every operation.
vec's Vector2
avoids all these problems:
Vector2
objects are immutable,- they make polar and cartesian coordinates both first-class citizens, and
- they strictly use radians for polar coordinates.
The Conceptual Model
Vector2
objects conceptually represent a vector. They can be
defined using either cartesian or polar coordinates, and any Vector2
can be queried for both its cartesian and polar coordinates.
Most vector objects in games are defined using cartesian coordinates.
Vector2
makes that easy, supporting any number of invocations
to create one. Discrete parameters,
iterables, and objects that support x
and y
attributes all work fine:
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))
All these define the same vector. That last example is there to demonstrate
that Vector2
can create a vector based on any object with x
and y
attributes.
Every Vector2
object supports both cartesian and polar coordinates.
You can define a Vector2
using cartesian coordinates, then examine
its polar coordinates. This:
v = vec.Vector2(0, 1)
print(v.theta, v.r)
prints 1.5707963267948966 1.0
. That first number is π/2 (approximately).
Conversely, you can define a Vector2
object using polar
coordinates, then examine its cartesian coordinates:
v2 = vec.Vector2(r=1, theta=math.pi/2)
print(v2.x, v2.y)
This prints 6.123233995736766e-17 1.0
. Conceptually this should
print 0.0, 1.0
--but math.pi
is only an approximation, which
means our result has an infinitesimal error.
Implementation Details
To define a valid Vector2
object, you must have a complete
set of either cartesian or polar coordinates--either is sufficient.
All other attributes will be lazily computed on demand.
Vector2
objects use slots, and rely on __getattr__
to implement this lazy computation. Only the known values of the
vector are set when it's created. If the user refers to an attribute
that hasn't been computed yet, Python will call Vector2.__getattr__()
,
which computes, caches, and returns that value. Future references to that
attribute skip this mechanism and simply return the cached value, which
is only as expensive as an attribute lookup on a conventional object.
Operations on Vector2
objects compute their result
using the cheapest approach. If you have a Vector2
object
defined using polar coordinates, and you call .rotate()
or .scale()
on it, all the math is done in the polar domain. On the other
hand, adding vectors is almost always done in the cartesian domain,
so if you add a polar vector to any other vector, its cartesian
coordinates will likely be computed--and the resulting vector will always
be defined using cartesian coordinates.
What's the exception? There's a special case for adding two polar vectors
which have the exact same theta
: just add their r
values.
That approach is much cheaper than converting to cartesian,
and more precise as well, returning a vector defined using polar
coordinates! Vector2
takes advantage of many such serendipities,
performing your vector math as cheaply and accurately as possible.
The API
Vector2(x=None, y=None, *, r=None, theta=None, r_squared=None)
-
Constructs a
Vector2
object. You may pass in as many or as few of these arguments as you like; however, you must pass in either bothx
andy
or bothr
andtheta
. Any attributes not passed in at construction time will be lazily computed at the time they are evaluated.You can also pass in a single object which will initialize the vector. Supported objects include:
- an existing
Vector2
object (just returns that object), - an object which has
.x
and.y
attributes, - a mapping object with exactly two keys,
'x'
and'y'
, and - an ordered iterable object with exactly two elements, which
will be used as
'x'
and'y'
respectively.
Vector2
only does some validation of its arguments. It ensures thatr
andtheta
are normalized. However, it doesn't check that(x, y)
and(r, theta)
describe the same vector. If you pass inx
andy
, and also pass in atheta
andr
that don't match, you'll get back theVector2
that you asked for. Good luck! - an existing
Attributes
Vector2
objects support five attributes:
x
, y
, r
, theta
, and r_squared
. It doesn't matter
whether the object was defined with cartesian or polar
coordinates, they'll all work.
r_squared
is equivalent to r*r
. But if you have
a Vector2
object defined with cartesian coordinates,
it's much cheaper to compute r_squared
than r
.
And there are many use cases where r_squared
works
just as well as r
.
For example, consider collision detection in a game. One way to decide whether two objects are colliding is to measure the distance between them--if it's less than a certain distance R, the two objects are colliding. But computing the actual distance is expensive--it requires a time-consuming square root. It's much cheaper to compute the distance-squared between the two points. If that's less than R2, the two objects are colliding.
Operators and protocols
Vector2
objects support the iterator protocol.
len()
on a Vector2
object will always return 2.
You can also iterate over a Vector2
object,
which will yield the x
and y
attributes in that order.
Vector2
objects support the sequence protocol. You can subscript
them, which behaves as if the Vector2
object is a tuple of length
2 containing the x
and y
attributes.
Vector2
objects support the boolean protocol; you may use them
with boolean operators, and you may call bool()
on them. When used in
a boolean context, the zero vector evaluates to False
, and all other
vectors evaluate to True
.
Vector2
objects are hashable, but they're not ordered.
(You can't ask if one vector is less than another.)
Vector2
objects support the following operators:
v1 + v2
adds the two vectors together.v1 - v2
subtracts the right vector from the left vector.v1 * scalar
mulitplies the vector by a scalar amount, equivalent tov1.scale(scalar)
.v1 / scalar
divides the vector by a scalar amount.+v1
is exactly the same asv1
.-v1
returns the opposite ofv1
, such thatv1 + (-v1)
should be the zero vector. (This may not always be the case due to floating-point imprecision.)v1 == v2
isTrue
if the two vectors are exactly the same, andFalse
otherwise. For consistency, this only compares cartesian coordinates. Note that floating-point imprecision may result in two vectors that should be the same failing an==
check. Consider using thealmost_equal
method, which allows for some imprecision in its comparison.v1 != v2
isFalse
if the two vectors are exactly the same, andTrue
otherwise. For consistency, this only compares cartesian coordinates. Note that floating-point imprecision may result in two vectors that should be the same passing an!=
check. Again, consider using thealmost_equal
method and negating the results.v[0]
andv.x
evaluate to the same number.v[1]
andv.y
evaluate to the same number.list(v)
is the same as[v.x, v.y]
.
Class methods
vec.from_polar(r, theta)
-
Constructs a
Vector2
object from the two polar coordinatesr
andtheta
.You can also pass in a single object which will be used to initialize the vector. Supported objects include:
- an existing
Vector2
object (just returns that object), - an object which has
.r
and.theta
attributes, - a mapping object with exactly two keys,
'r'
and'theta'
, and - an ordered iterable object with exactly two elements, which will
be interpreted as
'r'
and'theta'
in that order.
If
r
is0
,theta
must beNone
, andfrom_polar
will return the zero vector. Ifr
is not0
,theta
must not beNone
. - an existing
Methods
Vector2
objects support the following methods:
Vector2.almost_equal(other, places)
-
Returns
True
if the vector andother
are the same vector, down toplaces
decimal places. Like theVector2
class's support for the==
operator, the comparison is only done using cartesian coordinates, for consistency.
Vector2.scaled(scalar)
-
Returns a new
Vector2
object, equivalent to the original vector multiplied by that scalar.
Vector2.scaled_to_length(r)
-
Returns a new
Vector2
object, equivalent to the original vector with its length set tor
.
Vector2.normalized()
-
Returns a new
Vector2
object, equivalent to the original vector scaled to length 1.
Vector2.rotated(theta)
-
Returns a new
Vector2
object, equal to the original vector rotated bytheta
radians.
Vector2.dot(other)
-
Returns the "dot product"
self
•other
. This result is a scalar value, not a vector.
Vector2.cross(other)
Returns the "cross product"
self
⨯other
. This result is a scalar value, not a vector.Note: technically, there is no "cross product" defined for 2-dimensional vectors. This actually returns the "perpendicular dot product" of the two vectors, because that's what people generally mean when they ask for the "cross product" of two 2D vectors.
Vector2.polar()
-
Returns a 2-tuple of
(self.r, self.theta)
.
Vector2.lerp(other, ratio)
-
Returns a vector representing a linear interpolation between
self
andother
, according to the scalar ratioratio
.ratio
should be a value between (and including)0
and1
. Ifratio
is0
, this returnsself
. Ifratio
is1
, this returnsother
. Ifratio
is between0
and1
non-inclusive, this returns a point on the line segment defined by the two endpointsself
andother
, with the point beingratio
betweenself
andother
. For example, ifratio
is0.4
, this returns(self * 0.6) + (other * 0.4)
.Note that it's not an error to specify a
ratio
less than0
or greater than1
, andratio
isn't clamped to this range.
Vector2.slerp(other, ratio, *, reflected=None, epsilon=1e-6)
-
Returns a vector representing a spherical interpolation between
self
andother
, according to the scalar ratioratio
.ratio
should be a value between (and including)0
and1
. Ifratio
is0
, this returnsself
. Ifratio
is1
, this returnsother
.Note that it's not an error to specify a
ratio
less than0
or greater than1
, andratio
isn't clamped to this range.To facilitate compatibility with pygame, by default
slerp
produces identical results topygame.math.Vector2.slerp
. This includes the following behavior: whenratio
is <0
,slerp
negatesratio
and traces an alternate path betweenself
andother
. This makes it impossible to use actual negative values forratio
.If you want to use actual negative ratios, specify the
reflected
parameter. This both specifies which path to trace and disables reflectingratio
around0
.reflected=False
selects the path normally traced by positive ratios, andreflected=True
selects the path normally traced by negative ratios.(A previous implementation of
slerp
produced different results; it's still available, with the new nameVector2.vec_slerp
. It doesn't accept theepsilon
argument.)
Vector2.nlerp(other, ratio)
-
Returns a vector representing a normalized linear interpolation between
self
andother
, according to the scalar ratioratio
.ratio
should be a value between (and including)0
and1
. Ifratio
is0
, this returnsself
. Ifratio
is1
, this returnsother
.Note that it's not an error to specify a
ratio
less than0
or greater than1
, andratio
is not clamped to this range.
Constants
vector2_zero
-
The "zero"
Vector2
vector object.vec
guarantees that every zero vector is a reference to this object:>>> v = vec.Vector2(0, 0) >>> v is vec.vector2_zero True
Mathematically-speaking, the zero vector when expressed in polar coordinates doesn't have a defined angle. Therefore
vec
defines its zero vector as having an angle ofNone
. The zero vector must haver
set to zero andtheta
set toNone
, and any other vector must have a non-zeror
andtheta
set to a numeric value.
vector2_1_0
-
A predefined
Vector2
vector object, equivalent toVector2(1, 0)
. When constructing aVector2
object that is exactly equivalent to this vector, theVector2
constructor will always return a reference to this vector:>>> 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
-
A predefined
Vector2
vector object, equivalent toVector2(0, 1)
. When constructing aVector2
object that is exactly equivalent to this vector, theVector2
constructor will always return a reference to this vector:>>> 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
-
A predefined
Vector2
vector object, equivalent toVector2(1, 1)
. When constructing aVector2
object that is exactly equivalent to this vector, theVector2
constructor will always return a reference to this vector:>>> 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
Extending vec to handle other types
vec
does some input verification on its inputs.
Coordinates--x
, y
, r
, theta
--are required to be
either int
or float
.
(Technically theta
can also be None
.) This best serves
the intended use case of vec
as a 2D vector library for
game programming in Python.
If you want to experiment with vec
for other use cases,
you may want vec
to permit other types to be valid
coordinates. vec
provides a simple mechanism to allow
this. Simply call:
vec.permit_coordinate_type(T)
before creating your vector, passing in the type you want
to use as a coordinate as T
, and vec
will now accept
objects of that type as coordinates.
Note that the types you extend vec
with in this manner
should behave like numeric types, like int
and float
.
Changelog
0.7.1 2024/09/15
- Added the
reflected
parameter toVector2.slerp
, allowing you to render actual negative ratios. For compatibility withpygame.math.Vector2.slerp
, negative ratios normally trace an alternate path using a symmetric unit circle. Specifying a boolean value forreflected
disables this behavior, letting you explicitly select which path you want.
0.7 2024/09/15
- Breaking change: Retooled support for
slerp
(spherical linear interpolation) to increase cross-library compatibility:vec
's old implementation has been renamedvec_slerp
.- There's a new method,
pygame_slerp
, which produces essentially identical results topygame.math.Vector2.slerp
. - The
Vector2.slerp
method defaults topygame_slerp
. vec_slerp
now raisesTypeError
(instead ofValueError
) ifratio
is the wrong type. (Makes sense, right?)
- Change the
repr
forVector2
objects. If bothx
andy
are set, and they're both integers, it's more pleasant to see only those values, even if some/all of the other values are cached. This makesrepr(vector2_zero)
a lot easier to read. - Updated copyright dates to 2024.
0.6.3 2023/10/26
-
Added three new predefined vectors:
vector2_0_1
isVector2(0, 1)
vector2_1_0
isVector2(1, 0)
vector2_1_1
isVector2(1, 1)
Any expression that results in a vector that would be exactly equal to one of these vectors is guaranteed to return the predefined vector.
Vector2(1, 0) is vector2_1_0
evaluates toTrue
.
0.6.2 2023/06/14
- Added
Vector2.almost_equal
, which supports testing for slightly-inexact equality.
0.6.1 2023/06/14
- Enhanced the
Vector2
constructor: now it also accepts mappings. The mapping must have exactly two elements,x
andy
. - Enhanced
Vector2.from_polar
. It now accepts all the same stuff as theVector2
constructor:Vector2
objects, namespaces, mappings, and iterables. Where it examines names (attributes, keys) it naturally usesr
andtheta
instead ofx
andy
.
0.6 2023/06/14
A major improvement!
-
vec
now has a proper test suite. -
vec
now passes its test suite with 100% coverage. -
vec
explicitly supports Python 3.6+. -
Added more shortcut optimizations, e.g. rotating a cartesian vector by a multiple of
pi/2
. -
Tightened up the metaclass
__call__
logic a great deal. -
Implemented
Vector2.slerp
, and addedVector2.nlerp
. -
Allowed
vec.permit_coordinate_type
, to allow extending the set of permissible types for coordinates. -
Internal details:
- Now cache
_cartesian
and_hash
internally, as well as_polar
. (A vector can have a complete set of both cartesian and polar coordinates, so it's nice to know everything that's available--that can make some operations faster.)
- Now cache
-
Bugfix:
Vector2.dot()
was simply wrong, it was adding where it should have been multiplying. Fixes #3.
0.5 2021/03/21
Initial version.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file vec-0.7.1.tar.gz
.
File metadata
- Download URL: vec-0.7.1.tar.gz
- Upload date:
- Size: 33.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.31.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | afec651b457dba6afae810f409315f587305310e5297af5d7326f8191df24d17 |
|
MD5 | a5e5bff5db180b817dbe7acd58dd0b22 |
|
BLAKE2b-256 | 1b4b0390380ed6f0ee3ad20b89cd0c45c34d6b92202903a3c57ec15513883f30 |
File details
Details for the file vec-0.7.1-py3-none-any.whl
.
File metadata
- Download URL: vec-0.7.1-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.31.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3e77bffee2884b7151a4a200e38c60d8a80cc1c4dd0e1f44d3249bf3a2eea33b |
|
MD5 | 49e6c9234835d1936be6455b92521b0e |
|
BLAKE2b-256 | 5174cd1ff87cd5703acdcab829f6a419b01d74ef0097da70cefd7f722e2c36da |