Skip to main content

PlayPy is a lightweight Python library for creating games, tools, and interactive applications using a retained-mode UI and scene system built on top of pygame. It focuses on rapid prototyping, composable rendering, and simple but powerful layout primitives.

Project description

PlayPy (0.5.0.dev202606202327)

PlayPy is a lightweight Python library for creating games, tools, and interactive applications using a retained-mode UI and scene system built on top of pygame. It focuses on rapid prototyping, composable rendering, and simple but powerful layout primitives.

Requirements

  • Python >=3.11
  • pygame(-ce) >=2.6.1

Installation

pip install playpy

If that does not work:

python -m pip install playpy

Core Concepts

Module State

Use the following methods to control the state of the module:

plp.init()
plp.quit()

Workspace

Workspace owns:

  • the window
  • render loop
  • input state
  • scene stack
  • coroutine scheduler

Key methods:

ws.run()
ws.quit()  # keep in mind that this method does not call plp.quit()
ws.wait(seconds)

ws.queue_scene_change(scene)
ws.queue_scene_push(scene)
ws.queue_scene_pop(scene=None)

ws.step()

Scene Scope

Temporarily push a scene using a context manager:

with ws.scene_scope(scene) as (scn, handle):
    ...

Call:

handle.disconnect()

to remotely pop the scoped scene.

The scoped scene will automatically be disconnected if not disconnected manually.


Screen State

Use ws.resizable, ws.maximized, and ws.fullscreen to check and change current state. The screen must be resizable to be maximized or fullscreen, and cannot be maximized and fullscreen at the same time. Use ws.resized, ws.maxed, and ws.restored to check status on current frame.


Layout Values

PlayPy uses two rectangle value types:

FRect:

Relative scale values.

plp.FRect(x, y, w, h)

Rect:

Absolute pixel offsets.

plp.Rect(x, y, w, h)

Final element rectangle:

(scale * parent_size) + offset

Helpers:

plp.empty_rect() -> Rect(0, 0, 0, 0)
plp.empty_frect() -> FRect(0, 0, 0, 0)
plp.full_screen_rect() -> (FRect(0, 0, 1, 1), Rect(0, 0, 0, 0))

Rect types are iterable and support multiple constructor overloads.


Assets

Assets are reusable wrappers around external files such as images, animations, and sounds.

PlayPy assets load lazily: they store the path when created, then load the underlying pygame resource when needed. You can also call load() ahead of time to preload an asset and reduce latency during gameplay.

Sprites and Animations

Sprites load and store images. Initialize them by passing in a path. (Path or str object)

Animations store multiple Sprites and allow for changing of animation settings (FPS, loop, etc.) Pass in either sprite paths (Path or str object), or Sprites themselves.

Sounds

Sounds load and store user-imported sounds from sound files. Initialize them by passing in a path. (Path or str object)

Useful methods:

sound = plp.Sound("my_awesome_sound.wav")

sound.load()
sound.play(loop_amt=3, maxtime=10, fade_ms=500)
sound.stop()
sound.set_volume(.5)
volume = sound.get_volume()

Tweening

Tweening is a way to smoothly change values. (like element positions/colors)

Tween constructor:

tween = plp.Tween(
    tweened: list[TweenedValue],
    target: list[Any],
    easing_function: TweenEasingFunction | Callable[[float], float] = "cubic",
    easing_style: TweenEasingStyle | None = None,
    length: float = 1,
    looped: bool = False,
    clear_on_finish: bool = True,
)

Useful methods/properties include...

tween.looped # whether or not the tween loops
tween.length # the length of the tween
tween.elapsed # the current elapsed time in the tween
tween.finished # whether or not the tween has finished (always false on looped tweens)

tween.edit_easing(function, style) # changes the easing function and style of the tween
tween.play() # plays the tween at the current elapsed time
tween.pause() # stops the tween at the current elapsed time
tween.start() # plays the tween from the start, remapping the start points of the tween to the current values
tween.restart() # plays the tween from the start, reverting the current values of the tween to the start point
tween.stop() # instantly ends the tween; start() must be used to restart a stopped tween.

