direct (Python show framework)

The direct tree is Panda3D’s Python-facing application framework: a thin, event-driven object layer that bootstraps a window and scene graph, schedules per-frame work, animates characters and properties, drives UI, and (in the Disney lineage) replicates objects across a network. Almost every class here is a Python wrapper over a C++ core type — ShowBase orchestrates GraphicsEngine/AsyncTaskManager, Actor wraps Character/PartBundle, the interval Python classes subclass CInterval, DirectGui wraps PGItem, and ConnectionRepository subclasses CConnectionRepository. Three patterns recur everywhere and are worth internalizing before reading any single subsystem: (1) global singletons written into Python builtins by ShowBase (base, taskMgr, messenger, render, loader, ivalMgr); (2) DirectObject inheritance, which gives any class accept()/addTask() sugar over the messenger and task manager; and (3) the config_*.cxx registration of ConfigVariable* tuning knobs read by both C++ and Python. The C++ halves of these subsystems live under the same directories (e.g. direct/src/interval/cInterval.cxx, direct/src/distributed/cConnectionRepository.cxx), compiled into panda3d.direct.

showbase

What it is. The application bootstrap and foundation layer. ShowBase opens the graphics window, builds the 2-D and 3-D scene graphs, wires up input devices, installs the per-frame task loops, and publishes the well-known globals. Instantiating it (base = ShowBase()) is what turns a bare Python process into a running Panda app. The directory also holds the foundational utility classes that the rest of direct depends on: DirectObject, Messenger, Loader, EventManager, plus a large grab-bag of debugging/profiling tools (GarbageReport, ContainerLeakDetector, OnScreenDebug, BufferViewer).

Key classes and roles.

  • ShowBasedirect/src/showbase/ShowBase.py (~3550 lines). Subclasses DirectObject so it can hang hooks on window-event. Holds self.graphicsEngine (GraphicsEngine.getGlobalPtr()), self.win, self.render/self.render2d/self.aspect2d/self.pixel2d, self.cam/self.camera, self.cTrav (collision traverser), self.taskMgr, self.loader, self.eventMgr, self.messenger. Major setup methods: makeDefaultPipe(), openDefaultWindow()/openMainWindow(), setupRender(), setupRender2d(), setupRender2dp(), setupDataGraph(), setupMouse(), makeCamera().

  • DirectObjectdirect/src/showbase/DirectObject.py. The base class for “anything that responds to events.” Provides accept/acceptOnce/ignore/ignoreAll (forwarding to the global messenger) and addTask/doMethodLater/removeAllTasks (forwarding to taskMgr, tagging the task owner=self). Tracks per-object tasks in self._taskList and listening state for leak detection (detectLeaks()).

  • Messengerdirect/src/showbase/Messenger.py. The Python event bus. Nested dict eventName -> {objMsgrId -> [method, extraArgs, persistent]} plus a reverse objMsgrId -> set(eventNames) for fast ignoreAll. Thread-safe via a direct.stdpy.threading.Lock; supports cross-task-chain delivery through _eventQueuesByTaskChain. The singleton is MessengerGlobal.messenger.

  • EventManagerdirect/src/showbase/EventManager.py. Bridges the C++ EventQueue/EventHandler to the Python Messenger: its processEvents() drains the C++ queue each frame and re-throws into messenger. Singleton in EventManagerGlobal.eventMgr.

  • Loaderdirect/src/showbase/Loader.py. DirectObject wrapper over the C++ Loader (PandaLoader.getGlobalPtr(), stored as self.loader). loadModel() accepts sync or async (callback=/blocking=False, returns a future-like _Callback), plus loadFont, loadTexture, loadSfx/loadMusic, asyncFlattenStrong.

  • Support/utility: BulletinBoard (a global key/value store with change events), Audio3DManager, SfxPlayer, Transitions (fades/iris), PythonUtil (huge utility module imported widely), Factory, JobManager/Job (time-sliced cooperative jobs), and the debug suite (GarbageReport, ObjectReport, ContainerLeakDetector, OnScreenDebug, BufferViewer).

How it plugs in. ShowBase.__init__ writes the globals into builtins (base, render, render2d, aspect2d, taskMgr, loader, messenger, etc.) and also into the ShowBaseGlobal module. The per-frame engine is assembled in restart() as a fixed set of tasks with carefully chosen sort values: resetPrevTransform (sort -51) → dataLoop (-50, runs the data graph / mouse / input) → user tasks → ivalLoop (20, steps the interval manager) → collisionLoop (30) → garbageCollectStates (46) → igLoop (50, calls graphicsEngine.renderFrame()) → audioLoop (60, updates 3-D sound positions after cull). run() simply calls taskMgr.run(). This sort ordering is the single most important thing to understand about frame timing — if you add a task that must see fresh input but run before render, give it a sort between -50 and 50.

Where to start. To change startup/window/scene-graph wiring, read ShowBase.__init__, openDefaultWindow, setupRender*, and restart. For event flow, read Messenger.send/accept/__dispatch together with EventManager.processEvents. For asset loading, Loader.loadModel.

Gotchas / rationale (community). The builtins are deprecated for real projects — “Don’t use things like render, loader and run without the self or base prefix… they are deprecated” (discourse 27417); the supported alternative is the ShowBaseGlobal module, added precisely so code can from direct.showbase.ShowBaseGlobal import base instead of relying on builtins (commit 4f50f6ab “showbase: use ShowBaseGlobal module as alternative to builtin scope”). base itself is set in ShowBase.__init__ (historically “line 305”), which is why grepping for an assignment to base is confusing — it is builtins.base = self (discourse 7206). Animations that “won’t load from any point in the program” are usually a sign the task manager / base was not yet running (docs: Actor Animations).

actor

What it is. Actor is the Python wrapper over the C++ Character skeletal-animation system. It manages one or more model files (“parts”), the animations bound to each part, level-of-detail (LOD) switching, sub-parts (joint subsets sharing a bundle), and playback control (play/loop/pose/stop). It is the standard way to put an animated character into a Panda scene.

Key classes and roles.

  • Actordirect/src/actor/Actor.py (~2670 lines). Multiply inherits DirectObject and NodePath, so an Actor is a node you can reparentTo(render). Its state lives in three dicts keyed by LOD name: __partBundleDict (LOD → partName → PartDef), __animControlDict (LOD → partName → animName → AnimDef), and __subpartDict (subpart name → SubpartDef).

  • Nested Actor.PartDef — holds a partBundleNP, a PartBundleHandle, and the partModel (ModelRoot, kept so the ModelPool refcount stays accurate). getBundle() returns the underlying C++ PartBundle.

  • Nested Actor.AnimDef — tracks one animation per part: filename, animBundle, and a possibly-None animControl (None until the anim is bound).

  • Nested Actor.SubpartDef — a named subset of joints (PartSubset) of a real part, letting you, e.g., play a different anim on the upper vs. lower body of one bundle.

  • DistributedActordirect/src/actor/DistributedActor.py. Combines Actor with DistributedNode for networked characters.

How it plugs in. Loading uses the Loader with LoaderOptions.LFConvertSkeleton (models) and LFConvertAnim (anims) — see the class-level modelLoaderOptions/animLoaderOptions. Binding an animation to a part produces an AnimControl from the C++ side; Actor exposes this through getAnimControl, play, loop, pose, controlJoint/exposeJoint (the latter feed ActorInterval/LerpAnimInterval in the interval system). Parts are connected with attach(part, parent, joint); multipart actors expose joints to which other parts/props are parented.

Where to start. Actor.__init__ (the single-vs-multipart branching and copy-constructor path), loadModel/loadAnims, bindAnim/__bindAnimToPart, makeSubpart, and the LOD methods setLODNode/addLOD/setLOD. To debug “animation plays on the wrong joints,” look at SubpartDef and __animControlDict resolution.

Gotchas / rationale (community). autoBindAnims / auto_bind will rename animations based on the bundle name, which surprises people coming from glTF (discourse 29364); for multipart actors people ask exactly how autoBindAnims resolves parts from an Actor instance (discord). glTF historically did not support multiple parts in one file (discourse 27799). To duplicate a piece of a multipart actor, find() the part and copyTo() it rather than re-instancing the Actor (discourse 15016).

Config variables. Class-level ConfigVariableBools read at construction: validate-subparts (default True), merge-lod-bundles (default True — collapses LOD bundles for efficiency), allow-async-bind (default True — binds animations off-thread).

interval

What it is. A timeline system for scripted animation: smoothly interpolate (“lerp”) node properties, call functions at points in time, play sounds/animations, and compose all of these into nested Sequence/Parallel structures. The performance-critical pieces are C++ (CInterval and subclasses, CIntervalManager); the Python layer subclasses them and adds Python-only interval types and the factory/sugar API.

Central abstraction and inheritance. CInterval (direct/src/interval/cInterval.h) is the base “timeline component”: a named action with a duration, an open_ended flag, a State (S_initial/S_started/S_paused/S_final), and the priv_* step methods (priv_initialize, priv_step, priv_finalize, …) that the manager drives via priv_do_event(t, EventType). C++ subclasses (all in this dir): CLerpIntervalCLerpNodePathInterval (the workhorse — lerps pos/hpr/scale/color/tex on a NodePath) and CLerpAnimEffectInterval; CConstraintIntervalCConstrainPos/Hpr/PosHpr/TransformInterval; the instantaneous HideInterval/ShowInterval/WaitInterval; and CMetaInterval (the container). On the Python side, Interval (direct/src/interval/Interval.py) is a parallel base inheriting DirectObject for intervals that must be implemented in Python (e.g. ActorInterval, FunctionInterval); the Lerp Python classes instead subclass the C++ CLerpNodePathInterval directly (LerpNodePathIntervalLerpPosInterval, LerpHprInterval, … in LerpInterval.py).

Key classes and roles.

  • MetaInterval / Sequence / Parallel / Trackdirect/src/interval/MetaInterval.py, subclassing CMetaInterval. A Sequence plays children back-to-back, a Parallel overlaps them. The Python layer flattens a tree of nested MetaIntervals into the single root CMetaInterval for the C++ engine, using relative-start constants PREVIOUS_END/PREVIOUS_START/TRACK_START (CMetaInterval.RSPreviousEnd etc.). CMetaInterval stores its plan as a list of DefType records (DT_c_interval, DT_ext_index, DT_push_level, DT_pop_level).

  • IntervalManager (direct/src/interval/IntervalManager.py) extends the C++ CIntervalManager; the global is ivalMgr. step() calls the C++ step() then __doPythonCallbacks(), which fires privPostEvent on just-removed and event-bearing intervals and flushes a private EventQueue so interval doneEvents are serviced immediately rather than next frame.

  • ActorInterval / LerpAnimInterval (direct/src/interval/ActorInterval.py) drive Actor animation frames over time; SoundInterval, ParticleInterval, MopathInterval, ProjectileInterval (ballistic motion), FunctionInterval/Func/Wait (FunctionInterval.py), IndirectInterval. IntervalGlobal.py re-exports everything for the canonical from direct.interval.IntervalGlobal import *.

How it plugs in. ShowBase.restart adds the ivalLoop task (sort 20) which calls ivalMgr.step() once per frame. Intervals are awaitable in coroutines: cInterval_ext.cxx implements __await__ (line 47), so await Sequence(...) works in tasks. LerpNodePathInterval mutates TransformState/RenderState on a target NodePath directly in C++.

Where to start. To add a new lerp type, mirror an existing class in LerpInterval.py and (if it needs C++ speed) the CLerpNodePathInterval machinery; for a Python-only interval, subclass Interval and implement privInitialize/privStep/privFinalize (see ActorInterval.privStep). To debug timing/ordering, read cMetaInterval.cxx and the IntervalManager.__doPythonCallbacks flush.

Gotchas / rationale (community). The correct way to nest is append()/extend() on a Sequence/Parallel, not constructing one from another — “The correct way to add intervals, sequences or parallels is: SqOrParallel.append(OtherSqOrParallel)” (discourse 3331). Sequences are also called MetaIntervals; this is the C++ name leaking through (docs). Function intervals fire a callback at a single instant, distinct from function-lerp intervals which pass a value over a range (docs).

Config variables (direct/src/interval/config_interval.cxx): interval-precision (default 1000.0 — the default CMetaInterval::set_precision, controls fixed-point timing granularity) and verify-intervals (default false — assert if priv_* functions are called out of order; turn on when debugging).

task

What it is. A Python wrapper around the C++ AsyncTaskManager/PythonTask. It schedules per-frame callbacks, delayed callbacks (doMethodLater), and coroutines, supports multiple task chains (including threaded ones), and includes profiling helpers. This module replaced the old all-Python task system.

Key classes and roles.

  • TaskManagerdirect/src/task/Task.py. Wraps self.mgr = AsyncTaskManager.getGlobalPtr(). Public API: add(), doMethodLater(), remove(), removeTasksMatching(), step() (poll once, then doYield), and run() (loop until exception/KeyboardInterrupt). The global instance is TaskManagerGlobal.taskMgr (and base.taskMgr).

  • Task — the module exposes the C++ PythonTask plus the status sentinels cont, done, again, pickup, exit and helpers Task.pause(), Task.sequence(), Task.loop(), Task.gather(). A task function returns one of these to control rescheduling: Task.cont (run again next frame), Task.done (stop), Task.again (re-delay by the original delay).

  • FrameProfiler.py, TaskProfiler.py, Timer.py, MiniTask.py — profiling/diagnostics and a lightweight task variant.

How it plugs in. This is the heartbeat of the whole engine — ShowBase.restart registers all the loop tasks here, and DirectObject.addTask/doMethodLater funnel into it with owner=self so tasks are auto-removed when their owner is cleaned up. Coroutine support is first-class: a task function may be async def, and await-ing intervals, futures, or Task.pause(t) suspends without blocking the frame.

Where to start. TaskManager.add/doMethodLater for the scheduling contract, step for the per-frame poll + doYield (sleep/throttle) logic, and the coroutine path in Task.py. For frame-budget/threaded chains see setupTaskChain and the C++ AsyncTaskChain.

Gotchas / rationale (community). Task.gather() interoperability with raw coroutines vs. futures is subtle — you can await asyncio.gather(...) over a mix of Panda futures and Python coroutines, but Panda’s own gather expects awaitables and has historically tripped people up (discourse 27313, discord). Awaitables in tasks include all intervals and Task.pause() (docs: Coroutines). Task return value semantics are the classic footgun — forgetting to return Task.cont stops the task after one frame (docs: Tasks).

fsm

What it is. Two finite-state-machine frameworks for game logic. The modern FSM defines states implicitly by enterX/exitX/filterX method naming; the older ClassicFSM defines states as explicit State objects with a transition table. Both manage enter/exit handlers and guard illegal transitions.

Key classes and roles.

  • FSMdirect/src/fsm/FSM.py, subclasses DirectObject. State is a string in self.state; transitions go through request() (polite — may be denied) or demand() (forced — must succeed). Internals: defaultFilter, __setState, forceTransition, a transition-in-progress guard raising AlreadyInTransition, and a RLock (direct.stdpy.threading.RLock) for reentrancy. request() returns a Transition (a tuple subclass with __await__), so async enter/exit handlers are supported — the FSM stays in the transition state until the coroutine returns. stateArray supports cyclic next/prev requests.

  • State / StateDatadirect/src/fsm/State.py, StateData.py. The ClassicFSM building blocks: a State holds an enterFunc, exitFunc, and a list of allowed transitions; StateData is a state with associated load/unload data.

  • ClassicFSMdirect/src/fsm/ClassicFSM.py. The original table-driven machine. FourState/FourStateAI are higher-level patterns built on top.

How it plugs in. FSMs are plain DirectObjects — no special hook into the frame loop. They are driven by your code or by messenger events (accept('foo', self.demand, ['SomeState'])). The async transition support ties into the task/coroutine system. Distributed FSMs (FourStateAI) are common in the OTP-style server code.

Where to start. FSM.request/demand/defaultFilter/__setState is the whole contract for the new style. For the classic style read ClassicFSM.request and State. SampleFSM.py is a runnable example.

Gotchas / rationale (community). During a transition it is illegal to call request() — doing so raises AlreadyInTransition; use demand() (which queues) if you must change state from within an enter/exit (docs: Advanced FSM Tidbits). Every FSM starts and ends in the implicit "Off" state, and cleanup() returns it there (docs: Simple FSM Usage). Coroutine enter/exit is a deliberate, relatively recent feature: “Enter and exit funcs can be marked async, and the FSM will remain in the transition state until they return” (github #1037).

gui

What it is. DirectGui — the Python UI toolkit wrapping the C++ PGItem widget nodes (PGButton, PGFrame, PGEntry, PGSliderBar, …) from panda/src/pgui. Each widget is a scene-graph node configured through an option dictionary (Tkinter/Pmw-style ('name', default, handler) triples), with components composited from sub-widgets.

Central abstraction and inheritance. DirectGuiBase (direct/src/gui/DirectGuiBase.py) subclasses DirectObject and implements the option system: defineoptions/addoptions build self._optionInfo = {keyword: [default, current, handler]}, createcomponent/component manage composite sub-widgets, and configure/__setitem__/cget read/write options at runtime (calling the option’s handler). DirectGuiWidget(DirectGuiBase, NodePath) adds the actual scene-graph node: it instantiates the C++ widget via the pgFunc option (self.guiItem = self['pgFunc'](''), e.g. PGItem), assigns a guiId, and manages frame style/state/bounds. The widget hierarchy is DirectFrame(DirectGuiWidget)DirectButton(DirectFrame)DirectCheckButton/DirectRadioButton, with siblings DirectLabel, DirectEntry, DirectScrollBar/DirectSlider/DirectWaitBar, DirectScrolledList/DirectScrolledFrame, DirectOptionMenu, DirectDialog. DirectFrame uses pgFunc=PGItem; DirectButton uses pgFunc=PGButton.

Key files. DirectGuiGlobals.py (aliased DGG) holds the constants: mouse buttons (LMB/MMB/RMB), states (NORMAL/DISABLED), frame styles mapped to PGFrameStyle (FLAT/RAISED=TBevelOut/SUNKEN=TBevelIn/GROOVE/RIDGE/TEXTUREBORDER), dialog results, and event-name prefixes (DESTROY, B1PRESS, etc.). OnscreenText.py/OnscreenImage.py/OnscreenGeom.py are lighter non-interactive helpers (thin NodePath wrappers, not full DirectGui widgets).

How it plugs in. DirectGui nodes are reparented under aspect2d/pixel2d (set up by ShowBase.setupRender2d), which sit beneath a PGTop node whose PGMouseWatcherBackground routes mouse events. Widget events arrive through the messenger as event + self.guiId strings (bind()/unbind() in DirectGuiBase); command= callbacks are convenience wiring over that. Fonts default to TextNode.getDefaultFont().

Where to start. Read the long header comment in DirectGuiBase.py (it documents the 5-step option-resolution order), then DirectGuiWidget.__init__, then a concrete widget like DirectButton.py to see how it adds PGButton states and component text. For new widgets, copy a sibling and supply the right pgFunc + optiondefs.

Gotchas / rationale (community). “DirectGUI widgets are just nodes in the scene-graph, like any other… the classes you instantiate in Python inherit from NodePath” — so hide()/show()/reparentTo() all work and are the right way to toggle visibility (discourse 25615). The keyword/option list is famously under-documented; the authoritative source is the optiondefs in each widget’s source (e.g. text-shadow options) — contributors are routinely pointed at DirectGuiBase.py on GitHub to find them (discourse 28843, discourse 26913).

distributed

What it is. The distributed-networking system — Panda’s implementation of the Disney Online (“OTP”) architecture for replicating objects across clients and servers. Objects are described in DC files (.dc, parsed by the C++ dcparser into a DCFile/DCClass); each networked class has client (DistributedX), AI/server (DistributedXAI), owner-view (DistributedXOV), and UberDOG (DistributedXUD) flavors. Repositories own the connection and the table of live objects.

Central abstraction and inheritance.

  • DistributedObjectBase(DirectObject) (DistributedObjectBase.py) → DistributedObject (DistributedObject.py, the client base) with parallel DistributedObjectAI/DistributedObjectOV/DistributedObjectUD. Each DO carries self.dclass (its DCClass), doId, parentId, zoneId, and an activeState lifecycle: ESNew ESGenerating ESGenerated (disabled). The lifecycle hooks are generate() / generateInit() / announceGenerate() (object is now usable) and disable() / delete() / deleteOrDelay(). neverDisable/cacheable flags control whether the object survives zone changes or is cached on teardown (actors are expensive, so cacheable).

  • DistributedNode(DistributedObject, NodePath) adds scene-graph presence; DistributedSmoothNode(DistributedNode, DistributedSmoothNodeBase) adds dead-reckoning/smoothing (the heavy lifting is in C++ cDistributedSmoothNodeBase.cxx and a SmoothMover).

  • Repositories: ConnectionRepository(DoInterestManager, DoCollectionManager, CConnectionRepository) (ConnectionRepository.py) is the base — it multiply-inherits the C++ CConnectionRepository (cConnectionRepository.h), which manages the socket, reads/writes datagrams, and handles field-update datagrams entirely in C++ (passing unknown message types up to Python). ClientRepositoryBase(ConnectionRepository)ClientRepository (the CMU/built-in client). ServerRepository (ServerRepository.py) is a standalone Python server for the built-in (non-Astron) protocol.

Key supporting classes. DoInterestManager (zone interest), DoCollectionManager/DoHierarchy (the doId→object table and parenting), CRCache/CRDataCache (cached disabled DOs), ClockDelta (ClockDelta.pynetworkToLocalTime/localToNetworkTime for timestamp sync across machines), TimeManager/TimeManagerAI (the DO that performs clock sync), NetMessenger (a Messenger subclass that sends events across the network between AI/UD processes), ParentMgr/RelatedObjectMgr/InterestWatcher, and PyDatagram/PyDatagramIterator (Pythonic datagram builders over the C++ Datagram). MsgTypes.py/MsgTypesCMU.py enumerate the wire message codes. direct.dc is the built-in DC file.

How it plugs in. A repository connects, the server sends “generate” datagrams, CConnectionRepository decodes field updates and dispatches them by doId; Python constructs the right DistributedX from dclass, fills required fields, then fires announceGenerate. Smooth nodes register a per-frame task to interpolate buffered position telemetry. The system is largely independent of the render loop except through DistributedNode’s scene-graph membership and the smoothing task.

Where to start. DistributedObject.generate/announceGenerate/disable for the object lifecycle; ConnectionRepository.__init__ and the C++ cConnectionRepository.cxx for the I/O path; ClientRepository.py + ServerRepository.py for the built-in protocol end-to-end; SampleObject.py + direct.dc for a minimal example. ClockDelta.networkToLocalTime is the place to understand timestamp handling.

Gotchas / rationale (community). OTP proper is “proprietary Disney technology”; the open direct.distributed system is OTP-like but not OTP, and the heavyweight production path is Astron, “designed to support massively multiplayer games… much more scalable than the built-in server, but also more complicated” (discourse 27232, discourse 9033). When inspecting what a DistributedObject sends, read its DC class definition for the actual fields (stackoverflow). DistributedSmoothNode does “unique stuff in a task to help minimize the effects of laggy updates,” and mis-sized timestamps are a classic source of datagram-iterator assertion errors (discourse 8909, discourse 2130).

Config variables (direct/src/distributed/config_distributed.cxx): game-server-timeout-ms (20000), min-lag/max-lag (0.0 — artificially delay inbound messages to test latency tolerance), handle-datagrams-internally (true — let C++ handle field-update datagrams for speed; set false to force all handling into Python, useful when debugging).

stdpy

What it is. Drop-in replacements for several Python standard-library modules, re-implemented on top of Panda’s own threading and virtual-file-system primitives so that Panda threads (which the GIL-free C++ side knows about) cooperate correctly and so file I/O can read from mounted multifiles/VFS.

Key files and roles.

  • thread.py — re-implements the low-level _thread API using core.PythonThread; tracks threads in a module-level _threads dict, exposes start_new_thread, LockType, and Panda’s Thread.force_yield/consider_yield. Crucially these are Panda threads, so they show up in PStats and respect Thread.getCurrentThread().

  • threading.py / threading2.py — the higher-level threading API (Thread, Lock, RLock, Condition, Event) over the above. threading2 is a closer emulation of the CPython implementation. These are what Messenger and FSM import for their locks.

  • file.py — file objects backed by the VirtualFileSystem, so open() can read assets inside mounted .mf multifiles.

  • glob.py — globbing that searches the VFS as well as the real filesystem.

  • pickle.py — a pickle variant that tolerates Panda’s C++ objects.

How it plugs in. Used pervasively inside direct precisely so the framework’s own synchronization participates in Panda’s threading model rather than the OS’s. App code may also import them to make threading/open VFS-aware.

Where to start. thread.py (start_new_thread, _add_thread, _get_thread_wrapper) for the mapping to PythonThread; threading.py for the public lock/thread classes; file.py for VFS-backed I/O.

Gotchas / rationale (community). The whole point of direct.stdpy.file is to “interface more easily with Panda” assets/VFS rather than raw OS files (discourse 11938). Thread cleanup after a thread finishes has been a real bug surface — two upstream fixes touch exactly this: commit 1dc02f6a “stdpy: fix direct.stdpy.threading cleanup issue after thread runs” and 1f017997 “fix issues with direct.stdpy.threading thread cleanup” (both Fixes: #164). If you touch the thread-tracking dicts, mind those.

directnotify

What it is. The categorized logging/notification system used throughout direct. Every subsystem creates a named Notifier category (directNotify.newCategory("ShowBase")) and logs at debug/info/warning/error levels; categories are configured independently and the Python side integrates with the C++ Notify system so Python and C++ log lines share one stream.

Key classes and roles.

  • DirectNotifydirect/src/directnotify/DirectNotify.py. The registry: newCategory(name) creates/returns a Notifier, getCategory(name) looks one up, setDconfigLevels() reads per-category verbosity from config (e.g. notify-level-showbase debug).

  • Notifierdirect/src/directnotify/Notifier.py. The per-category logger. Methods debug/info/warning/error/setDebug/setInfo/setWarning. By default it pipes through a StreamWriter(Notify.out()) (gated by notify-integrate, default True) so Python output joins the C++ NotifyCategory stream, optionally with timestamps (notify-timestamp). error() raises; it can also forward to the C++ severities (NSDebug/NSInfo/NSWarning/NSError).

  • Logger / RotatingLogLogger.py, RotatingLog.py. Optional file-logging backend (rotating log files); the global default is LoggerGlobal.defaultLogger.

  • The registry singleton is DirectNotifyGlobal.directNotify.

How it plugs in. This is the most widely-imported module in directDirectObject, Messenger, Interval, Actor, every Distributed class etc. all hold a class-level notify = directNotify.newCategory(...). Because it wraps the C++ NotifyCategory, log levels set in a .prc file (notify-level-*) control both layers.

Where to start. DirectNotify.newCategory/setDconfigLevels for category management, Notifier.__init__/error/__str__ for how a line is formatted and routed, Notifier.setServerDelta for the networked timestamp offset.

Config variables. notify-integrate (default True — route Python notifies into the C++ Notify.out() stream), notify-timestamp (default False), and the per-category notify-level-<category> PRC variables consumed by setDconfigLevels.

controls

What it is. Avatar-control / locomotion systems: a family of “walker” classes implementing different movement and collision models (gravity, raw physics, gridless, swimming, ghost/dev/observer), plus a ControlManager that swaps the active walker and a per-process InputState that aggregates directional input from multiple sources.

Key classes and roles.

  • ControlManagerdirect/src/controls/ControlManager.py. Registry of named walkers (self.controls = {}); add(controls, name) registers, use(name, avatar) swaps the active one — disabling the old walker’s avatar controls and collisions, then enabling the new one for the avatar (setAvatar, setCollisionsActive, enableAvatarControls).

  • Walkers (all DirectObject subclasses): GravityWalker (GravityWalker.py, the standard one — uses a C++ CollisionHandlerGravity self.lifter with setGravity, and enter/again collision patterns), NonPhysicsWalker (simple, no gravity), PhysicsWalker (full physics engine), BattleWalker, SwimWalker, GhostWalker, DevWalker (no-clip), ObserverWalker, TwoDWalker. They share an interface (setAvatar, setWalkSpeed, enableAvatarControls, handleAvatarControls, setCollisionsActive) so ControlManager can treat them uniformly.

  • InputStatedirect/src/controls/InputState.py. A DirectObject that tracks named boolean inputs (set/isSet/watch/watchWithModifiers) from multiple inputSources, returning revocable tokens (InputStateWatchToken, InputStateForceToken, InputStateTokenGroup). Walkers poll it each frame for forward/reverse/turnLeft/turnRight/jump. The process-wide singleton is created in direct/src/showbase/InputStateGlobal.py as inputState (and base.inputState).

How it plugs in. A walker’s handleAvatarControls runs as a task, reads InputState, integrates motion, and feeds the avatar NodePath and a CollisionTraverser/CollisionHandlerGravity. Collision events come back through the messenger via the addInPattern("enter%in") / addAgainPattern hooks. ControlManager is the seam game code uses to switch movement modes (walking → swimming → ghost). These build directly on the C++ collision system (panda/src/collide) and physics (panda/src/physics).

Where to start. ControlManager.add/use for swapping; GravityWalker.__init__ (lifter/collision setup) and its handleAvatarControls/enableAvatarControls for the core movement loop; InputState.watch/set/isSet for the input plumbing. To add a movement mode, copy NonPhysicsWalker or GravityWalker and register it with the manager.

Gotchas / design notes. The walkers are an extracted-from-Toontown/Pirates lineage, so several (BattleWalker, SwimWalker, GhostWalker) carry game-specific assumptions; GravityWalker and NonPhysicsWalker are the general-purpose ones. GravityWalker has a _legacyLifter mode toggling CollisionHandlerGravity.setLegacyMode — relevant when porting old content whose floor-collision behavior changed.

Known shortcomings & footguns

The same global-state, dual-API, and reference-counting design that makes direct quick to live-code in also produces a well-worn set of traps. The entries below are community-mined opinion and history, preserved verbatim where quoted; severity/status tags reflect the catalogue’s assessment, not a guarantee.

The camelCase / snake_case dual interface (the “original sin”)

Severity: major · Status: mitigated (snake_case aliases auto-generated; direct.* incomplete)

Every public method exists under two spellings. The C++ codebase is snake_case; when first wrapped to a (pre-Python) scripting language that couldn’t use underscores, camelCase was invented and kept. The result buries discoverability, confuses newcomers about which name is “real,” and doubles autocomplete noise. (The interrogate binding generator that produces these aliases is covered in Cross-cutting concepts.)

“That is the origin of the camelCase convention. When this scripting language was changed to Python, the convention was kept, because PEP 8 didn’t really exist at the time.” — rdb (maintainer), t/28928

snake_case methods with camelCase arguments

Severity: major · Status: still-open

The migration aliased method names but not keyword-argument names in the hand-written direct.* tree, producing task_mgr.do_method_later(..., extraArgs=[...], appendTask=True).

“we forgot that there are arguments that are still in camelCase. So we now have a bunch of snake_case methods with camelCase arguments. What a mess.” — rdb (maintainer), #1795

Magic global builtins injected into __builtins__

Severity: major · Status: mitigated (ShowBaseGlobal escape hatch added; still injected)

Instantiating ShowBase slams ~8 names (base, render, loader, taskMgr, messenger, globalClock, camera, …) into Python’s builtin namespace so they “appear out of thin air” (see the showbase section above and its note on the supported ShowBaseGlobal alternative). This breaks static analysis, linters, IDE autocomplete, and Cython/freezing (the names don’t exist at compile time). Universally considered inelegant — kept for Disney-era live-coding convenience and backward compat.

“No one loves this builtin slamming, no one claims it is elegant, but we’ve got it for now.” — drwr (maintainer), t/5433

globals exist because “Walt Disney Imagineering uses the DIRECT library for live coding, and it’s convenient to have all the relevant variables handy in the interactive interpreter.” — rdb (maintainer), t/28928

Globals break IDEs/linters (undefined variable everywhere)

Severity: major · Status: mitigated (workarounds)

Because render/loader/base/taskMgr only materialize at runtime, PyCharm/VSCode/Pylint flag every use as an error with no autocomplete. drwr explicitly accepts the trade-off.

“that’s well worth the inconvenience of not being able to use tools like PyLint so easily.” — drwr (maintainer), t/5433

ShowBase is a process-wide singleton — can’t run two

Severity: major · Status: still-open (by-design)

Only one ShowBase can exist per process; a second raises “Attempt to spawn multiple ShowBase instances!”. This blocks multi-window/multi-context apps, embedding multiple viewports, and clean restart-without-process-exit. Rooted in the global-state design.

“Only being able to spawn a single instance is a known limitation for ShowBase. You could create multiple processes and have them communicate.” — Moguri, t/26613

Stringly-typed events fail silently on typo

Severity: minor · Status: by-design

Events are raw strings (self.accept("player-stopped", …)); a misspelled event name simply never fires — no error, no warning (see the Messenger description in the showbase section above). The messenger’s debug-watch also matches by substring, so "AvatarMove" fires for "AvatarMovementFrozen".

No bundled type stubs (.pyi)

Severity: major · Status: mitigated (3rd-party types-panda3d; not shipped)

Panda ships no .pyi stubs, so Pylance/mypy can’t see the C++-wrapped modules at all — no autocomplete, no type-checking — unless you install a separate community package. interrogate-generated bindings also yield wrong return types (e.g. loadModel inferred as list | Any | _Callback).

“vscode’s pylance language server does not recognize contents of these files… the reason is for lacking type stub files.” — BlackEagle1983, #1329

Cryptic C++ assertions leak into Python as AssertionError

Severity: major · Status: still-open (partial fix proposed in #966)

Invalid args surface as opaque AssertionError: _error_type == ET_ok — the literal C++ assert expression, meaningless to newcomers. rdb opened an issue to map these to proper IndexError/KeyError, but it’s a vast surface only incrementally addressed.

“the error messages can be awfully difficult to decipher for newcomers… we would translate some to IndexError, KeyError.” — rdb (maintainer), #966

loadPrcFileData("", "...") — the empty first arg, and config must precede ShowBase

Severity: minor · Status: by-design

The standard in-code config call has a mysterious empty first string (a page name) that trips up nearly every beginner, and it silently does nothing if called after ShowBase init (config is read at startup). The PRC config system itself lives in dtool — see the dtool page.

PRC config loads at static-init time — apps can’t control it

Severity: major · Status: still-open (acknowledged hard)

Config files are processed during static initialization, before main(), so app developers can’t control which files load; the documented customization path was to recompile Panda. CFSworks filed a design issue; rdb agrees but notes Panda itself uses config at static-init time, making it hard to untangle. (This is a dtool config-system behavior surfacing in Python apps — see the dtool page.)

“Don’t load anything at static init time: It takes away control… app developers are expected to recompile Panda(!).” — CFSworks (maintainer), #245

Naming your config Config.prc silently overrides Panda’s defaults

Severity: minor · Status: by-design (naming trap)

A user file named Config.prc shadows Panda’s bundled default, knocking out default display settings — surfacing as “Your Config.prc file must name at least one valid panda display library.” (PRC file discovery is a dtool concern — see the dtool page.)

Getters/setters instead of properties (un-Pythonic)

Severity: minor · Status: mitigated (property aliases added later)

The API was built on getX()/setX() chains (node.get_transform().get_pos().get_x()). rdb later added property access (node.transform.pos.x), but it was contentious and rolled out gradually; old style remains everywhere in docs/samples.

Python profiler hidden behind an undocumented config var

Severity: minor · Status: by-design (discoverability)

Panda’s built-in Python profiler only activates via the obscure pstats-python-profiler 1 (the FrameProfiler/TaskProfiler helpers in the task section above); users routinely don’t know it exists and reach for raw cProfile, which interacts awkwardly with the main loop.

Intervals/Actors get garbage-collected unless you store a reference

Severity: major · Status: by-design (recurring trap)

An interval/Actor created locally plays for a moment then silently dies when its only reference goes out of scope. The engine does not keep them alive for you. (This is the Python side of Panda’s intrusive reference counting — see the reference-counting discussion in Cross-cutting concepts.)

“You must keep a reference to an Actor, or else it will self destruct and become static geometry.” — teedee, t/13352

“you need to store a reference to the Actor object otherwise it goes out of scope and gets cleaned up.” — rdb (maintainer), Discord

Func(myfunction()) vs Func(myfunction) calls the function immediately

Severity: major · Status: by-design (footgun)

The trailing parens invoke the function at construction and store its return (None) instead of deferring the call. drwr calls it “a common error.” (See FunctionInterval/Func in the interval section above.)

“make sure you are doing Func(myfunction) and not Func(myfunction())… The second form calls myfunction right on the spot, and stores None… which is almost certainly not what you wanted.” — drwr (maintainer), t/9069

The Task.cont/again/done return-value model

Severity: minor (very common) · Status: by-design (confusion)

Forgetting to return anything defaults to done (task stops after one frame). Returning Task.cont from a task that starts an interval restarts the interval every frame so it never progresses. (See the task section above for the return-value sentinels.)

“The problem with returning Task.cont is that it means the play() method will be called again next frame, and again and again… so the interval is constantly being restarted.” — drwr (maintainer), t/1516

doMethodLater rejects a Task object; extraArgs clobbers the implicit task arg

Severity: minor · Status: by-design (API wart)

It expects a function, not a Task; and supplying extraArgs removes the implicit task parameter — which drwr admits is “difficult to do with the current definition.”

LerpInterval start/end values are captured at construction, not play time

Severity: minor · Status: by-design (surprising)

A Lerp built early with a node’s current position animates from a stale start once finally played — unlike normal Python evaluation expectations.

Coroutine tasks can’t be remove()d while awaiting; can’t be resumed

Severity: major · Status: mitigated (cancel() added; remove() still fails)

A coroutine task stuck in await can’t be killed via task.remove(). And because tasks inherit from Future, a finished/cancelled coroutine task can never be re-added — so a pause feature for coroutines is structurally impossible.

“futures (by their nature) can transition from the ‘running’ to the ‘done’ state only once… once a task is ‘done’… it cannot go back to the ‘running’ state.” — rdb (maintainer), #911

Panda’s async/gather doesn’t behave like stdlib asyncio

Severity: major · Status: still-open

Porting asyncio.gather examples discards results; Task.gather “does not seem to work”; and asyncio primitives are unsafe across task chains (see the Task.gather note in the task section above).

“please note that asyncio isn’t thread-safe, so you can’t use asyncio.Lock across multiple threads (this includes threaded task chains).” — rdb (maintainer), t/28380

Running the main loop off the main thread breaks

Severity: minor · Status: by-design

Starting run()/taskMgr.step() from a threading.Thread raises “ValueError: signal only works in main thread” and logs clock-jump warnings; the task loop is also non-reentrant (taskMgr.step() inside another step asserts).

NodePaths/scene-graph objects aren’t directly picklable

Severity: minor · Status: mitigated (BAM is the supported path; pickle partial)

Users trying to pickle NodePaths/nodes to save a scene hit walls — the supported serialization is BAM (see the Datagram/BamReader discussion in Cross-cutting concepts), and pickle support is partial (e.g. CollisionTraverser/CollisionHandler aren’t picklable, blocking any class that holds them — #1090).

Type-identity check reads uninitialized memory; pickle returns references not copies

Severity: minor · Status: fixed (specific cases in #554)

Panda detects “is this a Panda C++ object?” by reading a signature at a fixed struct offset — uninitialized memory for non-Panda objects (valgrind warnings, CPython-internal-dependent). (This is the interrogate DtoolInstance_Check/PY_PANDA_SIGNATURE mechanism described in Cross-cutting concepts.) The same thread surfaced a pickle bug where ParamNodePath.get_value() returned a reference, corrupting unpickled NodePaths (#554).

Where to start (this cluster)

A new contributor should read in this order:

  1. direct/src/showbase/ShowBase.py__init__, openDefaultWindow, setupRender*, and especially restart() (the sorted per-frame task pipeline). This is the spine everything else hangs from.

  2. direct/src/showbase/DirectObject.py (tiny, ~120 lines) — understand accept/addTask; nearly every other class inherits this.

  3. direct/src/showbase/Messenger.py + direct/src/showbase/EventManager.py — the event bus and its bridge to the C++ EventQueue.

  4. direct/src/task/Task.pyTaskManager.add/step/run; the frame heartbeat and coroutine support.

  5. Then pick a vertical: interval/cInterval.h + interval/Interval.py + interval/MetaInterval.py for animation timelines, actor/Actor.py for characters, gui/DirectGuiBase.py for UI, or distributed/DistributedObject.py + distributed/ConnectionRepository.py + distributed/cConnectionRepository.h for networking.

Throughout, remember the layering rule: the Python class is usually a thin wrapper, and the real algorithm is in the sibling c*.cxx/c*.h (compiled into panda3d.direct) or in the core (panda3d.core). When a Python method just marshals arguments and calls CSomething.method(self, ...), jump to the C++ for the actual behavior.