Sprites & Grupos
O batch
Um SpriteBatch é um pool de capacidade fixa de sprites que compartilham
uma textura e são desenhados com um draw call instanciado:
import fastobjects as fo
win = fo.Window(800, 600)
batch = fo.SpriteBatch("player.png", capacity=10_000)
capacityé o máximo de sprites vivos; exceder levantaCapacityErrordizendo a capacity exata de que você precisa.- A textura é qualquer imagem que o Pillow abra. Um batch = uma textura — se precisar de várias imagens hoje, crie um batch por imagem (texture atlas está no roadmap).
ctx/view_sizevêm da janela atual; passe-os explicitamente só para render offscreen ou testes.
Spawn — sempre vetorizado
spawn(n, ...) cria n sprites em uma chamada. Todo parâmetro aceita um
escalar (aplicado a todos) ou um array de tamanho n:
import numpy as np
rng = np.random.default_rng()
bunnies = batch.spawn(
5000,
x=rng.uniform(0, 800, 5000),
y=100.0, # escalar: igual para todos
rot=0.0,
color=(1.0, 1.0, 1.0, 1.0), # ou um array (n, 4)
)
O retorno é um SpriteGroup — um objeto Python leve para o grupo inteiro,
nunca um por sprite.
Grupos: views, não cópias
As propriedades de um grupo são views NumPy do armazenamento do batch. Escrever nelas é escrever nos sprites:
bunnies.pos += velocity * dt # move todos, uma operação de array
bunnies.color = (1.0, 0.2, 0.2, 1.0) # tinge todos de vermelho
bunnies.rot += 0.5 * dt # gira todos
bunnies[100:200].y = 0.0 # sub-slice: linhas 100..199
Propriedades disponíveis: x, y, w, h, rot (arrays 1D), pos
(n, 2), size (n, 2), color (n, 4). len(grupo) dá a contagem;
grupo[a:b] retorna um sub-grupo sobre o mesmo armazenamento.
Como funcionam os uploads — você paga pela mudança, não pela existência
Todo sprite sempre tem todas as propriedades, na CPU e na GPU. O que é otimizado é o upload por frame: posições sobem todo frame (mudam em qualquer app real); tamanho, rotação e cor sobem apenas nos frames em que você as toca. O rastreamento é automático e conservador — acessar a propriedade marca a coluna para upload, então uma mudança nunca deixa de aparecer na tela silenciosamente.
Uma regra: não guarde uma view de propriedade entre frames para
escrever nela depois. Reacesse a cada frame
(grupo.color[...] = ...) — o acesso é O(1) e é ele que mantém o
rastreamento correto.
Despawn
despawn(grupo) remove os sprites do grupo, compacta o batch com uma cópia
vetorizada por coluna e devolve a capacity:
a = batch.spawn(100)
b = batch.spawn(50)
batch.despawn(a)
len(b) # ainda 50 — o handle de b foi realocado automaticamente
batch.spawn(80) # a capacity de a está disponível de novo
Handles sobreviventes continuam funcionando. O grupo removido — e qualquer
sub-grupo que o sobreponha — fica inválido: tocá-lo levanta RuntimeError
mandando fazer spawn de novo. batch.clear() remove tudo e invalida todos
os handles.
Várias imagens (atlas)
Um batch pode guardar mais de uma imagem e ainda desenhar tudo em uma chamada. Passe uma lista (selecionada por índice) ou um dict (selecionado por nome); o FastObjects empacota tudo num único texture atlas na criação:
batch = fo.SpriteBatch(["heroi.png", "moeda.png", "inimigo.png"], capacity=10_000)
heroi = batch.spawn(1, x=400, y=300, image=0)
moedas = batch.spawn(50, x=xs, y=ys, image=1) # todas moedas
misto = batch.spawn(100, x=xs, y=ys, image=np.arange(100) % 3) # vetorizado
nomeado = fo.SpriteBatch({"heroi": "heroi.png", "moeda": "moeda.png"}, capacity=100)
nomeado.spawn(1, image="heroi")
spawn(..., image=i) aceita escalar ou array de tamanho n (índices/nomes).
Quando w/h ficam em None, cada sprite usa o tamanho em pixels da sua
própria imagem.
Animação de spritesheet — reatribua group.image para re-texturar um grupo
no lugar (ainda um draw call; image é uma coluna fria que só re-sobe quando
você muda):
frames = fo.SpriteBatch([f"walk{i}.png" for i in range(8)], capacity=100)
player = frames.spawn(1, x=400, y=300)
@win.frame
def update(dt):
player.image = (tick // 6) % 8 # avança a animação
...
O atlas é estático: montado uma vez a partir das imagens passadas. Todas
precisam caber numa textura (GL_MAX_TEXTURE_SIZE, tipicamente ≥ 8192); se não
couberem, você recebe um AtlasOverflowError acionável. Add/remove em runtime
ainda não é suportado. Veja
examples/atlas_animation.py.
Um exemplo completo
import numpy as np
import fastobjects as fo
win = fo.Window(800, 600, title="guia de sprites")
batch = fo.SpriteBatch("player.png", capacity=5000)
rng = np.random.default_rng(7)
n = 2000
group = batch.spawn(n, x=rng.uniform(0, 800, n), y=rng.uniform(0, 300, n))
vel = rng.uniform(-120, 120, (n, 2)).astype("f4")
@win.frame
def update(dt: float) -> None:
vel[:, 1] += 980.0 * dt # gravidade
group.pos += vel * dt
floor = group.y > 600
vel[floor, 1] *= -0.85 # quique
group.y = np.minimum(group.y, 600)
win.clear(0.08, 0.08, 0.10)
win.draw(batch)
if win.keys[fo.KEY_ESCAPE]:
win.request_close()
win.run()