Handling Tweened Values

TweenedValue handles all tweenable values.

val = plp.TweenedValue(
    base_object: object | None,
    key: Any
)

TweenedValues support:

  • attributes of a class/instance: TweenedValue(x, "y") = x.y
  • indices/keys of a list/dict: TweenedValue(x, y) = x[y]
  • standalone values: TweenedValue(None, x) = x.

Useful methods/properties include...

tweened_value.hierarchy_type # returns the hierarchy type (see above) of the TweenedValue ("standalone" / "attribute" / "key" / "index")

tweened_value.get() # gets the current value of the TweenedValue
tweened_value.set(value) # sets the current value of the TweenedValue

Integration Into Workspace

For the tween to update each frame, you must attach it to the workspace.

Active tween helper methods/properties:

ws.active_tweens  # returns all active tweens (cannot be overwritten)

index = ws.add_tween(tween)  # adds a new tween to the workspace and returns it

tween = ws.remove_tween(index)  # removes a tween based on its index and returns it
tween = ws.remove_tween(tween)  # removes a tween and returns it

tween = ws.get_tween(index)  # gets a tween from its index

cleared_tweens = ws.clear_tweens()  # clears all active tweens and returns them

Parenting

Any Element can contain children.

child.parent = parent

or:

parent.add_child(child)

Relationship helpers:

is_parent_of()
is_child_of()

is_ancestor_of()
is_descendant_of()

Properties:

parent
children
ancestors
descendants

Rendering System

PlayPy uses a compositing pipeline.

Element.draw() returns a SurfaceHandler instead of drawing directly to the workspace.

This enables:

  • outlines for arbitrary shapes
  • gradients inheriting shape alpha
  • subtree compositing
  • layered visual effects
  • future post-processing support

Elements

Core Elements

  • Panel
  • Text
  • Button
  • Textbox
  • Tooltip
  • Line
  • Scene

All elements have the following attributes:

Element(
    scale: FRectValue,  # position/size relative to the parent of the element
    offset: RectValue,  # absolute position/size of the element
    visible: bool = True,  # whether or not this element is visible (processed while drawing) (Tooltip does not have this as it automates visibility)
    enabled: bool = True,  # whether or not this element is enabled (processed when input handling)
    block_input_when_occluded: bool = False,  # whether or not to block the input of this element when it is hovered and another element is hovered above this one
    z: int = 0,  # the z layer of the object
    ignores_environment: bool = False,  # whether or not this element ignores its environment. If an object ignores its environment, it will not obide by its parent's components or child clipping.
)

Unless ignores_environment is set, elements natively cut off children that are outside of their bounds.

Effects

Effect is a subclass of Element that visually modifies its parent subtree.

Built-in effects:

  • Gradient
  • ButtonGradient
  • Outline
  • BorderRadius
  • VisualLayer

Effects are ordered using z.

Typical layering:

Negative z:
    backgrounds/gradients

Normal z:
    content

Positive z:
    outlines/glows/overlays

Components

Components modify behavior/layout but do not draw.

Built-in components:

  • Padding
  • Font
  • Camera
  • Scrollable
  • GlobalElement

Attach/get/remove:

element.set_component(component)

element.get_component(ComponentType)

element.remove_component(ComponentType)

or:

component.parent = element

Input State

Access input using:

ws.input

Controller profile changes are tracked on the workspace for the current frame:

ws.profile_changes
ws.profiles_added
ws.profiles_removed
ws.bad_joystick_indices

Other miscellaneous controller properties / methods:

ws.get_controller_name(profile)

ws.rumble_controller(profile, strength, duration_ms)
ws.stop_rumble_controller(profile)

Properties:

ws.input.keys_pressed
ws.input.key_downs
ws.input.key_ups

ws.input.mouse_buttons_pressed
ws.input.mouse_downs
ws.input.mouse_ups

ws.input.mouse_pos
ws.input.mouse_delta
ws.input.mouse_wheel

