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:
| Modulo | Contenuto |
|---|---|
PE_WINDOW | Classe WINDOW — finestra, contesto GL, event loop, GC. |
PE_DRAW | Classe DRAW (ereditata da WINDOW) + FontManager, shape data-classes. |
PE_CAMERA | CameraGPU, CameraCPU. |
PE_KEYS | PE_Event e tutte le costanti tastiera/mouse. |
PE_TIME | Scheduler a frame, timer, cooldown, async timer. |
PE_PAKER | Funzione 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()
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
| Parametro | Descrizione |
|---|---|
title | Titolo della finestra. |
geometry | (x, y, w, h). x/y possono essere "center". |
icon | Path a un file .png/.jpg/.bmp/.ico. |
fullscreen | Fullscreen borderless al boot. |
VSync | Sincronizza con il refresh del monitor. |
MSAA, MSS | Anti-aliasing multisample e numero di sample (2/4/8/16). |
max_fps | Cap manuale sugli FPS. None = illimitato. |
gc_auto | True = GC manuale periodico (più stabile). |
max_draw_elements | Dimensione 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)
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)
mode="frames": raccoglie ogniintervalframe.mode="time": raccoglie ogniintervalsecondi.mode="smart": raccoglie quandogen0supera 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
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)
# 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:
- Stringa:
"rect","circle","ellipse","triangle","line","rounded_rect","rounded_triangle","polygon","texture","text". - Data-class:
Rect(x,y,w,h),RotRect(x,y,w,h,rotation),Circle(cx,cy,r),Ellipse(cx,cy,rx,ry,rotation=0),Line(x1,y1,x2,y2,thickness=1),Triangle(...),RoundedRect(...),Polygon(points),TextureCollider(name,x,y,w,h,rotation=0,flip_x=False,flip_y=False).
MouseOver / MousePressed / MouseReleased / MouseClicked / MouseHeld / MouseDragging / MouseWheelOn
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:
| Attributo | Descrizione |
|---|---|
type | Uno di PE_KEYDOWN, PE_KEYUP, PE_MOUSEMOTION, PE_MOUSEDRAG, PE_MOUSEBUTTONDOWN, PE_MOUSEBUTTONUP, PE_MOUSEWHEEL. |
key | Codice tasto (per KEYDOWN/KEYUP). |
button | PE_MOUSE_LEFT/MIDDLE/RIGHT/X1/X2. |
x, y | Posizione del cursore (schermo). |
dx, dy | Delta di movimento del mouse. |
clicks | 1 = click singolo, 2 = doppio. |
wheel_x, wheel_y | Rotazione 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:
- Lettere:
PE_K_a … PE_K_z - Numeri riga tastiera:
PE_K_0 … PE_K_9 - Direzionali:
PE_K_UP,PE_K_DOWN,PE_K_LEFT,PE_K_RIGHT - Azione/controllo:
PE_K_SPACE,PE_K_ESCAPE,PE_K_RETURN,PE_K_BACKSPACE,PE_K_TAB,PE_K_INSERT,PE_K_DELETE,PE_K_HOME,PE_K_END,PE_K_PAGEUP,PE_K_PAGEDOWN - Sistema:
PE_K_CAPSLOCK,PE_K_NUMLOCK,PE_K_SCROLLLOCK,PE_K_PRINTSCREEN,PE_K_PAUSE,PE_K_LSUPER,PE_K_RSUPER,PE_K_MENU - Modificatori:
PE_K_LCTRL/RCTRL,PE_K_LSHIFT/RSHIFT,PE_K_LALT/RALT - Funzione:
PE_K_F1 … PE_K_F12 - Simboli:
PE_K_MINUS,PE_K_EQUALS,PE_K_LEFTBRACKET,PE_K_RIGHTBRACKET,PE_K_BACKSLASH,PE_K_SEMICOLON,PE_K_QUOTE,PE_K_BACKQUOTE,PE_K_COMMA,PE_K_PERIOD,PE_K_SLASH - Numpad:
PE_K_KP_0 … PE_K_KP_9,PE_K_KP_DIVIDE,PE_K_KP_MULTIPLY,PE_K_KP_MINUS,PE_K_KP_PLUS,PE_K_KP_ENTER,PE_K_KP_PERIOD
Costanti mouse
| Eventi | Pulsanti |
|---|---|
PE_MOUSEMOTION — solo movimentoPE_MOUSEDRAG — movimento con pulsante premutoPE_MOUSEBUTTONDOWN / PE_MOUSEBUTTONUPPE_MOUSEWHEEL
|
PE_MOUSE_LEFTPE_MOUSE_MIDDLEPE_MOUSE_RIGHTPE_MOUSE_X1 (indietro)PE_MOUSE_X2 (avanti)
|
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
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).
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.
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", )
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.