Shared pygame-ce utilities used across personal game projects. Provides an Application base, a component-based game-object layer, a YAML-driven panel/UI system, asset loaders, and a handful of helpers (camera, sprite sheet, audio, database).
See CHANGELOG.md for what changed between versions — useful before bumping a consumer project's submodule pointer past a MINOR version, since 0.y.z means breaking changes can land in a MINOR bump.
This package is consumed as a git submodule by the host project — typically vendored under src/pygamine/ and added to PYTHONPATH (or sys.path) by the host's entry point.
git submodule add https://github.com/vectorviolet/pygamine.git src/pygamineThen in the host's entry script:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src" / "pygamine"))
# Now imports work:
from pygamine import ApplicationThe package's public API is re-exported at the top level (pygamine/__init__.py), so from pygamine import Application, GameObject, Camera, ... covers the classes/functions most consumers need — deep-path imports (from pygamine.ecs.game_object import GameObject) still work too, for anything not surfaced there.
It can also be installed via pip directly from GitHub:
pip install git+https://github.com/vectorviolet/pygamine.gitRequires Python 3.12+ and pygame-ce.
Every current host project's pyproject.toml also carries:
[tool.uv.sources]
pygamine = { path = "src/pygamine", editable = true }This override only takes effect for a dependency uv actually resolves —
i.e. it requires "pygamine" to also be listed under
[project.dependencies]. Every current host project does list it there,
so uv sync installs this submodule in editable mode (uv.lock shows
source = { editable = "src/pygamine" }) rather than relying on
sys.path alone — the sys.path insertion above is still what makes
import pygamine work for anything invoked without going through uv
(e.g. a frozen PyInstaller build), so keep both. A host project that
skips "pygamine" in [project.dependencies] gets an inert
[tool.uv.sources] block — the override only ever matters alongside a
matching dependency entry.
| Module | Exports | Notes |
|---|---|---|
app.application |
Application |
Top-level run loop (init/run/handle_event/update/draw), fullscreen toggle, debug overlay hook, self.splash/show_splash(), opt-in fixed_aspect letterboxing |
app.mouse |
Mouse |
Cursor position + optional custom-cursor GameObject |
app.camera |
Camera |
Edge-scroll camera with world↔screen transforms |
app.splash_screen |
splash helper | Pre-loop splash render |
app.debug |
debug helpers | Wireframes and live state overlay |
| Module | Exports |
|---|---|
assets.asset_path |
AssetPath, ImagePath, FontPath, SoundPath, PathLike |
assets.asset_manager |
AssetManager — loads images/sounds/fonts from a YAML manifest |
assets.image |
load_image, scale, scale_by, nine_slice_scale |
assets.font |
load_font (asset-key first, falls back to SysFont) |
assets.sprite_sheet |
SpriteSheet (from_path, strip, grid, frame) |
assets.database |
Database, DatabaseError — thin SQLite wrapper, stores .db under databases/ |
assets.save_store |
SaveStore — JSON-backed key-value persistence for settings/saves under saves/, atomic writes |
assets.tilemap |
TiledMap — Tiled .tmx loader (pytmx-backed): tile dims, object-group iteration, offscreen pre-render, camera-aware draw |
assets.paths |
resource_root, resource_path — cwd anchor that works from source and inside a frozen PyInstaller bundle; a host's entry point chdir()s into resource_root() at startup |
| Module | Exports |
|---|---|
panels.panel_manager |
PanelManager — named panels with current_panel switching |
panels.panel_loader |
PanelLoader — YAML → objects via registered factories |
panels.panel_loader_ext |
PanelLoaderExt — adds object_templates inheritance |
panels.panel_factory |
make_factory, make_text_factory |
| Module | Exports |
|---|---|
ecs.game_object |
GameObject (active/hierarchy/invoke[_repeating]/components) |
ecs.game_object_dict |
GameObjectDict — named child container with lifecycle dispatch |
ecs.game_object_list |
GameObjectList — plain-list variant of the same dispatch (handle_event/update, skipping inactive objects) |
ecs.state_object |
StateObject, HoverableStateObject |
ecs.animated_sprite |
AnimatedSprite, AnimatedSpriteFactory |
ecs.game_audio |
GameAudio — music on a reserved channel, SFX on a pool of free channels (a full pool drops the new sound by default), with music, SFX and master volume control |
ecs.sound_manager |
SoundManager — static per-channel volume/play helpers on top of pygame.mixer |
ecs.components.component |
Component, Behaviour, MonoBehaviour |
ecs.components.transform |
Transform — anchored pos/size relative to parent |
ecs.components.sprite_renderer2d |
SpriteRenderer2D |
ecs.components.rigidbody2d |
Rigidbody2D |
ecs.components.animator |
Animator, AnimationClip |
| Module | Exports |
|---|---|
ui_widgets.text_object |
TextObject — single- or multi-state label |
ui_widgets.menu_controller |
MenuController — keyboard-navigable button list |
ui_widgets.input_box |
InputBox |
ui_widgets.tooltip |
Tooltip, TooltipManager — hover tooltip with key prompt |
| Module | Exports |
|---|---|
util.utils |
MouseInteractive, Anchorable, resolve_size, ANCHORS |
util.math_utils |
Vector and angle helpers |
util.spatial_grid |
SpatialGrid — uniform spatial hash grid; turns O(N²) neighbour/overlap queries into roughly O(N) |
GUI-free and application-agnostic — these classes move framed messages over TCP and report back via callbacks; they never hold a reference upward into application logic. Used by standoff (see its own docs for a worked example); most single-player projects don't need this section at all.
| Module | Exports |
|---|---|
net.protocol |
Protocol — wire framing (4-byte length prefix + body) and pluggable Codecs: JSONCodec (default, safe for untrusted peers), TypedJSONCodec, PickleCodec (trusted peers only — see its own warning), ProtocolError |
net.transport |
Connection, BaseClient, BaseServer — socket plumbing built on Protocol, callback-driven |
from pygamine import SpriteSheet, AnimatedSprite
frames = SpriteSheet.from_path("coin_strip4.png").strip(4)
coin = AnimatedSprite(frames=frames, fps=8, pos=(100, 100))
# inside the game loop:
coin.update()
coin.draw(surface)from pygamine import ImagePath, FontPath, SoundPath
ImagePath("player") # → assets/images/player.png
ImagePath("hero", "sprites") # → assets/images/sprites/hero.png
FontPath("comic") # → assets/fonts/comic.ttf
SoundPath("jump") # → assets/sounds/jump.oggAll AssetPath subclasses implement __fspath__, so they can be passed directly to pygame.image.load / pygame.font.Font / pygame.mixer.Sound.
from pygamine.util.localization import localization, tr, set_language
localization.load("config/lang") # en.yaml (required), tr.yaml, ...
localization.on_language_changed(
lambda code: volume_label.set_text(tr("settings.volume", percent=80))
) # text the game formats itself
set_language("tr") # re-renders every text_key label at onceconfig/lang/en.yaml: _meta: {name: English} plus nested keys
(settings: {title: Settings}). In panels YAML use text_key: settings.title
instead of text: (optional text_params: fills {placeholders}). Missing keys fall back to English; a key missing in English
raises KeyError. In your tests call localization.check_key_coverage() and
localization.check_glyph_coverage("assets/fonts/Game.ttf") (both return {} when
clean). Before reloading a panel call panel_manager.remove_panel(name) so its
labels stop following language changes. See
docs/decisions/0001-localization.md.
from pygamine.util.input_map import InputMap
keys = InputMap()
keys.declare("tower1", pygame.K_1, alternates=[pygame.K_KP1])
keys.declare_fixed("menu", pygame.K_ESCAPE)
if keys.is_action(event, "tower1"):
... # KEYDOWN of 1 or keypad 1
clash = keys.rebind("tower1", pygame.K_q) # None, or the action to swap with
settings["keys"] = keys.to_dict() # {"tower1": "q"}; keys.from_dict(...) loadsSee docs/decisions/0003-input-map.md.
from pygamine.ui_widgets.tooltip import TooltipManager
tips = TooltipManager(keys, font, icons={"f5": "assets/keys/f5.png"}) # icons optional
loader = PanelLoader(panel_manager, window, assets, tooltips=tips)pause_button:
type: button
tooltip: {text_key: hud.pause, action: pause} # action optionalShows after 0.4 s (delay_ms), hides on leave and click, stays on screen, one at a time.
icons maps pygame.key.name strings to image paths (or is a function doing so); keys
without an icon show as [F5]. Rebinds and language changes update it. Switching
panels hides it (PanelManager.current_panel); pass mouse= your Mouse if the game scales its window. See docs/decisions/0004-tooltips.md.
from pygamine import Database
db = Database("savefile")
rows = db.execute_safely(
"SELECT name, value FROM scores ORDER BY value DESC LIMIT 10",
fetch=True,
)execute_safely handles connect/commit/disconnect in one call; use connect/execute/commit/disconnect directly when you need to batch operations. Any failure raises DatabaseError (from pygamine import DatabaseError) — catch it (or let it propagate) rather than checking a return value.
- Formatting: Black-compatible, enforced by
ruff format(line length 88, double quotes, otherwise Black's defaults; see[tool.ruff.format]inpyproject.toml). - Lint:
ruff check, deliberately narrow (see[tool.ruff.lint]). - CI: runs both, plus
mypy.
Run both locally before committing:
uv run --group dev ruff format . # reformat
uv run --group dev ruff check . # lint
uv run --group dev ruff format --check . # what CI runs: fails if anything would be reformattedThe commit that first applied the formatter is listed in .git-blame-ignore-revs. To have
git blame skip it and show the real author of each line:
git config blame.ignoreRevsFile .git-blame-ignore-revs- Create
pygamine/<subpackage>/<module>.py— pick the subpackage from the module map above (app,assets,panels,ecs,ui_widgets,net,util); add a new subpackage only if the module genuinely doesn't fit any existing one. - Add the public names to
pygamine/__init__.py's_EXPORTSdict (and the matchingTYPE_CHECKINGimport, kept in sync with it) sofrom pygamine import <Name>works — this is the form consumers should use; deep-path imports (from pygamine.assets.image import load_image) still work too, for anything not surfaced there. - No reinstall required when consumed as a submodule on
sys.path.