ws.input.controller_buttons_pressed
ws.input.controller_ups
ws.input.controller_downs

ws.input.controller_left_sticks
ws.input.controller_right_sticks
ws.input.controller_left_sticks_deltas
ws.input.controller_right_stick_deltas

ws.input.text_input

ws.input.dt
ws.input.running_fps
ws.input.runtime
ws.input.quit

Helper methods:

ws.input.key_held()
ws.input.key_down()
ws.input.key_up()

ws.input.mousebutton_held()
ws.input.mousebutton_down()
ws.input.mousebutton_up()

ws.input.controllerbutton_held()
ws.input.controllerbutton_down()
ws.input.controllerbutton_up()

ws.input.left_stick(profile)
ws.input.right_stick(profile)
ws.input.left_stick_delta(profile)
ws.input.right_stick_delta(profile)

ws.input.input_action_held()
ws.input.input_action_down()
ws.input.input_action_up()

Use InputActions for easier keybind compatibility. Any matching key, mouse button, or controller button can trigger the action.

keybind = plp.InputAction(
    keys = [plp.Key.SPACE, plp.Key.RETURN],
    mouse_buttons = [plp.MouseButton.LEFT],
    controller_buttons = [plp.ControllerButton.A, plp.ControllerButton.B],
    profiles = [plp.InputProfile.KEYBOARD_MOUSE, plp.InputProfile.CONTROLLER_0]
)

InputAction event helpers can run handlers directly from those keybinds:

@plp.on_input_action_down(ws, keybind)
def activate(w: plp.Workspace):
    ...

@plp.while_input_action(ws, keybind)
def charge(w: plp.Workspace):
    ...

@plp.on_input_action_up(ws, keybind)
def release(w: plp.Workspace):
    ...

Raw input event helpers are available for direct key, mouse button, and controller button checks:

@plp.on_key_down(ws, plp.Key.ESCAPE)
def quit_game(w: plp.Workspace):
    w.quit()

@plp.on_mousebutton_down(ws, plp.MouseButton.LEFT)
def click(w: plp.Workspace):
    ...

@plp.on_controllerbutton_down(ws, plp.ControllerButton.A, plp.InputProfile.CONTROLLER_0)
def confirm(w: plp.Workspace):
    ...

Hover State

Workspace hover helpers:

ws.is_mouse_top(element)
ws.is_mouse_over(element)

ws.just_hovered(element)
ws.just_unhovered(element)

ws.just_hovered_inclusive(element)
ws.just_unhovered_inclusive(element)

Events

Decorator helpers create Event elements.

@plp.on_start(target)
@plp.on_update(target)
@plp.on_quit(target)

@plp.on_resize(target)
@plp.on_maximize(target)
@plp.on_restore(target)

@plp.on_scene_change(target)

@plp.on_profile_changed(target)
@plp.on_profile_added(target)
@plp.on_profile_removed(target)
@plp.on_controller_not_connected(target)

@plp.on_input_action_down(target, action)
@plp.on_input_action_up(target, action)
@plp.while_input_action(target, action)

@plp.on_key_down(target, key, profile=None)
@plp.on_key_up(target, key, profile=None)
@plp.while_key_held(target, key, profile=None)

@plp.on_mousebutton_down(target, mousebutton, profile=None)
@plp.on_mousebutton_up(target, mousebutton, profile=None)
@plp.while_mousebutton_held(target, mousebutton, profile=None)

@plp.on_controllerbutton_down(target, controllerbutton, profile=None)
@plp.on_controllerbutton_up(target, controllerbutton, profile=None)
@plp.while_controllerbutton_held(target, controllerbutton, profile=None)

@on_hover(...)
@on_unhover(...)
@while_hovered(...)

@on_hover_inclusive(...)
@on_unhover_inclusive(...)
@while_hovered_inclusive(...)

@plp.create_event(target, condition)

Events attached to the main workspace are global by default.

Example:

