pydamics
A small, chainable-syntax 2D physics engine for Python. 3D support planned.
Install
pip install pydamics # once published to PyPI
# or, from source:
pip install -e .
Usage
pydamics works two ways. Use whichever fits your project.
1. With the built-in Entity class
from pydamics import Entity, World
ball = Entity(mass=2.0, position=(0, 10))
ball.physics2d.gravity(force=9.8)
ball.physics2d.fluid(density=1.2, drag=0.3)
world = World()
world.add(ball)
# Option 1: step it yourself
for _ in range(120):
world.step(dt=1/60)
print(ball.position)
# Option 2: let the engine run itself on a background thread
world.run(dt=1/60)
...
world.stop()
2. As an extension on YOUR OWN class
pydamics doesn't force an Entity/World object model on you. If you
already have your own classes, attach() makes any object of yours
physics-capable in place -- no inheritance required:
import pydamics
from pydamics import World
class Spaceship:
def __init__(self, name):
self.name = name # your own attributes, untouched
ship = Spaceship("Falcon")
pydamics.attach(ship, mass=1500.0, position=(0, 20))
ship.physics2d.gravity(force=9.8)
world = World()
world.add(ship) # World.add() checks pydamics.has_physics(ship) and
# raises a clear TypeError if you forgot to attach()
world.step(dt=1/60)
Entity is just a thin convenience wrapper around attach() -- use
whichever suits how you're structuring your project.
Attachable forces (obj.physics2d)
| Method | Description |
|---|---|
.gravity(force=9.8, direction=None) |
Constant acceleration in a direction (default: down) |
.fluid(density=1.0, drag=0.1) |
Velocity-proportional drag (air/water resistance) |
.friction(coefficient=0.3, normal_force=9.8) |
Kinetic friction opposing motion |
.spring(anchor, stiffness=10.0, rest_length=1.0, damping=0.1) |
Hooke's-law spring toward a point or another physics object (anchor can be moving) |
.wind(force=2.0, direction=None, gust=0.0) |
Constant directional acceleration, optionally gusting |
.attractor(target, strength=50.0, min_distance=0.1) |
Inverse-square pull toward a point/object (orbital-style gravity) |
.custom(force) |
Attach your own Force subclass |
.remove(force) |
Detach a previously attached force |
.clear() |
Detach all forces |
Every attach method returns the Force object, so you can hold onto it and
remove/tweak it later:
g = ball.physics2d.gravity(force=9.8)
ball.physics2d.remove(g)
Attachable forces (obj.physics2d)
| Method | Description |
|---|---|
.gravity(force=9.8, direction=None) |
Constant acceleration in a direction (default: down) |
.fluid(density=1.0, drag=0.1) |
Velocity-proportional drag (air/water resistance) |
.friction(coefficient=0.3, normal_force=9.8) |
Kinetic friction opposing motion |
.spring(anchor, stiffness=10.0, rest_length=1.0, damping=0.1) |
Hooke's-law spring toward a point or another physics object (anchor can be moving) |
.wind(force=2.0, direction=None, gust=0.0) |
Constant directional acceleration, optionally gusting |
.attractor(target, strength=50.0, min_distance=0.1) |
Inverse-square pull toward a point/object (orbital-style gravity) |
.vortex(center, strength=20.0, min_distance=0.1) |
Tangential swirling force around a point |
.buoyancy(zone, radius=0.4, gravity=9.8) |
Archimedes-style float/sink force inside a FluidZone |
.custom(force) |
Attach your own Force subclass |
.remove(force) |
Detach a previously attached force |
.clear() |
Detach all forces |
Every attach method returns the Force object, so you can hold onto it and
remove/tweak it later:
g = ball.physics2d.gravity(force=9.8)
ball.physics2d.remove(g)
Collision
ball.physics2d.collider(radius=0.4, restitution=0.7) # bouncy
wall.physics2d.collider(radius=0.5, restitution=0.5, static=True) # never moves
World.step() automatically detects and resolves overlaps between any
entities that have a .physics2d.collider(...) -- impulse-based, with a
restitution (bounciness) you set per object; the lower of the two
objects' restitution values is used per collision.
SEO — Solid Environment Objects
For solid geometry (platforms, walls, floors) that things collide with,
.seo works whether or not the object is also physics-capable:
import pydamics
# a plain object, made purely static/solid -- doesn't need attach()
class Platform:
pass
platform = Platform()
pydamics.solidify(platform, position=(0, 0))
platform.seo.solid(width=8, height=1, restitution=0.4)
world.add_solid(platform) # register it for collision (not world.add() --
# it isn't physics-capable, so world.add()
# would reject it)
If the object is ALSO physics-capable (attach()-ed or an Entity), it
becomes a "physicsified" solid: movable/affected by forces, but still
solid -- e.g. a platform that falls under gravity but still carries a
ball resting on top of it. Physicsified solids just go through the
normal world.add() -- they're auto-detected as solids too, no need to
also call add_solid().
platform = pydamics.attach(Platform(), mass=50.0, position=(0, 10))
platform.physics2d.gravity(force=2.0)
pydamics.solidify(platform) # reuses the position attach() set
platform.seo.solid(width=8, height=1)
world.add(platform) # physics-capable -> world.add(), not add_solid()
.seo.solid() accepts either width+height (rectangle) or radius
(circle).
Fluid dynamics
Two different scopes, depending on what you need:
FluidZone (buoyancy) — lightweight: a rectangular region entities
float or sink in, via .physics2d.buoyancy(zone) (see the forces table
above). density is relative to your entities' own effective density
(mass / (pi * radius^2)) — not a literal real-world kg/m³ value; pick
values relative to what your entities' mass/radius actually imply, or
you'll get correctly-extreme (but probably undesired) results, the same
way a helium balloon dropped in water would rocket upward in real life.
pool = pydamics.FluidZone(min_point=(-5, 0), max_point=(5, 5), density=1.8, drag=1.5)
cork.physics2d.buoyancy(zone=pool, radius=0.3)
FluidSystem (full SPH) — real smoothed-particle-hydrodynamics: particles
with density/pressure/viscosity computed from their neighbors, genuinely
fluid-like behavior. Its own particle system (not the Entity/physics2d
model, since SPH forces are inherently pairwise):
from pydamics import FluidSystem, Vec2
fluid = FluidSystem(smoothing_radius=1.0, rest_density=1000.0, stiffness=150.0)
fluid.add_particle(position=(0, 5))
# ... add more particles ...
world.add_fluid_system(fluid, gravity=9.8) # steps alongside world.step()
# or drive it yourself:
fluid.step(dt=1/120, gravity=9.8)
fluid.apply_bounds(Vec2(-5, 0), Vec2(5, 10)) # optional container walls
Integration
Uses Velocity Verlet integration (not simple Euler, not full RK4) — it's the standard for force-based particle sims: stable, and integrates naturally with drag and collision impulses.
Tests
pip install -e ".[dev]"
pytest tests/
Visualization
Rendering (matplotlib GIFs, interactive pygame windows) lives in a separate companion package so this core library stays dependency-free:
pip install pydamicsvisual
See pydamicsvisual for details.
Roadmap
- 3D physics namespace (
entity.physics3d) - Polygon collision shapes (currently circles + AABB boxes only)
- Spatial hashing for SPH/collision broad-phase (currently naive O(n²), fine to a few hundred objects)
Publishing (for maintainers)
1. Push to GitHub
git init
git add .
git commit -m "Initial commit: pydamics 2D physics engine"
git branch -M main
git remote add origin https://github.com/<your-username>/pydamics.git
git push -u origin main
The .github/workflows/tests.yml workflow will auto-run the test suite on
every push.
2. One-time PyPI setup (Trusted Publishing — no API tokens needed)
- Create a PyPI account if you don't have one.
- Go to pypi.org → Your account → Publishing and add a new "trusted publisher":
- PyPI project name:
pydamics - Owner:
<your-github-username> - Repository name:
pydamics - Workflow name:
publish.yml - Environment name:
pypi
- PyPI project name:
- In your GitHub repo, go to Settings → Environments and create an environment named
pypi(this matches the workflow file — no secrets needed, trusted publishing handles auth).
3. Ship a release
Bump the version in pyproject.toml, commit, then on GitHub:
Releases → Draft a new release → tag v0.1.0 → Publish release.
That triggers .github/workflows/publish.yml, which builds the package and
uploads it to PyPI automatically. From then on, anyone can:
pip install pydamics
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
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydamics-0.3.0.tar.gz.
File metadata
- Download URL: pydamics-0.3.0.tar.gz
- Upload date:
- Size: 25.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d26fd070afbbb7225b5bb1567dc53b1addfe8cca6ee34b6c0ef8f8b50e5ff31
|
|
| MD5 |
572d1ccc8f76c5fc9a1ee5137b8c819e
|
|
| BLAKE2b-256 |
92393f29334288f1a91e40db3b7a3c6fba40f96bffd1bec7fcec376ae28503c5
|
Provenance
The following attestation bundles were made for pydamics-0.3.0.tar.gz:
Publisher:
publish.yml on Reeed-cell/Pydamics
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydamics-0.3.0.tar.gz -
Subject digest:
6d26fd070afbbb7225b5bb1567dc53b1addfe8cca6ee34b6c0ef8f8b50e5ff31 - Sigstore transparency entry: 2297102231
- Sigstore integration time:
-
Permalink:
Reeed-cell/Pydamics@48d9187cdc2b4b02bd3829ac71551a66b7ca1025 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Reeed-cell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48d9187cdc2b4b02bd3829ac71551a66b7ca1025 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pydamics-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pydamics-0.3.0-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7cc2fca4e43cfdbb9e923a826a878e3cead84ec3fb56ce1cbaad6ffc18259cf
|
|
| MD5 |
9c8ec9d514b0846492e15563937674c2
|
|
| BLAKE2b-256 |
876999a98d9d25344392a8a9064b44d3080a74a2fa8b9691392a93e400d27d0f
|
Provenance
The following attestation bundles were made for pydamics-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on Reeed-cell/Pydamics
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydamics-0.3.0-py3-none-any.whl -
Subject digest:
f7cc2fca4e43cfdbb9e923a826a878e3cead84ec3fb56ce1cbaad6ffc18259cf - Sigstore transparency entry: 2297102306
- Sigstore integration time:
-
Permalink:
Reeed-cell/Pydamics@48d9187cdc2b4b02bd3829ac71551a66b7ca1025 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/Reeed-cell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@48d9187cdc2b4b02bd3829ac71551a66b7ca1025 -
Trigger Event:
release
-
Statement type: