PyBitEngine v0.1.3

Motore 2D Python veloce, batch-oriented, basato su ModernGL + SDL2. Un unico oggetto WINDOW che eredita tutto il rendering, le collisioni e la gestione input. Batch NumPy/Numba per migliaia di primitive per frame.

Introduzione

PyBitEngine è composto da 7 moduli pubblici che vengono esposti tutti da un unico import:

ModuloContenuto
PE_WINDOWClasse WINDOW — finestra, contesto GL, event loop, GC.
PE_DRAWClasse DRAW (ereditata da WINDOW) + FontManager, shape data-classes.
PE_CAMERACameraGPU, CameraCPU.
PE_KEYSPE_Event e tutte le costanti tastiera/mouse.
PE_TIMEScheduler a frame, timer, cooldown, async timer.
PE_PAKERFunzione pack() per creare eseguibili con cx_Freeze.

Installazione

# Dipendenze runtime
pip install pysdl2 pysdl2-dll moderngl numpy numba pillow

# PyBitEngine
pip install PyBitEngine

Import unico

Tutto ciò che è pubblico si importa in un colpo solo:

from PyBitEngine import *

Questo porta nel namespace: WINDOW, DRAW, CameraGPU, CameraCPU, FontManager, pack, il modulo PE_TIME, la classe PE_Event e tutte le costanti PE_K_* / PE_MOUSE_*.

Quick Start — 20 righe

from PyBitEngine import *

class Game(WINDOW):
    def __init__(self):
        super().__init__(title="Hello PyBit",
                         geometry=("center", "center", 800, 600),
                         VSync=True)
        self.SetBackground((20, 24, 40))
        self.x = 100.0

    def update(self, dt, events):
        self.x += 120 * dt        # 120 px/s
        for ev in events:
            if ev.type == PE_KEYDOWN and ev.key == PE_K_ESCAPE:
                self.running = False

    def draw(self):
        self.DrawRect(self.x, 200, 120, 80, color=(94, 234, 212))
        self.DrawText(f"FPS: {self.GetFPS()}", 10, 10, size=20)

Game().Loop()
Pattern fondamentale: ereditare da WINDOW, sovrascrivere update(dt, events) e draw(), poi chiamare .Loop(). Il resto del framework fluisce da lì.

Classe WINDOW

WINDOW eredita DRAW: ogni metodo di disegno/collisione è disponibile direttamente su self.

Costruttore

WINDOW(title="PyEngine", geometry=("center","center", 800, 600), icon="", fullscreen=False, borderless=False, VSync=False, MSAA=False, MSS=4, max_fps=None, gc_auto=False, gc_mode="frames", gc_interval=600, gc_thresholds=None, gc_obj_number=700, max_draw_elements=131072)
ParametroDescrizione
titleTitolo della finestra.
geometry(x, y, w, h). x/y possono essere "center".
iconPath a un file .png/.jpg/.bmp/.ico.
fullscreenFullscreen borderless al boot.
VSyncSincronizza con il refresh del monitor.
MSAA, MSSAnti-aliasing multisample e numero di sample (2/4/8/16).
max_fpsCap manuale sugli FPS. None = illimitato.
gc_autoTrue = GC manuale periodico (più stabile).
max_draw_elementsDimensione massima dei buffer batch (default 131 072).

Hook del game loop: update / draw / Loop

Sovrascrivi update(dt, events) e draw() nella tua sottoclasse. Loop() avvia il main loop bloccante.

class Game(WINDOW):
    def update(self, dt, events):
        # dt: secondi trascorsi dall'ultimo frame
        # events: lista di PE_Event ricevuti in questo frame
        ...
    def draw(self):
        # Chiama i metodi Draw* qui
        ...

Game().Loop()

Cursore, titolo, sfondo, icona

SetCursor(cursor_type)

SetCursor(cursor_type: str | int)

Stringhe accettate: "arrow", "ibeam", "wait", "crosshair", "hand", "no", "sizeall", "sizenwse", "sizenesw", "sizewe", "sizens", "waitarrow".

self.SetCursor("hand")

SetCustomCursor(image_path, width, height, hot_x, hot_y)

self.SetCustomCursor("cursor.png", 32, 32, hot_x=0, hot_y=0)

SetCursorVisible(visible), SetFluidResize(enabled)

self.SetCursorVisible(False)   # nasconde il cursore
self.SetFluidResize(True)      # resize senza sospensione del loop

SetBackground(color), SetTitle(title), SetIcon(icon)

self.SetBackground((15, 20, 40))
self.SetTitle("Mio Gioco")
self.SetIcon("assets/icon.png")

FPS & fullscreen

GetFPS() → int  ·  SetMaxFPS(n)  ·  GetMaxFPS()

self.SetMaxFPS(144)
print(self.GetFPS(), self.GetMaxFPS())

SetFullscreen(fullscreen=None, mode="borderless")

mode: "borderless" (default) o "exclusive". Passando None toggle-a lo stato.

self.SetFullscreen(True, mode="exclusive")

GetScreenResolution() → (w, h)

w, h = self.GetScreenResolution()

Destroy()

Libera SDL/GL manualmente (raramente necessario: Loop() lo fa in finally).

Garbage Collector controllato

Su scene pesanti, delegare al GC di Python può causare hitch. PyBitEngine permette di disabilitarlo e forzare raccolte periodiche.

SetGCAuto(enabled, mode, interval, thresholds)

SetGCAuto(enabled: bool, mode: "frames" | "time" | "smart" = "frames", interval: float = 600, thresholds: tuple = None)
  • mode="frames": raccoglie ogni interval frame.
  • mode="time": raccoglie ogni interval secondi.
  • mode="smart": raccoglie quando gen0 supera la soglia.
self.SetGCAuto(True, mode="time", interval=2.0)  # ogni 2 secondi

ForceGC(gen=0), GetGCStats(), IsGCEnabled(), SetGCSmartThreshold(n)

collected, sec = self.ForceGC(2)   # full collection
print(self.GetGCStats())  # {'count': 12, 'total_time': 0.043}

DRAW — Rettangoli

DrawRect(x, y, w, h, color=(255,255,255), alpha=255, rotation=0.0)

self.DrawRect(50, 50, 200, 100, color=(124, 156, 255), rotation=15)

DrawRectOutline(x, y, w, h, thickness=1.0, color=..., alpha=255, rotation=0.0)

self.DrawRectOutline(50, 50, 200, 100, thickness=3, color=(255,200,0))

DrawRectsBatch(positions, sizes, colors, alpha=255, rotation=0.0) batch

positions : ndarray (N, 2) sizes : ndarray (N, 2) colors : ndarray (N, 3|4) oppure singola tuple rotation : scalare o ndarray (N,)
import numpy as np
N = 5000
pos = np.random.rand(N, 2) * [800, 600]
sz  = np.full((N, 2), 4.0)
col = (np.random.rand(N, 3) * 255).astype('u1')
self.DrawRectsBatch(pos, sz, col)

DrawRectsOutlineBatch(positions, sizes, colors, thickness=1.0, alpha=255, rotation=0.0)

Come sopra, ma solo contorno.

Rettangoli arrotondati

DrawRoundedRect(x, y, w, h, radius, color=..., alpha=255, rotation=0.0, softness=1.0)

self.DrawRoundedRect(100, 100, 240, 120, radius=16,
                     color=(94,234,212), softness=1.2)

DrawRoundedRectOutline(x, y, w, h, radius, thickness=1.0, ...)

self.DrawRoundedRectOutline(100, 100, 240, 120, radius=16,
                            thickness=3, color=(255,255,255))

DrawRoundedRectsBatch(positions, sizes, radius, colors, ...)

Cerchi & ellissi

DrawCircle(cx, cy, r, color=..., alpha=255)

self.DrawCircle(400, 300, 60, color=(255,120,80))

DrawEllipse(cx, cy, rx, ry, color=..., alpha=255, rotation=0.0)

self.DrawEllipse(400, 300, 80, 40, rotation=30, color=(94,234,212))

DrawCircleOutline / DrawEllipseOutline

self.DrawCircleOutline(400, 300, 60, thickness=4, color=(255,255,255))

DrawCirclesBatch, DrawEllipsesBatch, DrawCircleOutlineBatch, DrawEllipsesOutlineBatch

import numpy as np
centers = np.random.rand(2000, 2) * 800
self.DrawCirclesBatch(centers, radius=3.0, colors=(255,255,255,200))

Linee

DrawLine(x1, y1, x2, y2, thickness=1.0, color=..., alpha=255)

self.DrawLine(0, 0, 800, 600, thickness=2, color=(255,200,0))

DrawLinesBatch(x1, y1, x2, y2, colors, thickness=1.0, alpha=255, rotation=0.0)

Ogni argomento è un array (N,).

Triangoli

DrawTriangle(x1,y1,x2,y2,x3,y3,color=..., alpha=255)

self.DrawTriangle(100,300, 200,100, 300,300, color=(255,100,100))

DrawTriangleOutline(...), DrawTrianglesBatch(vertices, colors), DrawTrianglesOutlineBatch(...)

# vertices shape: (N, 6)  →  x1,y1,x2,y2,x3,y3
verts = np.array([[0,0, 50,0, 25,50],
                  [100,0, 150,0, 125,50]], dtype='f4')
self.DrawTrianglesBatch(verts, colors=(200,220,255,255))

Triangoli arrotondati

DrawRoundedTriangle(x1,y1,x2,y2,x3,y3, radius, color=..., alpha=255, softness=1.0)

self.DrawRoundedTriangle(100,300, 200,100, 300,300,
                         radius=14, color=(94,234,212))

DrawRoundedTriangleOutline, DrawRoundedTrianglesBatch, DrawRoundedTrianglesOutlineBatch

Curve di Bézier quadratiche

DrawBezierCurve(p0, p1, p2, thickness=2.0, segments=None, smooth=True, color=..., alpha=255)

self.DrawBezierCurve((50,500), (400,50), (750,500),
                     thickness=3, color=(255,200,0))

DrawBezierCurvesBatch(p0s, p1s, p2s, thickness=2.0, colors=..., segments=None, smooth=True, alpha=255)

API batch di alto livello (dict-list)

Alternativa comoda ai batch NumPy: passi una lista di dict, il motore li impacchetta.

DrawRects, DrawRectsOutline, DrawRoundedRects, DrawRoundedRectsOutline, DrawLines, DrawTriangles, DrawTrianglesOutline, DrawRoundedTriangles, DrawRoundedTrianglesOutline, DrawEllipses, DrawCircles, DrawEllipsesOutline, DrawCirclesOutline, DrawBezierCurves, DrawSprites, DrawTexts

self.DrawRects([
    {"x":10,"y":10,"w":40,"h":40,"color":(255,0,0)},
    {"x":60,"y":10,"w":40,"h":40,"color":(0,255,0),"rotation":15},
])

self.DrawTexts([
    {"text":"Hello", "x":10, "y":10, "size":28, "color":(255,255,255)},
    {"text":"World", "x":10, "y":50, "size":28, "color":(94,234,212)},
])

Texture & Atlas

LoadTexture(name, filepath, filter_mode="LINEAR")

filter_mode: "LINEAR" o "NEAREST" (pixel-art).

self.LoadTexture("player", "assets/player.png", filter_mode="NEAREST")

UnloadTexture(name)

self.UnloadTexture("player")

LoadTextureAtlas(name, filepath)

Carica un atlas (PNG). Le sottoregioni si specificano per pixel in DrawTexture con src=(u,v,w,h).

DrawTexture

DrawTexture(name, x, y, w=None, h=None, rotation=0.0, alpha=255, flip_x=False, flip_y=False, src=None, color=None)

self.DrawTexture("player", 100, 200)
self.DrawTexture("player", 300, 200, w=64, h=64,
                 rotation=45, flip_x=True, alpha=180)

DrawSpritesBatch batch

DrawSpritesBatch(sprites)

sprites : ndarray (N, 10) — (x, y, w, h, rot_deg, u0, v0, u1, v1, alpha)
# 10.000 particelle-sprite in un draw call
N = 10_000
arr = np.zeros((N, 10), dtype='f4')
arr[:, 0:2] = np.random.rand(N, 2) * [800, 600]  # pos
arr[:, 2:4] = [8, 8]      # size
arr[:, 4]   = 0.0         # rot
arr[:, 5:9] = [0, 0, 1, 1]# uv
arr[:, 9]   = 255         # alpha
self.DrawSpritesBatch(arr)

Testo — FontManager & DrawText

WINDOW ha un FontManager interno; le API sono esposte come metodi.

RegisterFont(alias, path)

self.RegisterFont("pixel", "assets/PressStart2P.ttf")

MeasureText(text, font="arial", size=24) → (w, h)

w, h = self.MeasureText("Hello", font="pixel", size=32)

DrawText(text, x, y, size=24, font="arial", color=(255,255,255), alpha=255, rotation=0.0, anchor="topleft")

anchor: "topleft", "center", "topright", "bottomleft", "bottomright".

self.DrawText("Game Over", 400, 300, size=48,
              color=(255,80,80), anchor="center")

DrawTextBatch(items)

self.DrawTextBatch([
    {"text":"HP",   "x":10, "y":10, "size":20},
    {"text":"MP",   "x":10, "y":40, "size":20, "color":(120,180,255)},
])

Collisioni geometriche

Tutte le funzioni Collide* sono metodi di WINDOW (via DRAW) e ritornano bool (o array booleani per le versioni Batch).

Point vs shape

self.PointInRect(px, py, x, y, w, h)
self.PointInEllipse(px, py, cx, cy, rx, ry)
self.PointInTriangle(px, py, x1,y1, x2,y2, x3,y3)
self.CollidePointCircle(px, py, cx, cy, r)
self.CollidePointRotatedRect(px, py, x, y, w, h, rotation)
self.CollidePointRoundedRect(px, py, x, y, w, h, radius, rotation=0.0)
self.CollidePointRoundedTriangle(px, py, x1,y1,x2,y2,x3,y3, radius)
self.CollidePointPolygon(px, py, points)   # points: [(x,y), ...]
self.CollidePointText(px, py, text, x, y, font="arial", size=24, rotation=0.0)
self.CollidePointTexture(px, py, name, x, y, w=None, h=None,
                         rotation=0.0, flip_x=False, flip_y=False,
                         alpha_threshold=1)   # pixel-perfect

Shape vs shape

self.CollideRectRect(x1,y1,w1,h1, x2,y2,w2,h2)
self.CollideCircleCircle(c1x,c1y,r1, c2x,c2y,r2)
self.CollideRectCircle(rx,ry,rw,rh, cx,cy,cr)
self.CollideEllipseEllipse(c1x,c1y,r1x,r1y, c2x,c2y,r2x,r2y, rot1=0, rot2=0)
self.CollideRotatedRectRotatedRect(ax,ay,aw,ah,a_rot, bx,by,bw,bh,b_rot)
self.CollideTriangleTriangle(a1x,a1y,a2x,a2y,a3x,a3y, b1x,b1y,b2x,b2y,b3x,b3y)
self.CollideLineLine(x1,y1,x2,y2, x3,y3,x4,y4)
self.CollideLineRect(x1,y1,x2,y2, rx,ry,rw,rh)
self.CollideLineCircle(x1,y1,x2,y2, cx,cy,cr)
# ...e tutte le combinazioni line/rect/rot-rect/circle/ellipse/triangle

Batch (un punto vs N shape)

hits = self.CollidePointRectBatch(px, py, x_arr, y_arr, w_arr, h_arr)
# hits è un ndarray bool di lunghezza N
hits = self.CollidePointCircleBatch(px, py, cx_arr, cy_arr, r_arr)
hits = self.CollidePointRotatedRectBatch(px, py, x_arr, y_arr, w_arr, h_arr, rotation_arr)
hits = self.CollidePointTextureBatch(px_arr, py_arr, name, x_arr, y_arr, ...)

Mouse: helper di alto livello

Ogni frame WINDOW.Loop() chiama UpdateMouseState(mx, my, events) per te. Poi puoi usare direttamente questi helper con una qualsiasi shape.

La shape può essere una stringa oppure una data-class:

MouseOver / MousePressed / MouseReleased / MouseClicked / MouseHeld / MouseDragging / MouseWheelOn

MouseXxx(shape, *shape_args, button=PE_MOUSE_LEFT, show=False, color=..., camera=None)
def draw(self):
    # Bottone: cambia colore quando ci passi sopra, esegue azione al click
    hovered = self.MouseOver("rect", 100, 100, 200, 50)
    color = (94,234,212) if hovered else (124,156,255)
    self.DrawRoundedRect(100, 100, 200, 50, radius=10, color=color)
    self.DrawText("Play", 200, 125, anchor="center", color=(0,0,0))

    if self.MouseClicked("rect", 100, 100, 200, 50):
        print("Start!")

    # Drag su un cerchio
    if self.MouseDragging("circle", self.ball_x, self.ball_y, 30):
        self.ball_x = self.mouse_x
        self.ball_y = self.mouse_y

MousePosition() → (x, y)

mx, my = self.MousePosition()

CheckCollision (universale)

CheckCollision(a, b, show=False, color=(0,255,0,255), camera=None)

Rileva la collisione tra due shape qualsiasi (mixing consentito). Le shape sono le stesse data-class già viste.

a = Rect(100, 100, 80, 80)
b = Circle(self.player_x, self.player_y, 20)
if self.CheckCollision(a, b):
    print("Hit!")

Input — PE_Event

Ogni elemento di events passato a update(dt, events) è un PE_Event con questi attributi:

AttributoDescrizione
typeUno di PE_KEYDOWN, PE_KEYUP, PE_MOUSEMOTION, PE_MOUSEDRAG, PE_MOUSEBUTTONDOWN, PE_MOUSEBUTTONUP, PE_MOUSEWHEEL.
keyCodice tasto (per KEYDOWN/KEYUP).
buttonPE_MOUSE_LEFT/MIDDLE/RIGHT/X1/X2.
x, yPosizione del cursore (schermo).
dx, dyDelta di movimento del mouse.
clicks1 = click singolo, 2 = doppio.
wheel_x, wheel_yRotazione rotellina.
def update(self, dt, events):
    for ev in events:
        if ev.type == PE_KEYDOWN:
            if ev.key == PE_K_ESCAPE: self.running = False
            if ev.key == PE_K_SPACE:  self.jump()
        elif ev.type == PE_MOUSEBUTTONDOWN and ev.button == PE_MOUSE_LEFT:
            self.shoot(ev.x, ev.y)
        elif ev.type == PE_MOUSEWHEEL:
            self.zoom += ev.wheel_y * 0.1

PE_Event ha anche shortcut di collisione col mouse (rimappano su DRAW):

for ev in events:
    if ev.type == PE_MOUSEBUTTONDOWN:
        if ev.CollideRect(self, 100, 100, 50, 50):
            print("clic su bottone")
        if ev.CollideCircle(self, 300, 300, 40, camera=self.cam):
            print("clic su nemico in world-space")

Costanti tastiera

Prefisso PE_K_. Elenco completo:

Costanti mouse

EventiPulsanti
PE_MOUSEMOTION — solo movimento
PE_MOUSEDRAG — movimento con pulsante premuto
PE_MOUSEBUTTONDOWN / PE_MOUSEBUTTONUP
PE_MOUSEWHEEL
PE_MOUSE_LEFT
PE_MOUSE_MIDDLE
PE_MOUSE_RIGHT
PE_MOUSE_X1 (indietro)
PE_MOUSE_X2 (avanti)
Nota: PE_MOUSEMOTION e PE_MOUSEDRAG sono mutuamente esclusivi per frame. Se ti serve sempre la posizione del cursore, ascoltali entrambi.

CameraGPU

Rendering in un FBO offscreen, blit finale con pan/zoom applicati dalla GPU. Ideale per scene dense (migliaia di sprite).

Costruttore & ciclo

CameraGPU(window) cam.begin() # da qui tutto va nell'FBO cam.end() # blit su schermo cam.release() # libera l'FBO alla chiusura

API pubblica

# Proprietà
cam.x, cam.y, cam.zoom   # getter/setter

# Movimento
cam.move(dx, dy)
cam.center_on(wx, wy)
cam.follow(target_x, target_y, speed=250, deadzone=0)
cam.stop_follow()

# Feedback
cam.shake(intensity=10, duration=0.4)

# Limiti mondo
cam.set_bounds(min_x, min_y, max_x, max_y)
cam.clear_bounds()

# Conversione coordinate
wx, wy = cam.screen_to_world(sx, sy)
sx, sy = cam.world_to_screen(wx, wy)

# Culling
if cam.is_visible(wx, wy, w, h, margin=0): ...
mask = cam.is_visible_batch(rects_np)          # (N,4) rects
mask = cam.is_visible_batch_numba(rects_np)
x, y, w, h = cam.viewport_rect()

# Loop tick (per follow/shake)
cam.update(dt)

Esempio completo

class Game(WINDOW):
    def __init__(self):
        super().__init__(title="CameraGPU", geometry=("center","center",800,600))
        self.cam = CameraGPU(self)
        self.px, self.py = 2000, 2000
        self.cam.set_bounds(0, 0, 4000, 4000)

    def update(self, dt, events):
        speed = 300
        keys = {ev.key for ev in events if ev.type == PE_KEYDOWN}
        if PE_K_SPACE in keys: self.cam.shake(14, 0.35)
        self.cam.follow(self.px, self.py, speed=400)
        self.cam.update(dt)

    def draw(self):
        self.cam.begin()
        for gx in range(0, 4000, 100):
            self.DrawLine(gx, 0, gx, 4000, color=(50,60,90))
        self.DrawCircle(self.px, self.py, 20, color=(255,200,0))
        self.cam.end()
        self.DrawText(f"FPS {self.GetFPS()}", 10, 10, size=18)

CameraCPU

Trasformazioni pure Python/NumPy. Zero overhead GPU nascosto. Ideale per scene rade, editor, frustum culling custom. Non ha begin()/end(): applichi tu world_to_screen o apply_rect alle tue coordinate.

cam = CameraCPU(window)

# Stesse proprietà/metodi di CameraGPU per pan, zoom, follow, shake, bounds...
cam.follow(player.x, player.y, speed=300)
cam.update(dt)

# Trasformazioni
sx, sy = cam.world_to_screen(wx, wy)
wx, wy = cam.screen_to_world(sx, sy)
sw, sh = cam.scale(world_w, world_h)
sx, sy, sw, sh = cam.apply_rect(wx, wy, w, h)

# Batch
screen_pts = cam.world_to_screen_batch(np.array([[100,200], ...], dtype='f4'))
mask = cam.is_visible_batch(rects_np, margin=10)

Esempio: sprite in world coords

def draw(self):
    for e in self.enemies:
        sx, sy, sw, sh = self.cam.apply_rect(e.x, e.y, 32, 32)
        if self.cam.is_visible(e.x, e.y, 32, 32):
            self.DrawRect(sx, sy, sw, sh, color=(255,80,80))

PE_TIME — Scheduler a frame

Timer non bloccanti, zero thread, aggiornati dal loop principale.

Modulo globale (uno Scheduler condiviso, comodo)

PE_TIME.After(2.0, on_boom)                # callback una volta dopo 2s
PE_TIME.Every(0.5, spawn, times=10)         # 10 volte ogni 0.5s
PE_TIME.Every(1.0, tick, immediate=True)   # subito + ogni secondo

# Nel game loop:
def update(self, dt, events):
    PE_TIME.Update(dt)

PE_TIME.Cancel(handle)
PE_TIME.CancelAll()
print(PE_TIME.Count())   # timer attivi

Scheduler dedicato

sch = PE_TIME.Scheduler()
h = sch.Every(0.1, self.spawn_particle)
sch.Update(dt)
sch.Cancel(h)

Countdown / Cooldown / Stopwatch

Countdown(duration)

cd = PE_TIME.Countdown(5.0)
cd.Start()

def update(self, dt, events):
    just_expired = cd.Update(dt)
    if just_expired:
        print("Tempo scaduto!")
    print(f"progress {cd.progress:.2f}")

Cooldown(duration)

fire_cd = PE_TIME.Cooldown(0.3)

def update(self, dt, events):
    fire_cd.Update(dt)
    for ev in events:
        if ev.type == PE_KEYDOWN and ev.key == PE_K_SPACE:
            if fire_cd.Trigger():   # True se pronto; riavvia il cd
                self.shoot()

Stopwatch

sw = PE_TIME.Stopwatch()
sw.Start()
...
sw.Pause()
print(f"elapsed = {sw.elapsed:.3f}s")
sw.Reset()

AsyncTimer & MainThread

Basati su threading, per attese background (I/O, download, calcoli).

NON toccare OpenGL/SDL dal callback: accoda con RunOnMainThread(fn, *args) e chiama PumpMainThread() una volta a frame.
t = PE_TIME.AsyncAfter(3.0, load_map_from_disk, "level1.json")
if t.alive: t.Cancel()

def on_data_ready(data):
    # chiamato dal thread background
    PE_TIME.RunOnMainThread(self.apply_data, data)

def update(self, dt, events):
    PE_TIME.PumpMainThread()   # esegue le callback accodate
    PE_TIME.Update(dt)

PE_PAKER — pack()

Crea un eseguibile standalone del tuo gioco con cx_Freeze.

pack( script: str, *, name="Game", version="1.0.0", description="", author="", output_dir="dist", include_dirs=(), include_files=(), icon=None, console=False, extra_packages=(), extra_excludes=(), optimize=2, clean=True, silent=False, ) -> Path
from PyBitEngine import pack

pack(
    "main.py",
    name="MioGioco",
    version="1.0.0",
    author="Tu",
    icon="assets/icon.ico",
    include_dirs=["assets"],
    include_files=["README.md"],
    console=False,
    output_dir="dist",
)
Requisiti: pip install cx_Freeze. Su Windows, con console=False viene usata la base gui (nessuna console nera).

Ricetta 1 — Mini Pong completo

from PyBitEngine import *

class Pong(WINDOW):
    def __init__(self):
        super().__init__(title="Pong", geometry=("center","center",800,500), VSync=True)
        self.SetBackground((15,20,40))
        self.pad_l = 200; self.pad_r = 200
        self.bx, self.by = 400, 250
        self.vx, self.vy = 300, 220
        self.score_l = self.score_r = 0
        self.keys = set()

    def update(self, dt, events):
        for ev in events:
            if ev.type == PE_KEYDOWN:
                self.keys.add(ev.key)
                if ev.key == PE_K_ESCAPE: self.running = False
            elif ev.type == PE_KEYUP:
                self.keys.discard(ev.key)

        speed = 400 * dt
        if PE_K_w in self.keys: self.pad_l -= speed
        if PE_K_s in self.keys: self.pad_l += speed
        if PE_K_UP   in self.keys: self.pad_r -= speed
        if PE_K_DOWN in self.keys: self.pad_r += speed

        self.bx += self.vx * dt
        self.by += self.vy * dt
        if self.by < 10 or self.by > 490: self.vy *= -1

        if self.CollideRectRect(20, self.pad_l, 14, 100, self.bx-8, self.by-8, 16, 16):
            self.vx = abs(self.vx)
        if self.CollideRectRect(766, self.pad_r, 14, 100, self.bx-8, self.by-8, 16, 16):
            self.vx = -abs(self.vx)

        if self.bx < 0:  self.score_r += 1; self._reset()
        if self.bx > 800: self.score_l += 1; self._reset()

    def _reset(self):
        self.bx, self.by = 400, 250
        self.vx = -self.vx

    def draw(self):
        self.DrawRect(20, self.pad_l, 14, 100, color=(124,156,255))
        self.DrawRect(766, self.pad_r, 14, 100, color=(94,234,212))
        self.DrawCircle(self.bx, self.by, 8, color=(255,255,255))
        self.DrawText(f"{self.score_l}   {self.score_r}", 400, 30,
                      size=32, anchor="center", color=(255,255,255))

Pong().Loop()

Ricetta 2 — 10.000 particelle a 60 FPS

from PyBitEngine import *
import numpy as np

class Particles(WINDOW):
    def __init__(self):
        super().__init__(title="10k particelle", geometry=("center","center",1000,700), VSync=True)
        self.SetBackground((10,10,20))
        N = 10_000
        self.pos = np.random.rand(N, 2).astype('f4') * [1000, 700]
        self.vel = (np.random.rand(N, 2).astype('f4') - 0.5) * 200
        self.sz  = np.full((N, 2), 3.0, dtype='f4')
        self.col = (np.random.rand(N, 3) * 255).astype('u1')

    def update(self, dt, events):
        self.pos += self.vel * dt
        # bounce sui bordi (vettorializzato)
        for i, lim in enumerate([1000, 700]):
            m = (self.pos[:, i] < 0) | (self.pos[:, i] > lim)
            self.vel[m, i] *= -1
            self.pos[:, i] = np.clip(self.pos[:, i], 0, lim)

    def draw(self):
        self.DrawRectsBatch(self.pos, self.sz, self.col)
        self.DrawText(f"FPS {self.GetFPS()}", 10, 10, size=18)

Particles().Loop()

Ricetta 3 — Camera che segue il player + click nel mondo

from PyBitEngine import *

class World(WINDOW):
    def __init__(self):
        super().__init__(title="World", geometry=("center","center",900,600), VSync=True)
        self.SetBackground((20,24,40))
        self.cam = CameraGPU(self)
        self.cam.set_bounds(0, 0, 3000, 2000)
        self.px, self.py = 1500, 1000
        self.enemies = [(x, y) for x in range(100, 3000, 200)
                                for y in range(100, 2000, 200)]
        self.keys = set()

    def update(self, dt, events):
        for ev in events:
            if ev.type == PE_KEYDOWN: self.keys.add(ev.key)
            elif ev.type == PE_KEYUP: self.keys.discard(ev.key)
            # Click sul mondo tramite CameraGPU
            elif ev.type == PE_MOUSEBUTTONDOWN and ev.button == PE_MOUSE_LEFT:
                wx, wy = self.cam.screen_to_world(ev.x, ev.y)
                self.enemies = [(x,y) for (x,y) in self.enemies
                                if (x-wx)**2 + (y-wy)**2 > 30**2]

        v = 400 * dt
        if PE_K_a in self.keys or PE_K_LEFT  in self.keys: self.px -= v
        if PE_K_d in self.keys or PE_K_RIGHT in self.keys: self.px += v
        if PE_K_w in self.keys or PE_K_UP    in self.keys: self.py -= v
        if PE_K_s in self.keys or PE_K_DOWN  in self.keys: self.py += v

        self.cam.follow(self.px, self.py, speed=500)
        self.cam.update(dt)

    def draw(self):
        self.cam.begin()
        # griglia
        for x in range(0, 3000, 100):
            self.DrawLine(x, 0, x, 2000, color=(40,50,80))
        for y in range(0, 2000, 100):
            self.DrawLine(0, y, 3000, y, color=(40,50,80))
        # nemici visibili
        for (x, y) in self.enemies:
            if self.cam.is_visible(x-10, y-10, 20, 20):
                self.DrawCircle(x, y, 10, color=(255,100,100))
        # player
        self.DrawCircle(self.px, self.py, 14, color=(94,234,212))
        self.cam.end()
        # HUD in screen space (fuori da begin/end)
        self.DrawText("WASD muovi, click uccidi", 10, 10, size=18)
        self.DrawText(f"Nemici: {len(self.enemies)}", 10, 36, size=18)

World().Loop()

PyBitEngine v0.1.3 — Documentazione generata come singolo file HTML.
from PyBitEngine import * è l'unico import di cui hai bisogno.