@plp.on_update(ws)
def tick(w: plp.Workspace):
    if w.input.key_down(plp.Key.ESCAPE):
        w.quit()

Coroutines

Event-like functions can yield.

If a handler returns a generator, PlayPy resumes it on future frames.

Example:

def flash(_: plp.Workspace):
    for _ in range(60):
        print("frame")
        yield

Supported in:

  • event callbacks
  • button callbacks
  • textbox callbacks
  • other event-like handlers

Textbox Features

Textbox supports:

  • placeholders
  • caret blinking
  • text confirmation/reverting
  • character filtering
  • maximum length
  • coroutine callbacks

Useful arguments:

is_char_accepted: Callable[[str], bool] | None,
on_text_updated: Callable[[workspace.Workspace], Generator[None, None, None] | None] | None,
confirm_on_click_off: bool,

Special keys:

Backspace -> remove character
Delete    -> clear text
Return    -> confirm text
Escape    -> revert text

Scenes

Scenes act as "sub-workspaces": when they are alive, only descendants of them (and global elements) are processed and drawn.

Use previously mentioned workspace scene lifecycle methods (ws.queue_scene_change, etc.) to manage alive scene.

Scenes support lifecycle hooks:

on_enter()
on_exit()
on_pause()
on_resume()

Scene changes are queued internally.


Cameras and Scrolling

Camera offsets descendant elements.

Scrollable extends camera functionality with built-in scrolling support.

Example:

scrollable = plp.Scrollable()
scrollable.parent = panel

VisualLayer

VisualLayers render Sprites, Animations, colors, and shader-like effects.

VisualLayer constructor:

VisualLayer(
    visual: plp.Sprite | plp.Animation | plp.ColorValue,
    blend_mode: plp.BlendMode | None = None,
    scale: plp.FRectValue = (0, 0, 1, 1),
    offset: plp.RectValue = (0, 0, 0, 0),
    z: int = -1_000_000,
    visible: bool = True,
    ignores_environment: bool = True
)

Use the previously mentioned ignores_environment for different types of VisualLayers.

# Overlay-style usage:
plp.VisualLayer(sprite, ignores_environment=True)

# Embedded panel/sprite usage:
plp.VisualLayer(sprite, plp.empty_frect(), (20, 20, 50, 50), ignores_environment=False)

Logging

PlayPy replaces standard exceptions/logging with a categorized logging system.

plp.log(severity, category, message)

Severities:

plp.Severity.INFO
plp.Severity.WARNING
plp.Severity.ERROR
plp.Severity.CRITICAL

ERROR and CRITICAL are treated as NoReturn.

Use plp.enter_debug_mode() to prevent logs from halting the program.

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

playpy-0.5.0.dev202606202327.tar.gz (37.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

playpy-0.5.0.dev202606202327-py3-none-any.whl (50.6 kB view details)

Uploaded Python 3

File details

Details for the file playpy-0.5.0.dev202606202327.tar.gz.

File metadata

  • Download URL: playpy-0.5.0.dev202606202327.tar.gz
  • Upload date:
  • Size: 37.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for playpy-0.5.0.dev202606202327.tar.gz
Algorithm Hash digest
SHA256 7e62eb2a5f2423e47f4e5eaa0ede0cb6ddb514173ea1d55a3adae6ecd5108a85
MD5 e1bab56406d86db92be9ab6d4c870ae6
BLAKE2b-256 c7b6e34e0d1f780706ec5e3e26ee79c4339b60ad67be6086a8222cb68de79162

See more details on using hashes here.

File details

Details for the file playpy-0.5.0.dev202606202327-py3-none-any.whl.

File metadata

File hashes

Hashes for playpy-0.5.0.dev202606202327-py3-none-any.whl
Algorithm Hash digest
SHA256 a65e94bd84129a155f3fb224e7b629e9035fc711f75a4e698f55c4608f83010c
MD5 ff1c1e542768d9967db0913945055ace
BLAKE2b-256 5ceb7a92d30628454a4dd9c2e4b79d8e9f02fdf0b1258e81045c201c320da88e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page