2D Game Engine using pyglet (OpenGL) for rendering
Project description
PyVerse2D
A batteries-included 2D game engine for Python, built on top of pyglet.
PyVerse2D gives you a complete ECS-based runtime: physics, lighting, particles, tilemaps, GUI, audio... behind a clean and minimal API.
Features
- ECS architecture - entities, components, and systems with dependency/conflict validation
- Physics engine - rigid bodies, gravity, collision detection and resolution (circles, capsules, polygons, ellipses, rounded rects)
- Rendering pipeline - layered scenes, cameras with letterboxing, viewport transforms, z-ordering
- Lighting system - ambient, point lights, cone lights, bloom, vignette, tint
- Particle system - line, circle, cone and point emitters with modifiers (wind, drag, gravity, attractor)
- Tilemap support - Tiled TMX loader, automatic collision injection, parallax cameras
- GUI system - widgets, tweens, behaviors (click, hover, focus, select), toggle buttons, scrollbars, labels
- Asset management - images, animations, fonts, sounds, music, playlists, video
- Input system - keyboard, mouse, combo listeners, repeat and condition support
- Built-in profiler - frame-accurate profiling with export
Installation
pip install pyverse2d
Or install the latest dev version directly from GitHub:
pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip
Requirements: Python 3.11+, pyglet 2.x
Hello World
The minimal setup: open a window and draw a bouncing ball.
import pyverse2d as pv
from pyverse2d import Window, LogicalScreen, Camera, Viewport
from pyverse2d import world, scene
# --- Window ---
screen = LogicalScreen(1920, 1080)
window = Window(screen=screen, caption="Hello World", vsync=True)
pv.set_window(window)
# --- Scene ---
camera = Camera(anchor=(0.5, 0.5), view_width=40, view_height=22.5)
viewport = Viewport(width=1920, height=1080, origin=(0.5, 0.5))
main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene)
main_world = world.World()
main_scene.add_layer(scene.WorldLayer(main_world), z=0)
# --- Systems ---
main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem())
# --- Ground ---
ground_shape = pv.shape.Rect(30.0, 1.0)
ground = world.Entity(
world.Transform(position=(0.0, -8.0), anchor=(0.5, 0.5)),
world.ShapeRenderer(shape=ground_shape, filling_color=(80, 80, 80)),
world.Collider(shape=ground_shape),
world.RigidBody(restitution=0.5, friction=0.4),
)
main_world.add_entity(ground)
# --- Ball ---
ball_shape = pv.shape.Circle(1.0)
ball = world.Entity(
world.Transform(position=(0.0, 8.0), anchor=(0.5, 0.5)),
world.ShapeRenderer(shape=ball_shape, filling_color=(80, 180, 255)),
world.Collider(shape=ball_shape),
world.RigidBody(mass=1.0, restitution=0.8, friction=0.2),
)
main_world.add_entity(ball)
# --- Run ---
pv.preload()
pv.run()
Core concepts
Window & screen
LogicalScreen defines the virtual resolution your game is designed for. Window wraps the OS window and handles letterboxing automatically: your game scales cleanly to any physical window size.
screen = LogicalScreen(1920, 1080) # virtual canvas
window = Window(screen=screen, resizable=True, vsync=True)
pv.set_window(window)
Scene & layers
A Scene is a self-contained game state. Layers stack inside a scene at a given z-index. WorldLayer, TileLayer, GuiLayer, LightLayer and ParticleLayer each handle a specific rendering domain.
main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene) # push onto the scene stack
main_scene.add_layer(scene.WorldLayer(main_world), z=0) # ECS world
main_scene.add_layer(scene.GuiLayer(), z=100) # UI on top
Entities & components
An Entity is a container of components. Components are plain data objects, logic lives in systems.
player = world.Entity(
world.Transform(position=(0.0, 5.0), anchor=(0.5, 0.0)),
world.ShapeRenderer(shape=pv.shape.Capsule(0.4, 2.0), filling_color=(255, 120, 60)),
world.Collider(shape=pv.shape.Capsule(0.4, 2.0)),
world.RigidBody(mass=60.0, friction=0.4),
world.GroundSensor(),
)
main_world.add_entity(player)
Available components: Transform, ShapeRenderer, SpriteRenderer, TextRenderer, Animator, Collider, RigidBody, GroundSensor, Follow, SoundEmitter, VideoPlayer.
Systems
Systems process entities every frame. Add them to a World and they run in order:
main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem())
main_world.add_system(world.AnimationSystem())
main_world.add_system(world.SteeringSystem())
main_world.add_system(world.SoundSystem(origin=camera))
Camera
Cameras define the point of view. They support smooth following, animated transitions, and parallax attachment.
# Follow a target with smooth lag
camera.follow(player.transform, offset=(0.0, 1.0), smoothing=0.05)
# Animated transition to a position
camera.goto((10.0, 0.0), duration=1.5, easing=pv.math.easing.ease_in_out_quad)
# Parallax-derived camera (e.g. for a background layer)
background_camera = Camera.derived_from(camera, parallax_x=0.3)
Physics
Entities with a Collider and a RigidBody participate in the physics simulation. Set gravity=False on a RigidBody to opt out of gravity. Use apply_force and apply_impulse for runtime interactions.
rb = entity.rigid_body
rb.apply_force(pv.math.Vector(500.0, 0.0)) # continuous force
rb.apply_impulse(pv.math.Vector(0.0, 300.0)) # instant impulse (e.g. jump)
Supported shapes: Circle, Rect, RoundedRect, Ellipse, Capsule, Polygon, RegularPolygon.
Inputs
# Single key listener
pv.inputs.add_listener(pv.key.K_SPACE, player.jump)
# Held key (repeat=True fires every frame)
pv.inputs.add_listener(pv.key.K_D, player.move_right, repeat=True)
# Combo
pv.inputs.when_all_of([pv.key.K_LEFT, pv.key.K_DOWN], move_downleft, repeat=True)
# Mouse
pv.inputs.add_listener(pv.mouse.B_LEFT, on_click)
Assets
# Image
img = pv.asset.Image("player.png", height=2.5)
# Animation from a folder (files matching a prefix)
anim = pv.asset.Animation.from_folder("assets/", prefix="run_", framerate=12, height=2.5)
# Sound with random variations
footstep = pv.asset.Sound.from_variations("assets/audio/", prefix="step_", cooldown=0.4)
# Music
music = pv.asset.Music("theme.ogg")
pv.audio.play_music(music, loop=True, fade_s=2.0)
Lighting
light_layer = pv.scene.LightLayer(
pv.fx.Ambient(intensity=0.2, color=(0, 0, 30)),
gamma=1.0,
exposure=3.0,
)
main_scene.add_layer(light_layer, z=3)
# Point light attached to the player
point = pv.fx.PointLight(radius=10, intensity=1.0)
point.attach_to(player.transform, offset=(0.0, 1.0))
light_layer.add_source(point)
# Cone light (e.g. spotlight from above)
cone = pv.fx.ConeLight(position=(0.0, 30.0), direction=(0.0, -1.0), angle=20, intensity=1.2)
light_layer.add_source(cone)
Particles
particle_layer = pv.scene.ParticleLayer(additive=True)
main_scene.add_layer(particle_layer, z=-1)
particle = pv.fx.Particle(
lifetime=(4, 8), speed=(5, 10),
size=(0.3, 0.8), size_end=0.1,
color_start=(0, 100, 255), color_end=(255, 255, 255),
)
emitter = pv.fx.LineEmitter(p1=(-50, 30), p2=(50, 30), normal=True, particle=particle, rate=80)
emitter.add_modifier(pv.fx.Wind(direction=-45, strength=1.5, turbulence=8))
particle_layer.add_emitter(emitter)
Tilemaps
# Load a Tiled .tmx file
stage = pv.tile.MapLoader.from_tiled_tmx("map/stage_0.tmx", tile_width=1.5, tile_height=1.5)
# Add layers to the scene
ground = stage["ground"]
main_scene.add_layer(pv.scene.TileLayer(ground), z=4)
# Inject tile colliders automatically into the world
pv.tile.CollisionMapper(ground).inject(main_world)
Profiling
pv.preload()
pv.profile(duration=10.0, on_update=on_update, export_path="profile_report.txt")
Game loop
def on_update(dt: float):
# your update logic
pass
def on_draw():
# your extra draw logic
pass
pv.preload()
pv.run(on_update=on_update, on_draw=on_draw)
pv.stop() cleanly shuts down the engine and closes the window.
Project structure
pyverse2d/
├── abc/ # Abstract base classes (Space, Component, System, Asset…)
├── asset/ # Image, Animation, Font, Sound, Music, Video, Text, Playlist
├── fx/
│ ├── light/ # Ambient, PointLight, ConeLight, Bloom, Vignette, Tint
│ └── particle/ # Emitters (Line, Circle, Cone, Point) + Modifiers
├── gui/ # Widgets, Tweens, Behaviors, SelectionGroup
├── math/ # Point, Vector, Line, easing functions, vertex helpers
├── scene/ # Scene, WorldLayer, TileLayer, GuiLayer, LightLayer, ParticleLayer
├── shape/ # Circle, Rect, RoundedRect, Ellipse, Capsule, Polygon, RegularPolygon
├── tile/ # Tiled TMX loader, CollisionMapper, TileMap
├── typing/ # Type aliases
└── world/
├── _component/ # Transform, Collider, RigidBody, Renderers, Animator, Follow…
└── _system/ # Physics, Gravity, Collision, Render, Animation, Steering, Sound, Video
License
MIT — © WhiteWolf45380
Project details
Release history Release notifications | RSS feed
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 pyverse2d-1.3.21.tar.gz.
File metadata
- Download URL: pyverse2d-1.3.21.tar.gz
- Upload date:
- Size: 265.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a546d84e09c94cd4f499e0e7c6bbaa82983c9761d8f376497d807713d6222cb
|
|
| MD5 |
32f4ee7cb2ecb040b9425f585d84ff61
|
|
| BLAKE2b-256 |
2b1a5f6e218a9d3226f9d9a2bb88786303b40686806caa43c0d8c716d55608e8
|
Provenance
The following attestation bundles were made for pyverse2d-1.3.21.tar.gz:
Publisher:
publish.yml on WhiteWolf45380/PyVerse2D
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyverse2d-1.3.21.tar.gz -
Subject digest:
0a546d84e09c94cd4f499e0e7c6bbaa82983c9761d8f376497d807713d6222cb - Sigstore transparency entry: 1586337864
- Sigstore integration time:
-
Permalink:
WhiteWolf45380/PyVerse2D@a76d56acb2cb4bb34c4556b5a3fb6a8b3f3bd6aa -
Branch / Tag:
refs/tags/v1.3.21 - Owner: https://github.com/WhiteWolf45380
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a76d56acb2cb4bb34c4556b5a3fb6a8b3f3bd6aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyverse2d-1.3.21-py3-none-any.whl.
File metadata
- Download URL: pyverse2d-1.3.21-py3-none-any.whl
- Upload date:
- Size: 373.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf7a81821f54a51a890f16887069a4bb5fd88d12fd02b5f59ef6e8f024cf7738
|
|
| MD5 |
d84d94d83b65e2aff473e6fd905f76d7
|
|
| BLAKE2b-256 |
135cd2e3b7d17c72463912da51c7bd863757189f8b92412a1e463a2325b78fc3
|
Provenance
The following attestation bundles were made for pyverse2d-1.3.21-py3-none-any.whl:
Publisher:
publish.yml on WhiteWolf45380/PyVerse2D
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyverse2d-1.3.21-py3-none-any.whl -
Subject digest:
bf7a81821f54a51a890f16887069a4bb5fd88d12fd02b5f59ef6e8f024cf7738 - Sigstore transparency entry: 1586337928
- Sigstore integration time:
-
Permalink:
WhiteWolf45380/PyVerse2D@a76d56acb2cb4bb34c4556b5a3fb6a8b3f3bd6aa -
Branch / Tag:
refs/tags/v1.3.21 - Owner: https://github.com/WhiteWolf45380
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a76d56acb2cb4bb34c4556b5a3fb6a8b3f3bd6aa -
Trigger Event:
push
-
Statement type: