Os exemplos do Guia assumem from scena import * e init(…) no começo e run() no fim. from scena import * é o único import: tudo o que o Guia mostra vem dele.
Catálogo de erros
Quando um uso está errado, o Scena levanta uma destas exceções. O nome diz a causa; a mensagem mostra a grafia que funciona.
Exceção
Causa típica
Correção
CallableAssignError
obj.campo = lambda ou st.x = lambda
ship["hp"] @= lambda: …
LinkedAttributeError
Escrever um campo que já tem @=
.unlink() antes, ou escreva no outro lado da fórmula
DerivedAttributeError
Escrever um campo derivado (só leitura)
Escreva o campo que o define (center, não um canto calculado). Para pôr um ponto calculado num lugar, mova o objeto por ele: r.move_to(1, 2, about=r.top_right). A mensagem do erro diz de onde o campo é calculado e mostra a forma certa.
DestroyedError
Mudança num objeto já destruído (move_by, rotate_by, visible, layer, tag, campo próprio, radius =)
if nave: antes da chamada. .center ainda lê. destroy() de novo não faz nada. move_by(..., time=), after e play_sound devolvem um Handle já cancelado, em silêncio (o som não toca)
CircularReferenceError
Um @= que fecha um laço: a["radius"] @= lambda: b.radius e depois b["radius"] @= lambda: a.radius
Ligue só um sentido; o outro lado recebe = (ou é escrito em update(dt)). A mensagem nomeia as duas pontas
ParallaxLayerError
clickable / draggable / colisão num objeto de camada com parallax (pan_response ≠ 1 …)
Camadas de parallax são só decoração. Ponha o objeto jogável numa camada sem parallax. Também é ValueError
"ui" é espaço de tela (HUD). Use uma camada do mundo. Também é ValueError
ActUsageError
Tratar um Act como um objeto do jogo: self.visible, self.layer, @collide_with num ato
O ato não desenha nem colide; a regra vai na classe do objeto, ou @collide(a, b) dentro de setup(). Também é TypeError / AttributeError
Cancelled
Um await num Handle que foi cancelado (espera async)
Não é erro seu: o mode acabou. try / finally para limpar. É BaseException — except Exception não pega
TypeError
Tipo errado: string onde vai número, bool, Circle(10); nome de parâmetro errado (Polygon(pionts=…))
A mensagem nomeia o parâmetro e um exemplo (Circle(center=(0, 0), radius=10)), ou Did you mean 'points'? + a lista Valid:
TypeError
Uma célula de st sem colchetes onde ela é ligada ou acompanhada: Slider(st.volume), Text(st.score), @on_change(st.vidas), tween(st.t, …), until(st.acabou), c["radius"] @= st.r
st.volume é o valor; a célula é st["volume"] (desde a 0.8.0). A mensagem mostra a grafia com colchetes
TypeError
Colchetes dentro de uma lambda ou usados como número: Text(lambda: f"Pontos: {st['score']}"), c["radius"] @= lambda: st["r"] * 2, @when(lambda: st["vidas"] == 0), if st["vivo"]:
Dentro da lambda, ponto: st.score. A lambda já mantém a conta viva; st["score"] é a célula, não o número. A mensagem mostra a grafia com ponto
TypeError
Na linha da classe: um método que o Scena nunca chamaria. Um método chamado update numa classe, com qualquer assinatura (até um ajudante def update(self): update é do Scena) (o gancho de cada frame é on_update; numa forma, update é o recálculo interno dos campos ligados); on_key_down / on_mouse_click / … (nomes do topo do arquivo) num ato ou num objeto; um nome a uma letra de um gancho (on_colide, on_updat, on_destory) ou de outro motor (on_collision); um método chamado collide_with
A mensagem diz o nome certo: on_update(self, dt); num ato, keyp.once("SPACE") / mouse.press() no on_update; num objeto, on_click(self); Did you mean on_collide?. Um ajudante seu (on_hit, on_frame) passa: só nomes a uma letra de um gancho são recusados
AttributeError
st.pos.x = 5: mudar por dentro o Vec2 que uma célula guarda (ninguém ligado a st.pos veria)
Troque o valor inteiro: st.pos = Vec2(5, st.pos.y) ou st.pos += Vec2(5, 0)
ValueError
Número fora do domínio: tamanho negativo, NaN, camada que não existe
A mensagem diz o intervalo ou Did you mean
Problemas que não impedem o jogo — arquivo de som ou imagem que falta, nome de cor desconhecido — viram um SCENA WARNING, não uma exceção: o jogo continua (silêncio, um quadriculado no lugar da imagem; nome de cor quase certo vira a cor mais próxima, nome desconhecido vira magenta) e uma faixa âmbar no canto mostra o aviso. Ctrl+Shift+E fecha a faixa.
Script mínimo
Todo script tem três partes. from scena import * e init(…) preparam o espaço do jogo. O corpo do arquivo cria os objetos — cada construtor já põe o seu objeto na tela. run() abre a janela e roda o jogo, chamando update(dt) a cada frame.
Define o espaço de design e a janela; não abre a janela (isso é o run()). Já roda sozinho no import scena com os padrões. Chame de novo logo após from scena import * se o script lê canvas.width / .height antes de run(). run() não aceita tamanho.
Parâmetro
Passar
Tipo (default)
Significado
width
nome=
número2000
Largura do espaço de design (canvas.width). Não é a janela OS em pixels.
height
nome=
número1200
Altura do espaço de design (canvas.height). y-up, origem no canto inferior esquerdo.
title
nome=
str"Scena"
Título da janela OS.
window_scale
nome=
número0.8
Fração da maior janela windowed que cabe na área de trabalho (aspecto do design, decorações ligadas, taskbar visível). 1.0 = tão grande quanto a área permite. 0.8 = 80% disso, centrada. Só no arranque: consumido quando a janela abre. Não existe canvas.window_scale.
display
nome=
str"windowed"
Atalho para canvas.display. Só "windowed" · "maximized" · "fullscreen". A janela windowed é criada primeiro (escala, centro); depois entra no modo.
ui_scale
nome=
númeroatual
Escreve ui.scale. Omitir deixa o valor atual (padrão de import: 1).
ui_font
nome=
stratual
Escreve ui.font (família ou arquivo .ttf / .otf). Omitir deixa o valor atual.
A janela OS deriva do aspecto do design, da área de trabalho do monitor, das bordas reais da janela e de window_scale. Sempre começa centrada. Não há x= / y= / window_width= em pixels — arraste se precisar de um lugar específico.
Não confundir
Parece
É
Não é
init(width=, height=) é o tamanho da janela OS
é o espaço de design. A janela windowed cabe na área de trabalho com o mesmo aspecto; window_scale=1 é o maior encaixe windowed. display="maximized" preenche a área (faixas pretas). display="fullscreen" cobre o monitor.
canvas.window_scale
Regras
Cinco regras que valem em todo o Scena.
Construtor coloca.
Circle() já aparece. Rain(), Vignette(), Slider() também.
Material e efeito só aparecem aplicados num objeto (ou no fundo).
Gradient, Color, Blur, Neon, Sheet sozinhos não desenham nada: vão em .fill, .visual, set_color, Sprite(sheet) ou canvas.background.
= copia agora. @= liga ao vivo.
c.radius = 10 · c["radius"] @= lambda: player.hp * 10 — ver Reativo.
Propriedade é estado. Método é ação.
c.visible = False · c.move_by(20, 0)
Medidas em unidades de design, y para cima, ângulos em graus.
A origem é o canto inferior esquerdo; 0° aponta para a direita. Ver unidades. Widgets ficam presos à janela e ignoram a câmera.
Cada tabela de parâmetros se explica sozinha: parâmetros comuns (layer=, group=, o 1º argumento dos widgets) aparecem de novo em cada uma, para você não precisar pular de seção.
Vocabulário
As palavras que o Guia usa sempre no mesmo sentido. Quando uma seção diz “unidades”, “ponto”, “Graphic” ou “frame”, é isto.
Termo
O que é
unidade de design (“unidades”)
A medida de tudo no Scena: posição, tamanho, raio, espessura, velocidade (unidades por segundo). O espaço vai de 0 a width e de 0 a height do init: com init(width=2000, height=1200), o centro da tela é (1000, 600). A janela escala esse espaço inteiro para caber no monitor, então 1 unidade não é 1 pixel — o jogo fica igual em qualquer tela. y cresce para cima; (0, 0) é o canto inferior esquerdo; ângulos em graus, 0 = para a direita, anti-horário.
frame
Uma volta do jogo: ler teclado e mouse, rodar update(dt) e os on_update, mover, colidir, desenhar. Uns 60 por segundo. dt é quantos segundos passaram desde o frame anterior — multiplique velocidades por ele (200 * dt = 200 unidades por segundo).
Entity
Todo objeto do jogo: Shape, Sprite, Text, SpatialFx, widgets e o Entity() sem desenho. Toda Entity pode ter campos próprios (ship.hp), timers (after / every), mode, grupos e destroy().
Graphic
Uma Entity com corpo desenhado e pose: Shape, Sprite (e Svg) e Text. Só um Graphic pode ser clicado ou arrastado, andar com move_to e ter .box. SpatialFx e widgets não são Graphic.
pose
Onde o objeto está e como está virado: center, angle, scale e o espelho (flip_x / flip_y). Os verbos de Mover mudam a pose; cor e visual não fazem parte dela.
Shape
Um Graphic geométrico: Circle, Rect, Square, Line, Ellipse, Polygon, RegularPolygon, EquilateralTriangle, Point, Track.
ponto
Onde a API pede um lugar (move_to, center=, about=, query_point…), vale qualquer um destes: uma tupla (x, y), um Point, um Vec2, um Graphic (vale o centro dele) ou mouse. A posição é copiada naquele instante; para acompanhar, use lambda: (Reativo). Mais em Pontos.
campo / Attribute
Um valor que o Scena acompanha: st.score, ship.hp, c.radius. Quando ele muda, quem depende dele (uma fórmula @=, um @when, um widget) fica sabendo. Attribute é o nome da classe por trás.
ligar (@=)
Fazer um campo seguir uma fórmula: c["radius"] @= lambda: ship.hp refaz a conta sempre que ship.hp muda. O oposto de =, que copia o valor uma vez.
Handle
O objeto que os verbos que levam tempo devolvem (after, tween, move_to(…, time=) ou speed=, play_sound…): serve para cancelar, pausar ou esperar aquela tarefa. Ver Handle.
mode
O capítulo de comportamento em que uma Entity está agora (e.mode = e.mergulhando). O que ele começa (timers, movimentos) para quando ele acaba. Ver Mode.
slot (name=)
Um nome que guarda uma tarefa só: after(2, dica, name="dica") chamado de novo com o mesmo nome substitui o anterior em vez de somar outro. O slot é de um lugar: o objeto da chamada (nave.after(…, name=)); numa chamada solta, quem está no mode que roda (dentro do corpo de um mode, inclusive o mode de um ato); fora de mode (topo do script, setup do ato, callbacks), o jogo / o ato atual. O mesmo nome em dois lugares são dois slots: um play_sound(…, name="musica") solto no setup do ato e outro solto no mode do ato tocam juntos. Para uma tarefa só, chame sempre do mesmo lugar — a trilha de um ato com self.play_sound(…, name="musica"), que é o mesmo slot de uma chamada solta no mode do ato.
colocar × viajar
Colocar põe o objeto direto no lugar (center = …, move_to sem time=) e atravessa paredes. Viajar anda até lá (move_by, move_to(…, time=) ou speed=, move_along), e uma parede de resolve() para no meio; o @collide de quem viaja rápido confere o caminho inteiro (Objeto rápido). Girar no lugar e escalar (rotate_* / scale_*) nunca são parados pela parede: ela só afasta o objeto depois. Girar em volta de outro ponto (rotate_by(…, about=)) leva o centro junto, e isso é viajar.
segurando × no frame do aperto
keyp("A") é True em todo frame em que a tecla está segurada; keyp.once("A") só no frame em que ela foi apertada. @when dispara quando a condição passa de False para True, não enquanto continua True.
canal
Nos Transientes, a vaga de um tipo de efeito: um flash novo substitui o flash em curso, mas convive com um pop.
HUD
Textos e controles presos à janela (placar, vidas, menus), na camada "ui": não andam com a câmera.
Arquivos de imagem
Onde o Scena procura os arquivos de imagem (PNG, JPG, SVG e atlas XML / JSON) que você passa por nome: ao lado do script ou na pasta media/ dele.
Sprite, Sheet, Svg, Texture e Wallpaper usam a mesma busca. O nome entra como está — o framework não desce em subpastas sozinho.
Caminho absoluto.
Só aquele arquivo. Existe ou falta.
Pasta do script.
Ao lado do .py que você rodou.
media/ ao lado do script.
Sprite("nave.png") acha media/nave.png.
Pasta de onde você rodou o Python.
O cwd. Último recurso.
Sprite("nave.png")
Sprite("sprites/nave.png") # media/sprites/nave.png ao lado do script
Texture("chao.png")
Wallpaper("ceu.jpg")
Um arquivo que não existe não para o jogo: aparece um quadriculado no lugar da imagem e um aviso, e o objeto funciona normalmente. No REPL ou com python -c, a busca começa na pasta atual. Atlas: o PNG fica ao lado do XML / JSON que o descreve. Um padrão com * (Sheet e strip, Padrão de arquivos) procura nas mesmas pastas e usa só a primeira que tiver algum arquivo que casa.
Objetos globais únicos
Estes objetos já existem depois de from scena import * — não se constroem, só se usam. As classes, funções e efeitos vêm pelo mesmo import; a busca no topo da página é o índice de todos.
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
st
State
lê / escreve
Os seus valores do jogo (placar, vidas, fase), acompanhados pelo Scena. Guardar valores.
canvas
Canvas
lê / escreve
A tela: câmera, fundo, efeitos de tela inteira, camadas. Mundo.
clock
Clock
lê / escreve
O tempo do jogo: dt, time, pausa e câmera lenta. clock.
keyp
KeyPressed
lê
Teclado: que tecla está segurada, qual acabou de ser apertada. Teclado.
Perguntas de colisão (quem está neste ponto?) e o desenho dos colisores. Colisão.
Callbacks do script
Funções com nome fixo que você escreve no script e o run() chama na hora certa: setup uma vez, quando a janela abre; update a cada frame; on_key_down quando uma tecla é apertada, e assim por diante. A tabela completa está em Callbacks do script.
Cada função pede só os parâmetros que quiser: o Scena passa os que ela declara. update(dt) e update(dt, canvas) valem os dois. Catálogo: Callbacks do script e Callbacks da Entity.
Reativo é o jeito de fazer um valor acompanhar outro sem escrever código a cada frame. c.radius = ship.hp copia o número uma vez; c["radius"] @= lambda: ship.hpliga: o raio passa a seguir o HP sozinho, sempre que o HP mudar. Para rodar uma ação quando algo muda (tocar um som, trocar de fase), use @when / @on_change; para agir depois de um tempo, Agendar.
Três operadores põem um valor num campo. = copia o valor de agora. @= liga o campo a uma fórmula: o Scena refaz a conta sempre que algo que ela lê muda. << copia os valores de outro objeto para dentro deste, sem trocar o objeto. O @= vai sempre com colchetes do lado esquerdo — c["radius"] @= lambda: … — porque c.radius já seria o número, não o campo.
st.score = 0
st.lives = 3
st["hud"] @= lambda: f"Stars {st.score}" # @= cria o campo se faltar
c.radius = 10
c.radius = other.radius
c["radius"] @= lambda: player.hp * 10
hero["grounded"] @= lambda: "bottom" in hero.touching_sides
line["p1"] @= lambda: rect.top_left
turret["angle"] @= lambda: dial.value
ship["scale"] @= lambda: 1 + 0.2 * st.pulse
# mouse inteiro é um ponto (link direto). .x / keyp ainda precisam de lambda — ver Frame
cursor["center"] @= mouse
c.center["x"] @= lambda: 1200 if keyp("D") else 800
# cópia agora
Circle(center=player.center, radius=player.hp)
# liga ao vivo
Circle(center=lambda: player.center, radius=lambda: player.hp * 10)
# << : substitui o conteúdo; o objeto da esquerda continua o mesmo
pos = Vec2(0, 0)
pos << Vec2(50, 50) # mesmo pos; valores novos
c.center << other.center # other continua no jogo
# c.absorb(other) # isso sim some com other
c["radius"].unlink()
c.unlink_all()
Regras
Situação
Significado
=
Copia o valor atual. Sem ligação depois.
<<
Substitui o conteúdo dentro do objeto da esquerda (A << B). A continua sendo o mesmo objeto (quem o guardava vê os valores novos). Não liga. Campo ligado: o mesmo LinkedAttributeError de =. Não é absorb (esse copia e destrói o outro).
["campo"] @=
Liga. Direita: algo reativo do Scena ou um callable. Número ou expressão cru: TypeError — use lambda: player.hp * 10.
Não equivale ao colchete: Python lê ship.campo antes. Num campo escalar (ship.hp, p.x) isso já é um float — TypeError do próprio Python, sem mensagem do Scena. Prefira ship["campo"] @= lambda: ….
.unlink() / unlink_all()
c["radius"].unlink() solta aquele campo da fórmula; c.unlink_all() solta todos os campos ligados do objeto (e das partes dele, como center.x).
Campo ligado + = / <<
LinkedAttributeError. Unlink primeiro. Nos verbos de pose / tween: o mesmo, salvo force=True (avisa e solta) — Mover. A mesma família de erro vale quando outro driver persistente já controla o campo.
Widget
O 1º argumento é a célula que o widget lê e escreve (Slider(st["volume"]), ligação nos dois sentidos). Não é @=.
Lambda que não lê nada que o Scena acompanha
TypeError — a fórmula precisa nomear algo reativo (ver O que pode ser ligado). Não congela em silêncio.
Não confundir
Parece
É
Não é
A << B
Copia os valores de B para dentro de A. A continua o mesmo objeto. B fica onde estava.
@= (não liga) · A = B (troca a referência Python) · absorb (copia e destrói o outro)
c.absorb(other)
c << other e depois destroy(other).
<< sozinho
O que pode ser ligado
Do lado esquerdo do @= fica o campo que vai mudar; do direito, a fórmula — em geral um lambda:. A fórmula precisa ler pelo menos uma coisa que o Scena acompanha (um campo, um objeto, st, clock, keyp, mouse): é assim que ele sabe quando refazer a conta.
A direita precisa nomear algo que o Scena acompanha. Variável Python comum não avisa quando muda: o @= recusa com TypeError.
O que é reativo (pode aparecer na direita e puxar atualização):
Point, Vec2, Shape (Circle, Rect, Line, …), Sprite, Entity — e os campos deles (c.radius, ship.center, hero.hp…)
st (e qualquer State) — st.score, st.lives…
clock, keyp, mouse — entrada do frame (sempre dentro de lambda:)
mic.level, audio.level, h.level (de um play_sound) e os .spectrum() deles — medidas do som, atualizadas uma vez por frame (Microphone)
Campo solto: Attribute(0.0)
Campos que você cria numa Entity já entram nesse mundo sozinhos — Guardar valores.
Formas canônicas
As duas grafias de todos os dias. A expressão à direita devolve algo compatível com o alvo, e nomeia pelo menos um objeto reativo:
# objeto["campo"] @= lambda: expressão compatível com o campo
dst["xyz"] @= lambda: src.abc
# Point: um lambda que devolve o par
circle1["center"] @= lambda: (a.x * 2 + 10, b.abc / 2)
# par de lambdas (um por eixo) — grafia do construtor:
# Circle(center=(lambda: a.x * 2 + 10, lambda: b.abc / 2))
Num for, cada lambda precisa do seu objeto: lambda h=host: h.angle — ver Lambda.
Dentro da fórmula, ponto. A lambda já mantém a conta viva: o Scena lê st.score no código e refaz a conta quando a célula muda. Colchetes dentro dela (lambda: st["score"] * 2, f"{st['score']}", c["radius"]) dariam a célula, não o número: o @= (e a lambda de um construtor) recusa com TypeError, apontando a linha e a grafia com ponto. Um colchete sozinho à direita, sem lambda, é a ligação direta: c["radius"] @= st["raio"].
Formas mais curtas, também legais
Funcionam, mas não são o caminho sugerido. Sem lambda, os dois lados precisam já ser objetos que o Scena acompanha (um campo, um Point, um Vec2) — o Python não calcula a expressão antes:
objeto1 @= src # se objeto1 e src forem objetos do Scena (Point, Vec2, campo)
dst["xyz"] @= src["abc"] # se xyz e abc forem campos reativos
line["p1"] @= rect.top_left # liga direto ao Point da direita, sem conta
orbit.center @= earth.center
Número ou conta já resolvida (host.angle, player.hp * 10) vira float antes do @= ver — TypeError. Use lambda:.
Função com nome
Quando a conta tem vários casos, escreva um def e ligue a função (sem parênteses). O Scena lê o código da função, uma vez, para saber do que ela depende — igual ao lambda:
def texto_placar():
if st.venceu:
return "Você venceu!"
if st.morto:
return f"Fim de jogo — estrelas {st.estrelas}"
return f"Estrelas {st.estrelas} Vidas {st.vidas}"
placar["text"] @= texto_placar # sem () — liga a função, não o resultado
placar["text"] @= lambda: texto_placar() # também vale: o Scena segue a função
Também vale já no construtor: Text(texto_placar, top_left=(20, 580)) faz a mesma ligação.
Pode usar if, variáveis locais, len / min / round, math, funções do Scena e outras funções do mesmo arquivo (até 3 níveis). O Scena recusa com TypeError o que ele não consegue enxergar lendo o código — senão a ligação ficaria parada sem avisar:
for / while / [… for … in …] — os itens só existem quando roda. Faça no update().
Ler um grupo (len(group["inimigo"])) — use update() ou uma fórmula de grupo.
Mudar um objeto do Scena dentro da fórmula (c.radius = 5) — a fórmula só lê e devolve.
Índice que muda numa lista de objetos (bolas[st.i].radius) — nomeie o objeto.
Chamar função de outro arquivo, getattr / eval, def ou lambda dentro da fórmula, try.
Não funciona — variável Python comum não avisa o Scena quando muda; número cru também não:
x = 10
c["radius"] @= lambda: x * 2 # TypeError: a fórmula não nomeia nada reativo
# também TypeError — host.angle já virou float
# twin["angle"] @= host.angle
# certo: colchetes no campo, ou lambda que nomeia o Shape
twin["angle"] @= host["angle"]
twin["angle"] @= lambda: host.angle
# certo: colocar em st (ou Attribute) e nomear isso
st.x = 10
c["radius"] @= lambda: st.x * 2
Dá para criar um valor reativo à mão, sem Shape:
from scena import Attribute, State
pulse = Attribute(0.0) # um campo solto, fora de qualquer objeto
hud = State(name="hud") # outro conjunto de campos, como o st
hud.level = 0
ring["scale"] @= lambda: 1 + pulse
label["text"] @= lambda: f"{hud.level}"
Não confundir
Parece
É
Não é
lambda: x + c.radius com x comum
Vale: atualiza quando c.radius muda (e relê x nesse momento). O @= só recusa quando a fórmula lê apenas variáveis comuns e nada que o Scena acompanha.
congelado
c.radius @= lambda: …
c.radius já é um float: TypeError do Python, sem mensagem do Scena. Escalar precisa de colchetes: c["radius"] @= …. Composto (c.center, um Vec2, um Attribute) aceita objeto @=, mas o caminho ensinado é o mesmo colchete + lambda.
outra grafia do colchete
c["glow"] @= … (pintura)
Pintura (.fill, .glow, .visual…) não liga. Use = e on_update / @when.
campo reativo
Entrada do frame no @=
clock / keyp / mouse sempre dentro de lambda: (lambda: clock.time, lambda: keyp("D"), lambda: mouse.x) — nunca número cru nem once / dx. Ver Frame.
valor de agora
Widget ligado a uma fórmula
O gesto do usuário não faz nada: quem manda é a fórmula.
controle livre
r["dano"] @= lambda: r.width * 2 (um campo do objeto lendo o tamanho dele)
Limite conhecido: um campo próprio não lê o tamanho do próprio objeto (width, radius…) nem os pontos calculados dele (cantos, o center de um Polygon) — eles saem do objeto inteiro, e o campo novo faz parte dele. CircularReferenceError com a explicação. Use uma propriedade na classe (@propertydef dano(self): return self.width * 2) ou calcule em update(dt). Os campos próprios do objeto (r.carga), o center de um Rect ou Circle e os campos de outros objetos podem ser lidos.
uma referência circular sua
Lambda
lambda: guarda a conta para o Scena refazer depois. Num for, prenda o objeto: lambda b=bola: b.center.
Sem lambda, o Python calcula na hora e o Scena recebe um número. Com lambda:, o Scena guarda a conta e refaz quando a fonte muda:
O lambda olha o nome na hora de atualizar, não na hora em que você escreveu a linha. Num for, o nome bola vai de um círculo para o próximo. No fim, todos os lambdas veem o último — e todas as linhas seguem ele.
# errado — as 20 linhas seguem a última bola
for i in range(20):
bola = Circle(center=canvas.game_view.center, radius=16)
Line(p1=(50, 50), p2=lambda: bola.center)
# certo — b=bola guarda esta bola agora
for i in range(20):
bola = Circle(center=canvas.game_view.center, radius=16)
Line(p1=(50, 50), p2=lambda b=bola: b.center)
No corpo use b.center, não bola.center. O centro continua ao vivo; só fica fixo qual bola. Se o nome nunca muda (um ponto1 criado uma vez), lambda: ponto1 basta — todos devem seguir o mesmo ponto.
Callback que escreve um atributo (after, on_done, …): lambda só aceita expressão — atribuição com = não entra. Use setattr:
Um objeto também pode nascer ligado: passe lambda: em vez de um valor e o parâmetro passa a seguir a fórmula, como um @= feito logo depois. Um valor sem lambda é copiado uma vez. Só os parâmetros de geometria (posição, tamanho, ângulo) aceitam lambda — não cor, camada ou tag.
Sem lambda, o Python calcula na hora e o objeto nasce com aquele número / ponto. Com lambda:, o Scena guarda a conta e refaz quando a fonte muda — o mesmo que um @= depois:
# cópia agora — player pode andar; este Circle fica onde nasceu
Circle(center=player.center, radius=player.hp)
# liga ao vivo — acompanha o jogador e o HP
Circle(center=lambda: player.center, radius=lambda: player.hp * 10)
Um ponto aceita um lambda que devolve o par, ou um par de lambdas (um por eixo). O par é grafia do construtor; depois da criação use c["center"] @= lambda: (…, …) ou ligue cada eixo.
Circle(center=lambda: player.center)
Circle(center=(lambda: a.x * 2 + 10, lambda: b.abc / 2))
p = Point(0, 300)
p["x"] @= lambda: mouse.x # um eixo ligado, o outro copiado
Line(p1=(50, 50), p2=lambda: bola.center)
A regra é a mesma em todo construtor de geometria: callable → liga; resto → copia. Um ponto sem lambda (center=player.center) copia — não acompanha. Para acompanhar, use lambda: (ou @= depois).
Quem aceita lambda
>Regra geral: parâmetros de geometria aceitam fórmulas lambda. Demais não.
Construtor
Parâmetros que ligam
Não ligam (cópia / recusam)
Shape — Circle
center, radius, perimeter_point, p1, p2, angle, spread
color, layer, group, box, use_center
Shape — Rect / Square / Ellipse
geometria do modelo: center, width, height, angle, top_left, p1, p2, vec, length, …
color, layer / group; relações (parallel_to=, circle=…) já observam sozinhas — não são lambda do autor
Shape — Polygon
cada vértice em points= (pode misturar: uns lambda, uns copiados)
quantidade de vértices (trava na construção)
Shape — RegularPolygon / EquilateralTriangle
center, radius, side, vertex, angle
n= (lados: inteiro, trava na construção)
Point
o ponto inteiro: Point(lambda: …); um eixo: Point(lambda: mouse.x, 0); par: Point((lambda: src.x, 10))
Point(outro_ponto) copia (não acompanha)
Vec2
cada componente: Vec2(lambda: a.x, lambda: a.y); o vetor inteiro: Vec2(lambda: v1 + v2)
resultado de conta já feita (Vec2(v1.x, v1.y) copia os floats)
Sprite / Svg
só center=
width / height / size (números na hora), frame, hitbox, arquivo, color / visual. fps= / cols= / atlas= vão no Sheet. Depois: sprite["width"] @= / ["angle"] @= / ["scale"] @=
Text
center=
text= é uma string, ou uma célula (st["score"]) que o Text passa a mostrar — não uma fórmula. font_size, color, top_left=. Depois: hud["text"] @= lambda: …
Track
center, radius, begin, end; cada ponto em points=
stroke, smooth, floating
SpatialFx (Fire, Rain, …)
center, radius; no fogo dirigido também width, length, angle
color, estilo, layer / group
Box
os mesmos da geometria do Rect (center, width, height, p1, p2, …)
Box não é Entity. Box(graphic) é uma cópia, tirada agora
Entity()
—
sem geometria no construtor. Campos próprios depois: self.hp = 3 (cópia) ou self["hp"] @= lambda: … (ligação)
Pintura, lugar no jogo e visual nunca ligam no construtor: color=, fill=, visual=, glow=, layer=, group=, visible=. Widget: o 1º argumento é o Attribute que ele lê e escreve, não um lambda.
Não confundir
Parece
É
Não é
Circle(center=player.center) “segue” o player
copia o ponto agora. Para seguir: center=lambda: player.center (ou c["center"] @= lambda: player.center depois).
o mesmo que @=
Sprite(width=lambda: …)
recusa — tamanho no construtor é número. Ligue depois: sprite["width"] @= lambda: ….
o center=lambda: do Sprite (esse vale)
Num for, Line(p2=lambda: bola.center)
todas as linhas seguem a última bola — prenda: lambda b=bola: b.center. Ver Lambda.
—
Guardar valores: st e campos da Entity
Dois lugares para os seus valores: st, para os valores do jogo como um todo (placar, vidas, onda, opções), e campos na própria Entity, para o que é dela (HP, grounded, munição). Nos dois, o campo nasce na primeira gravação (st.score = 0, ship.hp = 3) e já é acompanhado pelo Scena: fórmulas @=, @when e widgets podem ler dele.
Sprite, Circle, Rect, Line, Text, SpatialFx — e o Entity() sem desenho — são Entity. Geometria já existe (center, radius, width…); o que você inventa é campo próprio. Placar, onda, flag do app: st. HP, grounded, munição: na Entity que os possui.
O campo próprio (hp, grounded, wave) vira um Attribute na primeira gravação. A partir daí o Scena acompanha o campo: widgets, @=, @when e @on_change podem ler dele.
st.score = 0 # cria o campo
st.lives = 3
st["hud"] @= lambda: f"Stars {st.score}" # @= cria o campo se faltar
ship = Sprite("nave.png") # Sprite é Entity
c = Circle() # Circle também
diretor = Entity() # sem desenho, mesmos campos
ship.hp = 100 # cria o campo — já acompanhado
c["radius"] @= lambda: ship.hp * 10
# @= no colchete também cria, se o nome ainda não existe
hero["grounded"] @= lambda: "bottom" in hero.touching_sides
ship["shield"] @= lambda: st.lives * 10
Neste st
st já existe — é um State. Guarda os campos do jogo como um todo. O HP da nave fica na nave, não aqui.
Não constrói. from scena import * já traz o objeto. Widgets, @=, @when e @on_change leem daqui. Para outro conjunto separado (HUD, debug), crie outro State: hud = State(name="hud"). name é só o rótulo nas mensagens de erro — não é uma chave.
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
st.score = 0
valorst.score = 0
lê / escreve
Cria (ou grava) o campo. st.score lê o valor que está lá agora, como um campo de Entity: st.mic is None, type(st.score) e recorde = st.score (uma cópia) funcionam como numa variável comum.
st["score"]
célulaSlider(st["volume"])
leitura
A célula: o lugar onde o valor mora. É o que ligam e acompanham: widgets (Slider(st["volume"])), Text(st["score"]), @on_change(st["wave"]), @when(st["alive"]), tween(st["t"], …), @=. Sempre fora de uma lambda: dentro dela, st.score. Usada como número (if st["vivo"]:, st["score"] + 1, numa f-string), levanta TypeError com a grafia de ponto. .value lê o valor; .unlink() desliga uma fórmula.
st["hud"] @=
ligaçãost["hud"] @= lambda: …
lê / escreve
Liga aquele campo. @= cria o campo se faltar (valor inicial 0). Ler um campo que não existe levanta erro.
st.keys / st.exists("score")
nomes / bool
leitura
Lista os campos; diz se um já foi criado.
.name
str"st"
leitura
Rótulo de diagnóstico. O global é "st".
State(name="hud")
Statehud = State(name="hud")
—
Outro conjunto de campos, igual ao st. Vazio em name levanta ValueError.
Situação
Significado
st.key
Lê o valor. st.key = … grava. st.f **= 2, st.pos += Vec2(1, 0) e st.itens += [x] gravam na mesma célula e avisam quem está ligado. Mudar o valor por dentro não avisa: st.pos.x = 5 levanta (grave o valor inteiro: st.pos = Vec2(5, st.pos.y)), e st.itens.append(x) muda a lista sem avisar as ligações.
Slider(st.volume)
TypeError com a correção: st.volume é o número de agora, e o widget precisa da célula, st["volume"]. O mesmo para Text, @on_change, @when, until, tween e @= com um Vec2 (c["center"] @= st.pos, st.pos @= …). Com um número, st.x @= … dá o TypeError do próprio Python (unsupported operand type(s) for @=): escreva st["x"] @= …. Um texto em Text(st.nome) não dá erro: mostra o nome de agora e não acompanha; para acompanhar, Text(st["nome"]). Antes da 0.8.0, st.key devolvia a célula: st.mic is None era sempre False e recorde = st.score seguia o placar.
st["key"] @=
Mesma grafia da geometria. Cria o campo se faltar (valor inicial 0). A leitura (st.key, st["key"] à direita) exige que ela exista.
Nome inexistente na leitura → AttributeError. Typo aparece aqui, não no = / @=.
Pintura (.fill, .glow, .visual…)
Não é campo próprio. @= recusa. Use = e on_update / @when.
ship.x = 10
Graphic e SpatialFx não têm .x / .y — AttributeError, sem criar campo. Leia ship.center.x. Mova com move_to / move_by. Ligue com ship.center["x"] @=. Point.x continua. Entity nua ainda guarda e.x como campo próprio.
Não confundir
Parece
É
Não é
st.score e ship.hp são a mesma ideia (campo acompanhado)
lugares diferentes: o jogo todo vs este objeto. Nomes reservados (visible, layer, center…) não viram campo próprio.
criar ship.score no lugar do placar
Avançado
Drivers persistentes
Além de @=, outras coisas também escrevem um campo a cada frame. Só a fórmula @= é exclusiva — as outras se somam, em qualquer ordem.
Um driver persistente é o que escreve pose (ou um campo) sem o autor chamar um verbo pontual a cada frame: a fórmula @=, o arraste do mouse (draggable), a roda (wheel_rotate), um tween / move_to(..., time=) / move_along em voo, ou o resolve() de colisão em pé. Só a fórmula é exclusiva: um campo ligado com @= não aceita nenhum outro driver (e vice-versa), em qualquer ordem. Os outros convivem: um move_by(time=) antes ou depois do resolve() dá o mesmo resultado (para na parede), e um shape draggable pode ter um movimento em voo.
Driver
O que reivindica
Se você @= por cima
Como soltar
@= (fórmula)
o próprio campo ligado
—
.unlink() / unlink_all()
draggable / on_drag_to
partes que o move_to do arraste escreve (ex. center)
LinkedAttributeError na hora
draggable = False (ou limpar os callbacks)
wheel_rotate
partes do rotate_by (inclui angle)
LinkedAttributeError
wheel_rotate = False
tween / move_to(time=) / move_along
o alvo pelo tempo do Handle
LinkedAttributeError no @= (não no próximo frame)
handle.cancel() ou esperar o fim (com loops="forever", só cancel())
resolve(a, b)
em geral o center de cada lado
LinkedAttributeError no @= ou no próprio resolve
unresolve(a, b)
c.draggable = True
# LinkedAttributeError — o center já tem driver (draggable)
# c["center"] @= lambda: src
c.draggable = False
c["center"] @= lambda: src # agora ok
h = c.move_to(400, 200, time=2.0)
# LinkedAttributeError — "move_to(time=2) pushes it"
# c["center"] @= lambda: src
h.cancel()
c["center"] @= lambda: src
resolve(bola, "parede")
# LinkedAttributeError se bola.center já estiver ligado
unresolve(bola, "parede")
inimigo.draggable = True
inimigo.move_by(200, 0, time=2, loops="forever") # ok: convivem
# segurar o inimigo com o mouse congela a patrulha nele;
# soltar → ela continua de onde ele caiu
Regra: fórmula @= contra qualquer outro driver na mesma parte → erro duro, nas duas ordens (a mensagem diz a linha onde o objeto foi criado, ex. the Sprite made at jogo.py:151). Os outros drivers nunca se recusam entre si. Segurar com o mouse congela os movimentos (tween / move_*(time=)) nas partes que o arraste (ou a roda) escreve; ao soltar eles continuam, antes do on_drag_to — então um “voltar para casa” (move_to(casa, time=0.3)) no on_drag_to funciona. Enquanto você segura, o arraste vence também o que o seu on_update escreve no objeto (um move_by de gravidade por frame): a cada frame ele volta para baixo do cursor; ao soltar, o seu código volta a mandar. Isso não é handle.pause(): h.paused continua sendo só seu. Redeclaração do mesmo tipo (segundo tween na mesma chave, novo @= no mesmo campo) → avisa e substitui. force=True continua só nos verbos pontuais de Mover / tween — não nos flags draggable / wheel_rotate.
Não confundir
Parece
É
Não é
Ligar a partir do center arrastável (Line(p1=lambda: c.center) ou line["p1"] @= c.center) dá erro
OK — quem lê o center não disputa com o arraste; só quem escreve nele disputa
ligar para dentro de um center arrastável
radius ligado + draggable
OK — o arraste não escreve radius
bloquear todo o Circle
move_to / confine no seu on_update
chamada pontual do autor — não é driver persistente
Os objetos que aparecem na tela: formas (Shapes), imagens (Sprite), curvas (Track), texto (Text) e efeitos no mundo (SpatialFx). Cada construtor já põe o objeto na tela. Para posicionar uma frota ou uma grade: Colocar. Para mover e animar: Mover.
O que vale para todo objeto que aparece na tela — Shape, Sprite, Text e SpatialFx: esconder, estacionar, destruir, camada, grupo e .center. Fica aqui uma vez; as seções de cada tipo não repetem.
ship.visible = False
# estacionar: some, para timers e colisão
# (um som do ship continua tocando; o on_done dele espera o ship voltar)
ship.active = False
# cancela timers/tweens/mode; Entity continua viva
ship.cancel_process()
# some de vez. is_alive vira False
ship.destroy()
Construtor (todo objeto na tela)
Nome com =: passe pelo nome. Vale em Shape, Sprite e SpatialFx.
Parâmetro
Passar
Tipo (default)
Significado
layer=
nome=
str, int ou None"default"
Camada de desenho. Número = posição (cria na hora). None = fora da lista.
group=
nome=
str ou lista ou None
Tag, ou lista de tags. Ex.: group="wasp".
nave = Sprite("ship.png", layer="ui", group=["wasp", "enemy"])
# nave: a variável é como você acha este objeto depois.
# layer: desenha na camada "ui" (fora da ordem normal do mundo).
# group: entra em group["wasp"] e group["enemy"] ao mesmo tempo.
Não existe id= nem busca por nome: guarde o objeto numa variável. Para achá-lo onde a variável não chega (outra função, outra classe), dê uma tag e leia o grupo: Sprite("heroi.png", group="heroi") … group["heroi"].first.
Em todo objeto na tela
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.center
ponto(400, 300)
lê / escreve
O centro do objeto: o ponto que move_to põe no lugar e em volta do qual ele gira. Todo objeto na tela tem. Em Point, é o próprio Point (p.center is p).
visible
boolFalse
lê / escreve
Esconde o desenho. Continua na camada.
active
boolFalse
lê / escreve
Estaciona: esconde, tira colisão, pausa timers e on_update. As partes vão junto: os marcadores de show_handles() somem e não arrastam até voltar. Prefira a destruir/recriar.
is_alive
boolTrue
leitura
True até destroy(). if nave: é o mesmo teste. Independente de visible. As partes (nave.center, linha.p1) morrem junto: continuam legíveis, mas mudá-las levanta erro.
.layer
str, int ou None"default"
lê / escreve
Camada atual. Escreva para mudar (número cria na hora).
time_scale
número1
lê / escreve
Multiplicador extra neste Entity (timers, tweens, wait, on_update). Padrão 1. Ver clock.
Métodos
Método
Assinatura
Retorna
Significado
destroy()
Sem parâmetros
—
Tira o objeto do jogo: ele some e para de colidir. Cancela os timers, tweens e o mode dele, e os efeitos rápidos em curso (sem chamar on_done). Numa parte (c.center) levanta TypeError: a parte vive, estaciona e morre com o dono.
cancel_process()
Sem parâmetros
self
Cancela timers / tweens / sons / mode deste Entity; continua vivo. Não dispara on_destroy nem @exit_mode. Ver escopo.
move_to_front() / move_to_back()
Sem parâmetros
—
Ordem de desenho dentro da camada. top ainda fica acima de default. Não em SpatialFx.
tag / untag / tagged
Parâmetros: names (nomes); tagged usa name (nome)
tag/untag não retornam valor; tagged retorna True/False
tag põe o objeto em grupos, untag tira, tagged("x") diz se ele está no grupo "x". Mesmos nomes em colisão e group["wasp"]. Uma parte do shape (c.center, linha.p1, um vértice) não tem tags: tag nela levanta TypeError — marque o dono.
on_update
Parâmetros: e (objeto), dt (número)
—
fn(e, dt) a cada frame, inclusive durante await. Ver Frame.
on_destroy
Parâmetros: e (objeto)
—
Uma vez, de dentro de destroy().
setup(self)
Parâmetros: self (este objeto)
—
Método que você escreve numa subclasse; o Scena o chama logo depois do construtor. Não é o setup(canvas) do script.
clone(center=, scale=, layer=, group=)
Parâmetros: center (ponto, opcional), scale (número), layer (str), group (str ou lista)
o objeto copiado
Cópia. center= põe a cópia nesse ponto (omitido: onde está o original). Roda setup(). Sem timers / mode / callbacks de clique. Não em SpatialFx. Clone.
look_like(outro)
Parâmetros: other (outro)
—
Copia a aparência de outro objeto (cor, contorno, visual, complementos). Não em SpatialFx.
prepare_fx(*visuals)
Parâmetros: visuals (Visual)
—
Calcula antes a silhueta do Sprite para esses visuais (evita um engasgo na primeira troca). Não faz nada em Shape / Entity.
Não confundir
Parece
É
Não é
Esconder = remover
visible=False esconde e o objeto continua. destroy() remove — a referência Python continua, mas if nave: fica falso. nave.move_by depois disso levanta DestroyedError (a mensagem diz onde foi o destroy()). .center ainda lê.
objeto vivo
ship.enabled = False
AttributeError. Estacionar é ship.active = False. .enabled existe em outros lugares, com outro sentido: num widget, mostra desligado (esmaecido, sem responder); num Post, num fundo ou num efeito de som, liga e desliga aquele efeito.
o mesmo que .active
Shapes
Shapes são as formas geométricas: círculo, retângulo, linha, elipse, polígono, ponto. Aparecem assim que criadas; sem argumentos (Circle()) nascem com um tamanho médio no centro da tela. Todas aceitam color=, visual=, line_width=, layer= e group=.
Todo Shape (Circle, Rect, Line, Polygon…) aceita estes, além de layer= e group= (Todo objeto na tela):
Parâmetro
Passar
Tipo (default)
Significado
color=
nome=
cor"cyan"
Pinta o interior e um contorno num tom de contraste. Em Line, pinta o traço. Omitida: a cor padrão (set_defaults).
visual=
nome=
VisualNeon()
Já nasce com esse visual — o mesmo que c.visual = Neon() depois. Use color= ou visual=, não os dois.
line_width=
nome=
número1.5
Espessura do contorno (em Line, do traço), em unidades de design. Muda depois com .line_width.
Padrões para os próximos shapes
set_defaults(…) escolhe a aparência dos Shapes criados depois da chamada — para não repetir color= / visual= em cada um. Os já criados não mudam. Vale até a próxima chamada; num ato, a troca de ato volta ao que o topo do arquivo deixou.
O contorno: espessura, tracejado, pontas e cantos — os mesmos valores das propriedades de Neste Shape.
fill · line
nome=
cor, Gradient ou Texture
Material do interior / do contorno, como .fill / .line.
blend
nome=
str"add"
Como misturam com o que já está desenhado. Ver Blend.
fill_filter · line_filter
nome=
filtroBlur(sigma=2)
Filtro de pixels no interior / no contorno. Ver Filtros.
Neste Shape
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.color
cor"cyan"
lê / escreve
Cor sólida primária: preenchimento nos shapes de área, traço em Line. Escrever chama set_color. Lê a cor atual: com um visual, é a que o visual pinta agora, que pode não ser a que você deu — um Halo deixa o corpo translúcido, mais claro ou mais escuro (alfa incluído), então c.color = "red" seguido de c.color lê esse tom. Um visual que pinta o próprio material ou a própria paleta (Flame, Swell, Water, Metal(), Shield()) não tem uma cor só: aí .color lê a cor que você deu. Para guardar a sua cor, guarde-a numa variável ou em st.
.visual
Visual ou NoneNeon()
lê / escreve
Visual principal (um por objeto). None tira o visual; complementos ficam.
.glow / .shadow / .tail
bool ou cor ou GlowTrue
lê / escreve
Complementos. Sprite também .hue e .gray. Atalho: True / cor / número / objeto. Ler devolve o objeto de ajuste (Glow, Shadow…) ou None.
.fill / .line
cor ou GradientGradient("sunset")
lê / escreve
O material do interior (.fill) ou do contorno (.line): uma cor, um Gradient ou uma Texture. Um Gradient só aparece quando você o põe aqui. Com um visual posto, o visual passa a seguir o novo valor (um visual de contorno segue .line; um de corpo, .fill): escrever antes ou depois de pôr o visual dá o mesmo.
.blend
str"add"
lê / escreve
Como fill e line misturam com o canvas. Padrão "normal". Com um visual posto, como .line_width: um visual que fixa o seu blend (Halo soma luz) o mantém. Blend.
.line_width
número1.5
lê / escreve
Espessura do traço. Padrão 1.5 (Shape). Também keyword do construtor nas formas que traçam. Point e Anchor recusam. Continua gravável depois. Com um visual posto, dá o mesmo que escrever antes de pôr o visual: um visual que usa a espessura (Neon, Laser…) segue o novo valor; um que fixa a sua (Rim, Halo, Shield…) a mantém, e o valor aparece com visual = None. Text tem o seu.
.dash
str"dash"
lê / escreve
Padrão do traço. Padrão "solid". "solid" · "dash" · "dot" · "dot-dash". Com um visual posto, como .line_width.
.line_cap
str"round"
lê / escreve
Extremidade do traço. Padrão "round". "round" · "square" · "project".
.line_join
str"miter"
lê / escreve
Canto do traço. Padrão "miter". "miter" · "bevel" · "round".
.is_collapsed
boolFalse
leitura
Só leitura. True se a extensão não tem interior (eixo 0 / radius=0 / length=0). Não é fill nem visible. Silencioso — sem aviso. Point / Track / polígono livre: sempre False.
Métodos
Método
Assinatura
Retorna
Significado
set_color(…)
Parâmetros: cores (nome, Color ou RGB; posicionais)
—
Muda a cor do objeto: interior, contorno e o visual — o mesmo que color= no construtor, com ou sem visual (o contorno é derivado da cor, e um visual de contorno segue esse contorno). Todo visual que aceita color= mostra a nova cor: um visual criado com cor (Neon("red"), Laser("blue"), Rim("gold")…) passa a seguir o objeto, Metal() vira metal da nova cor. Um visual sem color= (Flame, Water, Shield(style="nebula")…) guarda a cor e avisa: ela aparece quando o visual sai. Mesmas formas de cor.
show_handles() / no_handles() / show_points()
Sem parâmetros
—
show_handles() mostra marcadores arrastáveis nos pontos que definem o shape (centro, pontas, vértices), para você ajustar com o mouse; no_handles() esconde. show_points() mostra todos os pontos, só para ver. Os marcadores somem com o shape no destroy().
clone(center=, scale=, layer=, group=)
Parâmetros: center (ponto, opcional), scale (número), layer (str), group (str ou lista)
o objeto copiado
Cópia livre (a geometria é copiada agora, sem ligações). center= move a cópia para esse ponto (o centro geométrico cai lá, como em move_to); uma função levanta TypeError (ligue depois: copia.center @= …). Não herda timers nem mode. frame= é só do Sprite.
look_like(outro)
Parâmetros: other (outro)
—
Copia a aparência de outro objeto (cor, contorno, visual, complementos).
save_visual() / restore_visual()
Sem parâmetros
—
Lembra a aparência atual (inclui complementos) e volta depois (guarda uma só aparência). Segundo save_visual não faz nada até restore_visual. Diferente de .visual = None (tira o visual; complementos ficam).
Modelo de construção incompleto (Circle(p1=…) sem p2) levanta ValueError. Não vira o padrão.
Um círculo preenchido — ou uma fatia dele, com angle= / spread=. Circle() já aparece no centro. Também Circle.from_points, Circle.from_corners e tangent_lines_from.
c = Circle(center=(400, 300), radius=50, color="cyan")
c = Circle(center=(400, 300), radius=50)
Circle(p1=(100, 200), p2=(300, 200)) # diâmetro
p = c.point_at_angle(45) # Point efêmero na borda
dots = Track(c).spaced_points(8) # 8 tuplas (x, y) no contorno
c = Circle(center=(400, 300), radius=50, color="cyan")
c.set_color("tomato")
c.visual = Neon()
Nome com =: passe pelo nome. (Circle não tem argumento só na ordem.)
Parâmetro
Passar
Tipo (default)
Significado
center
nome=
pontocentro do design
Ponto (x, y). Ex.: center=(400, 300).
radius
nome=
númerotamanho médio
Raio em unidades de design. Ex.: radius=50.
box
nome=
Boxcell
Uma região (Box): o círculo fica dentro dela, com raio = metade do lado menor. Com radius=, só centra nela. Ver box=.
color
nome=
corpadrão do jogo
Preenchimento + traço de contraste. Ex.: color="cyan".
layer
nome=
str ou None"default"
Camada de desenho
group
nome=
str ou lista ou None
Tag, ou lista de tags
Modelos de construção
Chamada
Significado
Circle()
Centro do design, tamanho médio
center=, radius=
O caminho usual
center=, perimeter_point=
Raio até esse ponto
p1=, p2=
Diâmetro
angle=, spread=
Arco: direção e abertura, em graus; igual ao query_sector. spread=360 é o círculo todo. Qualquer um dos dois, sozinho, vale. Com perimeter_point= ou p1=, p2=, angle= gira a borda até essa direção.
use_center
Padrão True: pizza. False: segmento (corda + arco). O clique e a colisão seguem essa área; spread=360 continua o disco.
Arco, em graus. .angle é a direção (0 = +x). .spread é a abertura inteira (60 = ±30). Num círculo posto pela borda (perimeter_point= ou p1=, p2=), .angle é a direção até .p1: o arco abre para esse ponto, e escrever .angle gira a borda em volta do centro.
.p1.p2
ponto(0, 0)
lê / escreve
Pontas do diâmetro. .p1 é o .perimeter_point e fica na direção .angle (no modelo center=, radius=): gira com rotate_by e espelha com o flip. circle.box.right_center é circle.p1.
.visual
Visual ou NoneNeon()
lê / escreve
Visual principal. Neon(), Halo(), … (não Glow — use .glow).
Parâmetros: cores (nome, Color ou RGB; posicionais)
—
Recolorir. Recolore também um visual criado com cor (Neon("red"), Laser("blue")…: passa a seguir o objeto) e um Metal / Shield() (com ou sem cor).
point_at_angle(angle)
Parâmetros: angle (ângulo, graus)
Point
Ponto na circunferência. 0 = +x, anti-horário. Resultado efêmero (não entra no jogo). Para N pontos espaçados: Track(c).spaced_points(n).
Não confundir
Parece
É
Não é
Circle com imagem
Imagem não entra no Circle. Use Sprite, ou c.fill = Texture("…").
—
Arco do Circle é só desenho
Clique e colisão seguem a fatia pintada (pizza ou segmento). spread=360 é o disco. intersections continua a circunferência. Parede curva: Track.arc.
—
Um retângulo, que pode girar (angle=) e ter cantos arredondados (corner_radius=). Põe-se pelo centro, por um canto ou por dois cantos opostos. Rect() aparece no centro, em tamanho médio. rect.corners devolve os quatro cantos.
Uma região (Box): o retângulo ocupa a região inteira (lugar, tamanho e ângulo dela). Ver box=.
angle
nome=
número0
Graus, anti-horário
corner_radius
nome=
número0
Cantos arredondados (unidades de design), só um número. O clique segue o preenchimento; a colisão continua o retângulo.
color
nome=
corpadrão do jogo
Preenchimento
Modelos de construção
Chamada
Significado
Rect()
Centro do design, tamanho médio
center=, width=, height=
O caminho usual
top_left=, width=, height=
Canto + tamanho
p1=, p2=
Cantos opostos, exatamente onde você os pôs (valor ou lambda). angle= inclina a caixa entre eles — a largura e a altura seguem; não gira um retângulo de tamanho fixo. Os nomes dos cantos (bottom_left…) são dados na primeira vez que você lê um canto; por isso, ligue p1 / p2 com @= antes de ler os cantos.
p1=, vec=
Canto preso + extensão local (largura, altura), com sinal, ao longo dos lados já girados. Não é um delta de mundo (isso é Line(p1=, vec=))
Cantos arredondados, só um número (não liga). O clique segue o preenchimento; a colisão continua o retângulo. Padrão 0.
.top_left.top_right.bottom_left.bottom_right
ponto(80, 200)
lê / escreve
Cantos, no quadro da caixa. Ganham nome no primeiro uso (como aparecem então); depois cada nome segue o seu ponto — ao arrastar, ligar com @=, girar ou espelhar. As bordas da tela: rect.box.top / .right.
Meios das arestas (anchor="cap" usa left_center/right_center)
.left_square_center.right_square_center
ponto(120, 300)
leitura
Centros dos quadrados de lado height nas pontas (anchor="square")
.box
Boxpanel.box
leitura
Uma cópia da moldura, tirada agora, com os nomes: rect.box.top_right é rect.top_right. Layout: panel.box.inset(8).grid(8, 8) — Colocar.
Não confundir
Parece
É
Não é
corner_radius muda a colisão
O clique segue o canto arredondado. A colisão continua o retângulo. Os cantos ligados continuam vivos. corner_radius é só um número — uma função levanta erro.
—
angle= em p1=, p2= gira a caixa
angle= inclina a caixa com os cantos presos e muda o tamanho. Para girar um retângulo de tamanho fixo, use center= + angle=, ou rotate_by.
—
.top_right é sempre o canto de cima à direita
É o canto que estava lá no primeiro uso, e o nome segue esse ponto. Arraste p1 para além de p2 e top_right continua sendo p2, mesmo embaixo à esquerda — quem liga top_right nunca pula de ponto. O .box guarda os mesmos nomes. Para a tela, use as bordas: rect.box.top / .right.
Nome recalculado a cada frame
flip_x num Rect gira a caixa
Espelha: cada ponto nomeado vai para o seu espelho em volta do center (top_left fica onde estava top_right). A caixa não sai do lugar.
Meia volta
Escrever um ponto derivado. Os meios das arestas (.top_center, .left_center…) e os centros dos quadrados das pontas são só leitura: para mudá-los, mude width / height / center. Para levar o retângulo até que um deles fique num lugar: r.move_to(1, 2, about=r.top_center).
p1=, p2=, height=, anchor=. Orientação vem de p1→p2; height é a espessura (≥ 0); .width fica o comprimento ao longo do eixo. anchor é obrigatório: "cap" (meios das paredes curtas = .left_center/.right_center), "square" (centros dos quadrados de lado height = .left_square_center/.right_square_center), "side_left" / "side_right" (cantos de uma aresta longa; a caixa fica à esquerda ou à direita de p1→p2). Esses pontos derivados existem em qualquer modelo, não só no modelo p1=, p2=, height=, anchor=.
Um quadrado: um Rect de lados iguais. O tamanho é length=; width / height só leem esse valor.
Põe o quadrado pelo canto de cima à esquerda, no lugar de center=.
box
nome=
Boxcell
Inscreve (lado menor). Com length=: só centra. box=.
angle
nome=
número0
Graus
corner_radius
nome=
número0
Cantos arredondados. O clique segue o preenchimento; a colisão continua o quadrado. Herdado do Rect.
Neste Square
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.length
número80
lê / escreve
Lado. Escreva aqui.
.width / .height
número120
leitura
Espelhos. Escrever levanta erro.
.corner_radius
número8
lê / escreve
Cantos arredondados. O clique segue o preenchimento; a colisão continua o quadrado.
Não atribua width / height num Square.
Um segmento de reta entre duas pontas (p1, p2) — ou definido por centro, comprimento e rumo. Só tem traço: color= / set_color pintam a linha, e line_width é a espessura.
Largura e altura totais (os eixos inteiros, não a metade), antes do giro.
box
nome=
Boxcell
Preenche a região uma vez. Não é from_rect= (ligação ao vivo). box=.
from_rect / padding
nome=
Rect / número— / 0
A elipse fica inscrita no Rect e o acompanha ao vivo (mover ou girar o Rect move a elipse). padding= encolhe essa caixa em cada lado: um Rect 100 × 50 com padding=8 dá uma elipse 84 × 34.
angle
nome=
número0
Graus
Modelos de construção
Chamada
Significado
Ellipse()
Centro do design, tamanho médio
center=, width=, height=
O caminho usual
fp1=, fp2=, major_axis=
Focos + eixo maior. Os três aceitam lambda. Um número pequeno demais levanta erro; uma fórmula que encolhe o eixo cola a elipse no segmento entre os focos
from_rect= liga a pose ao Rect. Mexer no Rect move a elipse.
—
Um polígono com os vértices que você der em points= (o número de vértices não muda depois). Sem pontos: um quadrilátero médio no centro. signed_area e perimeter_length leem a área e o perímetro.
Espelha no lugar: cada points[k] vai para o espelho do antigo points[k], e a lista passa a correr no sentido horário (signed_area fica negativo, como num Polygon livre espelhado). Quem liga hexagono.points[2] não pula de vértice.
Renumeração
n= não muda depois. Triângulo: EquilateralTriangle.
Um triângulo de lados iguais. angle é para onde a ponta (o ápice) aponta, como em todo rumo: 0 para a direita, 90 em pé. Bom para naves e setas: o giro aparece.
Para onde o ápice aponta: 0 → direita, 90 → em pé. face=True voa de ápice para a frente.
Não passe n=. É sempre 3.
Um Point é uma posição (x, y) que vive no jogo: pode ser ligada, arrastada e usada por outros objetos (a ponta de uma Line, o centro de um Circle). Invisível por padrão; color= o mostra como marcador e handle=True o torna arrastável (os dois vão para a camada top).
p = Point(200, 150)
p = Point(200, 150, color="cyan") # guia visível (não arrastável)
p = Point(canvas.game_view.center) # cópia do centro da vista
p.visible = True
p.handle = True
# Point visível + arrastável no top
a = Anchor(400, 300)
b = Anchor(500, 300, color="lime")
Construtor
Point(x, y, color=, visible=, layer=, group=) · Point(ponto, …) · Point(lambda: …) · Anchor(x, y, color=, group=)
Nome sem =: 1º, 2º na ordem; ou um único ponto / lambda. Sem visual= (marcador, não visual).
Parâmetro
Passar
Tipo (default)
Significado
x
1º
número0
Posição. Só x → y=0.
y
2º
número0
Posição
ponto
1º (só)
pontobox.center
Cópia: Point, Vec2, (x, y), Graphic (centro), mouse. Callable → ligação do Point inteiro (como center=).
color
nome=
cor—
Pinta o marcador e mostra como guia (visível, sem arrastar). Com color=, o marcador usa essa cor em vez da automática. Com visible=False: só pinta, fica escondido.
visible
nome=
boolFalse
Mostra / esconde. True mostra o Point (na camada "top" se não houver layer=). Vale mais que o efeito de color=.
layer
nome=
str ou NoneNone
Camada. Omitido = fora da lista de desenho até mostrar.
group
nome=
str ou lista ou None
Tag, ou lista de tags
Neste Point
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.x.y
número400
lê / escreve
Coordenadas
x, y = p
dois númerosx, y = ship.center
leitura
Desempacota as coordenadas de agora (como tuple(p)): uma cópia, os nomes não acompanham o ponto. Vale para Vec2 e mouse. p[0] levanta TypeError: use p.x. Passar o próprio Point continua igual (cópia em center=, ligação em @= / lambda).
.center
pontop
leitura
O próprio Point (p.center is p): existe para todo objeto ter .center, como Circle e Sprite. Para mover, escreva .x / .y ou use move_to.
.visible
boolFalse
lê / escreve
Guia na tela. Sem handle.
.handle
boolTrue
lê / escreve
True: o Point aparece e pode ser arrastado com o mouse. Num Point ligado com @=, levanta LinkedAttributeError.
Métodos
Método
Assinatura
Retorna
Significado
.distance(other)
Parâmetros: other (ponto)
número
Distância euclidiana. Também distance(p1, p2).
.heading(other)
Parâmetros: other (ponto)
número
Rumo até other, em graus (0 = +x, −180…180). Também heading(p1, p2).
set_color(…)
Parâmetros: cores (nome, Color ou RGB; posicionais)
self
Recolorir o marcador (só o interior). Depois disso, ele não usa mais a cor automática de guia / marcador arrastável.
Point.temp(x, y)
Parâmetros: x (número), y (número)
Point
Efêmero. Não entra no jogo.
== num Point compara o objeto, não a posição.elevador.center == (400, 120) é sempre False, mesmo com o elevador lá; o mesmo vale para um Vec2 e para outro Point no mesmo lugar. O Scena avisa no console (um aviso do Python apontando a sua linha). Para testar a posição: distance(elevador.center, (400, 120)) == 0 (ou < 1 se o movimento não chega exato). Para saber se é o mesmo objeto: a is b.
Anchor
Atalho: Point visível e arrastável no top, sempre: a.draggable = False (ou handle = False) levanta TypeError — para um ponto que o mouse não move, use Point(x, y, visible=True). Sem ligação de entrada: a["campo"] @= … levanta LinkedAttributeError. Aceita os mesmos pontos de Point(ponto) (cópia). color= pinta o marcador (omitido: vermelho automático).
a = Anchor(400, 300)
a = Anchor(400, 300, color="cyan")
a = Anchor(canvas.game_view.center)
b = Anchor(600, 300)
Line(p1=lambda: a.center, p2=lambda: b.center) # segue o arraste
Ligar a um Anchor pede lambda.Line(p1=a.center, …) (ou p1=a) copia a posição uma vez: a linha fica onde nasceu quando o Anchor é arrastado. Para acompanhar: p1=lambda: a.center (Fórmulas nos construtores).
Parâmetro
Passar
Tipo (default)
Significado
x, y / ponto
posição
número ou ponto
Igual a Point (cópia; sem fórmula).
color
nome=
corvermelho (auto)
Cor do marcador. Omitido: a cor automática dos marcadores arrastáveis.
group
nome=
str ou lista
Tag(s)
Não confundir
Parece
É
Não é
Point(canvas.game_view.center) acompanha a vista
Copia a posição uma vez (game_view é um Box novo a cada leitura). Para acompanhar: Point(lambda: canvas.game_view.center) ou @=. Um único lambda liga o Point inteiro; para um eixo só: Point(lambda: mouse.x, 0).
ligação
Point(…, color=…) pode ser arrastado
color= só mostra o marcador. Para arrastar: p.handle = True ou Anchor(…). Todos os pontos de um shape: shape.show_handles().
handle
p.handle()
handle é propriedade: p.handle = True.
método
x, y = ship.center acompanha a nave
Dois números de agora (uma cópia). Para acompanhar, leia ship.center.x onde precisar ou ligue com @= / lambda.
ligação
Sprite
Um Sprite é uma imagem na tela — um PNG parado ou uma animação. O Sprite é o objeto (posição, tamanho, giro, colisão); o Sheet é o catálogo de quadros (frames) e animações (clips) de onde ele tira a imagem, e sozinho não aparece. Um PNG parado não precisa de Sheet. Aqui, “frame” também é cada quadro da animação (frame= escolhe um); “a cada frame” continua sendo cada volta do jogo.
# Um PNG parado: sem Sheet.
pocao = Sprite("pocao.png", height=48, center=(200, 300))
# Um PNG em grade com uma animação: fps= no Sheet registra o clip, e o Sprite já toca.
caracol = Sheet("caracol.png", cols=8, fps=6)
Sprite(caracol, height=36, center=(400, 300))
# Vários clips: o Sheet nomeia, o Sprite escolhe com play.
folha = Sheet("heroi.png", cols=4, rows=3)
folha.clip("andar", [3, 4, 5, 6], fps=12)
folha.clip("pular", [7, 8, 9], fps=10)
heroi = Sprite(folha, height=64, center=(600, 300))
heroi.play("andar")
# Um PNG por frame: * no lugar da parte que muda pega todos,
# em ordem (Idle__000.png, Idle__001.png, …).
ninja = Sheet(anchor="bottom", pixel_size=0.16)
ninja.strip("parado", "ninja/Idle__*.png", fps=8)
ninja.strip("correndo", "ninja/Run__*.png", fps=12)
lutador = Sprite(ninja, center=(800, 300))
lutador.play("correndo")
Tipo
Papel
Entra no jogo?
Sheet
Catálogo: os pixels, como cortar (grade, atlas, vários arquivos) e os clips nomeados (clip, strip).
Não. Vai em Sprite(sheet). Um Sheet serve a muitos Sprites.
Sprite
O objeto na tela: posição, tamanho, qual frame / clip toca (play), orientação e colisão (hitbox).
Sim, no construtor. Arquivo: busca. Sumido: checker + aviso, e o Sprite continua.
Nome sem =: 1º na ordem: o arquivo de uma imagem parada (PNG) ou um Sheet. Nome com =: pelo nome. Catálogo (cols= / atlas= / fps= / name= / yoyo=) fica no Sheet — no Sprite é TypeError.
Parâmetro
Passar
Tipo (default)
Significado
source
1º
str ou Sheet
Um PNG (imagem parada) ou um Sheet. Busca: Arquivos de imagem. Ex.: Sprite("ship.png"). Vários arquivos: Sheet("a.png", "b.png") depois Sprite(sheet). Um padrão com * é TypeError (são vários frames): Sprite(Sheet("robot/Run (*).png", fps=12)).
height / width / size
nome=
númerotamanho do PNG
Unidades de design, não pixels. size = lado maior. Mede o frame do construtor (frame=; sem ele, o primeiro frame do primeiro clip) e fixa a escala do Sprite. Sem nenhum deles, vale o pixel_size do Sheet (se houver). Ver Tamanhos diferentes.
center
nome=
pontocentro do design
(x, y). É o pivô do frame (com Sheet(anchor="bottom"), os pés).
box
nome=
Boxcell
Encaixa (aspecto do PNG) ou só centra se vier height= / width= / size=. Não misture com center=. box=.
frame
nome=
número inteiro ou str0
A célula mostrada parada: índice ou nome no atlas. Com um clip no Sheet, frame= explícito vence o autoplay.
offset
nome=
par de números(0, 0)
Desloca desenho e colisor em relação ao center, em unidades de design. Gira com o Sprite e espelha com o flip. Ex.: offset=(10, 0). Por clip, em pixels da arte: clip(offset=).
A hitbox principal: qual forma, de qual arte, que ponto fica parado, quanto encolhe. Depois: sprite.hitbox.shape / .source / .anchor / .scale. Ver Hitbox do Sprite.
Célula atual. Índice ou nome no atlas. Escrever um valor para a animação e mostra essa célula.
sheet
Sheet
leitura
Catálogo deste Sprite. Mutação (clip / strip) é no Sheet, antes de colocar a frota — não a partir do catálogo compartilhado de Sprite("x.png").
width / height / size
número64
lê / escreve
Tamanho do frame atual, em unidades de design. play / frame= mudam a leitura quando o frame novo tem outro tamanho. Escrever reescala todos os clips.
offset
par(10, 0)
lê / escreve
Deslocamento do desenho e do colisor em relação ao center, em unidades de design.
.box
Boxnave.box
leitura
A célula desenhada inteira (com a margem transparente do PNG), onde ela é desenhada: pivô, offset, flip e scale contam. Com os nomes: depois de um flip_x, box.top_center continua sobre a cabeça. Acima na tela: box.top. Medir.
.art_box
Boxnave.art_box
leitura
Só os pixels visíveis do frame na tela (sem a margem). Anda com a animação. Medir.
.is_collapsed
boolFalse
leitura
True se a moldura desenhada não tem interior (w ou h zero, inclusive com scale). O construtor rejeita tamanho ≤ 0; colapsar depois é legal e silencioso.
Propriedades — orientação
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
flip_x / flip_y
boolTrue
lê / escreve
Espelha o desenho e o colisor. Reescreve .angle. Espelhar.
art_angle
número90
lê / escreve
Para onde o nariz do PNG aponta, em graus (0 = +x, 90 = +y), antes do flip. Apontar.
rest_heading
número90
leitura
Rumo em que o PNG aparece como desenhado: art_angle depois do flip. reset_rotation() volta aqui.
tilt
número · contato · "ground""ground"
lê / escreve
Inclina só o desenho, em volta do hitbox.anchor. Não gira o corpo. Inclinar.
Propriedades — colisão
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
hitbox
Hitboxhero.hitbox
leitura
A hitbox principal (pisa, bate, resolve): .shape / .source / .anchor / .scale (escrever em jogo vale), .active, .box. Hitbox.
hitboxes
tupla de Hitboxhero.hitboxes
leitura
Todas: a principal primeiro, depois as extras de add_hitbox.
Propriedades — aparência
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.visual
Visual ou NoneHalo()
lê / escreve
Visual principal. None tira o visual; complementos ficam.
.alpha
número0.5
lê / escreve
Opacidade do corpo, 0–1. Vale só para a imagem: visuais que envolvem o Sprite (Shield, Electric…) continuam com a opacidade deles.
.blend
str"add"
lê / escreve
Como a imagem se mistura com o que já está desenhado. Mesmos nomes que Shape. Padrão "normal". Blend.
.glow / .shadow / .hue / .gray / .tail
bool ou cor ou GlowTrue
lê / escreve
Complementos. .gray tira a cor (0–1). Ver Complementos.
Parâmetros: group (str ou lista), shape (str), source (padrão "each"), anchor, scale, active (True/False), px_rect (x, y, w, h), px_circle (x, y, r)
Hitbox
Hitbox extra, só detecção. px_rect / px_circle: forma à mão, no lugar dos quatro parâmetros da arte. Partes, Caixas de ataque.
showing(frames)
Parâmetros: frames (nome de clip, índice, nome de frame, ou tupla / lista)
bool
O frame na tela é um destes? Vai pelo frame (acerta com yoyo, next=, golpe interrompido). Caixas de ataque.
remove_hitbox(part)
Parâmetros: part (Hitbox)
self
Tira a hitbox extra. O Sprite sai dos grupos em que ela o pôs; um grupo em que ele já estava por conta própria fica. A principal não sai: sprite.hitbox.active = False.
reset_rotation()
Opcional: time, ease, then, on_done
—
Gira até rest_heading e zera o tilt. Não limpa o flip.
clone(center=, layer=, group=, frame=, scale=)
Parâmetros: center (ponto), layer (str), group (str ou lista), frame (número), scale (número)
Nome registrado com sheet.clip / strip / fps= no Sheet. Ex.: sprite.play("walk") ou sprite.play(clip="walk").
loops=
nome=
número inteiro ou None
loops=1 toca uma vez e segura o último frame (ou o primeiro quando yoyo=True), salvo next=. Omitido: para sempre.
next=
nome=
str ou None
Clip a seguir após loops. Não é then=. Ex.: next="idle".
min_hold=
nome=
número0
Segundos. Enquanto o clip atual não tiver rodado esse tempo, pedidos de outro clip são ignorados.
resume=
nome=
boolFalse
Ao voltar a este clip, continua a fase lembrada em vez de reiniciar no frame 0. Deixe off em hit/hurt que devem recomeçar sempre.
Sheet
Um Sheet guarda os quadros de uma ou mais imagens e as animações (clips) feitas com eles. Ele não aparece na tela: entregue-o a um Sprite (Sprite(sheet)). Um Sheet serve a muitos Sprites.
PNG, padrão de arquivos ("robot/Run (*).png", Padrão) ou lista de PNGs (2 Arquivos), ou omitido com Sheet() (alternativa do 5 Strip). Só atlas: um XML/JSON sozinho resolve o PNG listado. Busca: Arquivos de imagem. Ex.: Sheet("hero.png", cols=4, rows=3).
arquivos extra
2º…
str
Atalho: Sheet("a.png", "b.png") = Sheet(["a.png", "b.png"]). Um padrão aqui ou na lista vira os arquivos dele, no lugar. Incompatível com cols= / rows= / cell= / atlas= (use strip). Lista em source= + extras: TypeError.
cols / rows
nome=
número inteiro ou None
Grade regular. Ex.: cols=4. Juntos: cols=4, rows=3. rows= sem cols=: TypeError (faixa vertical é cols=1).
cell
nome=
par de números ou None
Tamanho da célula em pixels. Ex.: cell=(32, 32). Não misture com cols= / atlas=.
atlas
nome=
str ou None
JSON/XML, com o PNG em source. Ex.: atlas="sheet.json".
fps
nome=
número ou None
Registra um clip sobre todas as células já carregadas. Sem name=: "default". Grade e lista. Atlas empacotado: TypeError.
name
nome=
str ou None
Nome desse clip. Exige fps=. No 5 Strip, name="idle" no primeiro arquivo casa com os strip seguintes.
yoyo
nome=
boolFalse
Vai-e-volta no clip do construtor. Exige fps=.
anchor
nome=
str ou par"center"
Ponto de cada PNG que fica no center do Sprite: "center", "bottom", "top", "left", "right" ou (ax, ay). Pivô escrito no atlas vale mais. Tamanhos diferentes.
pixel_size
nome=
número > 0 ou NoneNone
Tamanho de um pixel do PNG, em unidades de design: pixel_size=3 → cada pixel vale 3 (pixel art em 3×); 0.5 → dois pixels por unidade (PNG grande). Sprites deste Sheet sem height= / width= / size= / box= desenham no tamanho do PNG × pixel_size — todos na mesma escala, sem medir cada um. Tamanho explícito no Sprite vale mais. Ex.: Sheet("tiles.png", cell=(16, 16), pixel_size=3).
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
nframes
número inteiro8
leitura
Quantas células o catálogo tem agora.
names
tupla de str
leitura
Nomes das células (atlas / vários arquivos). Grade regular costuma usar índices.
anchor
str ou par"center"
leitura
O anchor= do construtor.
pixel_size
número ou None3.0
leitura
O pixel_size= do construtor (None se não foi dado).
Métodos
Método
Assinatura
Retorna
Significado
clip(name, frames, fps=, yoyo=, offset=)
Parâmetros: name (str), frames (lista ou omitido), fps (número, padrão 8), yoyo (bool), offset (par de números)
o Sheet
Nomeia uma sequência das células já no catálogo (índices ou nomes do atlas). Sem frames: todas as células atuais. offset=(dx, dy): Tamanhos diferentes. Ex.: hero.clip("walk", [3, 4, 5, 6], fps=12).
Parâmetros: name (str), source (str, padrão com * ou lista), cols / rows / cell, fps (número), yoyo (bool), offset (par de números)
o Sheet
Acrescenta mais frames e registra o clip (5 Strip). source: um arquivo de faixa (grade com cols=, ou um frame só), um padrão ("ninja/Run__*.png": um arquivo por frame, em ordem — Padrão de arquivos) ou uma lista de arquivos. cols= / rows= / cell= com padrão ou lista: TypeError. Nome primeiro, como clip. Chame no setup, não a cada frame. Ex.: hero.strip("walk", "walk.png", cols=8, fps=12), ninja.strip("correndo", "ninja/Run__*.png", fps=12).
frame_index(key)
Parâmetros: key (número ou str)
número inteiro
Resolve índice ou nome de célula.
Não confundir
Parece
É
Não é
Sprite("hero.png", cols=4, fps=8)
TypeError apontando o Sheet. cols= / rows= / cell= / atlas= / fps= / name= / yoyo= são do catálogo.
atalho no Sprite
Sprite("x.png").sheet.clip(…)
Recusa (TypeError). Sprite("x.png") compartilha um catálogo único entre todos os Sprites daquele arquivo. Para clips, monte um Sheet antes.
cópia sob demanda
play(…, fps=)
Não existe. O ritmo vive no clip — outro ritmo é outro clip.
—
Sprite · Como fazer
Carregar sprites
Seis formas, conforme os arquivos que você tem. Em todas, o Sheet monta o catálogo e o Sprite coloca.
Todos os Sprite("shot.png") compartilham um catálogo (decodifica e calcula o hitbox uma vez). Por isso esse catálogo recusa clip / strip. Um Sheet("x.png") sem layout é um catálogo de uma célula só seu, que aceita clips.
Vários arquivos, cada um é um frame. Um padrão com * pega todos de uma vez; Sheet([...]) (ou vários posicionais) lista um por um; clip nomeia sequências; play no Sprite.
robo = Sheet("robot/Run (*).png", fps=12) # Run (1).png, Run (2).png, … em ordem
Sprite(robo, height=56, center=(x, y)) # um clip → começa a tocar
hero = Sheet(["Idle_0.png", "Idle_1.png", "Run_0.png", "Run_1.png"])
# mesmo: Sheet("Idle_0.png", "Idle_1.png", "Run_0.png", "Run_1.png")
hero.clip("idle", [0, 1], fps=6, yoyo=True)
hero.clip("run", [2, 3], fps=12)
s = Sprite(hero, height=48, center=(x, y))
s.play("run")
"robot/Run (*).png" carrega todos os arquivos daquela pasta que casam, um por frame, em ordem. Regras: Padrão de arquivos.
Padrão, lista ou posicionais extra = um arquivo por frame. Nesta forma, cols= / atlas= é TypeError — isso é o 5 Strip. fps= no construtor registra um clip sobre todos os arquivos. Os frames também têm nome (o do arquivo, sem a extensão): hero.clip("idle", ["Idle_0.png", "Idle_1.png"]).
PNGs de tamanhos diferentes por animação (um deslize mais baixo, uma queda mais larga) são comuns em pacotes recortados. Ver Tamanhos diferentes.
Um PNG + atlas empacotado (Kenney XML ou JSON). Clips pelos nomes dos frames. Forma ensinada: imagem + atlas=.
Conveniência: Sheet("space_sheet.xml") resolve o PNG listado no XML/JSON. Prefira nomear os dois. Se source= e o atlas listam PNGs diferentes, vale o source do autor (aviso uma vez). Se o source sumiu, aviso e cai no PNG listado (ou placeholder). fps= / name= / yoyo= no construtor de um atlas empacotado é TypeError — não toca todas as células. O atlas também pode trazer pivôs (Tamanhos diferentes) e caixas de colisão (Caixas do atlas).
Um arquivo em grade cols= × rows= (ou cell= em pixels). Vários clips, ou um loop com fps= no Sheet.
run = Sheet("hero.png", cols=4, rows=3)
run.clip("walk", [3, 4, 5, 6], fps=12)
run.clip("jump", [7, 8, 9], fps=10)
Sprite(run, center=(200, 400)).play("walk")
snail = Sheet("snail.png", cols=8, fps=6) # clip "default" em todas as células
Sprite(snail, height=36, center=(x, y)) # um clip → começa a tocar
Layout: cols= sozinho (uma linha), cols= com rows=, ou cell=(w, h). A imagem precisa dividir de forma exata. Faixa vertical: cols=1, rows=8. cols= + cell= é TypeError.
Cada strip carrega uma animação e já dá o nome do clip: um arquivo de faixa, ou um padrão com os PNGs soltos daquela animação.
name= no primeiro arquivo dá o nome do primeiro clip. Faixas são grade ou um frame só (atlas empacotado fica no Sheet(..., atlas=)). Sprite num Sheet ainda vazio é ValueError.
Pacote de PNGs soltos (um arquivo por frame, um prefixo por animação): o Sheet começa vazio e cada strip recebe um padrão. Uma linha por animação, cada nome escrito uma vez.
Regras do *: Padrão de arquivos. "ninja/Jump__*.png" não pega Jump_Throw__000.png: o resto do nome vale letra por letra. Uma lista também serve: ninja.strip("voo", ["a.png", "b.png"], fps=8). Com padrão ou lista, cols= / rows= / cell= é TypeError (não há imagem para cortar).
Um PNG + atlas; cada Sprite trava num frame nomeado. Sem clip, sem play.
frame= (índice ou nome) escolhe a célula. Padrão 0. Um Sheet para toda a frota: os pixels e o atlas são lidos uma vez.
Defina os clips no Sheet antes (ou enquanto) cria os Sprites. Um Sheet compartilhado por uma frota é o mesmo objeto: um clip novo aparece para todos.
Padrão de arquivos: *
Pacotes de arte costumam trazer um PNG por frame: Run__000.png, Run__001.png… Em vez de listar um por um, escreva o nome com * no lugar da parte que muda. O * quer dizer “qualquer coisa aqui”: o Scena carrega todos os arquivos daquela pasta cujo nome casa, um frame por arquivo, em ordem.
# Sheet: uma animação só; fps= registra o clip.
# Run (1).png, Run (2).png, … Run (8).png
robo = Sheet("robot/Run (*).png", fps=12)
Sprite(robo, height=56, center=(200, 300))
# strip: uma animação por linha, o nome do clip primeiro.
# "parado" = Idle__000.png … Idle__009.png
ninja = Sheet(anchor="bottom", pixel_size=0.16)
ninja.strip("parado", "ninja/Idle__*.png", fps=8)
ninja.strip("correndo", "ninja/Run__*.png", fps=12)
ninja.strip("pulando", "ninja/Jump__*.png", fps=10)
heroi = Sprite(ninja, center=(500, 300))
heroi.play("correndo")
# Entre outros arquivos: o padrão vira os arquivos dele.
mix = Sheet("ninja/Idle__*.png", "ninja/Dead__009.png")
Só o * é especial.
Ele vale qualquer trecho do nome (até nenhum). O resto vale letra por letra: "ninja/Jump__*.png" não pega Jump_Throw__000.png. Maiúsculas contam: "ninja/idle__*.png" não acha Idle__000.png.
Ordem natural.
Os números do nome contam como números: Run (2) vem antes de Run (10), com ou sem zeros na frente (Run__002).
Uma pasta só.
O * vai no nome do arquivo, não na pasta ("*/Run.png" é ValueError). A busca é a de Arquivos de imagem, e vale a primeira pasta que tiver algum arquivo que casa — nunca mistura pastas.
Nada casou? O jogo segue.
Um quadriculado no lugar e um aviso com o padrão e as pastas onde procurou (e os nomes que só diferem em maiúsculas). O clip existe e toca.
Um arquivo por frame.
Não há uma imagem para cortar: cols= / rows= / cell= / atlas= com padrão é TypeError, mesmo que só um arquivo case.
Onde vale: onde vários arquivos valem — no Sheet (como source, numa lista ou entre os arquivos extra) e no strip. Sprite("ninja/Run__*.png") é TypeError: um Sprite com nome de arquivo é uma imagem parada. Escreva Sprite(Sheet("ninja/Run__*.png", fps=12)). Os frames ganham o nome do arquivo sem a extensão ("Run__003"), para usar em clip, frame= e showing. Dois arquivos com o mesmo nome em pastas diferentes (idle/1.png e run/1.png) não deixam o nome "1" valer para nenhum dos dois (ValueError citando os dois); use o caminho ("idle/1.png") ou o índice.
Sem padrão, a mesma coisa em lista: ninja.strip("correndo", ["ninja/Run__000.png", "ninja/Run__001.png"], fps=12). Serve para escolher só alguns frames ou mudar a ordem.
Sprite · Como fazer
Animar sprites
O clip mora no Sheet (quais frames, fps, yoyo); o Sprite escolhe com play. Animação é visual, não mode: sobrevive a e.mode = ….
Sequência nomeada no Sheet; play no Sprite. O mesmo clip de novo, enquanto toca, não faz nada — seguro chamar em update. Um clip de loops=1 que já acabou recomeça: para tocar uma vez só a partir do update, pergunte antes (if not heroi.showing("morto"):).
hero = Sheet("hero.png", cols=4, rows=3)
hero.clip("idle", [0, 1, 2], fps=6, yoyo=True)
hero.clip("walk", [3, 4, 5, 6], fps=12)
sprite = Sprite(hero, center=(200, 400)) # 2+ clips → parado no primeiro frame de "idle"
sprite.play("walk")
sprite.play("walk") # não faz nada: a animação segue
sprite.frame = 3 # para a reprodução e mostra a célula 3
O Sheet tem
Sprite(sheet) começa
0 clips
Parado em frame (padrão 0).
1 clip
Tocando esse clip (autoplay).
2+ clips
Parado no primeiro frame do primeiro clip registrado, até .play.
frame= explícito
Parado nessa célula — vence o autoplay.
yoyo=True vai e volta; yoyo=False recomeça do início. fps omitido no clip = 8. O tempo é o do mundo × time_scale do Sprite: pausa com o mundo, e sprite.time_scale = 0.5 anima em câmera lenta.
Em que frame estou?sprite.showing("andar"), showing(3) ou showing((3, 7)) dizem se o frame na tela é um destes — por exemplo, poeira nos frames do passo: if heroi.showing((3, 7)): soltar_poeira(). Para ligar uma caixa de golpe nos frames: Caixas de ataque.
Reação curta: toca uma vez e volta ao clip de base.
hero = Sheet("hero.png", cols=4, rows=3)
hero.clip("idle", [0, 1, 2], fps=6, yoyo=True)
hero.clip("hit", [9, 10], fps=12)
hero.clip("die", [11], fps=8)
sprite = Sprite(hero, center=(200, 400))
sprite.play("hit", loops=1, next="idle") # uma vez, depois "idle"
sprite.play("die", loops=1) # uma vez e segura o último frame
loops=1 sem next= segura o último frame (o primeiro, num clip yoyo). next= é um nome de clip, não um callback — para rodar código no fim, use after.
Escolher o clip a cada frame a partir do estado. min_hold e resume evitam tremer e reiniciar.
# grounded pode oscilar um frame — min_hold evita reiniciar o clip;
# resume=True continua a fase ao voltar (deixe off em reações de uma vez só).
if not grounded:
sprite.play("jump", min_hold=0.06)
elif sprite.vel.x != 0:
sprite.play("walk", min_hold=0.08, resume=True)
else:
sprite.play("idle", min_hold=0.08, resume=True)
Defaults off: sem min_hold / resume, cada troca vale na hora e recomeça do frame 0. Virar o herói para o lado da corrida: Espelhar.
Pacotes recortados por animação têm PNGs de tamanhos diferentes. Uma escala por Sprite e um ponto de registro comum resolvem sem editar a arte.
ninja = Sheet(anchor="bottom")
ninja.strip("parado", "ninja/Idle__*.png", fps=8)
ninja.strip("deslizando", "ninja/Slide__*.png", fps=12)
heroi = Sprite(ninja, height=70, center=(x, y)) # center = os pés
heroi.play("deslizando") # ~56 de altura, no chão
# ou: a escala na arte, uma vez — sem height= em cada Sprite
tiles = Sheet("tiles.png", cell=(16, 16), pixel_size=3)
chao = Sprite(tiles, center=(x, y)) # célula de 16 px × 3 = 48
Regra
O que faz
Sheet(pixel_size=)
A escala da arte, uma vez no Sheet: tamanho de um pixel do PNG em unidades de design (3 = pixel art em 3×; 0.5 = dois pixels por unidade). Sprites do Sheet sem tamanho no construtor desenham no tamanho do PNG × pixel_size: um tile de 16 px e um herói de 32 px continuam 1:2, sem medir cada Sprite. Um tamanho no Sprite vale mais (para aquele Sprite). PNG avulso: Sprite(Sheet("arvore.png", pixel_size=3)).
Uma escala por Sprite
height= / width= / size= medem o frame do construtor (frame=; sem ele, o primeiro frame do primeiro clip). Os outros frames desenham no seu tamanho de PNG × essa escala: um deslize de 351 px ao lado de um parado de 439 px, com height=70, fica com 56. sprite.height lê o frame atual. Escrever o tamanho reescala todos os clips.
Sheet(anchor=)
Que ponto de cada PNG fica no center. "bottom": o centro-baixo de todo frame fica nos pés, e um frame mais baixo continua no chão. Valores: "center" (padrão), "bottom", "top", "left", "right" ou (ax, ay), y para cima.
Pivô do atlas
Um pivô escrito no atlas (pivot / anchor / origin no JSON, pivotX / pivotY no XML) vale mais que anchor=.
clip(…, offset=(dx, dy))
O ajuste fino quando um clip não casa com os outros: move a arte desse clip em pixels da arte (y para cima, +x = direita na arte sem flip). Espelha com flip_x. Também em strip(…, offset=). Uma célula tem um offset só: a mesma célula em dois clips com offsets diferentes é ValueError.
Sprite(offset=)
Outro ajuste: desloca o Sprite inteiro (desenho e colisor) em unidades de design, em todos os clips.
O corpo de colisão mede o frame de referência dele, não o frame que está tocando: o herói não cresce nem encolhe ao trocar de clip. Ver Corpo.
Sprite · Como fazer
Orientar sprites
Três perguntas diferentes: para onde o PNG aponta (art_angle), para onde o Sprite aponta no mundo (.angle) e quanto o desenho inclina sem mexer no colisor (tilt). flip_x / flip_y espelham.
art_angle diz para onde o nariz está desenhado no PNG. Com ele, .angle passa a ser o rumo no mundo — o mesmo número de heading().
# A nave está desenhada apontando para cima (+y).
nave = Sprite("nave.png", height=48, art_angle=90)
nave.angle # 90: aparece como desenhada
nave.rotate_to(heading(nave, mouse)) # o nariz aponta para o mouse
tiro.move_to(alvo, time=1.2, face=True)
nave.reset_rotation() # volta a rest_heading (90)
Campo
Significado
art_angle
Graus, mesma convenção do mundo (0 = +x, 90 = +y): o nariz dos pixels, antes do flip. Um valor por Sprite, para todos os frames. Padrão 0 (arte olhando para a direita). Escrever depois não reescreve .angle.
.angle
Rumo no mundo: para onde a frente pintada aponta. O construtor começa em rest_heading, então o PNG aparece como desenhado.
rest_heading
Só leitura. O rumo em que o PNG aparece sem giro: art_angle depois do flip. Nave art_angle=90 → 90; ninja art_angle=0 + flip_x → 180. Shape: 0.
reset_rotation()
Gira até rest_heading e zera o tilt. Não limpa o flip. Aceita time= / ease= como os outros movimentos.
Espelha no pivô, antes da rotação. O colisor e as partes espelham junto, sem recalcular.
# O ninja está desenhado olhando para a direita.
if keyp("LEFT"):
heroi.flip_x = True # olha para a esquerda; angle passa a 180
elif keyp("RIGHT"):
heroi.flip_x = False # angle volta a 0
Ligar um flip reescreve.angle (flip_x → 180 − θ, flip_y → −θ) para que .angle continue a ser o lado para onde a frente pintada aponta. Escrever o mesmo valor de novo não faz nada. clip(offset=), Sprite(offset=), hitbox_anchor e o pivô do tilt espelham junto. Não use scale negativo para espelhar — o desenho usa o valor absoluto.
tilt inclina só o desenho — numa rampa, por exemplo. O corpo fica em pé, e o center fica onde o resolve o pôs.
heroi = Sprite(ninja, hitbox_shape="rect", hitbox_anchor="bottom")
def update(dt):
heroi.tilt = "ground" # ou AUTO_GROUND: segue o chão; no ar volta a 0
# Outras formas:
heroi.tilt = 22 # só esse ângulo
heroi.tilt = 0 # desliga
Valor
Efeito
número
Gira o desenho esse ângulo. Não desloca.
"ground" / AUTO_GROUND
Lê a cada frame o slope_angle de um contato "bottom" (touching). No ar: 0.
um contato c
Usa c.slope_angle e fica até você sobrescrever. No update, zere antes do loop dos contatos.
O pivô é o hitbox_anchor do colisor calculado (não o retângulo do PNG). Com âncora "bottom" e contato "bottom", "ground" e tilt = c também encostam o desenho no plano da rampa, só ao longo da normal (não escorregam de lado). Com "center", o desenho não desce até a rampa. Partes seguem o tilt; o corpo não. reset_rotation() zera.
Não confundir
Parece
É
Não é
Na rampa, heroi.angle = rest_heading + ângulo
Gira o colisor junto e o herói tropeça. Use tilt: só o desenho.
tilt
nave.rotate_to(heading(nave, alvo) + art_angle)
Soma o art_angle duas vezes. .angle já é rumo no mundo: nave.rotate_to(heading(nave, alvo)).
ajuste manual
Frame “rotated” num atlas empacotado
É o empacotamento do atlas, resolvido na leitura. Não tem relação com art_angle.
art_angle
Sprite · Como fazer
Hitbox do Sprite
Com que forma o Sprite colide. Toda hitbox é um Hitbox: a principal (sprite.hitbox) para chão, parede e resolve; extras (add_hitbox) que só detectam — hurtbox, ponto fraco. As duas têm as mesmas propriedades: shape · source · anchor · scale · active · box.
Principal (sprite.hitbox)
Extra (add_hitbox)
Criada por
O construtor: Sprite(hitbox_shape=…, hitbox_scale=…)
sprite.add_hitbox(group=…, scale=…)
Quantas
Uma (todo Graphic tem; num Shape é a própria geometria)
Várias
Serve para
resolve (pisar, bater), @collide que nomeia o Sprite
As duas usam os mesmos quatro parâmetros. No construtor do Sprite eles têm o prefixo hitbox_ (o Sprite tem o próprio scale); na hitbox, não: Sprite(hitbox_scale=0.8) é sprite.hitbox.scale = 0.8, e na extra add_hitbox(scale=0.8) / extra.scale = 0.8.
Quando é calculado. Na primeira vez que o Sprite entra na colisão (@collide, resolve, collidable = True). Sprite só decorativo nunca paga. O resultado fica em cache por imagem: cem Sprite("tiro.png") compartilham um único cálculo. Depois a hitbox principal fica congelada enquanto o clip toca — exceto com "each" (a cada frame) e "clip" (a cada troca de clip). Ver:collision.show_colliders() ou Ctrl+Shift+7 — principais de resolve em dourado, outras principais em verde, extras em ciano.
Medir o Sprite
Leia
Mede
Qual frame
sprite.width / height
O tamanho que você deu (o ajuste; escrever reescala).
fixo
sprite.box
A célula desenhada inteira, com a margem transparente, onde é desenhada.
o da tela
sprite.hitbox.box
A hitbox principal, como a colisão a usa. É esta para floor_below(dx=).
o do source (padrão: fixo)
extra.box
Aquela hitbox extra (None num frame sem colisor).
o do source (padrão: o da tela)
sprite.art_box
Só os pixels visíveis do frame na tela.
o da tela
Cada leitura é um Box novo, do jeito que está agora: largura = heroi.hitbox.box.width. As caixas de hitbox são alinhadas à tela (left / right / bottom / top); box e art_box giram com o Sprite. Medir não inscreve nada na colisão.
A hitbox principal. Escolha a forma com hitbox_shape= e a arte que a define com hitbox_source= (depois: sprite.hitbox.shape / .source).
Testa círculo, círculos, elipse, duas elipses, casco convexo e retângulo, e fica com o melhor ajuste pelo menor custo. Prefere errar para dentro: toque fantasma é pior que toque perdido.
Naves, inimigos, objetos soltos.
"rect"
A caixa da silhueta: cobre toda a arte, base plana.
Quem anda — plataforma.
"ellipse"
Elipse na caixa da silhueta. Quase quadrada vira círculo.
Corpos ovais.
"circle"
Círculo que cobre a arte.
Bolas, moedas.
"hull"
Casco convexo da silhueta (um pouco maior que a arte).
Formas pontudas.
Nomear uma forma é cobrir a arte com ela. Só "auto" escolhe pela precisão.
source — de qual arte
Uma regra: um frame é aquele frame; vários frames são a união deles (as silhuetas são sobrepostas pelo pivô e ajustadas uma vez — não é a união de formas).
Valor
Qual arte
Calcula
omitido
O frame do construtor (frame=; sem ele, o primeiro frame do primeiro clip).
uma vez
3 / "Idle__004"
Aquele frame (índice ou nome no atlas).
uma vez
"correndo"
União dos frames desse clip.
uma vez
("parado", "correndo")
União desses clips, só deles. Tupla vazia ou nome desconhecido: ValueError.
uma vez
"all"
União de todos os clips registrados. Sem clips (frota de imagens paradas): o frame do construtor. Não é o atlas inteiro.
uma vez
"clip"
União do clip que está tocando. Nada tocando: o frame do construtor.
a cada troca de clip
"each"
O frame atual.
a cada frame
A tupla é a escolha certa quando um clip é muito diferente dos outros: acima, os frames largos de "morto" não alargam a hitbox de quem anda. Com "auto" + "each" / "clip", o tipo de forma é escolhido na primeira vez; depois só o tamanho muda. A principal mede o seu frame de referência, não o frame que toca: o herói não cresce quando um clip mais alto toca.
"each" e "clip" mudam a linha dos pés a cada frame / clip. Para quem anda sobre o chão, use um conjunto fixo ("all", um clip ou uma tupla).
A forma calculada cobre a silhueta. scale a encolhe ou estica em volta de anchor (no construtor: hitbox_scale= / hitbox_anchor=).
# Receita de plataforma: caixa estável, pés no chão, mais estreita que a arte.
heroi = Sprite(
folha,
hitbox_shape="rect",
hitbox_source="all",
hitbox_anchor="bottom",
hitbox_scale=(0.55, 1.0),
)
Sprite("espadachim.png", hitbox_shape="hull", hitbox_anchor=(-0.5, 0))
Âncora
Par
Ponto do colisor
"center"
(0, 0)
centro (não desloca nada)
"bottom"
(0, -1)
centro da base
"top"
(0, 1)
centro do topo
"left"
(-1, 0)
meio da esquerda
"right"
(1, 0)
meio da direita
—
(-0.5, 0.2)
à esquerda do centro, um pouco acima
A âncora faz três coisas:
Fixa a escala. (0.55, 1.0) + "bottom" estreita sem levantar os pés.
Prende a forma à arte. O ponto da âncora fica alinhado à caixa da silhueta dos frames deste cálculo (source) — não ao Sheet inteiro. Um círculo ou elipse com "bottom" encosta a base na linha dos pés.
É o pivô do tilt. Com flip, o ponto espelha junto com o colisor.
O par não é limitado a −1…1 (passa da borda). Escrito como nome, volta como nome; como par, volta como par. scale: um número vale para os dois eixos; a leitura é sempre (sx, sy); 0 achata o eixo; negativo, nan ou inf é ValueError. A margem transparente do PNG fica no arquivo — height= continua medindo essa tela; é o cálculo do hitbox que acha a silhueta.
Uma hitbox principal única dá estabilidade. Quando a pose muda de verdade (o herói entra na água e nada na horizontal), troque-a à mão: os quatro parâmetros aceitam escrita em jogo.
Continua o mesmo Sprite: grupos, funções de colisão e resolve não mudam. Escrever shape ou source recalcula (cache depois da primeira vez); anchor e scale só reposicionam. Também dá para trocar a forma: heroi.hitbox.shape = "ellipse".
Tema
Regra
Troca à mão × "clip"
hitbox_source="clip" faz a troca sozinho, mas para todo clip: a linha dos pés acompanha cada um. Serve para quem voa ou para visão de cima. Para quem anda, troque à mão só nos momentos que pedem.
A hitbox cresce contra a parede
Se a forma nova invade um sólido que estava livre, o resolve empurra de volta por aquela face. Ver resolve.
Nome errado
Checado na escrita: clip ou frame desconhecido é ValueError. Se o Sheet perde o clip depois, o cálculo usa o frame do construtor e avisa uma vez.
Uma hitbox extra, dona de um grupo, que só detecta. A principal continua cuidando do chão e das paredes.
heroi = Sprite(ninja, group="heroi", hitbox_shape="rect", hitbox_source="all",
hitbox_anchor="bottom", hitbox_scale=(0.55, 1.0))
dor = heroi.add_hitbox(group="dor") # silhueta do frame atual; só detecta
resolve(heroi, "chao") # a principal pisa no chão
@collide("bala", "dor") # a parte responde por "dor"
def ferido(bala, alvo, info): # alvo é o heroi; info.b_hitbox é dor
alvo.hp -= 1
bala.destroy()
O que group="dor" faz. Duas coisas: o Sprite entra no grupo "dor" (como qualquer tag: group["dor"] lista o Sprite, heroi.tagged("dor") é True), e a parte reivindica o grupo — na colisão, debaixo de "dor", quem aparece é a extra, não a principal. Uma parte pode reivindicar vários grupos (group=["dor", "alvo"]). Dois colisores do mesmo Sprite não respondem pelo mesmo grupo: uma segunda parte com "dor" levanta erro.
Quem responde
Você escreve
Quem colide
@collide(bala, heroi) · ANYTHING · on_collide
A principal. Nomear o Sprite é a principal (@collide(bala, heroi.hitbox) levanta erro: nomeie o Sprite).
@collide("bala", "dor")
A parte que reivindica "dor".
@collide("bala", dor)
A parte (o objeto Hitbox).
Função de colisão
Recebe sempre o Sprite. info.a_hitbox / info.b_hitbox dizem qual parte (None = a principal). CollisionInfo.
resolve(dor, …)
TypeError: parte não resolve.
resolve("inimigo", …) com partes em "inimigo"
Move os Sprites pela principal. Chefe com ponto fraco e vespas no mesmo grupo ficam de pé.
info.push_apart() / bounce_apart() / resolve() num contato de parte
Levanta erro. normal, point e depth continuam legíveis.
touching_sides / touching_contacts
Só a principal. Extras nunca entram.
Um Sprite nunca colide com as próprias partes, e duas partes do mesmo Sprite nunca formam par.
Ligar e desligar
Quero
Escreva
Efeito
Invencível por um tempo
heroi.untag("dor") … heroi.tag("dor")
O Sprite sai do grupo; a parte continua dona de "dor" e volta com o tag.
Desligar só a extra
dor.active = False
Sai de jogo (dispara as saídas). True religa.
Desligar só a principal
heroi.hitbox.active = False
Atravessa chão e paredes (fantasma); as extras continuam detectando. True religa. Tudo: collidable = False.
Desfazer a posse
dor.remove_from_group("dor")
A parte larga o grupo e o Sprite sai dele.
Remover a parte
heroi.remove_hitbox(dor)
O Sprite sai dos grupos em que ela o pôs. O chefe com group="inimigo" continua em "inimigo" quando o ponto fraco add_hitbox(group=["inimigo", "fraco"]) sai.
O grupo vale para detecção e resolução. Se "inimigo" é lado de resolve, untag("inimigo") também tira o Sprite do chão. Para invencibilidade, alterne um grupo que não seja lado de resolve (como "dor").
Ciclo de vida
Morre com o Sprite: destroy() dispara as saídas e mata as partes (is_aliveFalse, hostNone).
heroi.active = False e collidable = False desligam todas as hitboxes; voltar religa.
Frame sem arte (com "each") = parte ausente nesse frame. @collide / @collide_exit disparam quando ela aparece / some.
Segue o desenho: pose, flip, scale, offset, frame atual e tilt.
clone() não copia partes. Crie no setup() da classe:
class Heroi(Sprite):
def setup(self):
self.dor = self.add_hitbox(group="dor")
Consultas e custo
Consulta sem grupo (query_point, query_circle, raycast(group=None), collides_with()) vê só as principais. Com o grupo, vê as partes: collision.raycast(torre, p2=heroi, group="dor"), collides_with("dor"). Uma consulta nunca acerta as partes de quem consulta. Ver Consultas.
Cada parte custa ~11–12 µs por passo nesta máquina de desenvolvimento: 20 Sprites animados com uma parte somam ~0,26 ms; 100, ~1,2 ms. Bom para herói, chefe e um esquadrão. Uma frota de 100 que só precisa de um colisor fica só com a principal.
O atlas JSON / XML pode trazer caixas de colisão por frame (hitboxes). Todo cálculo "auto" do Sprite as usa no lugar da silhueta.
lutador = Sheet("lutador.png", atlas="lutador.json") # frames com "hitboxes"
# A principal com forma explícita ignora as caixas: fica a silhueta.
p1 = Sprite(lutador, hitbox_shape="rect", hitbox_source="all", hitbox_anchor="bottom")
# Uma extra "auto" + "each" lê as caixas do frame atual.
caixas = p1.add_hitbox(group="golpe")
Valem para todo cálculo "auto" — a principal inclusive, porque "auto" é o padrão. Uma forma explícita ("rect", "ellipse", "circle", "hull") as ignora.
Hoje, na prática, é um conjunto de caixas por Sprite: dê à principal uma forma explícita e deixe uma extra "auto" ler as caixas.
Com "each", cada frame usa as suas caixas. Um cálculo de vários frames de uma vez (clip, tupla, "all") usa as caixas do primeiro frame do conjunto.
hitbox_anchor="bottom" encosta nas caixas escritas, não na silhueta.
A área que acerta (a espada, o soco): uma parte com a forma posta à mão, que liga sozinha nos frames do golpe.
folha.clip("golpe", [10, 11, 12, 13, 14, 15], fps=14)
GOLPE = (12, 13, 14) # frames com a espada esticada
espada = heroi.add_hitbox(group="ataque", px_rect=(430, 180, 90, 80))
espada["active"] @= lambda: heroi.showing(GOLPE)
def atacar():
heroi.play("golpe", loops=1, next="parado") # a caixa liga e desliga sozinha
@collide("ataque", "inimigo")
def acertou(heroi, inimigo, info):
inimigo.destroy()
Forma à mão: px_rect e px_circle
Parâmetro
Números
px_rect=(x, y, w, h)
Canto de cima à esquerda e tamanho, em pixels do frame.
px_circle=(x, y, r)
Centro e raio, em pixels do frame.
Os números são os que o editor de imagem mostra para aquele frame: origem no canto de cima à esquerda, y para baixo. É um endereço na imagem; o resto do Scena continua com y para cima. A caixa fica presa ao desenho: Sheet(anchor=), clip(offset=), offset, flip, giro, scale e tilt a levam junto — você nunca soma offset. Pode passar da borda do frame.
Um PNG por frame, ou um frame no Aseprite: o número que o editor mostra.
Folha em grade (cols=) aberta como uma imagem só: desconte a origem da célula (coluna × largura da célula, linha × altura).
Atlas empacotado: pixels do frame original, sem corte; o Scena desfaz o corte e a rotação.
Os mesmos números num frame de outro tamanho caem em outro lugar. Ligue a parte só nos frames que você mediu.
Uma forma por parte, e não mistura com shape= / source= / anchor= / scale= (TypeError). espada.px_rect = None volta para a arte.
Só em Sprite de folha: num Svg dá TypeError.
Ligar nos frames: showing e @=
heroi.showing(frames) diz se o frame na tela é um destes: nome de clip, índice, nome de frame, ou uma tupla / lista misturando. Vai pelo frame, então acerta com yoyo, resume=, next= e golpe interrompido.
espada["active"] @= lambda: … liga o interruptor a uma fórmula. O Scena a calcula no começo da etapa de colisão de cada frame — depois do update, do on_update e de qualquer play() — e a cada leitura de espada.active. A fórmula pode ler qualquer coisa (heroi.showing(…)), mesmo valores que não são campos do Scena — como em group[...] @=.
Quero
Escreva
Efeito
Caixa só nos frames do golpe
espada["active"] @= lambda: heroi.showing(GOLPE)
Entra no primeiro frame do golpe, sai depois do último.
Soltar a fórmula
espada["active"].unlink()
Fica o último valor; .active = … volta a valer.
Invencível por 1,5 s
dor.active = False e after(1.5, religar)
O tempo decide, não o desenho: escrita simples.
Escrever com a fórmula ligada
espada.active = True
LinkedAttributeError: solte antes com unlink().
Por que não @when? Ele roda depois da colisão: a caixa liga e desliga com um frame de atraso (uma volta do jogo). Uma linha no update (espada.active = heroi.showing(GOLPE)) funciona, mas atrasa se um play() vier depois dela no mesmo frame do jogo, e fica ligada se o herói trocar de mode no meio do golpe e ninguém escrever a linha de novo.
A fórmula só lê: não mova nem troque grupos dentro dela. Se ela levantar erro, a mensagem aponta a linha do @= e a do add_hitbox. Caixas escritas no atlas: Caixas do atlas.
Hitbox
Toda hitbox. A principal é g.hitbox em qualquer Graphic (num Shape, a própria geometria: shape só leitura, os outros três levantam TypeError; Text não tem). Extras: sprite.add_hitbox(…) — Hitbox(…) direto levanta TypeError. Não é Entity: sem pose, sem desenho, sem campos próprios. Extras em Sprite e Svg (px_rect / px_circle só em Sprite de folha).
Depois de remove_hitbox ou do destroy() do dono, ler continua valendo (hb.shape, hb.scale; hb.box é None) e toda escrita levanta DestroyedError dizendo onde foi destruído — guarde com if hb.is_alive:. Campos só de leitura (box, host, is_alive, layer, group) dizem o que escrever no lugar.
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
shape / source / anchor / scale
str, par ou tupla"each"
lê / escreve
Os quatro parâmetros. Na extra, source padrão "each".
.box
Box ou Noneheroi.hitbox.box.width
leitura
Esta hitbox como está agora, alinhada à tela. None num frame sem colisor. Mede mesmo desligada. Medir.
px_rect / px_circle
tupla ou None(430, 180, 90, 80)
lê / escreve
Só extras. Forma à mão em pixels do frame (y para baixo). Escrever uma limpa a outra; None volta para a arte. Caixas de ataque.
.active
boolTrue
lê / escreve
Liga / desliga só esta hitbox (na principal, as extras continuam; collidable desliga todas). Pode ser ligado a uma fórmula (abaixo); aí escrever levanta LinkedAttributeError.
.group
frozenset{"dor"}
leitura
Extra: os grupos que ela reivindica. Principal: os grupos do Sprite menos os que uma extra reivindica (quem responde por eles é a extra). Só leitura: mude com hb.tag / untag (principal: sprite.tag).
.host
Sprite ou None
leitura
O dono. None depois de removida ou com o dono destruído.
Só extras. Reivindica / larga um grupo. Largar tira o Sprite do grupo também. Na principal, tagged(nome) responde (é nome in hitbox.group); os outros levantam TypeError.
tag(*names) / untag(*names) / tagged(name)
Parâmetros: names (nomes); tagged usa name (nome)
a parte / bool
Mesma coisa, no vocabulário de tags.
parte["active"] @= fn / parte["active"].unlink()
Parâmetros: fn (função sem argumentos que devolve True / False)
—
Liga o interruptor a uma fórmula (principal ou extra), calculada na etapa de colisão de cada frame. unlink() solta e guarda o último valor. Só "active" se liga.
Não confundir
Parece
É
Não é
sprite.box.width como largura da hitbox
sprite.hitbox.box.width. A caixa do desenho inclui a margem transparente do PNG.
o colisor
sprite.hitbox.active = False = collidable = False
Desliga só a principal; as extras continuam. collidable = False desliga todas.
desligar o Sprite
Track(sprite) como hurtbox
Track(sprite) é o contorno vivo (para montar, desenhar). Hurtbox é add_hitbox.
colisor de dano
Clique acerta o hitbox
@click / contains_point olham o alfa do frame atual. collision.query_point olha os corpos.
o hitbox
hitbox_source="all" = o Sheet inteiro
União dos clips registrados. Um atlas com lasers e naves não entra inteiro.
toda célula
px_rect com y para cima
Pixels do frame como o editor mostra: y para baixo, origem no canto de cima à esquerda. O resto do Scena é y para cima.
coordenada do mundo
showing("golpe") = o clip golpe está tocando
Os frames do clip golpe estão na tela. Um frame que dois clips usam conta nos dois.
nome do clip atual
@when para ligar a caixa do golpe
parte["active"] @= lambda: … é calculado antes da colisão do mesmo frame do jogo. @when roda depois: um frame de atraso.
o jeito de ligar a caixa
O Shield segue o hitbox
O Shield usa a silhueta dos frames (× shield_scale). Os parâmetros de hitbox não o mudam.
o colisor
Sprite
Recursos adicionais do Sprite
Depois do básico: copiar um Sprite, dar padrões a uma classe, usar SVG como sprite, aparência e clique.
Um novo Sprite com a aparência deste. Útil para montar uma frota a partir de um carimbo. Não é um pool.
class Nave(Sprite, source="playerShip1_blue.png", height=48, group="nave"):
pass
_NAVE = Nave(layer=None) # carimbo: fora da tela
_NAVE.active = False # não roda nem colide
naveA = _NAVE.clone(center=(120, 400))
naveB = _NAVE.clone(center=(240, 400))
naveC = _NAVE.clone(center=(360, 400))
Copiado (foto de agora)
Novo
Sheet, frame, tamanho, offset, parâmetros de hitbox, art_angle
setup() roda sempre
Pose: angle, scale, flip, tilt (o clone aponta para onde o original aponta; clone(scale=) multiplica)
Visual e complementos (look_like)
Campos próprios, mode, timers, callbacks de clique
Grupos (tags incluídas), salvo group=
Partes de hitbox (crie no setup())
visible
Posição da animação
layer= é colocação: omitido, usa o padrão da classe; layer=None fica fora da tela (carimbo). group= substitui os grupos inteiros. Clone um carimbo limpo, não um herói já marcado ("shielded" viria junto). Para atirar, construa de novo: Tiro(center=…) custa o mesmo que clone().
Formas, Text e Track seguem a mesma regra: o clone fica igual ao original, com a mesma pose e os mesmos pontos (o p1 de um Rect continua sendo o p1), e center= o põe em outro lugar (_ROCHA.clone(center=(300, 50))). frame= só existe no Sprite: nos outros levanta TypeError. Um Text preso por top_left= gera um clone preso pelo mesmo canto.
Os padrões de aparência vão na linha da classe. Cada instância só diz onde fica.
space = Sheet("space_sheet.png", atlas="space_sheet.xml")
class Tiro(Sprite, source=space, frame="laserBlue01.png", height=18,
art_angle=90, group="tiro"):
def setup(self):
self.dano = 1
Tiro(center=nave.center)
Tiro(center=nave.center, frame="laserRed01.png") # a instância vence
Valem na linha da classe: source, frame, height / width / size, os quatro parâmetros de hitbox, offset, visual, shield_scale, group, flip_x / flip_y, art_angle. Uma subclasse herda e sobrescreve. Colocação (center, layer, box) é recusada na classe. Não escreva frame = … ou height = … como atributo no corpo da classe: o Scena não lê o valor dali, e a definição da classe levanta TypeError com a correção. Passe na linha class.
Svg é um Sprite cujo corpo vem de um SVG: markup <svg …> ou arquivo. Uma imagem parada: sem clips, sem play.
Os mesmos parâmetros do Sprite; "all" / "each" / "clip" dão no mesmo (um frame). add_hitbox também vale.
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.svg
str"<svg …>"
lê / escreve
Markup atual — o texto, nunca o caminho. Atribuir recarrega (markup ou arquivo). String igual não faz nada. Falha: aviso, e fica o último corpo bom. None limpa: volta ao xadrez, como Svg(None).
.box
Boxicon.box
leitura
Uma cópia da moldura visual, tirada agora, com os nomes: depois de um flip_x, box.top_center continua sobre a cabeça. Acima na tela: box.top. Colocar.
Vira bitmap ao carregar: o zoom da câmera suaviza, não redesenha o vetor. cols= / atlas= / fps=, play e strip são TypeError. clone() copia o markup.
Os visuais e complementos de Aparência funcionam no Sprite. Alguns detalhes são só dele.
nave.alpha = 0.5
nave.blend = "add"
nave.glow = True
nave.visual = Halo()
nave.shield_scale = 1.6 # oval do Shield maior que a silhueta
# Tela de carregamento: aquece antes de trocar de visual em jogo.
nave.prepare_fx(Halo, Aura)
alpha e blend valem para a imagem.
Visuais de silhueta (Halo, Aura, Rim, Laser…) calculam a silhueta do clip atual quando você atribui .visual. prepare_fx(*visuais) calcula todos os frames antes; sem argumentos, o visual atual. Quais visuais o Sprite aceita: Visual.
Shield envolve a união das silhuetas dos frames, vezes shield_scale (padrão 1.4). Trocar de clip não faz a casca pular.
look_like(outro) copia visual e complementos; save_visual() / restore_visual() guardam e voltam.
Clique: @click e contains_point olham o alfa do frame atual (tilt incluído). Pixel transparente não clica. Não é o hitbox.
Track
Uma curva. Serve para duas coisas: trilha de move_along e traço desenhável (parede curva, anel que se enche). Padrão: invisível, como Point. Qualquer Shape também é trilha: bug.move_along(hexagon, time=4).
Ancorada ou flutuante. É a decisão que muda o comportamento de uma trilha:
Ancorada
Flutuante
Onde fica
Num lugar do mundo. Quem viaja vai até ela.
Sem lugar próprio para a viagem: a curva começa onde está quem viaja (ou no ponto at= do move_along).
Quando
Passou uma palavra de colocação: center=, points=, p1=, start=, ou um Shape (Track(hexagon)).
Só tamanhos: Track.eight(width=90), Track.circle(radius=80), vectors= sem start=, Track(), um nome de preset ("eight").
Uso típico
Órbita em volta de um planeta, parede curva, caminho fixo do nível.
“Faça um oito a partir de onde você está”: patrulha, desvio, dança.
floating= explícito sempre manda. Toda Track tem .center real — flutuar não é “sem posição”; .is_floating só diz se a pose prende quem viaja.
Dar lugar depois ancora. Uma trilha que flutua só porque nasceu sem colocação ancora quando você lhe dá um lugar depois: ligar o centro (orbit["center"] @= lambda: earth.center), escrever .center = ou mover (move_to, move_by). É a colocação que faltou, igual a center= no construtor; quem já viaja nela passa a segui-la. Girar ou escalar não é colocação. Com floating=True (ou is_floating = True) ela continua flutuando: mover só muda onde ela é desenhada.
Visível ou não. Uma trilha só de movimento (sem color= / visual= / line_width= / visible=True / layer=) fica fora da lista de desenho — dá para viajar, e some quando ninguém a segura. Com estilo, visible=True ou layer= explícita, entra no canvas como Shape (traço, clique e colisão). Para ver as trilhas durante o desenvolvimento: show_tracks(True) (magenta; não liga visible) — ver Debug.
Quatro jeitos de dar a forma: posições (points=), passos relativos (vectors=), o contorno de um Shape, ou um preset.
Track() # Track.eight(); flutua
Track(points=[(60, 80), (200, 300), (420, 120)]) # curva (smooth=1)
Track(points=[...], smooth=0, closed=True) # polilinha
Track(vectors=[Vec2(50, 0), (0, 80), (-40, 20)], smooth=0) # passos relativos, flutua
Track(hexagon) # contorno vivo; clone() congela
Track(sprite) # contorno vivo da silhueta (hurtbox é add_hitbox)
Track(ellipse, floating=True) # contorno vivo; posição não prende quem viaja
Track.circle(center=(400, 300), radius=120) # ancorado no mundo
Track.eight(width=90, height=40) # flutua até quem viaja
points= são posições no mundo (um Vec2 ali dentro também é posição); ligar um ponto com lambda faz a trilha acompanhar. vectors= são passos a partir do anterior (caminho tartaruga). Os dois juntos (ou com radius=) levantam TypeError. smooth=1 (padrão) passa pelos pontos em curva; 0 é polilinha. Track(shape) segue o contorno vivo do Shape; Track(sprite), a silhueta pintada. u=0 fica no primeiro vértice (ângulo 0 nos redondos), sentido anti-horário.
Formas prontas. Família centrada (center=): circle, arc, eight, star, spiral, shuttle. Família começada (p1= / start=): line, wave.
Track.line(length=200) # +x, flutua
Track.line(length=200, angle=90) # +y
Track.line(vec=Vec2(120, -40)) # tamanho + direção
Track.line(length=400, angle=-30, p1=(1050, 450)) # plantado, u=0 em p1
Track.line(p1=(0, 0), p2=(200, 0)) # dois pontos, ancorado
Track.circle(radius=80, center=(400, 300))
Track.arc(radius=80, from_angle=0, to_angle=180)
Track.eight(width=90, height=40)
Track.eight(width=90, height=40, center=(400, 300), floating=True) # desenha lá; quem viaja faz o oito a partir de onde está
Track.star(points=5, radius=70)
Track.spiral(turns=3, radius=80)
Track.wave(length=200, amplitude=30, waves=3)
Track.wave(length=200, start=(100, 300))
Track.shuttle(length=180) # vai e volta pelos dois lados, fechado
Presets começados plantam u=0 com p1= / start=; center= neles não vale. .center continua sendo o meio geométrico. Tabela completa: Presets, abaixo.
Ler pontos da curva por u: 0 é o começo, 1 é o fim, e u cresce por igual com a distância (u=0.5 é o meio do caminho) — para posicionar coisas ao longo dela.
x, y = track.at(0.25)
ang = track.angle_at(0.25)
tx, ty = track.tangent_at(0.25)
pts = track.spaced_points(8) # fechado: sem repetir o início
pts = Track(linha).spaced_points(5) # aberto: inclui as pontas
track.u = 0.5
ship["center"] @= lambda: track.cursor # coloca — para viajar: move_along
As amostras devolvem tuplas, não Points, e já incluem a pose da trilha (local_at não). .cursor é o Point em u: ligar um centro a ele coloca (não varre parede).
begin / end recortam o traço: desenho, clique e colisão. Animar end de 0 a 1 “desenha” a curva.
ring = Track.circle(center=(400, 300), radius=80, color="gold", line_width=5)
ring.end = 0
tween(ring["end"], 1, time=1.4, ease="ease_out")
# janela que anda num loop (fechado envolve)
crawl = Track.circle(center=(400, 300), radius=100, color="cyan", line_width=4)
crawl.begin = 0
crawl.end = 0.2
A janela não muda a curva: at(u), length e move_along seguem a curva inteira. Fechado com begin > end envolve o loop; aberto com begin >= end não desenha nem colide. O trecho de uma viagem é outra coisa: span= no move_along.
Deformam a curva no próprio objeto e devolvem o Track (encadeáveis).
base = Track(points=[(80, 200), (520, 200)], smooth=0)
base.zigzag(amount=24, waves=6) # muda este Track, devolve self
base.orbit(radius=12, laps=4)
base.wander(amount=10, seed=3)
# clone() antes se quiser a reta original
Para manter a original, clone() antes: o clone congela a vista atual (incluindo reversed() e modificadores) numa polilinha.
Um traço com line_width colide como uma linha grossa: dá uma parede curva.
A colisão é o traço (espessura line_width), recortado por begin / end. Fechado = anel, não disco. Fatia preenchida (clique e colisão no disco): Circle(angle=, spread=). Trilha só de movimento: deixe sem inscrição (padrão).
Só tamanhos → flutua até o move_along (at= escolhe outro ponto de partida para esta viagem). Palavra de colocação dada → ancorada. floating= explícito sempre manda. u=0 no primeiro vértice (ângulo 0 nos redondos), sentido anti-horário. Mover, girar ou escalar a trilha (center / angle / scale) não refaz os pontos; at(u) já devolve a posição com o lugar e o giro atuais. angle= no construtor é pose inicial.
Parâmetro
Passar
Tipo (default)
Significado
source
1º
Shape ou Sprite
Track(hexagono): a trilha é o contorno vivo desse Shape (ou a silhueta do Sprite). Ancorado (pose ligada à fonte). floating=True desliga só a posição — o contorno / rotação / escala da fonte continuam vivos. clone() congela.
points
nome=
lista de pontos
Posições no mundo. Cópia se for tupla; ligação se for callable / algo reativo do Scena. Mínimo 2. Ancorado. Com pontos ligados, .center é derivado (o meio dos pontos; só leitura). Vec2 aqui é posição, não passo. floating=True solta a pose dos pontos listados.
vectors
nome=
lista de Vec2 ou (dx, dy)
Caminho tartaruga: cada item é um passo a partir do anterior. Sem start= flutua; com start= ancora (u=0 nesse ponto). Aberto. Não misture com points= / radius=. Não use center=.
Amostras por segmento. Só se pedir — nunca escolhido sozinho por ter ligação.
center / radius
nome=
ponto / número
Com radius=: modelo círculo (sem center= flutua). center= planta presets centrados (circle / arc / star / eight / spiral / shuttle). Não misture radius= com points= / vectors=. Em line / wave / vectors= use p1= / start=.
start
nome=
ponto
Só em vectors= (e no preset Track.wave). Planta u=0. Omitido → flutua.
floating
nome=
bool ou omitido
Explícito sempre manda. Sem isto: colocação dada → ancorado; só tamanhos → flutua até receber um lugar (ligar / escrever .center, move_to, move_by), que a ancora. Com floating=True mover não a ancora.
angle
nome=
graus0
Rumo inicial (pose). Não entra na polilinha local. Em line / shuttle, angle= é esse rumo: o segmento é feito ao longo de +x e girado, então Track.line(angle=30).angle é 30.
color / visual
nome=
cor ou Visual
Estilo. color= escreve o traço. Qualquer um torna o Track visível e o coloca na camada. Track é Shape: mesmo .dash / .line_cap / .line_join (strings).
line_width
nome=
número
Espessura do traço. Também conta como estilo. Não é width= — isso é tamanho do Track.eight.
begin / end
nome=
número 0…10 / 1
Janela do traço (desenho, clique e colisão). Padrão: o caminho inteiro. Fora de 0…1 limita. Não muda length / at / move_along.
visible
nome=
boolFalse
Como Point: True sobe para a camada. Sem estilo e sem isto, fica fora da lista de desenho.
layer / group
nome=
como todo objeto na tela
layer= explícita também registra o Track no canvas.
Modelos de construção
Chamada
Significado
Track()
Igual a Track.eight(). Só tamanhos — flutua.
points=
Curva (padrão smooth=1). Aberto. Ancorado nos pontos. Posições no mundo — Vec2 também é posição. Pontos ligados: .center é derivado (só leitura).
points=, smooth=0
Polilinha.
vectors=
Passos relativos (Vec2 ou (dx, dy)). Aberto. Sem start= flutua. Não misture com points= / radius=.
Track(shape)
Contorno vivo. Ancorado. floating=True desliga a posição. Se a fonte for destruída, a viagem continua com o último contorno lido.
center=, radius=
Círculo. Ancorado só com center= (a menos de floating=).
Presets
Aceitam os mesmos smooth=quality=line_width=color=begin=end=floating=angle=layer=group= do construtor. Família centrada: center=. Família começada: p1= (line) ou start= (wave). Posição no preset → ancorado; só tamanhos → flutua; floating= explícito sempre manda.
Segmento aberto. smooth=0. Só tamanhos → flutua; p1= planta u=0. p1=/p2= ancora (os dois obrigatórios); .center é derivado (o meio; só leitura). length=/angle= ou vec= — não os dois, e não misture com p1=/p2=. angle em graus (0 = +x). Comprimento omisso = tamanho médio. Não use center=. Vários passos: Track(vectors=[…], smooth=0).
Track.circle(radius=, center=)
Círculo fechado. Sem center= flutua. radius omisso = tamanho médio.
Traço fechado 1-D: sai dos dois lados a partir do center e volta. length= é a largura total (como em Track.line). Comprimento de arco por volta = 2 * length. Sem center= flutua. length=/angle= ou vec= — não os dois. loops="forever" não precisa yoyo=.
Neste Track
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.center
ponto
lê / escreve
Meio geométrico / pose. Escreva / ligue para mover a trilha sem reconstruir os pontos. Em points= ligados e em Track.line(p1=, p2=) é derivado (escrever levanta DerivedAttributeError). Numa trilha que flutua por falta de colocação, ligar / escrever ancora; com floating=True não troca is_floating.
.radius
número
lê / escreve
Só no modelo círculo / arco.
.length
número
leitura
Comprimento total da curva, em unidades de design. Só leitura: para mudar, mude a forma (radius=, points=…).
.u
número0
lê / escreve
Parâmetro 0…1. Escrever move o cursor.
.begin / .end
número 0…10 / 1
lê / escreve
Janela do traço (desenho, clique e colisão). Tween: tween(track["end"], 1, time=1.2). Fechado com begin > end: envolve o loop. Aberto com begin >= end: não desenha (nem colide). Fill some enquanto a janela não é o caminho inteiro. Não é span= do move_along.
.cursor
Point
leitura
Ponto em u; só é calculado quando você o lê ou liga. ["center"] @= lambda: cursorcoloca — não varre.
.closed
bool
leitura
Loop. Só leitura.
.is_floating
bool
lê / escreve
Se a pose desta trilha prende quem viaja. True: a curva começa onde está quem viaja (ou em at=). Toda Track tem .center real — flutuar não é “sem posição”. Escrever aqui (ou floating= no construtor) é a sua palavra: nada mais a troca. Sem ela, uma trilha que nasceu sem colocação flutua até receber um lugar (ligar / escrever .center, move_to, move_by) e então ancora — quem já viaja nela passa a segui-la.
.smooth / .quality
número / "high" ou "low"
lê / escreve
Leia e escreva. Escrever recalcula os pontos da curva.
.visible
boolFalse
lê / escreve
Como Point: True promove à camada; False tira da lista de desenho.
Métodos
Método
Assinatura
Retorna
Significado
at(u)
Parâmetros: u (0 a 1)
(x, y)
O ponto da curva em u, já com o lugar e o giro atuais da trilha. Não devolve Point.
local_at(u)
Parâmetros: u (0 a 1)
(x, y)
Amostra no espaço local (sem pose).
angle_at(u)
Parâmetros: u (0 a 1)
graus
Rumo da tangente. 0 = +x, anti-horário.
tangent_at(u)
Parâmetros: u (0 a 1)
(tx, ty)
Tangente unitária no mundo.
sample_span(a, b, n=32)
Parâmetros: a (0 a 1), b (0 a 1), n (número inteiro)
lista de (x, y)
Trecho no mundo, n amostras.
spaced_points(n)
Parâmetros: n (número inteiro)
lista de (x, y)
n pontos a velocidade constante (comprimento de arco). Fechado: sem repetir o ponto de volta. Aberto: inclui as duas pontas. Tuplas, não Point. Contorno de Shape: Track(linha).spaced_points(8).
reversed()
Sem parâmetros
Track (vista)
Instantâneo: a mesma trilha, percorrida ao contrário. reversed().reversed() é o original. A vista não tem pose própria: center, angle, scale, flips e os verbos são os da trilha original. Esta viagem só: span=(1, 0).
zigzag(amount=20, waves=6, *, kind="sine")
Parâmetros: amount (número), waves (número), kind ("sine" ou "saw")
self
Ondula no próprio objeto. clone() se quiser a cópia lisa.
Parâmetros: floating (bool, omitido copia o original)
Track
Congela a vista atual (inclui reversed e modificadores) numa polilinha. Copia is_floating; clone(floating=True) desliga a pose no clone.
Funções
Método
Assinatura
Retorna
Significado
show_tracks(enabled=True)
Parâmetros: enabled (ligado, True/False)
—
Overlay magenta das viagens ativas e dos Tracks parados numa camada. Não liga visible. Também collision.show_tracks. Ctrl+Shift+7 (o zoom de inspeção do Ctrl+Shift+8 não mostra as trilhas). Desligado: zero trabalho extra no frame. É debug, não lógica de jogo — ver Debug.
Não confundir
Parece
É
Não é
Track(center=lambda: earth.center) segue a Terra
Não vê a dependência. Use lambda: (earth.center.x, earth.center.y), ou ligue depois: track["center"] @= lambda: earth.center.
a mesma grafia
Espessura do traço é width=
É line_width=. Track.eight(width=…) é o tamanho do oito.
Track(..., width=)
Track.arc e Circle(angle=, spread=)
Track.arc(from_angle=, to_angle=) é o caminho (com direção, colide como traço). Circle(angle=, spread=) é a fatia preenchida.
a mesma forma
reversed() e span=(1, 0)
track.reversed() é uma vista invertida da trilha (O(1)). span=(1, 0) inverte só uma viagem.
o mesmo efeito
History
Uma History guarda os últimos objetos (ou posições) que você acrescenta e tira os mais velhos sozinha — passando de max_points= ou de duration= segundos. Serve para rastros de peças (bolinhas, segmentos), cobras, filas. Ela não desenha: quem aparece são os objetos que você põe nela (add), e o que sai é destruído. snapshot guarda só uma posição. Para um traço contínuo que esmaece: Contrail.
Os dois limites são independentes e reescrevíveis depois. Baixar um limite expulsa na hora (mais antigo primeiro, saída completa). duration= é conferido a cada frame; só max_points= (ou nenhum) fica inerte até add / snapshot / pop / discard / clear.
Parâmetro
Passar
Tipo (default)
Significado
max_points=
nome=
int ou NoneNone
Capacidade. Ao estourar, o mais antigo sai. Crescer o rastro: h.max_points += 1.
duration=
nome=
número ou NoneNone
Tempo máximo, em segundos, que cada coisa fica guardada; a mais velha sai sozinha mesmo sem add novo. None: sem limite de tempo.
on_leave=
nome=
função ou Nonelambda d: …
Efeito colateral na saída. Um argumento: a coisa, ainda viva. Não substitui o destroy. Também h.on_leave = fn. Não misture com método on_leave na subclasse.
destroy_on_leave=
nome=
boolTrue
Padrão: Entity própria é destruída na saída (espera terminar um dissolve_out / fade_out / implode / burn_out em curso). False: esquece sem destruir (cartas tocadas). Só no construtor; depois é só leitura.
Ler / escrever / remover
Nome
Assinatura
Retorno
Significado
add(thing)
Parâmetros: thing
thing
Guarda o objeto. Na saída: on_leave, depois destrói se for Entity viva (adiado enquanto dissolve_out / fade_out / implode / burn_out toca). Parte de outro objeto e valor vivo (mouse, Vec2) levantam TypeError na hora; o mesmo objeto guardado duas vezes levanta ValueError (seria destruído duas vezes).
snapshot(point) / snapshot(x, y)
Parâmetros: point (ponto) ou x, y
Point
Guarda uma cópia da posição, que não acompanha nada. Nada a destruir.
history[i] / [a:b]
índice / fatia
valor / lista
0 = mais antigo, -1 = mais novo. Fatia é cópia (não vista viva).
.first / .last
qualquer
leitura
Atalhos de [0] e [-1].
len(history) / for v in history
—
—
Do mais antigo ao mais novo.
pop(index=-1)
Parâmetros: index (int)
valor
Tira vivo. Sem on_leave, sem destroy — passa a ser seu.
discard(value)
Parâmetros: value
bool
Remove por identidade (não igualdade). Saída completa. Sem erro se faltar.
clear()
Sem parâmetros
—
Tudo sai, do mais antigo ao mais novo (saída completa).
é extra. O destroy só sai com destroy_on_leave=False. Um verbo de morte no on_leave termina antes do destroy.
substituto do destroy
snapshot de entidade destruída (ou de .center / .p1)
DestroyedError, com a dica de usar e.every(...).
último ponto em silêncio
Contrail
Um Contrail desenha um rastro: uma linha contínua pelas últimas posições de um objeto, que pode esmaecer da cabeça para a cauda (fade=). Você só diz quando guardar uma posição — contrail.snapshot(nave), em geral com nave.every(0.05, …) — e o traço se atualiza sozinho. É uma History com um traço, só de posições (add levanta).
trail = Contrail(max_points=100, duration=2.0, fade=True, ease="ease_out",
visual=Laser("red"), line_width=3)
dst.every(0.05, lambda: trail.snapshot(dst)) # para no destroy; pausa com dst.active = False
# polilinha facetada
Contrail(smooth=0, line_join="miter", visual=Laser("red"))
# cresce como cobra
trail.max_points += 1
# alpha base (o fade multiplica por cima — cabeça = 0.5, cauda → 0)
Contrail(duration=1.0, color=Color("cyan").with_alpha(0.5), fade=True)
Esvazia sem on_leave e destrói o traço privado. Chamar de novo não faz nada. Depois, mutar (snapshot, add, pop, atribuições) levanta DestroyedError. Ler (len, índice) continua.
Track(points=list(contrail), smooth=0)
—
Track
Congela a forma atual para move_along — sem API especial.
Não confundir
Parece
É
Não é
Contrail é um Track
é um History com um traço privado mínimo. Sem move_along / presets / begin/end.
Track(points=…)
line_join="round" curva o rastro
só arredonda o canto da polilinha (smooth=0). A curva é smooth=1.
Catmull-Rom
fade= pinta opaco na cabeça
multiplicador sobre o alpha da cor — cabeça = alpha do autor, cauda → 0.
rampa absoluta até 1.0
Amostragem no construtor
dst.every(0.05, lambda: trail.snapshot(dst)) — cadência à parte do armazenamento. O timer é de quem é lido.
heroi.every(...) para no destroy e pausa com active = False. O every do módulo levanta DestroyedError no próximo disparo e não pausa quando o objeto é estacionado. every solto cabe em mouse ou no corpo de um modo.
o framework aposenta o timer em silêncio
Teleporte continua o traço
rastro.clear() junto da nova posição — senão o próximo sample desenha um risco desde onde estava.
o framework adivinha que o salto foi um teleporte
snapshot depois do destroy, ou de uma fonte destruída
DestroyedError. Cancele o handle ao destruir o Contrail; amarre o timer na fonte com e.every.
nada acontece / último ponto
Círculos / segmentos no Contrail
use History e add(Circle(...)) / add(Line(...)).
shape_factory= / add no Contrail
Text
Um texto na tela — placar, legenda, título. Fica preso à janela (HUD) ou no mundo, junto dos objetos, conforme a colocação (tabela abaixo). A caixa acompanha o texto (ou tem width= fixa) e cresce para baixo.
Text("SCORE") # HUD, na coluna automática
hud = Text("", top_left=(20, 780), font_size=20)
hud["text"] @= lambda: f"Stars {st.score}"
Text("HP", center=ship.center) # no mundo, anda com a câmera
Text("GAME OVER\nPress SPACE", center=(500, 400))
Colocar com
Onde fica
nada (só o texto)
Camada ui, na coluna automática — a mesma pilha dos widgets. stack=False fica fora da coluna.
top_left= / top_center= / top_right=
Prende esse ponto da caixa; o texto cresce para longe dele (sempre para baixo) — uma linha nova ou font_size maior não empurra o topo. Com layer omitido: camada ui (e avança a coluna). Com layer="default": legenda no mundo.
center=
Sem layer=: no mundo (default), segue a câmera. Com layer="ui": na janela.
box=
Centro da caixa no centro da região, largura da região. Fora da coluna. Prefira canvas.ui_view.
Linhas.\n é quebra dura (altura = n × métrica). Sem width= a caixa acompanha a linha mais larga; com width=, o excesso corta (sem quebra automática). align= alinha cada linha dentro da caixa e nunca move a âncora. A coluna HUD não se reorganiza se o número de linhas mudar depois da construção.
1º: o texto, ou uma célula que ele mostra (st["score"]). Omita center=/layer= → camada ui + pilha automática (a mesma dos widgets). center= sem layer → mundo default. Um ponto (top_left= …) na camada ui também avança a coluna. Não existe x1= / y1= / pos=: o erro aponta o ponto.
Parâmetro
Passar
Tipo (default)
Significado
text
1º
str, célula ou função"SCORE"
O texto; ou uma célula (st["score"]), que o Text mostra e acompanha num sentido só (como t["text"] @= st["score"]: t.text = … levanta LinkedAttributeError; para mudar o que aparece, mude a célula); ou uma função, que liga. Um número levanta erro: escreva um texto (Text(str(n))). Text(st.nome) é o texto de agora e não acompanha; a célula é st["nome"].
top_left= / top_center= / top_right=
nome=
ponto(20, 780)
Um só. Prende esse ponto da caixa (topo visual); o texto cresce para baixo e para longe dele — uma linha nova ou font_size maior não empurra o topo. Sem nenhum: pilha da coluna. layer omitido → ui (avança a coluna); layer="default" → legenda no mundo, fora da coluna. Valor copia; lambda liga, como Rect(top_left=): top_left=lambda: panel.top_left, ou tupla mista (lambda: mouse.x, 760) (só x liga). Um Point passado é copiado. Mover (move_by, move_to, time=, tween, arrastar) leva o ponto junto; move_to(x, y) põe o meio da string atual em (x, y). Com âncora ligada, mover levanta LinkedAttributeError (use t["top_left"].unlink()).
width=
nome=
número120
Caixa fixa. Omitido → acompanha a string. Excesso corta (sem wrap). \n é quebra dura (altura = n × métrica da linha). A coluna não se reorganiza se o número de linhas mudar depois.
align=
nome=
str"left"
"left" / "center" / "right" (ou XPos.LEFT / XPos.CENTER / XPos.RIGHT). Alinha cada linha dentro da caixa (width= ou a linha mais larga); nunca move a âncora. Padrão segue a âncora: top_left → "left", top_right → "right", top_center / center= / box= → "center". Qualquer combinação vale (ex.: bloco em top_right= com linhas à esquerda).
stack=
nome=
boolTrue
False: o Text fica fora da coluna automática do HUD (útil para um contador de FPS). Padrão True no HUD.
center=
nome=
ponto(400, 300)
Pose no mundo (ou na janela se layer="ui") — centro da caixa
box=
nome=
Boxcanvas.ui_view.inset(24)
Centro da caixa no centro da região (center= na ui), width da região se omitido. Fora da coluna. Não misture com center= / top_left= …. Prefira ui_view. box=.
font= / .font
nome=
str"pixel.ttf"
Família instalada, ou arquivo .ttf / .otf (mesma busca que imagem). Omitir usa ui.font. .font devolve o nome da fonte (texto). Atribuir remede. Fonte ausente: aviso e a padrão. Sem bold / italic.
font_size=
nome=
número13
Altura da letra, em unidades de design. Omitido: o do tema (13 × ui.scale). Aceita lambda:.
color=
nome=
cor"white"
Cor das letras. Depois: set_color(…) ou .fill = Gradient(…).
angle=
nome=
número0
Giro em graus, em volta do centro da caixa. Aceita lambda:.
background=
nome=
cor"maroon"
Placa opaca atrás do texto, do tamanho da caixa.
glow= / .glow
nome=
bool ou cor ou GlowTrue
Único complemento. True / cor / Glow / CheapGlow. Sem visual .visual.
.fill / .line
—
cor ou GradientGradient("sunset")
Preenchimento / contorno (Gradient ok)
.top_left / .top_center / .top_right / .center
—
Pointscore.top_right
Pontos da caixa atual como desenhada — girada e escalada pelo centro (a leitura já vê a string de agora). A âncora escreve; os outros são derivados (escrever levanta erro). rotate_by / scale_by giram pelo centro e levam a âncora junto (âncora ligada com lambda levanta erro; force=True desliga). Text não espelha: flip_x / flip_y = True levanta erro (texto espelhado não se lê). Para seguir: Line(p1=lambda: score.top_right, …).
.box
—
Boxhud.box.height
Cópia da caixa de layout (medir, colocar ao lado).
Não confundir
Parece
É
Não é
Text(…, layer="top") fica preso à janela
A camada "top" ainda anda com a câmera. HUD: layer="ui", ou omita center=.
HUD
align="right" prende a borda direita
align= só alinha as linhas dentro da caixa. Para prender a borda direita: top_right=.
SpatialFx é um efeito que existe num lugar do mundo — uma fogueira, chuva numa área, um portal, um jato. Como um Shape, o construtor já põe o efeito na tela, e ele tem camada, grupos, timers, destroy() e pode colidir (pela área que ocupa). Diferente de um Shape, não tem contorno exato nem pode ser clicado ou arrastado.
center= e o tamanho em radius= (o Fire de jato usa width=)
Área
Rain
Um retângulo: p1= / p2= (cantos opostos) ou center=, width=, height=
Feixe
Jet
Um segmento: p1= / p2=, ou origin= com angle= e length=
Depois de criado, os parâmetros mudam por atribuição (fogo.amount = 2) e valem no próximo frame. Para mover, escreva ou ligue o centro: fogo.center = (300, 120), fogo["center"] @= lambda: tocha.center ou tween(fogo.center, (300, 120), time=1) — SpatialFx não tem move_to / rotate_to. Todos aceitam soft_edge=: a largura da borda que esmaece, como fração do tamanho (0 = borda dura; cada efeito tem o seu padrão). Não confundir com um visual (pinta um objeto), um Post (a tela inteira) ou um Impacto (um estouro que some sozinho).
Sempre com nome: Fire(200, 150), at= e x= levantam TypeError com a grafia certa (center=). shape.visual = Rain() e canvas.effects.add(Fire()) também levantam TypeError: o efeito já está no mundo assim que é criado.
Como ler os parâmetros.color= tinge o efeito; omitido, cada efeito usa a sua cor própria (fogo laranja, Warp azul…). amount= é a intensidade: 1 é o padrão, 0 apaga, 2 é forte. speed= é o ritmo da animação: 1 normal, 2 o dobro, 0 congela. center=, radius=, width= e length= estão em unidades de design. Todos mudam depois por atribuição: fogo.amount = 2.
Entity
Entity() cria um objeto do jogo que não desenha nada: um diretor de ondas, um gerador de inimigos, um relógio. Ele tem tudo o que os outros objetos têm — campos próprios, timers, mode, grupos, on_update — menos o desenho. Shape, Sprite e SpatialFx também são Entity; este construtor é o que não pinta.
director = Entity()
director.after(1.5, start_wave, name="wave")
class Director(Entity):
def setup(self):
self.wave = 0
def on_update(self, dt):
if ready("spawn", 1.2):
start_wave()
Construtor Entity
Entity(layer=, group=)
Nome com =: passe pelo nome.
Parâmetro
Passar
Tipo (default)
Significado
layer
nome=
str ou NoneNone
None = fora da lista de desenho (o usual aqui).
group
nome=
str ou lista ou None
Tag, ou lista de tags. Ex.: group="wasp".
class Spawner(Entity):
def setup(self):
self.timer = 0
spawner = Spawner(group="spawners")
# Sem desenho (Entity puro): só timers, mode e on_update.
# group deixa achar todos com group["spawners"].
Neste Entity
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.layer / .group
str"hud"
lê / escreve
.layer (str ou None), .group (escreva str ou lista; lê um frozenset só leitura — .group.add levanta AttributeError, use tag / untag). Os mesmos de todo objeto na tela.
active
boolFalse
lê / escreve
Estaciona: esconde e pausa. Sem desenho, visible quase não importa. Para remover: destroy().
mode
mode ou Nonee.mode = e.diving
lê / escreve
Capítulo de comportamento. Ver Mode. Mesmo mode não faz nada.
campos próprios
Attributeself.hp = 3
lê / escreve
Atributo criado no setup (self.hp = 3) vira Attribute. Guardar valores.
Métodos
Método
Assinatura
Retorna
Significado
destroy()
Sem parâmetros
—
Remove. Sem desenho, visible quase não importa. .active vira False.
cancel_process()
Sem parâmetros
self
Cancela timers / tweens / sons / mode; Entity continua viva. Mode vira None sem @exit_mode. Ver escopo.
goto_mode(fn)
Parâmetros: fn (função do capítulo)
—
Força saída+entrada, mesmo se já estiver nesse mode. Ver Mode.
after / every
Parâmetros: segundos (número), fn (função)
Handle
Timers neste Entity — sobrevivem a troca de mode; morrem no destroy / cancel_process. Ver escopo.
tween
Parâmetros: target (alvo, Attribute ou Point), to (mesmo tipo do valor atual)
Handle
Interpola Attribute / Point neste Entity (número, tupla, Vec2, Color…). Ver tween.
setup(self)
Parâmetros: self (este objeto)
—
Depois do construtor. Aqui nascem os campos próprios: self.hp = 3. Escreva setup, não __init__: um __init__ que não chama super().__init__(**kwargs) levanta TypeError ao criar o objeto.
Uma vez, de dentro de destroy(). Não dispara em troca de mode.
Subclasse de Shape / Sprite / SpatialFx / Text
Parâmetros na linha class viram o padrão daquele tipo. Pose (center, layer) fica na instância. Vale igual para um efeito espacial — class Energia(Orb, radius=16, color="gold", group="energia") — e para um texto: class Placar(Text, font_size=32, color="gold", group="hud"), e Placar("0", top_left=(20, 580)).
O texto e a posição (centertop_left … boxlayerstackangle)
—
Widgets, Entity, Endless: sem parâmetros de classe (erro com a correção); passe na instância
SpatialFx: os parâmetros do tipo (radiusamountcolorlength…), groupsoft_edge
Pontos (centerp1p2origin); style= de Fire — escolhe outra classe: escreva Fire(style=…)
Não confundir
Parece
É
Não é
class Bolt(Circle): radius = 8
Parâmetros da linha class, não atributos de corpo. Um valor no corpo com um nome do Scena (radius = 8, visible = False, group = "x") levanta TypeError na definição da classe, com a correção: a linha class (class Bolt(Circle, radius=8)) ou o setup (self.visible = False).
—
def __init__(self): numa subclasse
Substitui o construtor do Scena. O que o objeto faz ao nascer vai em def setup(self):, que o Scena chama com o objeto pronto. Um __init__ seu (para parâmetros próprios) recebe **kwargs e chama super().__init__(**kwargs); sem essa chamada, criar o objeto levanta TypeError com a correção.
Como um objeto aparece na tela: a cor e o material (cor sólida, degradê, imagem), como ele se mistura com o fundo, os efeitos permanentes (visual, complementos, filtros) e os passageiros (transientes). Cor, Gradient e Texture só aparecem quando aplicados num objeto — em .fill, .visual ou set_color.
Três jeitos de pintar: uma cor sólida (Color ou só o nome, "coral"), um degradê (Gradient) ou uma imagem (Texture). Sozinhos não aparecem: vão em .fill, .visual, set_color ou canvas.background. RGB em 0–1. Nome quase certo ("blu"): aviso + a cor mais próxima ("blue"). Nome desconhecido: aviso + magenta, com Did you mean. Para escolher cores (rampas, paletas): Paletas e rampas.
Onde o framework pede cor (color=, set_color, Glow("cyan"), canvas.background = "navy", Text(..., color=) / background=), vale qualquer uma destas formas:
Harmonias de Color: triadic, analogous, shades, contrast_color, muted_version.
Neste Color
Imutável: lighter / with_alpha / jitter devolvem um Color novo. Uma cor é compartilhada (todas as formas com a cor padrão usam a mesma), então cor.alpha = 0.5 levanta AttributeError com a correção: cor.with_alpha(0.5) ou forma.set_alpha(0.5).
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.rgb
tupla(1, 0.8, 0, 1)
leitura
Tupla (r, g, b, a) em 0–1.
.alpha
número0.8
leitura
Alfa 0–1. Outro alfa: with_alpha(a) (Color novo) ou forma.set_alpha(a).
O nome vira uma cor? Aceita as mesmas grafias de Color(nome) (maiúsculas, hífen, junto ou separado, dark / light / faded na frente: "lightcyan" é "light cyan"; erro de digitação próximo vira a cor mais perto, com aviso). get devolve a tupla 0–1; nome desconhecido: aviso + magenta, como Color.
Color.random(saturation=, value=, alpha=)
Parâmetros: saturation / value (número ou (lo, hi)), alpha
Parâmetros: color1 (Color), color2 (Color), t (número), color_space (ColorSpace)
Color
Mistura. t tipicamente 0–1. Padrão ColorSpace.OKLAB (uniforme na percepção — RGB puro passa por cinza/marrom no meio). Também: HSL / HSV (caminho mais curto no círculo de matiz; uma ponta cinza usa a matiz da outra, então branco → azul fica azulado) · RGB.
a.lerp(b, t)
Parâmetros: b (Color), t (número)
Color
Atalho de instância para Color.interpolate (OKLAB). Usado por lerp / tween.
complementary()
Sem parâmetros
Color
Cor oposta no círculo.
Gradient("sunset")
Gradient("navy", "cyan", angle=90)
Gradient("plasma", type="radial")
Gradient("heat", type="radial", size=700)
c.fill = Gradient("sunset", angle=-90)
bloco.visual = Gradient("ruby") # fill + contorno mais escuro
g = Gradient("ocean").darker(0.15) # nova rampa (cada stop)
Neste Gradient
Gradient(*cores, type="linear", size=, angle=0)
Nome sem =: cores na ordem (preset, duas cores, ou rampa). O resto pelo nome.
Parâmetro
Passar
Tipo (default)
Significado
*cores
1º, 2º…
str ou Color"rdyl"
Um nome de preset, ou a rampa. Uma cor: escuro → claro.
type
nome=
str"linear"
linear · radial · sweep.
angle
nome=
número0
Graus. Linear e sweep. 0 = da esquerda para a direita; 90 = de baixo para cima (a primeira cor embaixo); -90 = de cima para baixo. O mesmo numa forma e no Wallpaper. Os presets de céu ("sunset", "day") começam pelo alto: angle=-90.
size
nome=
número ou Nonecabe no objeto
Em unidades de design. Linear: a distância do centro até cada ponta do degradê. Radial: o raio. Sweep ignora.
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.type
str"linear"
leitura
Só leitura. "linear" · "radial" · "sweep". O mesmo valor passado em type=.
Métodos
Método
Assinatura
Retorna
Significado
darker(amount=0.1)
Parâmetros: amount (número)
Gradient
Nova rampa mais escura: cada stop com Color.darker(amount). amount padrão 0.1 (quanto sobe/desce a luminosidade HSL). Tipo, size e angle ficam iguais. Uso: g2 = Gradient("ocean").darker(0.2) · bloco.visual = g.darker().
lighter(amount=0.1)
Parâmetros: amount (número)
Gradient
Nova rampa mais clara: cada stop com Color.lighter(amount). amount padrão 0.1. Mesma geometria da rampa. Uso: g2 = Gradient("navy", "cyan").lighter(0.15).
mean_color()
Sem parâmetros
Color
Média ponderada dos stops (integral RGB ao longo da rampa). Uso: c.line = g.mean_color().darker(0.2).
set_alpha(alpha)
Parâmetros: alpha (número 0–1)
Gradient
Alfa em todos os stops (mutável, in-place: muda as formas que usam este Gradient). Diferente de darker / lighter, que devolvem uma cópia. forma.set_alpha(a) dá à forma um Gradient novo e não mexe nas outras.
Material: precisa ser aplicado em .fill, .visual, set_color ou canvas.background. Em Shape de área, visual = Gradient(…) / set_color(Gradient(…)) / Paint(Gradient(…)) preenche e desenha um contorno com mean_color().darker(0.2) (a espessura line_width não muda). Paint(g, line=False) ou Fill omitem o contorno. Texture / ProceduralFill continuam só fill.
No lugar de source: o caminho pelo nome, ou uma imagem Skia já carregada. Raro — o comum é o 1º argumento.
fit
nome=
str"cover"
Como a imagem ocupa a forma: "cover" (cobre tudo, corta o que sobra) · "contain" (cabe inteira, sobra borda) · "stretch" (estica nos dois eixos) · "tile" (repete em ladrilhos de tile_size). Mesmos nomes do Wallpaper.
tile_size
nome=
número ou (l, a)40
Com fit="tile": o tamanho de cada ladrilho, em unidades de design. Com outro fit é TypeError (não mudaria nada).
scale
nome=
número1
Zoom extra na imagem (maior que 0): 2 mostra a imagem com o dobro do tamanho.
angle
nome=
número0
Gira a imagem, em graus, no sentido anti-horário (como angle do Gradient): dentro da forma, ou num Wallpaper em torno do centro da tela (com fit="tile", em torno do ponto (0, 0) do mundo). Girada, uma imagem "cover" deixa os cantos vazios; tile_mode="mirror" os preenche.
shift
nome=
HueShift ou RecolorHueShift(amount=0.33)
Muda as cores dos pixels: HueShift gira o matiz; Recolor troca uma faixa de cor por outra. Ver Complementos / Visual.
color
nome=
cormédia da imagem
Não pinta a imagem: é a cor que os visuais usam como “cor do objeto” (o brilho de um Neon, por exemplo). Omitida: a média dos pixels visíveis.
tile_mode
nome=
str"decal"
O que aparece fora da imagem: "decal" (nada, transparente) · "clamp" (estica a última linha de pixels) · "repeat" (repete) · "mirror" (repete espelhando). Omitido: "repeat" com fit="tile", senão "decal". Em tile, "clamp" vira "repeat".
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.fit
str
leitura
Só leitura. O mesmo valor passado em fit=.
.tile_mode
str
leitura
Só leitura. O mesmo valor passado em tile_mode=.
lago = Rect(center=(300, 120), width=240, height=60)
lago.fill = WaterFill() # água vista de lado
lago.fill = WaterFill(kind="top", speed=0.5) # vista de cima, mais lenta
tocha = Circle(center=(100, 200), radius=30)
tocha.fill = FlameFill()
tocha.fill = WaveFill("teal", amount=0.8) # líquido de qualquer cor
tocha.fill = AuroraFill(color="lime")
tocha.fill = SwellFill()
Neste Procedural
Preenchimentos animados prontos. Vão em .fill (ou set_color); o contorno continua o seu. São o material por trás dos visuais Water / Flame / Swell / Wave / Aurora — use o visual quando quiser o efeito completo (borda, brilho), e o fill quando quiser só o interior.
Parâmetros: kind (nome do shader), amount, speed, seed (números), color (cor)
ProceduralFill
A classe por trás das fábricas. Prefira as fábricas; kind desconhecido levanta ValueError com a lista.
amount é a intensidade (0 = apagado, 1 = padrão); speed multiplica o tempo da animação. Fill(…) só aceita cor sólida: para um visual com procedural, Paint(WaterFill()).
placa = Rect(center=(200, 150), width=120, height=60)
placa.visual = Fill("red") # só preenchimento, sem contorno
placa.visual = Stroke("navy", width=4) # só contorno
placa.visual = Paint(Gradient("sunset")) # rampa + contorno mais escuro
placa.visual = Paint("coral", line=False) # cor sem contorno
Neste Paint / Fill / Stroke
Visuais simples de cor, para usar onde se usaria um visual (shape.visual = …, set_defaults(visual=…)). Não são visuais animados: só decidem fill, contorno e espessura.
Classe
Assinatura
Retorna
Significado
Paint(material, alpha=None, *, color=, line=True)
Parâmetros: material (cor, Gradient, Texture ou ProceduralFill), alpha (0–1), color (o mesmo que material, pelo nome), line (True/False)
Paint
Preenchimento e contorno de uma vez, como set_color: o material pinta o interior e o contorno ganha um tom de contraste. Sem argumentos: steel blue + contorno de contraste. line=False tira o contorno.
Fill(color="steel blue", alpha=None)
Parâmetros: color (cor sólida), alpha (0–1)
Fill
Só preenchimento. Gradient / Texture / visual levantam TypeError — use Paint(Gradient(…)).
Stroke(color="black", width=None)
Parâmetros: color (cor sólida), width (número)
Stroke
Só contorno; width troca o line_width.
Visual e Recipe são as classes por trás de todos os visuais do catálogo (servem para isinstance(v, Visual)). Você não as constrói: escolha um visual pronto.
Nomes de cor
Básicas
black
white
red
green
blue
yellow
cyan
magenta
Cinzas
gray
grey
dim gray
dim grey
silver
Vermelhos
indian red
salmon
crimson
firebrick
maroon
coral
Rosas
pink
hot pink
deep pink
Laranjas
orange
peach
tomato
Marrons
brown
chocolate
sienna
tan
sandybrown
sandy brown
peru
Amarelos
gold
goldenrod
khaki
lemonchiffon
green yellow
Verdes
forest green
lime
lime green
lawn green
yellow green
sea green
olive
teal
sage
emerald
aqua
Azuis
navy
royal blue
sky blue
deep sky blue
dodger blue
steel blue
cadet blue
cornflower blue
turquoise
Roxos
purple
violet
blue violet
indigo
lavender
plum
orchid
medium orchid
Metálicos
silver
bronze
copper
Presets de Gradient
Rampas harmônicas
emerald
ruby
safira
copper
amethyst
deep_ocean
outono
bliz
retrowave
floresta
sakura
citrus
vaporwave
moss
nebula
tropical
cyberpunk
mediterranea
drosa
synthwave
aurora
candy
gold
lava
acid
mana
rainbow
linho
azul_ambar
gotic
terrasol
campo
oliva_areia
azul_creme
creme_ambar
azulejo
marmore
terracota
plasma
magma
vrds
ocean
ylorrd
white_blue
bugn
inferno
heat
grayscale
red
orange
yellow
green
blue
cyan
purple
magenta
rdyl
Através do branco / especular
neon
beam
flare
chrome
Escalas
seismic
coolwarm
cool
brbg
rdbu
health
Cênicas
day
dawn
sunset
dusk
night
space
overcast
desert
forest
toxic
ember
ice
underwater
Paletas temáticas
carrara
deep_blue
mata
vitral
sema
marroco
8bit
papagaio
especiarias
klimt
lisboa
concreto
monet
toscana
chartres
sumie
Não confundir
Parece
É
Não é
Color(255, 128, 0)
ValueError: RGB é 0–1. Divida por 255, ou use hex ("#ff8000").
0–255
line.set_color("red") pinta o preenchimento
Numa Line, set_color pinta o traço.
fill
Paletas e rampas
Ferramentas para escolher cores que combinam (elas não pintam nada). Colormap é uma rampa contínua: peça a cor em qualquer ponto t de 0 a 1. Palette são N cores tiradas da rampa, acessadas por índice que dá a volta. As duas devolvem Color e usam os mesmos presets de Gradient.
Rampa de cores que você amostra (não desenha sozinha). É a tabela por trás de Gradient("plasma") e de Palette("plasma"). Crie a rampa, depois peça uma cor em t (0–1). Interpolação padrão: ColorSpace.OKLAB.
Mesmos presets de Gradient, ou um nome de cor (escuro → claro). Padrão OKLAB.
Colormap([(t, cor), …], color_space=)
Parâmetros: colors (lista de pares: posição 0–1, cor), color_space (ColorSpace.OKLAB)
Colormap
Paradas explícitas. Padrão OKLAB.
get_color(t)
Parâmetros: t (número 0–1)
Color
Cor na posição t da rampa.
random()
Sem parâmetros
Color
Atalho de get_color(random.random()) — cor aleatória entre as paradas.
Palette
Amostras discretas de uma rampa. Não desenha sozinha — devolve Color. Mesmos presets de Gradient. Padrão: 6 pontos, interpolação OKLAB. O índice dá a volta (módulo); get_next() avança um cursor próprio.
Nome sem =: preset, lista de cores, ou cores na ordem. steps e color_space pelo nome.
tints = Palette("plasma") # 6 cores do preset
tints = Palette("plasma", steps=8)
# lista ou argumentos soltos — mesma rampa
Palette(["red", "yellow", "blue"])
Palette("red", "yellow", "blue", steps=5)
# índice cíclico: qualquer contador serve
hud.fill = tints[wave_number]
# ou sequencial, sem guardar índice
def spawn():
Circle(fill=tints.get_next())
# a partir de um Gradient / Colormap já criado
accents = Palette(Gradient("sunset"), steps=4)
Parâmetro / método
Assinatura
Retorna
Significado
steps=
número inteiro, padrão 6
—
Quantas cores a paleta tira da rampa, igualmente espaçadas do começo ao fim.
color_space=
ColorSpace, padrão OKLAB
—
Como as cores intermediárias são misturadas. ColorSpace.OKLAB (padrão) dá passos que parecem iguais ao olho; RGB passa por tons acinzentados no meio. Também HSL / HSV.
Palette(preset)
Parâmetros: preset (str), steps=6, color_space
Palette
Amostra o preset (mesma tabela de Gradient). Sem argumentos: "plasma".
Palette(["red", "yellow", "blue"])
Parâmetros: lista ou *cores, steps, color_space
Palette
Rampa com essas cores, depois steps amostras. Também: paradas [(t, cor), …], um Colormap ou um Gradient.
palette[i]
Parâmetros: i (int)
Color
Amostra no índice. Dá a volta: palette[6] == palette[0] se steps=6. Fatia (palette[:3]) não dá a volta.
get_next()
Sem parâmetros
Color
Próxima amostra; cursor próprio, cíclico. for cor in palette percorre uma vez só (não é este cursor).
reset(index=0)
Parâmetros: index (int)
Palette
Reposiciona o cursor de get_next (módulo). Encadeável.
random()
Sem parâmetros
Color
Uma das amostras, ao acaso (discreto). Colormap.random() amostra o contínuo.
Color.random() = matiz qualquer (viva). ramp.random() = amostra contínua desta rampa. palette.random() = uma das N amostras. color.jitter() = variação perto de uma cor.
o mesmo sorteio
Palette("plasma")
Amostra a tabela compartilhada de presets (igual Colormap.from_preset)
Não constrói um Gradient — Gradient pinta; Palette devolve Color
palette[i]
Índice cíclico (nunca IndexError em int)
Lista comum — fatia não dá a volta; for é finito
Blend
Blend escolhe como as cores de um objeto se combinam com o que já está desenhado atrás dele: por cima ("normal", o padrão), somando luz ("add", para lasers e brilhos), escurecendo ("multiply", para sombras)… Mesmo ajuste em Shape e Sprite: .blend = "add".
No Shape, mistura fill e line. No Sprite, mistura o imagem (o PNG). Leitura devolve a mesma string. Não liga com @=.
# Shape: traço de luz (some com o fundo)
raio = Line(p1=(120, 80), p2=(280, 240), color="cyan")
raio.set_alpha(0.3)
raio.blend = "add"
# Sprite: PNG mais brilhante sobre o destino
ship.blend = "add"
# Tinta / sombra (multiplica as cores)
mancha.blend = "multiply"
ship.blend = "normal" # volta
"add" clareia até PNG opaco (soma RGB onde se sobrepõem). PNG com alfa (vidro, buracos) entra na mistura. sprite.alpha é outro ajuste: deixa a imagem transparente (0–1) sem mudar a fórmula — os dois combinam.
Do dia a dia
Nome
Faz
"normal"
Padrão. Fonte por cima, honrando o alfa.
"add"
Soma as cores. Laser, tiro, brilho, partículas de luz. Estoura para branco onde se sobrepõem.
Inverso do multiply. Clareia. Fumaça, glow suave (estoura menos que add).
Contraste
Nome
Faz
"overlay"
Multiply no destino escuro, screen no claro.
"hard-light"
Como overlay, com a fonte no papel de luz dura.
"soft-light"
Versão suave do hard-light.
Mais escuro / mais claro
Nome
Faz
"darken"
Fica o canal mais escuro de fonte e destino.
"lighten"
Fica o canal mais claro.
Comparação
Nome
Faz
"difference"
Diferença absoluta. Invert / compare.
"exclusion"
Como difference, com menos contraste.
"subtract"
Subtrai a fonte do destino. Hoje pinta igual a "difference".
Clareia / queima
Nome
Faz
"color-dodge"
Clareia o destino conforme a fonte. Brilho forte.
"color-burn"
Escurece o destino conforme a fonte.
HSL (cor da fonte, resto do destino)
Nome
Faz
"hue"
Matiz da fonte; saturação e luminosidade do destino.
"saturation"
Saturação da fonte; o resto do destino.
"color"
Matiz + saturação da fonte; luminosidade do destino.
"luminosity"
Luminosidade da fonte; cor do destino.
Nome desconhecido: TypeError com a lista. Não liga com @=. Use =. look_like / save_visual / restore_visual copiam o blend. Um visual (Halo, Neon…) pode reescrever o blend do fill enquanto está ligada; visual = None devolve o seu. sprite.alpha / set_alpha some só a imagem — em Shape, set_alpha mexe no alfa do fill/line. Ver Sprite — aparência.
Complementos
Complementos são cinco efeitos que se somam à aparência do objeto, cada um na sua propriedade: brilho em volta (.glow), sombra (.shadow), rastro de movimento (.tail) e, só em Sprite, matiz girado (.hue) e tons de cinza (.gray). Ligam-se com uma atribuição e convivem com o visual e entre si.
Propriedade
O que faz
Em
Atalhos
.glow
Halo de luz em volta do corpo.
Shape, Sprite, Text
True = brilho na cor do objeto · uma cor ("cyan") · Glow(…) / CheapGlow(…) para ajustar
.shadow
Sombra deslocada para baixo e à direita.
Shape, Sprite
True · uma cor · Shadow(…)
.tail
Cópias fantasmas atrás do objeto quando ele anda.
Shape, Sprite
True · uma cor · Tail(…)
.hue
Gira as cores do PNG no círculo de matiz (vermelho → verde → azul).
Sprite
um número de voltas (0.33 = um terço: vermelho vira verde) · True = 0.33 · HueShift(…)
.gray
Tira a cor (botão desligado, personagem petrificado).
Sprite
True = todo cinza · um número de 0 a 1 (0.5 = meio cinza) · Grayscale(…)
Para desligar, atribua False ou None. Ler a propriedade devolve o objeto de ajuste (Glow, Shadow…) ou None. visual = None tira só o visual: os complementos ficam. save_visual() / restore_visual() guardam e devolvem os complementos junto. CheapGlow também vai em .glow (não há .cheapglow); atribuir Glow / Shadow a .visual levanta TypeError com a dica.
tail = True e Tail(…)semangle= seguem o movimento (rastro atrás da velocidade; some ao parar). Tail(angle=180) trava a direção (90 = para cima). angle / size / amount aceitam lambda: — reavaliam todo frame. Não use ship.visual = Glow().
Como ler os parâmetros.radius= e offset= em unidades de design; sigma= é quanto borra (maior = mais suave); amount= é a intensidade (1 = padrão); alpha= é a opacidade, de 0 a 1.
Visual
Um visual é uma aparência pronta, quase sempre animada, que muda o jeito de um objeto ser desenhado — neon, holograma, metal, água, escudo de energia. Cada objeto tem um visual de cada vez, em .visual: ship.visual = Neon(). Sem argumentos já funciona, na cor do objeto. As exceções têm paleta própria: Metal() é cromado e Shield() é a bolha verde-limão, qualquer que seja a cor do objeto (e a cor do objeto não muda); Metal("gold") / Shield("cyan") dão a cor. None tira o visual e volta à aparência de antes (os complementos ficam). save_visual / restore_visual restauram a aparência na tela (inclui complementos).
Há visuais de corpo (Neon, Gel, Sphere, Aurora, Metal, Halo(style="fill")…) e de contorno (Rim, Laser, Ink, Halo, ElectricBolt, Tracer…). Sem color=, um visual de corpo segue a cor do objeto e um de contorno segue o contorno do objeto (.line). A cor do objeto (color=, set_color) dá um contorno derivado dela — mais escuro numa cor clara, mais claro numa escura —, e é esse que o visual de contorno segue: Circle(color="red") com Laser() dá um laser rosado; para um laser vermelho, Laser("red") ou .line = "red". color= no construtor pinta o visual e fica no objeto: num visual de corpo vira a cor do objeto, num de contorno vira só o contorno (Rim("red") é o mesmo que .line = "red" e depois Rim(); tirado o visual, o interior continua o seu). Omitido, segue o objeto (ou a paleta própria, em Metal e Shield). Escrever .fill / .line, .line_width, .blend ou .dash antes ou depois de pôr o visual dá o mesmo desenho. Um visual que fixa a sua espessura, blend ou tracejado (Rim, Halo, Shield, Bolt…) mantém o seu enquanto está posto — a borda do Rim muda com Rim(width=) — e o valor escrito aparece quando o visual sai (visual = None). A regra: um visual que aceita color= mostra set_color — set_color / .color = recoloram todo visual que aceita color=, criado com cor ou não (Neon("red"), Laser, Rim, Halo(style="line")…, Metal, o Shield bubble); um visual sem color= guarda a cor e avisa. Visual não é transiente. Halo em massa: .glow, não N× Neon.
Cada efeito declara se vale em Shape, Sprite ou nos dois (chips abaixo e etiqueta no painel). Na assinatura, nome sem = é o 1º, 2º… na ordem (Neon("cyan")). Nome com = vai pelo nome (amount=1.2).
visual = None ≠ restore_visual(): None volta à aparência sem visual (complementos ficam); o par save / restore volta exatamente à aparência guardada (com complementos). Um slot por objeto; use name= no after para renovar a duração.
Como ler os parâmetros.color= tinge o efeito; omitido, segue a cor do objeto. amount= é a intensidade: 1 é o padrão, 0 apaga, 2 é forte. speed= é o ritmo da animação: 1 normal, 2 o dobro, 0 congela. width= é uma espessura em unidades de design. Visuais que pintam cor própria (brilho, borda, corpo) aceitam color= como 1º argumento; materiais procedurais (Flame, Swell, Water) e operadores de pixel (Grayscale, HueShift, Recolor, EmissiveBloom) não — mude a cor com set_color no objeto.
Transientes
Transientes são efeitos curtos num objeto: piscar ao levar dano, crescer de repente e voltar ao tamanho, sumir ao morrer, surgir ao nascer. Você chama o verbo (ship.flash(0.12)), ele acaba sozinho e a aparência permanente (.visual) não muda. Cada tipo de efeito tem um canal: um efeito novo no mesmo canal substitui o anterior, e canais diferentes rodam juntos. Vale em Shape e Sprite.
Primeiro número = duração. O resto em nome — inclusive style= (quando houver) e on_done= (callback ao terminar ou cancelar). Abas abaixo: assinatura e parâmetros de cada verbo.
O primeiro número é sempre a duração em segundos. Cor, escala, intensidade, quantidade, style vão em nome: pop(scale=1.4), flash(intensity=0.6), dissolve_out(style="noise"). style de dissolve: noise · vortex · wipe · fade. Não use visual = Neon() só para um flash. Um efeito novo no mesmo canal substitui o que estava em curso e chama o on_done do velho na hora (o mesmo on_done do fim normal — ele não recebe aviso de que foi cortado). Gatilho de fim: Ao terminar.
morte: visible=False; spawn: mostra no começo; on_done
flash
flashblink
limpa; on_done
text
flash_text(text, duration=1, on_done=None) — só Text e widgets. Mostra text por duration segundos no lugar do texto de sempre (placar.flash_text("+100", 0.5))
volta a mostrar o texto de sempre (se o texto está ligado a um campo, continua ligado); on_done
pulse
pulse_outline
limpa; on_done
pop
pop
limpa; on_done
knock
knock
limpa; on_done
spark
spark_burst
limpa; nunca esconde; on_done
—
clear_transients
cancela todos; chama o on_done de cada um
Como ler os parâmetros. O 1º número é sempre a duração, em segundos. intensity= / amount=: 1 é o padrão. on_done= é a função chamada no fim (ou quando outro efeito do mesmo canal o interrompe).
Filtros
Um filtro trata os pixels já desenhados de um objeto — desfoca, projeta sombra, engrossa, faz brilhar. Ele vai numa de duas propriedades: fill_filter age no interior, line_filter no contorno (ou no traço de uma Line). Vale em Shape, Sprite e Text.
Para efeitos prontos, prefira o que já existe: o brilho e a sombra comuns são os complementos.glow / .shadow; uma aparência animada é um visual; um efeito na tela inteira é um Post. O filtro é o ajuste fino de um objeto só. None tira o filtro. Uma mesma instância pode ir em muitos objetos (halo = GlowFilter(…) numa frota inteira sai mais barato que um por objeto). Dois filtros juntos: input= de um recebe o outro, ou ComposeFx(de_fora, de_dentro). Um visual ou um SpatialFx aqui levanta TypeError.
Como ler os parâmetros.radius=, offset= e sigma= em unidades de design (sigma é quanto borra). input= encadeia: o filtro age sobre o resultado de outro (Blur(sigma=2, input=DropShadow())).
O filtro vai em fill_filter (interior) ou line_filter (contorno). Para dar brilho a muitos objetos, use o complemento .glow ou um único GlowFilter compartilhado — não um visual Halo em cada um.
O que é da tela inteira, não de um objeto: o fundo, os efeitos sobre a imagem toda, os estouros de um instante, a câmera, a janela e a qualidade, as camadas e os grupos.
O fundo da tela, desenhado atrás de tudo. Tem uma base — cor, degradê, céu ou imagem; sempre uma — e extras empilhados por cima: estrelas, nuvens, névoa, poeira. Cada fundo aparece assim que é criado; guarde o retorno para mudar parâmetros, esconder ou tirar. Uma cor, um Gradient ou uma Texture viram base atribuídos a canvas.background.
SkyGradient("night")
Stars()
Stardust()
Chromadust()
Nebuladust()
cl = Clouds(amount=0.45)
cl.amount = a * b + 0.5
cl.enabled = False
cl.remove()
canvas.background.remove(cl) # igual
canvas.background.clear() # só extras; a base fica
canvas.set_fly(Tunnel())
Wallpaper(Gradient("black", "navy", angle=90), pan_response=0.05)
Wallpaper("bg1.jpg", tile_mode="mirror", pan_response=0.15)
Poeira espacial: Stardust (duas cores), Chromadust (paleta que gira; reflect=True faz vai-e-volta), Nebuladust (três cores em manchas). Não são SpatialFx Starburst, o post Chroma nem Nebula (faixas).
canvas.background
A atribuição substitui a base. Todo fundo (Wallpaper, SkyGradient, Stars, Tunnel…) já aparece ao ser criado. Gradient / Color / Texture precisam ser aplicados aqui (ou dentro de Wallpaper). Arquivo de imagem: Wallpaper("bg1.jpg"). Parâmetros do construtor atribuem na instância e valem no próximo frame — exceções em Wallpaper / SkyGradient abaixo.
Ler devolve o fundo inteiro (base + extras), o vivo — não uma cópia. Atribuir uma cor, Gradient ou Texture troca só a base; atribuir o que se leu (canvas.background = canvas.background) não muda nada. Para guardar o fundo e voltar a ele depois, guarde a base: antes = canvas.background.base … canvas.background = antes.
.base
WallpaperColor("navy")
lê / escreve
O papel de parede. Sempre tem um.
.enabled
boolFalse
lê / escreve
Esconde só os extras. A base ainda desenha.
Métodos
Método
Assinatura
Retorna
Significado
.add(field)
Parâmetros: field (um fundo: Stars, Clouds…)
o fundo
Empilha um extra. Se ele já está na pilha, não faz nada. Serve para um fundo criado com Stars.temp(…) (cria sem empilhar) ou tirado com remove. Devolve a instância.
.remove(field)
Parâmetros: field (um fundo: Stars, Clouds…)
—
Tira um extra. Chamar de novo não faz nada. Igual a field.remove(). A base não sai por aqui.
.clear()
Sem parâmetros
—
Tira só os extras. A base fica.
.reset_base()
Sem parâmetros
—
Base volta ao preto padrão.
list(canvas.background)
Parâmetros: a pilha (canvas.background)
lista
Só extras, não a base.
canvas.set_fly(Tunnel())
Parâmetros: field (um fundo, ou None)
—
Limpa extras e fica com aquele fundo. None = só clear.
Wallpaper
A base do fundo: uma cor, um degradê, uma imagem ou uma Texture cobrindo a tela inteira. Criar um Wallpaper já o põe como base (canvas.background.base).
Nome sem =: 1º na ordem (cor, gradient, texture ou arquivo). O resto pelo nome. Sem args: preto opaco. String com . ou / é arquivo; senão é cor. Parâmetros de imagem só valem para arquivo. Criar um Wallpaper já o põe como base. Busca: Arquivos de imagem.
O material da base. Nome de cor, tuple, Color, Gradient, Texture, arquivo ("bg1.jpg") ou Image. Uma Texture mantém aqui o que faz numa forma: angle, shift e set_alpha. Visual / SpatialFx / filtro levantam TypeError. Ex.: Wallpaper("navy").
filename
nome=
strNone
Em vez de source: caminho do arquivo.
image
nome=
ImageNone
Em vez de source: skia.Image já carregada.
fit
nome=
str"cover"
Só imagem: cover · contain · stretch · tile.
tile_mode
nome=
str"decal"
Só imagem: o que aparece além da borda da imagem quando a câmera anda ou afasta o zoom. "decal" (nada; aparece o underlay) · "clamp" (estica a última linha de pixels) · "repeat" (repete) · "mirror" (repete espelhando, sem emenda). Omitido: "repeat" com fit="tile", senão "decal". Em tile, "clamp" vira "repeat".
scale
nome=
número1
Só imagem: zoom extra. Tem que ser > 0.
tile_size
nome=
número ou par de números¼ do lado menor
Só imagem / fit="tile": célula em unidades de design. Com outro fit é TypeError.
underlay
nome=
corpreto
Só imagem: cor das faixas que sobram em fit="contain" e de tudo além da borda com tile_mode="decal". None = não pinta as sobras.
pan_response
nome=
número0
Resposta ao pan da câmera. 0 = colado na tela. 1 = 1:1 com o mundo.
zoom_response
nome=
número0
Quanto o zoom da câmera afeta o padrão.
rotation_response
nome=
número0
Fração da rotação da câmera.
scroll
nome=
ponto(0, 0)
Deriva em unidades de design / s, independente da câmera.
enabled
nome=
boolTrue
Esconde o desenho sem tirar da pilha.
fit
Comportamento
cover
Escala uniforme até cobrir; recorta o resto.
contain
Escala uniforme até caber; letterbox com underlay.
stretch
Estica nos dois eixos.
tile
Repete uma célula de tile_size.
Câmera que anda: em cover / contain / stretch a imagem termina na borda. Com pan_response, zoom_response ou rotation_response, a câmera chega lá: passe tile_mode="mirror" (a imagem continua espelhada, sem emenda) ou "repeat". Sem tile_mode, além da borda aparece o underlay (preto). Ex.: Wallpaper("bg1.jpg", tile_mode="mirror", pan_response=0.1).
Atribuição de cor / tuple / Gradient / Texture já envolve num Wallpaper. Imagem: o próprio construtor. fit= numa cor levanta TypeError. Leitura de .fit / .tile_mode no source de imagem devolve a mesma string. Depois: source, câmera (pan_response / zoom_response / rotation_response / scroll) e enabled atribuem na instância. filename / image / fit / tile_mode / scale / tile_size / underlay ficam fixos no source — para mudar, troque .source ou construa outro Wallpaper. wp.remove() na base levanta TypeError; substitua canvas.background.base.
SkyGradient
Um céu em degradê vertical (topo, horizonte, chão), como base do fundo. horizon é a altura da linha do horizonte, de 0 (embaixo) a 1 (em cima). Criar já o põe como base; o último Wallpaper / SkyGradient criado ganha.
Nome sem =: 1º na ordem (o preset). O resto pelo nome. Sem args: "day". palette / top / middle / bottom ficam fixos na construção — para mudar a rampa, troque a base. horizon / fade / câmera / enabled valem ao vivo. sky.remove() levanta TypeError.
day · dawn · sunset · dusk · night · space · overcast · desert · forest · toxic · ember · ice · underwater. Qualquer top / middle / bottom pula o preset e completa com day.
top
nome=
cordo preset
Cor do zênite (topo da tela).
middle
nome=
cordo preset
Cor da atmosfera, no horizonte.
bottom
nome=
cordo preset
Cor abaixo do horizonte (sólido). Com horizon=0 não entra.
horizon
nome=
número0
Linha do horizonte (0–1, y-up desde a base). 0 = céu até embaixo (middle na base). Alinhe com Ground.
fade
nome=
número0.003
Mistura middle → bottom abaixo da linha. 0 = corte seco. Mesmo parâmetro do Ground.
pan_response
nome=
número0
Resposta ao pan da câmera. 0 = colado na tela. 1 = 1:1 com o mundo.
zoom_response
nome=
número0
Quanto o zoom da câmera afeta o padrão.
rotation_response
nome=
número0
Fração da rotação da câmera.
scroll
nome=
ponto(0, 0)
Deriva em unidades de design / s, independente da câmera.
enabled
nome=
boolTrue
Esconde o desenho sem tirar da pilha.
canvas.background = Neon() / Rain() / Blur() levantam TypeError. Material sozinho não se coloca: atribua ou use Wallpaper(…). SkyGradient.remove() / wallpaper na base: TypeError — troque .base.
Como ler os parâmetros.amount= é densidade ou brilho (1 = padrão); speed= é o movimento próprio do fundo (0 = parado); alpha= é a opacidade, de 0 a 1. Os ajustes de câmera, em todo fundo: pan_response= 0 deixa o fundo preso à tela e 1 o faz andar junto com o mundo (valores no meio dão parallax); zoom_response= / rotation_response= fazem o mesmo para zoom e giro; scroll=(dx, dy) é uma deriva constante em unidades por segundo; enabled=False esconde. res_scale= é a resolução interna (1 = cheia; menor = mais barato e mais borrado).
Post
Um Post é um efeito sobre a imagem inteira do mundo, aplicado depois de tudo desenhado: vinheta, bloom, grão de filme, tela de CRT, luz. Ele fica ligado enquanto está na pilha canvas.effects — criar já empilha. Guarde o retorno para mudar parâmetros, esconder ou tirar. Para um estouro de um instante (clarão, tremida, explosão num ponto, escurecer a tela), use um Impacto.
Criar um efeito já o põe na pilha. .add serve para recolocar um efeito tirado com remove, ou um criado com Bloom.temp(…) (que cria sem empilhar). A ordem em que os efeitos se aplicam é fixa, não a ordem em que você os criou: primeiro desfoques, depois luz somada, cor, sobreposições, distorções e, por fim, os efeitos especiais (shader). Parâmetros do construtor atribuem na instância e valem no próximo frame. Impactos (flash, *_at, capa) não entram nesta pilha.
bloom = Bloom(intensity=0.8)
bloom.intensity = 1.0
bloom.enabled = False # fica na pilha, não desenha
bloom.remove()
canvas.effects.remove(bloom) # igual
canvas.effects.clear()
canvas.effects.add(bloom) # recoloca
list(canvas.effects)
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
canvas.effects
pilhacanvas.effects
lê
A lista dos Posts ligados. Criar um Post já o põe aqui; não atribua canvas.effects = ….
.enabled
boolFalse
lê / escreve
False desliga todos os Posts de uma vez. Cada efeito também tem .enabled.
Métodos
Método
Assinatura
Retorna
Significado
.add(effect)
Parâmetros: effect (post)
o efeito
Empilha. Se ele já está na pilha, não faz nada. Serve para um efeito criado com Bloom.temp(…) (cria sem empilhar) ou tirado com remove. Devolve a instância.
.remove(effect)
Parâmetros: effect (post)
—
Tira da pilha. Chamar de novo não faz nada. Igual a effect.remove(). O objeto Python continua; .add recoloca.
.clear()
Sem parâmetros
—
Tira todos os posts.
list(canvas.effects)
Parâmetros: a pilha (canvas.effects)
lista
Ordem de construção.
ColorWash: depois da construção a paleta é .colors (não color / color2 / color3). Haze.kind só escolhe defaults na construção — mude amount / speed / bias.
Esconda post com enabled, não visible. add(Neon()) é a família errada.
Como ler os parâmetros.amount= / intensity= é a força (0 desliga). speed= é o ritmo da animação (0 congela). target= / center= é um ponto ou um objeto; omitido, segue o mouse. Medidas em “pixels da tela” dependem do tamanho da janela.
Impactos
Impactos são estouros de um instante, disparados pelo canvas: você chama, acontece e some sozinho — não há nada para guardar ou tirar. Na tela toda (flash, shake, glitch), num ponto (explosion_at e os outros *_at) ou como uma capa que escurece a tela (fade_out / fade_in). O mesmo efeito num objeto são os Transientes; algo que fica num lugar é um SpatialFx; um efeito que dura na tela é um Post.
*_at aceitam (x, y) ou um ponto (tupla, Point, Vec2, Graphic, info); None não faz nada. Depois de um ponto, os outros parâmetros vão por nome (amount=, radius=). Capa cobre depois dos widgets (o HUD também) e segura até o fade_in; ela anda mesmo com clock.paused (é transição de tela, não tempo de jogo); canvas.covered é 0…1 (só leitura). Sem then= — só on_done. Íris: fade_out(…, style="iris", center=heroi) — center= é um ponto, copiado agora (omita = centro do quadro).
Como ler os parâmetros. O lugar vem primeiro: x, y ou um ponto (tupla, Point, objeto, info de colisão); o resto vai pelo nome. amount= é a força (1 = padrão); radius= está em unidades de design; shake= / flash= ligam ou desligam a tremida e o clarão que acompanham o efeito.
Câmera
A câmera escolhe que parte do mundo aparece na janela: deslocar a vista (pan), aproximar (zoom) e girar. Ela não move os objetos — só o olhar; widgets e a camada ui ficam parados na janela. Sem time= a mudança é na hora; com time= ela anima e devolve um Handle, com os mesmos parâmetros de Mover (ease=, loops=, yoyo=…).
canvas.set_origin("center")
canvas.pan_to(200, 80) # x, y
canvas.pan_to((200, 80)) # tupla
canvas.pan_to(Vec2(200, 80)) # Vec2
canvas.pan_to(boss) # Point / Graphic (centro)
canvas.pan_by(40, 0) # dx, dy
canvas.pan_by((40, 0)) # tupla
canvas.pan_by(Vec2(40, 0)) # Vec2 — não Point
canvas.pan_to(200, 80, time=0.8, ease="ease_out")
canvas.pan_to(door, speed=300) # mesma velocidade, perto ou longe
canvas.zoom_to(2.0, about=(mouse.x, mouse.y), time=0.6)
canvas.pan_to(400, 300, time=1.0, loops="forever", yoyo=True)
await parallel(
canvas.pan_to(boss, time=1.0),
canvas.zoom_to(2.2, time=1.0),
)
canvas.frame_to(focus, pad=0.1, time=1.2)
canvas.reset_view(time=0.5)
canvas.frame_to(group["fleet"].box, pad=0.15) # None = fica parado
# margem em unidades (não fração): canvas.frame_to(box.outset(40))
Seguir um objeto. Não há canvas.follow, e time= / speed= não servem (o destino é lido uma vez, no momento da chamada, e a câmera vai até onde o alvo estava). Chame pan_to a cada frame no update, com folga (deadzone) e atraso (chase; para seguir em velocidade fixa, approach no lugar de chase):
Tamanho do espaço de design (canvas.width / height) e canvas.playfield: Colocar › Pais.
Neste canvas (câmera)
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
canvas.zoom
número1
leitura
Escala uniforme. 1 = identidade.
canvas.pan_x / .pan_y
número80
leitura
Ponto do mundo agora no centro da janela.
canvas.rotation
número1
leitura
Rotação da vista, em graus.
canvas.game_view
Boxcanvas.game_view
leitura
O que a câmera vê agora, no mundo. Câmera parada no início: de (0, 0) a (canvas.width, canvas.height), centro no meio. Um Box novo a cada leitura. Com a câmera girada é o retângulo reto (sem giro) que contém a vista: os cantos dele ficam fora da tela. Ver Colocar.
canvas.ui_view
Boxcanvas.ui_view
leitura
A janela inteira, de (0, 0) a (width, height), para o HUD. Não se mexe com a câmera. Um Box novo a cada leitura.
canvas.cull_offscreen
boolTrue
lê / escreve
Não desenha o que está fora da vista. Padrão True. Os objetos continuam rodando update e colidindo. False desenha tudo: Apresentação.
Parâmetros: where (str), time, ease, loops, yoyo, then, on_done
—
Só pan. "corner": (0, 0) no canto inferior esquerdo. "center": no meio da janela. Zoom e rotação ficam. Com time=, os fundos e camadas com pan_response acompanham a câmera durante a animação, sem salto no fim. Sem force=.
zoom_to / zoom_by
Parâmetros: valor (número), time, ease, loops, yoyo, then, on_done, about
—
Absoluto / multiplica. about= é o ponto âncora (mundo): (x, y), Point, Vec2 ou Graphic (centro). Omitido: o centro da vista — o pan não muda. Parâmetros de tempo: time= / ease= / loops= / yoyo= / then= / on_done= / about=. Sem force=.
pan_to
x, y · (x, y) · Vec2 · Point · Graphic; time, speed, ease, loops, yoyo, then, on_done
—
Coloca o mundo no centro. Formas: dois números; tupla (x, y); Vec2; Point; Graphic (usa o centro). Parâmetros de tempo: time= ou speed= (unidades do mundo por segundo; a duração é a distância do olhar atual até o destino ÷ speed) / ease= / loops= / yoyo= / then= / on_done=. Não aceita ângulo + distância. Sem about= / force=.
pan_by
dx, dy · (dx, dy) · Vec2; time, speed, ease, loops, yoyo, then, on_done
—
Desloca o conteúdo. Formas: dois números; tupla (dx, dy); Vec2. Não Point nem Graphic. Aceita os mesmos parâmetros de tempo de pan_to, inclusive speed= (o tamanho do deslocamento ÷ speed). Não aceita ângulo + distância. Sem about= / force=.
rotate_to / rotate_by
Parâmetros: valor (número), time, ease, loops, yoyo, then, on_done, about
—
Gira a vista, em graus. about= opcional (padrão: centro da vista); o mesmo tipo de ponto do zoom. Com time=: arco curto em rotate_to; rotate_by usa o sinal escrito. Aceita os parâmetros de tempo e about=. Sem force=.
Parâmetros: target (alvo: shape, Box, retângulo ou None), pad (número), rotate (True/False), time, ease, loops, yoyo, then, on_done
—
Enquadra. Alvo: shape / Box / tupla (left, top, w, h), ou None (nada a enquadrar: fica parado; com time= um Handle que não move nada pelo tempo todo — ainda ocupa os três movimentos). pad é fração do alvo em cada lado (0.1 → 10%; tamanho × (1+2p)) — mesma família de playfield.pad. Negativo aperta o enquadramento. Ar em unidades de design: frame_to(box.outset(40)), não pad=. Box colapsado (largura/altura 0) enquadra apertado, sem erro. rotate=True alinha o ângulo do alvo. O destino é lido no início. Sem force=.
Parâmetros: time, ease, loops, yoyo, then, on_done
—
Volta a câmera ao início: zoom 1, rotação 0, origem no canto. Com time=, os fundos e camadas com pan_response acompanham a câmera durante a animação, sem salto no fim. Sem force=.
time= / ease= / loops= / yoyo= / then= / on_done=: iguais aos verbos de Mover. about= só em zoom_* / rotate_*. Sem force= (a câmera não é Entity). Sem time=: na hora, devolve canvas. Com time=: Handle. yoyo=True precisa loops > 1 (ou "forever"); com loops=1 levanta erro.
Três movimentos
A câmera tem três movimentos independentes: pan, zoom e giro. Dois verbos do mesmo movimento não somam: o segundo cancela o primeiro. Movimentos diferentes rodam juntos.
Movimento
Quem usa
pan
pan_to / pan_by / set_origin. Também zoom/rotação quando há about=.
zoom
zoom_to / zoom_by.
rotação
rotate_to / rotate_by.
os três
frame_to / reset_view.
Movimentos diferentes juntos: pan_to(…, time=1) e zoom_to(…, time=1) (sem about=).
Mesmo movimento: a segunda chamada cancela a primeira.
about= cola um ponto do mundo na tela — isso é pan. Não misture com outro pan_to (a última vence).
O destino é lido no momento da chamada: about= / pan_to / frame_to não perseguem um shape.
then= só de dentro de um mode (igual tween do script).
Não confundir
Parece
É
Não é
canvas.pan_to(hero, time=0.5) segue o herói
O destino é lido no momento da chamada: a câmera vai até onde o herói estava. Para seguir, pan_to a cada frame (acima). shake é feedback de hit, não pan. frame_to(group["…"].box) aceita None (grupo vazio) sem guarda.
canvas.follow
Apresentação
Como o jogo aparece: o modo da janela (canvas.display), quantos frames por segundo (canvas.framerate), a qualidade dos efeitos (canvas.fx_quality) e se o que está fora da vista é desenhado (canvas.cull_offscreen). Por padrão a janela é "windowed" e o Scena ajusta o resto sozinho (FX Auto): começa em qualidade "high" a ~60 fps e baixa a qualidade se o computador não der conta. Escrever framerate ou fx_quality desliga esse ajuste.
init(width=1000, height=700, window_scale=1.0)
init(width=1000, height=700, display="fullscreen")
canvas.display = "maximized"
canvas.display = "windowed"
canvas.framerate = "vsync60" # padrão: preso ao monitor, ~60
canvas.framerate = "vsync30" # preso ao monitor, ~30
canvas.framerate = "vsync" # toda atualização do monitor (144 num de 144 Hz)
canvas.framerate = 30 # relógio fixo em 30 fps
canvas.framerate = "max" # sem teto (bench)
canvas.fx_quality = "high"
canvas.fx_quality = "medium"
canvas.cull_offscreen = False # desenha tudo, até o que está fora da vista
canvas.display
O modo da janela do sistema. String. Padrão "windowed". Atribuir põe aquele modo (não alterna). Pode atribuir antes de run() (vale quando a janela abrir) ou durante o jogo (muda na hora, como os atalhos). Leia de volta para sincronizar um checkbox. Só estes três valores — sem "full" / "max".
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
"windowed"
str"windowed"
lê / escreve
Padrão. Janela no aspecto do design, centrada na área de trabalho. Sem faixas pretas: a janela é o retângulo de design. Arraste redimensiona com o aspecto travado. Tamanho inicial: window_scale.
"maximized"
str"maximized"
lê / escreve
Preenche a área de trabalho (taskbar fica). O conteúdo não estica: faixas pretas nas sobras. O botão Maximizar da barra de título é o mesmo modo. Atalho: Ctrl+Shift+M.
"fullscreen"
str"fullscreen"
lê / escreve
Cobre o monitor: sem borda, sem taskbar. Faixas pretas nas sobras, como em maximized. Ctrl+Shift+F de novo volta ao modo de antes (ex.: maximized). Atalho: Ctrl+Shift+F.
init(display=) faz o mesmo que atribuir canvas.display. Widgets ficam no retângulo de design (não nas faixas). O design nunca estica.
canvas.framerate
Como os frames são entregues. String ou número. Padrão "vsync60". Atribuir desliga o FX Auto; com o Auto ligado, a leitura devolve o degrau atual ("vsync60" ou "vsync30"). Pode atribuir antes de run(). Um valor errado dá erro na hora e não muda nada ("vsinc" → Did you mean 'vsync'?). Atalho de debug: Ctrl+Shift+4 (vsync → vsync60 → vsync30 → 60 → 30 → max; o HUD mostra a taxa real).
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
"vsync60"
str"vsync60"
lê / escreve
Padrão. Preso ao monitor, no ritmo mais perto de 60: acompanha cada atualização do monitor num de 60 Hz, uma a cada duas num de 120 Hz, 72 por segundo num de 144 Hz. Cada frame aparece exatamente quando o monitor redesenha (o vblank), sempre no mesmo ritmo, e dt vira um múltiplo exato do período do monitor.
"vsync30"
str"vsync30"
lê / escreve
Preso ao monitor, no ritmo mais perto de 30 (o dobro do intervalo do "vsync60"). É o último recurso do FX Auto quando o computador não dá conta.
"vsync"
str"vsync"
lê / escreve
Um frame a cada atualização do monitor: a taxa cheia (120 num de 120 Hz). Mais suave, mas o trabalho por segundo sobe junto.
um número
número30
lê / escreve
Relógio fixo nessa taxa (finito, > 0). Não segue o monitor: o movimento pode tremer por um tempo quando essa taxa fica perto da do monitor. Prefira um "vsync…".
"max"
str"max"
lê / escreve
Sem teto: tantos frames quanto o computador conseguir. Para medir desempenho.
canvas.fx_quality
A qualidade dos efeitos visuais. String ou FxQuality (FxQuality.LOW / MEDIUM / HIGH / MAX — os mesmos valores das strings). Padrão "high". Atalho: Ctrl+Shift+3 (cicla low → medium → high; max fica de fora).
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
"high"
str"high"
lê / escreve
Padrão. Efeitos de tela (Post) em meia resolução; efeitos nos objetos completos.
"medium"
str"medium"
lê / escreve
Efeitos de tela em meia resolução; SpatialFx e efeitos pesados em versão mais simples. Também: "med".
"low"
str"low"
lê / escreve
Efeitos de tela em ¼ da resolução; versões leves de tudo.
"max"
str"max"
lê / escreve
Tudo em resolução cheia. Para diagnóstico; não entra no ciclo do atalho.
canvas.cull_offscreen
Pular o desenho do que a câmera não vê. bool. Padrão True. O Scena testa cada objeto: se a caixa dele (com folga para brilho, contorno e efeitos) não toca a vista, ele não é desenhado naquele frame. Só o desenho muda: fora da vista, o objeto continua rodando update, se movendo e colidindo. Pode atribuir antes de run() ou durante o jogo (uma tela de opções pode mudar).
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
True
boolTrue
lê / escreve
Padrão. Testa os objetos e pula os que estão fora da vista. Camadas com parallax (pan_response diferente de 1), widgets, o fundo e os efeitos de tela são sempre desenhados.
False
boolFalse
lê / escreve
Desenha todo objeto visível; o que fica fora da janela simplesmente não aparece.
O teste quase não custa nada. Um objeto parado guarda a caixa até mudar; um que só anda leva a caixa junto. Numa camada em que nada (ou quase nada) ficou fora da vista, o Scena espera alguns frames antes de testar de novo. Por isso um jogo de uma tela só fica praticamente igual, e um mundo maior que a janela fica bem mais rápido. Medido: uma fase de 1500 tiles desenha em 3 ms em vez de 8; a fase do tutorial 25 roda ~9% mais rápida; um bullet hell com tiros que saem da tela, ~7%. False serve para comparar.
Liga ou desliga o FX Auto, o ajuste automático de qualidade e fps. Padrão ligado (qualidade "high" a ~60 fps). Ele mede quanto tempo cada frame leva e, se o computador não der conta, baixa a qualidade (high → medium → low, em "vsync60") e depois os fps (low em "vsync30"). Atalho: Ctrl+Shift+2.
Parâmetro
Passar
Tipo (default)
Significado
enabled
1º
boolTrue
False desliga o ajuste: qualidade e fps ficam onde você os puser.
refresh_hz
nome=
número ou NoneNone
A taxa do monitor para o plano (60, 120…). None mede sozinho.
params
nome=
objeto de ajusteNone
Recurso de exceção para testes de desempenho. Deixe omitido.
canvas.framerate e canvas.fx_quality desligam o FX Auto (o mesmo que Ctrl+Shift+2). Benches devem chamar set_fx_auto(False) (e em geral canvas.framerate = "max"). Tamanho do design: init(width=, height=), só leitura em canvas.width / .height. window_scale=1 é a maior janela windowed (aspecto travado, desktop nas sobras) — não é maximize. Maximize / fullscreen: canvas.display. Não existem canvas.maximized / canvas.fullscreen / toggle_fullscreen().
Camadas
Camadas decidem a ordem de desenho: o que está numa camada mais alta aparece na frente. Três já existem: default (onde tudo nasce), top (na frente do mundo) e ui (presa à janela, para o HUD). Crie outras por número (layer=10: só a posição) ou por nome (canvas.layers.create: com parallax e esconder tudo de uma vez). above / below devolvem o número vizinho de uma camada.
Circle(center=(100, 100), radius=20, layer=10) # z=10, criado na hora
Sprite("cloud.png", layer=below("default")) # atrás do default
far = canvas.layers.create("far", pan_response=0.15, below="default")
Sprite("montanha.png", layer="far") # anda a 15% da câmera
Text("SCORE") # camada ui (janela)
Rect(center=(80, 700), width=120, height=36, layer="ui")
canvas.layers["top"].post_fx = False # top nítido; ui já fica após o post
Camada por número
layer=<int>
Primeiro uso cria a camada com z igual ao número (neutra, visível). O mesmo número junta na mesma camada. O z fica congelado — a posição é o nome dela. Namespace separado de strings: 10 ≠ "10". canvas.layers.create(10) → TypeError (só string). Sem parâmetros de parallax — para isso, dê um nome. Teto: z ≤ 100. bool não vale. Camada default tem valor 0.
above / below
above(name) · below(name)
Funções globais (como chase). name é str ou int e precisa existir. Devolve um int para usar em layer=. above() recusa na hora (ValueError) um número que caia em top ou acima dele (teto: z ≤ 100). below() não tem piso.
Chamada
Tipo
Significado
below("default")
int-1
Um passo abaixo da camada âncora.
above("default")
int1
Um passo acima.
above(10)
int11
Âncora numérica também vale (depois que a camada 10 existe).
Nome sem =: 1º na ordem (só str). O resto pelo nome. Resposta da câmera congela na criação; só a velocidade de scroll muda depois (layer.scroll).
Parâmetro
Passar
Tipo (default)
Significado
name
1º
str
Nome da camada. Ex.: canvas.layers.create("spawn"). Int → TypeError.
pan_response
nome=
número1
0 = colada na tela. 1 = acompanha o mundo. >1 = primeiro plano.
zoom_response
nome=
número1
Quanto o zoom da câmera afeta esta camada.
rotation_response
nome=
número1
Fração da rotação da câmera.
scroll
nome=
ponto(0, 0)
Deriva em unidades de design / s, independente da câmera. Soma com pan_response. Para começar parada: crie com velocidade e depois layer.scroll = (0, 0) (scroll=(0, 0) na criação deixa a camada neutra).
z
nome=
número inteiro0
Ordem. Ou below= / above= — um só. z ≤ 100.
below / above
nome=
str/int ou None
Ordem relativa: o z do alvo menos / mais 1. default e top são reservadas e neutras. Ex.: below="default". Duas camadas criadas com below="default" ficam no mesmo z; camadas com o mesmo z desenham na ordem de criação (a mais nova por cima). Para empilhar, encadeie: create("fundo", below="pista").
visible
nome=
boolTrue
Camada visível ao criar. Depois: canvas.layers["name"].visible.
canvas.layers
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.default
str"default"
lê / escreve
Camada de construção quando layer= é omitido (Shape/Sprite; Text sem center= usa ui).
"far" in canvas.layers
bool"far" in canvas.layers
leitura
A camada existe? Também: 10 in canvas.layers.
canvas.layers["far"]
camadacanvas.layers["far"]
leitura
O objeto da camada — ver caixa abaixo. Também: canvas.layers[10].
Parâmetros: name (str); z (int) ou above= / below= (nome de camada); visible (True/False); pan_response, zoom_response, rotation_response (parallax, números); scroll (par)
a camada
Nova camada nomeada (devolve o mesmo objeto de camada que canvas.layers["far"]). Parallax congela na criação. Só string.
using(name="far")
Parâmetros: name (nome da camada)
Use com with
Dentro do with, .default vira essa camada.
list()
Sem parâmetros
lista
Lista {name, z, visible, object_count} na ordem de desenho.
clear(hard=False)
Parâmetros: hard (True/False)
—
Tira os objetos de todas as camadas. hard=True dá destroy.
canvas.layers["name"] (a camada)
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.visible
boolcanvas.layers["far"].visible = False
lê / escreve
Mostra ou esconde a camada inteira.
.z
número inteirocanvas.layers["far"].z = -1
lê / escreve
Reordena. Não vale em "top" / "ui" / camada numérica. z ≤ 100.
.post_fx
boolcanvas.layers["top"].post_fx = False
lê / escreve
False: a camada top não recebe os Posts (Bloom etc.) e fica nítida. Só vale em "top"; a ui nunca recebe Posts.
.scroll
pontonuvens.scroll = (0, 0)
lê / escreve
Velocidade da deriva (unidades / s). Muda sem pulo: (0, 0) para, chase(nuvens.scroll, (0, 0), 3) freia. Só em camada criada com resposta (pan_response= … scroll=); numa camada neutra levanta erro com o create certo.
.view
Boxmontes.view.right
leitura
A janela que esta camada mostra, nas coordenadas dela. Camada neutra = canvas.game_view; ui = canvas.ui_view; camada de parallax = o pedaço que ela mostra agora (anda menos que a câmera). Para criar cenário entrando pela direita: montes.view.right + 200.
.name
str ou intcanvas.layers["far"].name
leitura
Nome (str) ou posição (int) — só leitura (escrever levanta AttributeError). Para renomear: crie outra camada e mova as formas com forma.layer = "novo".
Métodos
Método
Assinatura
Retorna
Significado
.delete(destroy_shapes=True)
Parâmetros: destroy_shapes (True/False)
—
Tira uma camada custom (nomeada ou numérica). default / top / ui não saem.
.clear(hard=False)
Parâmetros: hard (True/False)
—
Tira os objetos desta camada. hard=True dá destroy.
No objeto
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
layer="default" / "top" / "ui"
str"ui"
lê / escreve
Camadas que já existem. ui fica presa à janela e não recebe os Posts. No clique, quem responde primeiro: widgets, depois ui, depois o mundo.
layer=10
int10
lê / escreve
Posição. Cria na hora; z congelado. Também: layer=below("default").
shape.layer
str, int ou None"top"
lê / escreve
Muda a camada deste objeto.
Camada parallax é decoração: sem física de jogo. Colisão / SpatialFx em ui levantam erro. top ≠ HUD. Número ≠ string com o mesmo dígito (10 ≠ "10"); z numérico é congelado; sem pan_response em camada numérica — dê um nome.
Mundo sem fim
Endless enche a parte de uma camada que a câmera mostra: corta o eixo em vagas (slots), monta cada vaga um pouco antes de ela entrar na tela e destrói as que ficaram bem para trás. Para corredor, flappy, nave com caverna, céu de nuvens que não acaba. Ele decide quando e onde; você decide o quê.
canvas.layers.create("predios", pan_response=0.5)
canvas.layers.create("pista", below="default") # camada própria, atrás do jogador
Endless("predios", ["predio1.png", "predio2.png"], height=300, bottom=80, gap=(40, 200))
def canos(slot):
if slot.i < 2:
return [] # vaga vazia
meio = slot.rng.uniform(230, 650) # mesma vaga, mesmos números
return [Rect(top_left=(slot.left, meio - 90), width=70, height=meio - 170, group="parede"),
Rect(top_left=(slot.left, 800), width=70, height=710 - meio, group="parede")]
pista = Endless("pista", canos, step=260)
def update(dt):
canvas.pan_to(passaro.center.x + 120, canvas.pan_y) # a câmera anda
canvas.playfield.set(canvas.game_view.outset(60))
st.pontos = max(0, pista.index_at(passaro.center.x) - 1)
Peças: um caminho de imagem (com height=: o tamanho de cada uma), uma lista de caminhos (uma por vaga, sorteada) ou uma função fn(slot) que devolve um objeto, uma lista de objetos ou [] (vaga vazia). A função monta nas coordenadas absolutas da vaga (slot.left, slot.right…). O Endless nunca move as peças.
Sorteio: troque random. por slot.rng. (e chance(p) por slot.chance(p)). Cada vaga tem seu próprio sorteio: voltar a uma vaga monta a mesma peça. O random global daria outra peça na volta, sem aviso. seed= com um número repete o mesmo mundo em toda partida; sem seed, um mundo novo por partida (.seed diz qual saiu).
Uma camada só dele, criada com canvas.layers.create(...). default / top / ui levantam erro: peças criadas no meio do jogo pintariam por cima do jogador. Camada de parallax: só decoração (nada colidível).
pieces
2º
caminho, lista de caminhos ou função
O que vai em cada vaga. Lista mista (imagem + função) levanta erro: varie dentro da função com slot.rng.choice([...]).
axis
nome=
"x" / "y" / "xy""x"
Horizontal (corredor), vertical (nave subindo) ou grade (mapa visto de cima).
step
nome=
número, par ou funçãoNone
Tamanho da vaga. Obrigatório com função. "xy": par (largura, altura). Larguras variadas: step=lambda slot: slot.rng.choice([160, 320]). Sem step (só imagens): o tamanho da imagem + gap.
gap
nome=
número ou (mín, máx)0
Espaço depois de cada imagem; um par sorteia por vaga. Só para imagens sem step.
start
nome=
número ou parNone
Onde começa a vaga 0. Padrão: a borda da tela quando o Endless nasce (a primeira tela já vem cheia).
seed
nome=
int ou NoneNone
Mesmo número = mesmo mundo. None = mundo novo a cada partida.
bottom
nome=
númeroNone
Imagens com axis="x": o y da base. Padrão: a base da tela.
left
nome=
númeroNone
Imagens com axis="y": o x da esquerda. Padrão: a esquerda da tela.
group
nome=
str ou listaNone
Grupo das imagens. A função dá grupo às suas peças.
height · width · size
nome=
númeroNone
Tamanho de cada imagem, como em Sprite(caminho, height=…). Sem ele, a imagem fica com o tamanho de um Sprite sem tamanho (48 no lado maior). Uma faixa de cenário quase sempre quer height= da tela. A função dá tamanho às suas peças.
Leituras e controle
Membro
Retorna
Significado
.pieces
lista
As peças vivas que ele montou, na ordem do eixo.
.index_at(x)
int (ou (i, j))
A vaga que contém x (com "xy": index_at((x, y))). Placar por trecho, checkpoint, dificuldade.
.seed
int
O seed em uso — passe em seed= para repetir a partida.
.active = False
—
Congela o enchimento (as peças ficam).
.destroy()
—
Para e destrói as peças dele. Num Act, change_act faz isso sozinho.
slot (o que a função recebe)
Campo
Retorna
Significado
slot.i · slot.j
int
Índice: 0, 1, 2… à frente de start; −1, −2… atrás. j só em "xy".
slot.left · slot.right · slot.bottom · slot.top
número
Bordas no eixo infinito, em coordenadas do mundo (left/right em "x", bottom/top em "y", as quatro em "xy"). O outro eixo é seu.
slot.box
Box
A célula ("xy").
slot.rng
random.Random
O sorteio desta vaga: slot.rng.uniform(…), slot.rng.choice(…), slot.rng.randint(…).
slot.chance(p)
bool
Como chance(p), no sorteio da vaga.
Dois jeitos de fazer um jogo sem fim
Todo jogo sem fim tem duas referências: o mundo (chão, canos, moedas, torres) e a tela (a nave do jogador, formações de inimigos). A câmera decide qual fica parada; o que vive na outra custa uma linha de carona por quadro (group[...].move_by).
A câmera anda
A câmera fica parada
Gêneros
corredor, plataforma, flappy, nave com caverna
nave vertical com ondas de inimigos
Coisas do mundo
montadas uma vez nas coordenadas do slot; não se movem
carona: group["solo"].move_by(0, -v * dt)
Coisas da tela
carona: group["tela"].move_by(dx, 0)
tela = mundo: Tracks com pontos fixos funcionam
Parallax
sozinho (pan_response)
scroll da camada (layer.scroll muda a velocidade)
Endless
jogo e decoração
só decoração; o jogo chega por tempo (every)
Sumir o que sai
canvas.playfield.set(canvas.game_view.outset(m)) por quadro
o playfield padrão basta
Parar no fim de jogo
pare a câmera: tudo para
nuvens.scroll = (0, 0)
A carona funciona porque deslocamentos somam: um move_along em andamento mais um move_by por quadro dão a soma dos dois.
# Câmera parada (nave vertical): o céu anda, as ondas chegam por tempo.
canvas.layers.create("nuvens", scroll=(0, -180))
def nuvem(slot):
x = slot.rng.uniform(0, 600)
return Sprite(slot.rng.choice(["nuvem1.png", "nuvem2.png"]), center=(x, slot.bottom))
Endless("nuvens", nuvem, axis="y", step=240)
def chefe_chegou():
canvas.layers["nuvens"].scroll = (0, 0) # o cenário para
Guardar o que mudou
Voltar a uma vaga monta tudo de novo: a moeda pega reaparece. Para guardar, use slot.i como chave:
De quem é cada peça: o que a função devolve é do Endless (ele destrói quando a vaga fica para trás, pela posição da vaga). O que ela cria e não devolve é seu — um inimigo que sai andando não deve ser devolvido. Peça que você destrói (moeda pega) só volta quando a vaga sair da tela e voltar. Outros objetos na mesma camada não são tocados.
Terreno: o que precisa casar na emenda entre vagas (a altura do chão) é uma função do x do mundo — vagas vizinhas perguntam pelo mesmo x. slot.rng é para decisões dentro da vaga (uma árvore aqui, uma torre ali).
Câmera parada + Endless numa camada de jogo: a janela não se move, então ele enche a primeira tela e para. Mover as peças (o hábito da esteira) as tira da tela e nada novo chega — o Scena avisa uma vez. Com câmera parada, ande a câmera ou use uma camada com scroll só para decoração. Crie o Endless no Act que o usa.
Grupos
Um grupo é uma tag. Quem tem a tag está no grupo; group["wasp"] é uma vista viva desse conjunto — quem entrar depois aparece na próxima leitura.
wasp = Sprite("x.png", group="wasp") # entra no grupo "wasp"
wasp.hp = 1
group["wasp"].move_by(0, 10) # age em todos
foe = group["wasp"].nearest(player.center, where=lambda w: w.hp > 0)
group["wasp"].destroy()
Entra-se num grupo com uma tag: no construtor (group=) ou depois (tag). Não há .add / .remove no grupo.
for x in (100, 200, 300):
Circle(center=(x, 100), radius=10, group="wasp")
ship = Sprite("ship.png", group=["player", "hurt"])
ship.tag("shielded")
ship.untag("hurt")
ship.tagged("player") # True
copy = ship.clone() # copia as tags
elite = ship.clone(group="elite") # substitui
Uma Entity pode estar em vários grupos. group= em clone substitui as tags (não mistura); omitir copia as atuais; group=None = nenhuma. Clone um carimbo limpo, não um objeto já marcado. Não existe id: a tag é o único nome para achar um Entity depois (group["heroi"].first). Um objeto destruído sai dos grupos na hora: group["wasp"] lido logo depois do destroy() já não o contém.
Contar, testar, pegar pela posição e percorrer. A ordem é a de entrada no grupo — não a posição na tela.
for x in (100, 200, 300):
Circle(center=(x, 100), radius=10, group="wasp")
group["wasp"].hp = 3
w = group["wasp"][1]
group["wasp"].count # ou len(group["wasp"])
group["wasp"].empty
w in group["wasp"]
leader = group["wasp"].first # ou [0]
newest = group["wasp"].last # ou [-1]
i = group["wasp"].index(w) # 0 = primeiro que entrou
mid = group["wasp"][1:3] # lista, não um grupo
for w in group["wasp"]:
w.hp -= 1
O for usa quem estava no grupo quando o loop começou: dá para destruir um membro no meio sem quebrar; quem entra no meio só aparece na próxima leitura. Quem sai e volta vai para o fim. .index(w) e .first / .last usam esse rank ao vivo — se alguém na frente sai, os de trás sobem. Para formação, ordene (sorted(group["wasp"], key=lambda w: float(w.center.x))) ou guarde um número fixo num campo próprio (w.slot = i, ou phase= no move_along).
group["wasp"].move_by(0, 10) chama o mesmo método em cada membro, com os mesmos argumentos. É o for, escrito curto.
for w in group["wasp"]:
w.move_by(0, 10)
group["wasp"].move_by(0, 10) # a mesma coisa
group["wasp"].hp = 3 # escreve em todos
group["wasp"].tag("shielded") # do grupo: tag em todos
O que volta depende do que o método devolve em um membro:
Se cada um devolve…
O grupo devolve…
o próprio objeto — ação na hora, tipo move_by sem time=
o grupo (dá para encadear)
outra coisa — Handle, cópia, canal…
uma lista com o resultado de cada um
# com time=: cada um devolve um Handle → lista
handles = group["wasp"].move_by(0, 80, time=0.4)
await parallel(*handles)
# com speed=: cada um calcula a própria duração (o de longe demora mais)
handles = group["wasp"].move_to(400, 120, speed=250)
# move_along sempre devolve Handle → lista
handles = group["wasp"].move_along(track, time=4)
await parallel(*handles)
# clone devolve uma cópia → lista de cópias
copies = group["wasp"].clone()
Ler um atributo: um membro → o valor; vários → lista de valores. Escrever (group["wasp"].hp = 3) põe o mesmo valor em todos. Quem não tiver o método (ou o nome) dá erro. Num grupo vazio não há membro para chamar: o método não faz nada e devolve None — nem o grupo nem uma lista (com time= ou speed=, devolve um Handle só; com speed= ele termina no frame seguinte, pois não há distância a percorrer); ler qualquer campo (center, angle, hp…) levanta AttributeError dizendo que o grupo está vazio — não há membro de onde ler. Para a posição de um grupo que pode esvaziar, use centroid ou box (None se vazio). Se você vai usar o retorno (ex.: await parallel(*handles)), teste group["wasp"].empty antes.
.destroy(), .tag(), .untag(), .nearest(), .filter(), .index(), .first / .last são do grupo (ver referência), não “chama em cada um e junta”.
Argumentos iguais para todos. Fase, destino ou tempo diferente por membro: escreva o for. Não há group.every / group.on_update: o loop da frota fica em update ou num Director.
Achar um membro, contar os que passam num teste, medir a frota.
foe = group["wasp"].nearest(player.center, where=lambda w: w.hp > 0)
close = group["wasp"].nearest(player.center, within=200) # ou None
n = group["wasp"].count_where(lambda w: w.hp > 0)
if group["wasp"].any(lambda w: w.hp <= 1):
canvas.flash(0.2)
group["wasp"].filter(lambda w: w.hp <= 0).destroy()
mid = group["wasp"].centroid # (x, y) ou None
box = group["wasp"].box # Box ou None — frame_to(box) segura se vazio
nearest devolve um membro ou None, não uma lista. filter devolve uma cópia (não acompanha o grupo). centroid e box são None num grupo vazio.
Operadores de conjunto entre grupos. O resultado é uma cópia: quem entrar depois não entra.
alive = group["wasp"] | group["drone"] # união
hurt = group["wasp"] & group["hurt"] # interseção
solo = group["wasp"] - group["elite"] # diferença
group["elite"] <= group["wasp"] # subset
group["wasp"].issuperset("elite") # superset
snapshot = group["wasp"].as_set() # set Python dos membros de agora
Cada operador tem um método com o mesmo efeito, que também aceita o nome da tag: .union("drone"), .intersection(), .difference(), .symmetric_difference(), .issubset(), .issuperset(). .is_live diz se a vista ainda acompanha o grupo: True em group["wasp"] e num grupo fórmula; False numa cópia. Para uma combinação que acompanha: Grupo vivo nomeado.
Dá um nome a uma combinação viva: recalcula a cada leitura. A conta precisa ficar dentro de lambda:, para ser refeita a cada leitura.
group["inimigos"] @= lambda: group["vespa"] | group["chefe"] # obrigatório: | & - dão uma cópia; o lambda: refaz a conta a cada leitura
group["inimigos"] @= group["vespa"] # opcional: grupo vivo nu é um alias
group["inimigos"] @= "vespa" # opcional: string de tag nua é um alias
group["inimigos"] @= (group["a"] | group["b"]) # proibido sem lambda: levanta erro com dica para envolver em lambda
await until_empty("inimigos")
group["inimigos"].unlink() # o nome volta a ser um grupo-tag vazio
Alias sem lambda só vale para um grupo vivo ou o nome da tag. União, interseção, filter já calcularam: o @= recusa (não congela em silêncio). Isto não usa a ligação @= dos campos: se dois nomes dependem um do outro (A usa B e B usa A), o erro só aparece ao ler. .is_linked diz se o nome tem fórmula.
Parecido, em outro lugar: o interruptor active de uma caixa de ataque também se liga com @= (espada["active"] @= lambda: heroi.showing(GOLPE)) e é recalculado na etapa de colisão de cada frame. Ver Caixas de ataque.
@collide não acompanha grupo fórmula — use as tags de origem. @click(group["…"]) liga só os membros do momento da decoração.
Esperar a onda acabar: await until_empty(...), num mode async.
await until_empty("wasp")
await until_empty(group["wasp"])
await until_empty("inimigos") # grupo fórmula também
Aceita o nome, group["wasp"] ou um grupo fórmula — não o resultado de filter / | (cópia). Não use until(lambda: group["wasp"].empty): use until_empty. Ver espera async.
Neste group["…"]
Entrar e sair (na Entity)
Nome
Assinatura
Retorna
Significado
group=
Parâmetros: str ou lista
—
No construtor. Tag, ou lista de tags. Ex.: group="wasp".
Quem está no grupo agora. Colchetes, como um dict — group("wasp") não vale mais. Sem .add / .remove — entra e sai com tag / untag.
.count / len(…)
número inteiro3
leitura
Quantos membros agora. Tag: O(1). Grupo fórmula: reconta (O(n)).
.empty
boolTrue
leitura
True se não tem ninguém. Não use em until() — use until_empty.
w in group["wasp"]
boolTrue
leitura
Este Entity está no grupo? Uma string ("nave" in group["wasp"]) levanta TypeError: para testar uma tag, w.tagged("wasp").
.first / .last
objetow
leitura
O primeiro que entrou / quem está no fim agora. O mesmo que [0] / [-1]. Grupo vazio: erro (não None).
group["wasp"][0]
objetow
leitura
O membro nessa posição, como numa lista. [-1] = o último, [-2] = o penúltimo. Fora da faixa / grupo vazio: erro. O inverso é .index(w).
group["wasp"][1:3]
lista[w, w]
leitura
Fatia: uma lista daqueles membros (cópia, não um grupo vivo). Faixa vazia → lista vazia, sem erro. A lista não tem .destroy() / .first — use .filter ou um for.
.index(item)
Parâmetros: item (Entity)
número inteiro
Posição na ordem de entrada (0 = o primeiro). Recebe o próprio objeto (uma string levanta TypeError). Se não está no grupo, dá erro — não devolve -1. Rank ao vivo. No for, use enumerate.
Agir em massa
Nome
Assinatura
Retorna
Significado
group["wasp"].metodo(…)
Os argumentos do método
o grupo, ou lista
Chama em cada membro. Ver a regra do retorno em Agir em massa.
group["wasp"].hp = 3
atribuição em todos
—
Põe o valor em cada membro. Se algum não tiver esse nome, dá erro.
.tag(*names) / .untag(*names)
Parâmetros: names (nomes)
o grupo
Põe / tira a tag de todos os membros atuais.
.destroy()
Sem parâmetros
o grupo
Destroy em todos os membros atuais.
Buscar
Nome
Assinatura
Retorna
Significado
.nearest(point, where=, within=)
Parâmetros: point (ponto), where (função), within (número)
objeto ou None
O mais perto. point é (x, y) ou algo com .x / .y (ex.: player.center). where= escolhe quem entra (lambda w: …). within= = distância máxima. Sem ninguém (ou ninguém perto o bastante) → None. Lista / mix: nearest(origem, itens).
.filter(pred)
Parâmetros: pred (função)
uma cópia
Cópia só dos que passam no teste. Essa cópia não muda depois. Depois .destroy().
.any(pred) / .all(pred)
Parâmetros: pred (função)
True/False
Algum / todos passam no teste. all num grupo vazio: True.
.count_where(pred)
Parâmetros: pred (função)
número inteiro
Quantos passam no teste.
.centroid
ponto ou None
leitura
Ponto médio dos membros. Vazio: None.
.box
Box ou None
leitura
Box novo que cobre os membros (ângulo 0). Vazio: None. Um membro (ou todos no mesmo ponto) pode ser colapsado (largura/altura 0). Serve em frame_to sem guarda.
Combinar (cópias)
Nome
Tipo
Retorna
Significado
group["a"] | group["b"]
cópia do grupo
grupo (cópia)
União. Também .union().
group["a"] & group["b"]
cópia do grupo
grupo (cópia)
Interseção. Também .intersection().
group["a"] ^ group["b"]
cópia do grupo
grupo (cópia)
Só num dos dois. Também .symmetric_difference().
group["a"] - group["b"]
cópia do grupo
grupo (cópia)
Diferença. Também .difference().
group["a"] <= group["b"] / .issubset(other)
boolTrue
True/False
Subconjunto: todo membro de a está em b (iguais também). Aceita o grupo ou o nome. Grupo vazio: True. Não é cópia.
group["a"] >= group["b"] / .issuperset(other)
boolTrue
True/False
Superconjunto: a contém todos de b (iguais também). Aceita o grupo ou o nome. Não é cópia.
.as_set()
Sem parâmetros
set
Congela a vista num set Python dos membros de agora.
.is_live
boolTrue
leitura
A vista acompanha o grupo? True numa tag ou grupo fórmula; False numa cópia.
Grupo vivo nomeado e espera
Nome
Assinatura
Retorna
Significado
group["enemies"] @= lambda: …
Parâmetros: lambda, grupo vivo ou nome (str)
o grupo
Nomeia uma combinação viva (recalcula a cada leitura). Sem o lambda:, uma união ou filtro do lado direito dá erro. Alias: group["e"] @= group["wasp"] ou @= "wasp".
.is_linked
boolTrue
leitura
Este nome tem fórmula?
.unlink()
Sem parâmetros
o grupo
Tira a fórmula. O nome volta a ser um grupo-tag vazio.
await until_empty("wasp")
Parâmetros: group_or_id (nome ou o grupo)
Espera (use await)
Espera o grupo esvaziar. Aceita o nome, group["wasp"], ou um grupo fórmula. Não vale o resultado de filter / |.
Não confundir
Parece
É
Não é
group["a"] | group["b"] acompanha quem entrar depois
É uma cópia de agora. Combinação viva: group["enemies"] @= lambda: group["a"] | group["b"].
grupo vivo
group["wasp"][0] é o mais à esquerda
É o primeiro que entrou no grupo. Re-tag vai para o fim; .index muda se alguém na frente sai. Formação: sorted(…) ou um número fixo num campo próprio.
posição na tela
@collide(player, …) com o objeto
Some no destroy(). Nomes de grupo no @collide veem quem entrar depois — prefira-os se a Entity morre e volta.
Ferramentas para posicionar objetos. Um Box é uma região retangular invisível: você a mede, recua (inset), fatia em faixas (strip) e grades (grid), acha a vizinha (beside) e cria objetos encaixados nela com box=. Rect desenha; Box só mede — ele não é um objeto do jogo.
Construtores / cantos / inset / strip / beside → Box
Frota / tabuleiro → grid Rect(box=) / Sprite / Text → box=
Tela no mundo / HUD / sair da fase → Regiões da tela
Fase feita no editor (Level, make()) → Level
Box
Um Box é uma região retangular que não aparece na tela: serve para medir e posicionar. Constrói-se como um Rect (center= + width= + height=, um canto, p1= / p2=…) ou a partir de um objeto (nave.box), mas não tem cor, camada nem grupo — e não há Box() vazio.
Um canto (p1) e um vetor até o canto oposto (vec), em unidades de design
p1=, p2=, height=, anchor=
Eixo + espessura — iguais ao Rect ("cap" / "square" / "side_left" / "side_right")
Box(source)
source é um Graphic (Rect, Sprite, Text…) ou outro Box. A mesma cópia que graphic.box — a moldura com os nomes (um Box é um Rect que não desenha)
Medição de grupo
Método
Assinatura
Retorna
Significado
group["wasp"].box
sem argumentos
Box ou None
Caixa alinhada aos eixos que cobre todos os membros (um Box novo), ou None se vazio. Ângulo 0. Não é construtor de Box.
Leitura
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.center.width.height.angle
ponto ou número(400, 300)
lê / escreve
Geometria. Cantos: .top_left · .right_center · … — como no Rect, cada nome fica preso ao seu canto: depois de girar, .top_left continua sendo o mesmo canto, mesmo que não esteja mais em cima à esquerda na tela. Um Box feito de outro (graphic.box, Box(b), inset, células) guarda os nomes: a casa (0, 0) de um tabuleiro continua a mesma depois de girar ou espelhar.
.left.right.bottom.top
número80
leitura
Bordas. y para cima: top é o maior y. Só leitura: para mudar a área, crie outro Box (ex.: box.outset(40)).
.col_index / .row_index
int ou None0
lê / escreve
Na célula de uma grade. None num Box solto.
.is_collapsed
boolFalse
leitura
Só leitura. True se largura ou altura for 0 (linha ou ponto). Mesma regra de “sem interior” que contains_point. Sem aviso.
Métodos
Método
Assinatura
Retorna
Significado
inset(margin)
Parâmetros: margin (número)
Box
Recuo uniforme para dentro. Cópia. Folga no box=: box=cell.inset(8) — não há padding= no construtor. Margem negativa levanta ValueError.
outset(margin)
Parâmetros: margin (número)
Box
Recuo uniforme para fora (gêmeo de inset). Cópia, mesmo centro e ângulo. Margem negativa levanta ValueError. Fonte colapsada (linha/ponto) ganha interior. Não é pad= / padding= (fração) — margem em unidades: frame_to(box.outset(40)).
strip(side, thickness)
Parâmetros: side ("left"/"right"/"top"/"bottom"), thickness (número)
Box
Faixa dentro, rente a uma aresta nomeada ("top" = a aresta de top_left / top_right). A faixa tem a espessura pedida e ocupa o lado inteiro do Box. Se a espessura for maior que o Box, a faixa fica do tamanho do Box, sem aviso. Espessura negativa / lado desconhecido levantam ValueError. Contraste: beside (fora); grade para frotas.
beside(side, *, width=, height=, gap=0)
Parâmetros: side; width/height opcionais; gap (número)
Box
Vizinho fora, rente a uma aresta nomeada. Tamanhos omitidos herdam; os dois omitidos → mesmo tamanho. gap é ar entre as bordas. Tamanho/gap negativos ou lado desconhecido levantam ValueError. Contraste: strip (dentro).
grid(cols, rows, gap=0)
Parâmetros: cols (int), rows (int), gap (número ou par)
Geometria. Não inscreve clique. Tabuleiro: g.cell_at(mouse.x, mouse.y).
rotate_to / rotate_by
valor (número); about= (ponto, opcional); force=False
o mesmo Box (muta)
Gira a região no lugar e devolve self — não é cópia (diferente de inset / outset). Sem time=: este não é o rotate_to de Mover (lá: objetos na tela, com time= e Handle). about= pivô (padrão: centro) — tupla, Point, Vec2, Graphic ou mouse, copiado agora. Grade em cima herda o ângulo.
Box() levanta erro. Não há região padrão. Quatro números posicionais também não: cantos vs x, y, w, h. isinstance(box, Entity) é False. Não há @click / on_click em Box. inset / gap grande demais viram linha ou ponto sem aviso — use .is_collapsed se precisar detectar. outset não colapsa; outset(m).inset(m) recupera sempre, mas inset(m).outset(m) só quando o inset não apertou até o resto. strip = faixa dentro; beside = vizinho fora; grade para frotas / células iguais.
Grid
Uma grade que fatia um Box em colunas e linhas iguais — para frotas, tabuleiros, inventários. box.grid(8, 3) devolve a grade; percorra as células com for ou peça uma com .cell(col, row). A linha 0 é a de cima (o topo visual do Box).
zone = Box(center=(canvas.game_view.center.x, 520), width=820, height=220)
fleet = zone.grid(8, 3, gap=16) # ou gap=(gx, gy); o mesmo que Grid(zone, 8, 3, gap=16)
for cell in fleet:
Wasp(space, box=cell, height=40, group="wasp")
top_left = fleet.cell(0, 0) # col, row
third = fleet.cell(2) # índice plano: col = n % cols
for row_cells in fleet.rows:
...
hit = fleet.cell_at(mouse.x, mouse.y) # None no vão ou fora
if hit is not None:
piece = Rect(box=hit)
Neste Grid
Método
Assinatura
Retorna
Significado
for cell in g
iterável
—
Linha 0 primeiro, esquerda → direita.
g.cell(col, row)
col (int), row (int)
Box
Coluna primeiro, como (x, y).
g.cell(n)
n (int)
Box
Plano: col = n % col_count, row = n // col_count.
.rows / .cols
listas
—
Listas de listas. Contagens: .row_count / .col_count.
.cell_at(x, y)
ponto ou x, y
Box ou None
Inverso. Grade rotacionada: transforma para o local primeiro. Vão → None. Célula de área zero continua endereçável por .cell(col, row), mas é invisível para cell_at.
cell.col_index / .row_index
int
—
Pertence à grade. Sem ponteiro de volta para g. Vizinho: g.cell(cell.col_index + 1, cell.row_index).
g[col, row]
TypeError
—
Reservado. Use g.cell(col, row).
Grade em Box com angle= carimba células no mesmo ângulo. cell_at honra isso. Um clique no tabuleiro girado cai na célula certa.
box=
box= cria um objeto já encaixado numa região (um Box, uma célula de grade): o Rect a preenche, o Circle fica inscrito, o Sprite cabe guardando a proporção da imagem. Com um tamanho explícito, o objeto só fica centrado nela. Para deixar folga, encolha a região antes: box=cell.inset(8).
cell = canvas.game_view.inset(24).grid(4, 3).cell(0, 0)
Rect(box=cell, color="navy") # preenche
Circle(box=cell) # inscreve (lado menor)
zone = Box(p1=(200, 200), p2=(100, 400))
Sprite(box=zone) # cabe inteiro, guarda o aspecto
Sprite(box=cell, height=40) # só centra; height vence
Text("Score", box=canvas.ui_view.grid(1, 8).cell(0, 0), layer="ui")
squares = panel.box.inset(8).grid(8, 8)
for cell in squares:
Square(box=cell, color="gold")
Contrato de encaixe
Tipo
box= sozinho
Com tamanho explícito
Rect / Ellipse
Preenche (pose + tamanho + ângulo)
Misturar levanta erro
Circle / Square / N-gon regular
Inscreve (lado menor). Circle não herda o ângulo
Só centra (radius= / length= / side=)
Sprite / Svg
Cabe (aspecto do PNG; herda ângulo)
Só centra (height= / width= / size=)
Text
center= o centro da região (camada ui); align="center"; width da região se omitido; fora da coluna
Não misture com center= / top_left= / top_center= / top_right=
Quem tem quadro retangular ganha .box — uma cópia da moldura, tirada agora, com os nomes do Graphic (rect.box.top_right é rect.top_right; bordas da tela: .left / .top / …): Rect, Square, Ellipse, Circle (quadrado do diâmetro, girado até o ponto da borda p1), Sprite, Svg. Line / Polygon livre / Track / Point levantam TypeError. Verbos de layout ficam no Box (panel.box.inset(8).grid(8, 8)), não no Shape. Limitar a pose a um Box: confine.
box= não mistura com center= / width= no Rect. Ellipse(from_rect=…) é outra coisa: liga a pose ao vivo a um Rect. HUD depois de pan: ui_view, não game_view. Se a região encolheu até virar uma linha ou um ponto (um inset grande demais), o objeto nasce invisível; confira com .is_collapsed no Box ou no objeto.
Regiões da tela
Três regiões prontas do canvas: game_view (o pedaço do mundo que a câmera mostra), ui_view (a janela, para o HUD) e playfield (a área de jogo, para saber quando algo saiu dela). Leem-se do mesmo jeito; servem para coisas diferentes.
Circle(box=canvas.game_view.inset(80).grid(3, 3).cell(1, 1))
hud = canvas.ui_view.strip("top", 80)
Text("SCORE", box=hud, layer="ui")
rail = canvas.game_view.strip("left", 120)
sidebar = canvas.game_view.beside("right", width=120)
# sair da fase — não é layout:
canvas.playfield.pad = 0.4
@leave_playfield("bolt")
def leftover(shot):
shot.destroy()
safe = canvas.playfield.inset(40) # Box; .pad do playfield não muda
outer = canvas.playfield.outset(40) # halo maior; .pad não muda
band = canvas.playfield.strip("top", 80) # cópia; .pad não muda
Janela no mundo (o que a câmera vê). Para naves, formações, “topo da tela”. Cada leitura é um Box novo. Câmera girada: é o retângulo reto que contém a vista girada, maior que a tela.
canvas.ui_view
Box novocanvas.ui_view
leitura
Janela na vista[0, W] × [0, H]. HUD / layer="ui" / Text de vista. Não se mexe com a câmera.
canvas.playfield
playfieldcanvas.playfield
leitura
Área de saída para @leave_playfield, fixa no mundo (padrão: o espaço do init com uma margem de 25% em cada lado, ou seja 1,5× maior; não segue a câmera). Não é Box. inset / outset / grid / beside / strip devolvem cópias Box e não escrevem .pad. Alias: canvas.bounds.
Não há canvas.center / canvas.screen / canvas.view. Centro do que você vê: canvas.game_view.center (mundo) ou canvas.ui_view.center (HUD).
Box cortado de game_view no setup fica no mundo quando a câmera anda. Em geral o que uma formação quer. Relayout que segue a vista: leia game_view de novo no update. HUD com mundo depois de pan fica errado. Sair da fase: @leave_playfield.
Câmera girada: game_view é maior que a tela. Ele é o retângulo reto (sem giro) que contém a vista girada, então os cantos dele, e as células de uma grade feita dele, ficam fora da tela; confine(canvas.game_view) deixa o objeto sair pelos cantos. Para prender na tela com a câmera girada, gire a região junto: Box(center=canvas.game_view.center, width=canvas.width / canvas.zoom, height=canvas.height / canvas.zoom, angle=-canvas.rotation).
Level
Lê um arquivo de fase feito no editor de fases (python -m scena_editor). Só lê: Level("fase1.json") não cria nada na tela. O código decide o que cada item vira.
fase = Level("fase1.json")
for p in fase.group("plataforma"):
bloco = p.make() # o Rect como foi desenhado, no grupo "plataforma"
bloco.mass = STATIC
for e in fase.group("estrela"):
Sprite("star_gold.png", size=28, center=e.center) # um ponto é um lugar
fase["fundo"].make() # imagem: o Sprite no tamanho e no giro do editor
lago = fase["lago"].make(color="steel blue")
area = fase["arena"].box # ou só a região (Sprite(box=), confine, grade…)
inicio = fase["inicio"].center
rota = fase["rota_drone"].points # lista de Vec2 para move_along
Neste Level
Level(path) — caminho relativo procurado como as imagens do Sprite: junto do script, na pasta media/ dele, na pasta atual.
Level
Membro
Retorna
Significado
Level(path)
Level
Lê o arquivo .json salvo pelo editor. path é o nome do arquivo, procurado como as imagens.
level["nome"]
LevelItem
O item com esse nome (nomes são únicos na fase). Nome desconhecido levanta ValueError, com sugestão; se o nome for de um grupo, a mensagem diz level.group(...).
level.group("g")
lista de LevelItem
Os itens que têm o grupo g, na ordem do arquivo (ordem de desenho). Grupo que nenhum item tem levanta ValueError — um erro de digitação não vira lista vazia.
for item in level · len(level) · "nome" in level
—
Todos os itens, na ordem do arquivo. Guias (imagens só de referência no editor) não fazem parte da fase.
.names · .groups
tupla de nomes
Todos os nomes; todos os grupos usados (ordem do primeiro uso).
.size · .box
(largura, altura) · Box
A área da fase, de (0, 0) até size. .box é um Box novo a cada leitura.
Nome e grupos dados no editor. Um item pode estar em vários grupos.
.center
Vec2
Centro. Linha: o meio; polígono e caminho: o meio da caixa que os envolve; ponto: o próprio ponto.
make(**kwargs)
o shape novo
Cria o shape como foi desenhado, nos grupos do item: rect → Rect, ellipse → Ellipse, circle → Circle, line → Line, polygon → Polygon, regular_polygon → RegularPolygon, track → Track (com o smooth do item; 0 se o arquivo não tem), point → Point, image → Sprite (tamanho e giro do editor). Outras palavras-chave passam adiante (layer=, color=, visual=); group=soma aos grupos do item. Geometria (center=, width= …) levanta TypeError: ela vem da fase. Mesma tabela do “Copiar como código” do editor.
LevelItem — por tipo
Tipo
Campos
.box
rect · ellipse
.width.height.angle
Box orientado (com o giro)
image
.image (caminho absoluto) .width.height.angle
Box orientado
circle
.radius
o quadrado em volta
line
.p1.p2 (Vec2)
não tem — levanta AttributeError com dica
regular_polygon
.radius (centro ao vértice), .n (lados), .angle (rumo do vértice 0)
não tem
polygon · track
.points (lista nova de Vec2); track também .closed e .smooth (0 = retas, 1 = curva; numa trilha suave os pontos são os de controle — move_along(item.make()) segue a curva)
não tem
point
só .center
não tem
O editor de fases
O editor vem junto com o Scena (pip install scena). No terminal, qualquer uma das duas formas abre (ou cria) a fase: scena-editor fase1.json ou python -m scena_editor fase1.json — a segunda sempre funciona, porque usa o mesmo Python dos seus jogos.
Um ícone para abrir o editor com dois cliques: rode uma vez scena-editor --shortcut (ou python -m scena_editor --shortcut). No Windows ele aparece no menu Iniciar e na área de trabalho; no macOS, em Aplicativos (~/Applications); no Linux, no menu de aplicativos. Se reinstalar o Python, rode de novo. Erros do editor ficam num arquivo de log, cujo caminho aparece na barra de status.
Ferramentas (à esquerda, ícones com a tecla no canto): Retângulo (R), Elipse (E), Círculo (C), Linha (L), Polígono (G), Polígono regular (N), Trilha (A), Ponto (P), Imagem (I). Arrastar desenha; um clique cria a forma com 100 de lado. Na barra de cima: Desfazer / Refazer, Selecionar (V) e Mover a vista (H, ou Espaço+arrastar). O painel à direita dá nome, grupos (separados por vírgula), posição e tamanho. Shift ao arrastar: sem grade. Cmd/Ctrl+C copia os itens — e o código que os cria (Rect(center=…)) vai para a área de transferência. Os números do editor são os mesmos do código.
Não confundir
Parece
É
Não é
item.make() vs item.box
make() = o shape desenhado no editor. .box = a região, para outro uso (Sprite("x.png", box=item.box), confine, grade). Dois propósitos, não duas grafias.
Level(...) não cria nada sozinho; nem level.group(...).
Grupos da fase
Rótulos do editor. make() os aplica ao shape, então group["plataforma"] encontra os blocos feitos assim.
Não são grupos vivos: level.group("g") lê o arquivo, não o registro.
Números do editor
Os mesmos do código: unidades de design, y para cima, centro, giro anti-horário em graus.
Não há conversão. Mudar um valor lido (item.center) não muda a fase: são cópias.
O que se lê a cada frame: o tempo (clock), o teclado (keyp) e o mouse. Pergunte no update — “a seta está apertada?” — e aja. Os três também podem entrar numa fórmula @=. Para rodar algo uma vez quando uma condição vira verdade, veja Reagir.
Segurando / todo frame → Frame
Quando vira verdade / sai da tela → Reagir
Corpos se tocam → Colisão
Daqui a N segundos / a cada N → Agendar
Este número vai de A até B → Mover
Esta Entity está neste capítulo → Mode
clock
clock é o relógio do jogo: dt (segundos deste frame), time (segundos desde o começo) e os controles de pausa e câmera lenta (paused, scale). Os tempos já vêm na escala do jogo — em câmera lenta dt diminui; pausado, é 0. clock.real_dt / clock.real_time são o tempo de verdade (o relógio de parede), sem escala. e.time_scale deixa uma Entity só mais lenta ou mais rápida. Para um ritmo, “uma vez a cada N segundos”: ready.
Tempo do jogo: dt = segundos deste frame, time = segundos desde o início. Encolhe em câmera lenta (scale) e é 0 pausado. No máximo 0,25 s por frame: depois de um travamento (breakpoint, arrastar a janela) o mundo anda só isso — .time fica atrás do relógio de parede.
clock.real_dt
número0.016
leitura
Segundos reais deste frame (relógio de parede): ignora pausa e câmera lenta e não tem o limite de 0,25 s. Útil para widgets e para medir FPS.
clock.scale
número1
lê / escreve
1 normal, 0 pausa, 0.5 câmera lenta.
clock.paused
boolFalse
lê / escreve
Atalho de scale == 0. False devolve o scale anterior.
e.time_scale
número1
lê / escreve
Multiplicador extra neste Entity (timers, tweens, wait, on_update). Padrão 1.
Métodos
Método
Assinatura
Retorna
Significado
e.on_update
Parâmetros: e (objeto), dt (número)
—
fn(e, dt) a cada frame desta Entity, inclusive durante await. Na classe: def on_update(self, dt).
Em @= (reativo)
clock pode entrar numa fórmula: nomeie-o num lambda. A fórmula passa a depender do relógio inteiro (clock.time não é um campo que se ligue sozinho) e é refeita a cada frame.
from math import cos, sin
moon["center"] @= lambda: (
earth.center.x + 200 * cos(clock.time),
earth.center.y + 200 * sin(clock.time),
)
st["pulse"] @= lambda: abs(sin(clock.time * 3))
Fórmula com clock.time só atualiza quando algo muda
Todo frame avança o clock; uma fórmula que lê clock.time reavalia a cada frame. Se nenhuma fórmula lê o clock, isso não custa nada. O primeiro dt conta desde o início do run() (depois do setup), não desde o init.
—
chance / ready / recently. Rolagem pontual (30% de drop): chance. Cadência / cooldown: ready. Lembrar um valor por X segundos: recently.
Teclado
keyp diz, no update, como está o teclado. keyp("A") é True enquanto A está segurada (Shift junto não atrapalha; Ctrl / Alt / Cmd sim); keyp.once("A") só no frame em que A foi apertada (para pular, atirar uma vez); keyp.only("A") exige que nenhuma outra tecla esteja segurada; keyp("CTRL+R") lê um atalho. Também entra numa fórmula @= (abaixo).
def update(dt):
if keyp("LEFT"):
player.move_by(-200 * dt, 0)
if keyp.held("A"):
player.move_by(-280 * dt, 0)
if keyp.only("SPACE"):
shoot()
if keyp.once("R"):
canvas.reset_view()
if keyp.once("CTRL+R"):
restart()
if flag1 and flag2 and keyp.once("SPACE"):
act()
Neste keyp
Método
Assinatura
Retorna
Significado
keyp("A")
Parâmetros: key (tecla)
True/False
A segurada, sem Ctrl/Alt/Cmd (eles fazem atalhos). Shift não atrapalha: "segure Shift para correr" continua lendo keyp("RIGHT") e keyp.once("SPACE"); com Shift e R, keyp("R") e keyp("SHIFT+R") são os dois True. Outra tecla comum segurada junto não muda nada: com LEFT e RIGHT seguradas, keyp("LEFT") e keyp("RIGHT") são True (só keyp.only exige que ela esteja sozinha, sem Shift também).
keyp.held("A")
Parâmetros: key (tecla)
True/False
A segurada; modificadores ignorados.
keyp.once("A")
Parâmetros: key (tecla)
True/False
True só no frame do aperto de A. A primeira leitura gasta o aperto: depois dá False até a tecla ser solta e apertada de novo. Um aperto que ninguém leu naquele frame não aparece depois: em if pronto and keyp.once("SPACE"), um SPACE apertado enquanto pronto era False não conta quando pronto virar True. Para guardar o aperto, guarde você: if keyp.once("UP"): st.quer_pular = True.
keyp.only("A")
Parâmetros: key (tecla)
True/False
keyp("A") e nenhuma outra tecla física — nem Shift: keyp.only("R") não é Shift+R.
keyp("SHIFT+A")
Parâmetros: key (atalho, ex.: "SHIFT+A")
True/False
Atalho: True enquanto Shift e A estão seguradas juntas. Outra tecla comum segurada ao mesmo tempo não atrapalha; um modificador a mais faz dar False — num atalho, Shift também conta: keyp("CMD+Z") não é Cmd+Shift+Z.
"SPACE""ESC""LEFT""F1"
Nomes de tecla (str)
—
As outras teclas especiais: "ENTER""TAB""BACKSPACE""DEL""INS""HOME""END""PAGEUP""PAGEDOWN", as setas ("UP""DOWN""LEFT""RIGHT") e "F1"…"F12". Família de modificador: "SHIFT""CTRL""ALT""CMD". Pontuação é "K1"…"K12" (tabela abaixo). Maiúscula/minúscula tanto faz. Nome desconhecido falha na hora: keyp("ESCAPE") → ValueError sugerindo "ESC"; keyp(5) → TypeError. keyp["A"] não é consulta: TypeError pedindo keyp("A") ou keyp.once("A").
mouse.press(Mouse.BTN_LEFT)
Parâmetros: button (botão, int 0–7)
True/False
Botão por número (Mouse.BTN_LEFT / BTN_RIGHT / BTN_MIDDLE). Um aperto que um widget (ou um SpriteButton) pegou lê False até subir, como um TextInput focado esconde as teclas do keyp (mouse). mouse.press("left") → TypeError com a dica.
Teclas de pontuação (K1–K12)
A tecla de pontuação tem número, não caractere: keyp("K11"), key == "K7". O nome é a posição no teclado — a mesma em qualquer layout. O que está impresso na tecla muda (no ABNT2, a K7 é o Ç). keyp(".") ou keyp("PERIOD") → ValueError sugerindo "K11".
def update(dt):
if keyp.once("K10"): # vírgula (US e ABNT2)
camera_anterior()
if keyp.once("K11"): # ponto
proxima_camera()
if keyp("CTRL+K3"): # Ctrl + a tecla do =
canvas.zoom_by(1.02)
Nome
Onde fica
Tecla no US
Tecla no ABNT2
"K1"
à esquerda do 1
`
'
"K2"
à direita do 0
-
-
"K3"
2ª à direita do 0
=
=
"K4"
à direita do P
[
´
"K5"
2ª à direita do P
]
[
"K6"
acima do Enter (US) / à esquerda do Enter (ABNT2 e outros ISO)
\
]
"K7"
à direita do L
;
Ç
"K8"
2ª à direita do L
'
~
"K9"
à esquerda do Z (só teclado ISO)
—
\
"K10"
à direita do M
,
,
"K11"
2ª à direita do M
.
.
"K12"
3ª à direita do M
/
;
Limites do teclado físico: a tecla /? do ABNT2 (à direita da K12) não tem nome — o sistema não a identifica. Em teclado ISO da Apple, as teclas à esquerda do 1 e à esquerda do Z chegam trocadas (K9 ↔ K1). Teclado numérico e Caps Lock não têm nome: keyp e on_key não os veem.
Não confundir
Parece
É
Não é
Quero o caractere digitado (?, :, ç)
on_char(char) — respeita layout e Shift. keyp é posição: controle de jogo.
keyp("?") — ? é Shift + K12 só no US.
Em @= (reativo)
keyp pode entrar numa fórmula: apertar ou soltar uma tecla faz o Scena refazer as fórmulas que o leem. Na fórmula, use as leituras de “segurando” (keyp("D"), held, only, atalho) — não once.
c.center["x"] @= lambda: 1200 if keyp("D") else 800
gate["open"] @= lambda: keyp.held("SHIFT")
# “quando apertar”: @when, não once dentro de @=
@when(lambda: keyp("Y"))
def on_y():
restart()
Pode
Não pode
campo @= lambda: … if keyp("D") … / held / only / atalho
keyp.once dentro de @= — o lambda pode reavaliar; once consome
@when(lambda: keyp("Y")) — dispara na passagem False→True
@when + keyp.once juntos — @when já dispara só na passagem para True
Só ler no update — sem custo extra enquanto nenhuma fórmula usa o keyp
keyp["A"] @= … — o keyp não tem campos para ligar; ele só é lido
Segurando: update ou @=. Apertou uma vez: keyp.once no update, ou @when(lambda: keyp("…")).
Não confundir
Parece
É
Não é
keyp.once no começo de um and
keyp.once consome a tecla na verificação. Para combinar condições, coloque once no fim: if flag1 and flag2 and keyp.once("SPACE"): dispara uma vez, no frame do aperto, só quando os dois flags já são True. Se once vier antes, o and gasta o aperto mesmo com flag False.
—
Aperto um ou mais frames antes da condição (pulo antes de aterrissar): recently("pulo", keyp.once("UP"), hold=0.12). o pedido fica vivo pelo hold. O gotcha acima só cobre o mesmo frame.
keyp.once vê o aperto mesmo se a tecla já tiver sido solta na mesma leitura (frame lento). keyp("A") não: ele só diz o que está segurado agora.
Teclas já seguradas quando a janela abre contam como seguradas:keyp("A") já dá True, mas keyp.once("A") não dispara — não houve aperto.
Tecla com um TextInput focado. TextInput focado fica com as teclas de texto e de edição (letras, números, espaço, setas, Enter, Esc, Tab…) e com Cmd/Ctrl + A / C / X / V / Z / Y: callback e keyp não veem. As outras passam para o jogo — Cmd/Ctrl + outra letra (keyp.once("CMD+S") funciona), F1–F12, Page Up / Down, e Shift / Ctrl / Alt / Cmd sozinhos — e a caixa grava o texto antes, como um Enter, mas sem perder o foco e sem rodar on_submit: o que a caixa mostra é o que o jogo lê. Atalho ("CTRL+R") nos callbacks de tecla: ver Callbacks. Para reagir quando uma condição do jogo vira verdade: @when. Sobreposição de corpos: Colisão.
Mouse
mouse diz onde está o ponteiro e que botões estão apertados, nas mesmas coordenadas do mundo que os objetos (a câmera já é levada em conta). Ele mesmo é um ponto: move_to(mouse), sem (mouse.x, mouse.y). Para clicar ou arrastar um objeto, use Clique / arraste. Também entra numa fórmula @= (abaixo).
mouse é um ponto: vale onde a API espera um lugar (move_to(mouse), query_point(mouse), about=mouse…). Isso copia a posição agora. Para acompanhar: Circle(center=lambda: mouse) ou center @= mouse. Dois números de uma vez: x, y = mouse (também uma cópia).
Neste mouse
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
mouse.x / .y
número300
leitura
Posição do ponteiro em unidades de design, no espaço do mundo (a câmera já é levada em conta). Lida uma vez por frame: depois de canvas.pan_by / zoom_by no mesmo update, mouse.x ainda é o valor de antes da câmera andar, até o próximo frame. x, y = mouse copia os dois de uma vez.
mouse.dx / .dy
número0
leitura
Quanto o ponteiro andou desde o frame anterior, em unidades.
Métodos
Método
Assinatura
Retorna
Significado
mouse.press()
Sem parâmetros
True/False
Algum botão (fora os que um widget pegou, como na linha abaixo).
mouse.press(Mouse.BTN_LEFT)
Parâmetros: button (botão: Mouse.BTN_LEFT / RIGHT / MIDDLE)
True/False
Aquele botão. Um aperto que um widget pegou (arrastando um Slider, segurando um Button ou um SpriteButton, mesmo desligado) lê False até o botão subir: o pincel não pinta embaixo do slider nem do ícone de ferramenta. Aperto em outro objeto (draggable, @click, um Sprite com on_click) continua True.
mouse.click_pos(button)
Parâmetros: button (botão)
ponto
(x, y) do último clique. Um clique num widget ou sob um menu modal não conta.
Em @= (reativo)
mouse é reativo (como clock / keyp). O ponteiro inteiro liga direto (center @= mouse). .x / .y / .press são propriedades — nomeie mouse num lambda. Mesma regra que o teclado: segurando (nível) vale; “acabou de apertar” não.
canvas também é reativo: lambda: canvas.game_view.center, lambda: canvas.zoom ou lambda: canvas.layers["fundo"].view.left seguem cada movimento da câmera e cada rolagem de camada (canvas.ui_view só muda com init).
cursor["center"] @= mouse
cursor.center["x"] @= lambda: mouse.x
st.aiming = 0.0
st["aiming"] @= lambda: 1.0 if mouse.press(Mouse.BTN_LEFT) else 0.0
# ainda ok no update (movimento / gesto pontual)
def update(dt):
if mouse.press(Mouse.BTN_RIGHT):
canvas.pan_by(mouse.dx, mouse.dy)
campo @= mouse.x — número cru → TypeError; use lambda ou @= mouse no ponto inteiro
@when(lambda: mouse.press(Mouse.BTN_LEFT))
mouse.dx / .dy em @= — delta (como keyp.once); leia no update
Só ler no update — sem custo extra enquanto nenhuma fórmula usa o mouse
mouse["x"] @= … — o mouse não tem campos para ligar; ele só é lido
["center"] @= mouse é colocação. Como é colocar (não viajar), o objeto atravessa as paredes de resolve(). Para clicar ou arrastar um objeto: Clique / arraste. mouse.x / .y estão no espaço do mundo; um objeto do HUD (camada "ui") fica preso à janela, então essas coordenadas não servem direto nele: com a câmera em pan / zoom, botao.contains_point(mouse) responde no mundo e erra. Para saber se o ponteiro está sobre um objeto do HUD, leia botao.pointer_hover (o clique e o hover já usam a vista).
@= para mouse / mouse.x está vivo no primeiro frame desenhado. O motor amostra o ponteiro quando a janela abre (e de novo se a câmera andar com o cursor parado). Circle(center=mouse) ou center=(mouse.x, mouse.y) no corpo do script ainda é (0, 0): não há janela no init. mouse.dx / on_mouse_move não disparam nessa amostra.
Não confundir
Parece
É
Não é
Ordem de quem recebe o clique
Press/scroll: widgets → shapes (top, depois o resto) → callback do script. on_mouse_move corre junto, não é exclusivo.
Mover, girar e escalar objetos — na hora ou ao longo de um tempo (time=), em linha reta, por uma curva ou presos numa região — e animar qualquer campo de um valor a outro (tween). Para chamar uma função depois de um tempo: Agendar. Para uma sequência de passos com esperas: Mode.
Verbos que mudam a posição, o ângulo e o tamanho de um Graphic (Shape, Sprite, Text). Um SpatialFx se move pelo .center — ver SpatialFx. Sem time= a mudança é na hora e o verbo devolve o próprio objeto. Com time= ela acontece aos poucos durante esses segundos e o verbo devolve um Handle; aceita também ease= / loops= / yoyo= / then= / on_done= / force= (ver Parâmetros de tempo). Mover e girar também viajam por velocidade: speed= no lugar de time= (unidades ou graus por segundo), e a duração sai da distância.
Absoluto (destino)
Relativo (delta)
Mover
move_to(x, y) — Vec2 / Point, about=
move_by(dx, dy) — Vec2, polar angle= / distance=, só distance= (para a frente)
Colisão: colocar × viajar. Na hora (= / *_to sem time=) coloca — atravessa a parede. move_by, move_along e move_to(…, time=) (ou speed=) viajam — a parede de resolve() para no meio, e o @collide de quem viaja rápido confere o caminho (Objeto rápido). Girar no lugar e escalar (rotate_* / scale_*, com ou sem time=) nunca são parados pela parede: ela só afasta o objeto depois, se ele ficou encostado. Girar em volta de outro ponto (rotate_by(…, about=)) viaja: o centro anda, e a parede o para. Reset / respawn: hero.center = nascer. Ver Colocar × viajar.
Lugar absoluto. Sem time=coloca. Com time=viaja — a cada frame ele anda um pedaço, a parede pode parar no meio. Com speed= no lugar de time=, viaja a essa velocidade (unidades por segundo): longe demora mais, perto chega logo. Por padrão, o centro chega no destino; about= escolhe outro ponto do shape.
c.move_to(400, 200) # x, y
c.move_to(Vec2(400, 200)) # Vec2 no 1º
c.move_to(p) # Point no 1º
c.move_to(400, 200, time=1.2, ease="ease_out")
c.move_to(400, 200, time=1.0, loops="forever", yoyo=True)
missile.move_to(target, time=1.2, face=True)
hero.move_to(mouse.click_pos, speed=220) # clicar para andar
nave.move_to(nave.vaga, speed=300, ease="ease_out") # todas voltam no mesmo passo
Destino num único argumento: qualquer ponto — tupla (x, y), Point, Vec2, um Graphic (vale o centro dele) ou mouse. Não aceita angle= / distance= (isso é do move_by).
time= ou speed= (distância do ponto que viaja até o destino, medida na chamada; um eixo None não conta) · ease= · loops= · yoyo= · face= · then= · on_done= · force=.
Desloca o objeto a partir de onde ele está — é um deslocamento, não um destino. É sempre uma viagem: uma parede de resolve() o para no meio. Escreva o deslocamento de um destes jeitos, um por chamada: dx, dy · um Vec2 · angle= e distance= · só distance=, para onde o objeto aponta. Com time= (ou speed=), o deslocamento é calculado no início e não muda durante a viagem.
c.move_by(40, 0) # dx, dy
c.move_by(Vec2(40, 0)) # Vec2 no 1º
c.move_by(angle=90, distance=40) # polar (os dois juntos)
nave.move_by(distance=200 * dt) # para a frente (sem angle=: usa nave.angle)
nave.move_by(distance=-80 * dt) # ré: anda para trás, sem virar
c.move_by(200 * dt, 0)
c.move_by(0, 80, time=0.4)
c.move_by(40, 0, time=0.5, loops="forever", yoyo=True)
bolt.move_by(angle=45, distance=500, time=1.0, face=True)
guarda.move_by(300, 0, speed=60, loops="forever", yoyo=True) # patrulha a 60 por segundo
Delta num único argumento. Point levanta TypeError — use move_to. Tupla (dx, dy) não entra.
angle=, distance=
nome=
número90
Polar. Graus, 0 = +x. Sem angle=, anda para onde o objeto aponta: lê o angle na hora da chamada (como raycast(length=)); negativo dá ré e a direção não muda. angle= sem distance= levanta TypeError. Não misture com dx/dy na mesma chamada.
time= ou speed= (o tamanho do deslocamento ÷ speed) · ease= · loops= · yoyo= · face= · then= · on_done= · force=. Sem about=.
Gira o objeto até o ângulo θ (graus, anti-horário, 0 = para a direita). Sem time= gira na hora; com time= gira aos poucos pelo caminho mais curto (com exatamente 180° de diferença, gira no sentido anti-horário). Com speed= (graus por segundo), a duração é esse arco curto ÷ speed. Uma parede de resolve() não interrompe o giro: ela só afasta o objeto depois, se ele ficou encostado.
c.rotate_to(45)
c.rotate_to(180, time=0.6, ease="ease_in_out")
c.rotate_to(90, time=0.5, loops="forever", yoyo=True)
turret.rotate_to(alvo_ang, speed=90) # torre gira a 90° por segundo
time= ou speed= (graus por segundo) · ease= · loops= · yoyo= · then= · on_done= · force=.
Gira mais dθ graus a partir do ângulo atual (positivo = anti-horário, negativo = horário). Pode passar de 180°: 360 dá uma volta inteira. O .angle acumula, sem voltar para 0–360 (50°/s por 8 s lê 400); para comparar direções, use % 360.
c.rotate_by(30 * dt)
c.rotate_by(360, time=1.2)
c.rotate_by(45, time=0.4, loops=4, yoyo=True)
roda.rotate_by(-720, speed=180) # duas voltas a 180° por segundo: 4 s
Estado absoluto. angle é o rumo — para onde o objeto aponta (0 = +x, anti-horário) — e é um campo, como width: =, @= e o parâmetro do construtor fazem o mesmo, e o ponto pelo qual o objeto foi posicionado fica parado (centro, top_left, p1, vertex; um Rect p1=, p2= inclina). Onde a direção vem dos pontos (Line p1=, p2=, Rect anchor=, Ellipse por focos, Circle pela borda, Polygon), mudar angle gira pelo centro. Sprite / Svg / Text / Point / Track são posicionados pelo centro (Text com âncora: pela âncora). Os verbos rotate_to / rotate_by giram o objeto inteiro pelo centro (ou about=). scale é atalho de scale_to pelo centro. Uma Entity que não é Graphic (como Entity()) não tem angle de verdade: ler levanta AttributeError, e escrever só cria um campo próprio com esse nome, sem girar nada. .scale multiplica o desenho e o colisor: .width / .height continuam o tamanho que você deu (um Sprite com height=40 e scale_to(1.08) ainda lê height 40); o tamanho na tela é .box.
.rest_heading
número90
leitura
O ângulo em que o objeto fica “sem rotação” — para onde a arte aponta; reset_rotation() volta a ele. Shape: 0. Sprite: a direção da arte (art_angle), já contando o flip — ver Orientar sprites.
.tilt
número · contato · "ground""ground"
lê / escreve
Sprite. Inclina só o desenho, sem girar o colisor. Ver Inclinar.
Parâmetros: time (número), speed (graus por segundo), ease (str), then, on_done, about (ponto), force
—
Desfaz a rotação: volta o objeto ao ângulo de rest_heading (Shape: 0; Sprite: a direção da arte). Zera também .tilt. Não limpa flip. Aceita os mesmos parâmetros de tempo de rotate_to (inclusive speed=), sem loops= / yoyo= (destino único).
Não confundir
Parece
É
Não é
move_by(p) com Point
TypeError. Destino é move_to(p). Delta: dx, dy, Vec2 ou angle=/distance=. Tupla nua não entra em move_by. Polar não existe em move_to.
delta com Point
move_by(distance=…, face=True) vira para onde anda
TypeError. Sem angle= a direção já é o angle; face= giraria o objeto pelo próprio movimento (com distância negativa, meia-volta a cada chamada). Ré: tire face=. Meia-volta: rotate_by(180) antes. Ou passe angle=.
virar
move_to(..., about=, face=True) gira em torno do centro
Gira em torno do ponto about= — senão a âncora sairia do destino no instante da rotação. Só move_to tem about= entre os verbos de mover.
o centro
turret["angle"] @= … gira em volta de qualquer ponto
A ligação (@=) escreve o campo: o ponto de posicionamento fica parado (ou gira pelo centro, onde a direção vem dos pontos). Outro pivô: chame o verbo com about=.
rotate_to(…, about=)
x.angle = 30 faz o mesmo que x.rotate_to(30)
Só num objeto posicionado pelo centro. Um Rect top_left= mantém o top_left com angle =; rotate_to gira pelo centro — como top_left = e move_to.
—
Parâmetros de tempo (time=)
Os parâmetros que todo verbo que leva tempo aceita: quanto dura (time=) ou, em vez disso, a que velocidade vai (speed=), com que curva (ease=), quantas vezes repete (loops=, yoyo=) e o que fazer no fim (then=, on_done=). Valem para move_to, move_by, rotate_to, rotate_by, scale_to, scale_by, move_along, reset_rotation (sem loops= / yoyo=), tween e a câmera (sem force=). speed= vale só nos verbos que percorrem uma distância: move_to, move_by, move_along, rotate_to, rotate_by, reset_rotation e, na câmera, pan_to / pan_by.
h = ship.move_to(400, 200, time=1.2, ease="ease_out")
ship.rotate_by(360, time=1.0, loops="forever")
ship.scale_to(1.2, time=0.3, loops=4, yoyo=True)
ship.move_to(600, 200, time=1.0, on_done=lambda: play_sound("pop"))
hero.move_to(mouse, speed=200) # 200 unidades por segundo: longe demora mais
turret.rotate_to(90, speed=45) # 45 graus por segundo
Sem time= nem speed= (exceto move_along, que exige um dos dois, e tween, que exige time=): na hora, devolve o objeto. Com time= ou speed=: interpola e devolve Handle. time= e speed= juntos levantam TypeError: os dois dizem quanto dura. ease= / then= / on_done= / loops= / yoyo= exigem time= ou speed= (sem eles não há curva: rotate_by(10, ease="ease_in_out") levanta erro). start= existe só em tween — para colocar e depois viajar com um verbo de pose, use duas linhas (center = … e depois move_to(..., time=)). O contrato de fim: Ao terminar.
Parâmetro
Passar
Tipo (default)
Significado
time=
nome=
número ou NoneNone
Segundos de uma volta. Sem: na hora. Com: interpola e devolve Handle. Em tween é obrigatório; em move_along, passe time= ou speed=.
speed=
nome=
número > 0 ou NoneNone
Velocidade em vez de duração: unidades por segundo (em move_to / move_by / move_along / pan_to / pan_by) ou graus por segundo (em rotate_to / rotate_by / reset_rotation). A duração é a distância ÷ speed, medida uma vez, na chamada: em move_to, do ponto que viaja até o destino (um eixo None não conta; com about=, a partir desse ponto); em move_by, o tamanho do delta; em move_along, o comprimento da trilha × o trecho do span; em rotate_to, o arco curto. Depois disso é a mesma viagem de time=: o destino não muda se o alvo se mexer, e uma trilha que cresce depois não muda a duração. Com ease=, speed é a velocidade média (no meio ela passa disso). Com loops=, vale por volta (por perna com yoyo=). Distância zero termina no frame seguinte, com on_done. 0, negativo, nan ou texto levantam erro. Não existe em scale_*, tween nem nos outros verbos da câmera (escala e zoom são razões, não distâncias): lá, speed= levanta TypeError.
ease=
nome=
nome da curva ou função"linear"
Curva. Padrão "linear". Nomes: linear · ease_in · ease_out · ease_in_out · quad · inv_quad · smoothstep · sigmoid · bounce · elastic · steps · xpsin · erratic, ou uma função sua que recebe t de 0 a 1 (o tempo já passado) e devolve quanto do caminho já foi feito (0 a 1). Nome desconhecido → ValueError com a lista. spring / wobble não entram aqui — só em remap(…, modifier=). Com loops= / yoyo=, a curva corre para a frente em cada perna (não é espelhada no tempo).
loops=
nome=
1, N ou "forever"1
Mesma grafia de move_along / play_sound. Padrão 1. "forever" nunca dispara then= / on_done= — pare com handle.cancel() ou destroy.
yoyo=
nome=
boolFalse
Voltas ímpares invertem a direção (trocam início/fim) em vez de recomeçar. Precisa loops > 1 (ou "forever"); com loops=1 levanta erro. loops=N finito: pousa no início se N é par, no fim se N é ímpar. Em move_along, inverte o span, não o phase.
face=
nome=
boolFalse
Só em move_to / move_by / move_along. Opcional: escreve .angle na direção da viagem (reta ou tangente). Delta ~0 / sem movimento: mantém a rotação. Com yoyo (e em move_along também span=(1,0)) inverte 180° no sentido contrário. Não exige time= em move_to / move_by.
then=
nome=
função do capítulo
Mode a entrar quando o tween acaba (voltas finitas). Passe a função do capítulo (leaving, self.diving), sem (). Não é callback.
on_done=
nome=
função ou NoneNone
Callback quando o tween acaba (sem mudar mode). Passe uma função para chamar. O mesmo que passar fn em after. Não use junto com then=.
about=
nome=
ponto ou NoneNone
Um ponto de referência do movimento. Vale em rotate_* / scale_* / reset_rotation / move_to, e na câmera zoom_* / rotate_*. Padrão: centro do objeto (câmera: centro da vista). Em rotate_* / scale_* o ponto fica fixo; em move_to o ponto é o que chega exatamente no destino. move_to(..., about=, face=True) roda em torno desse mesmo ponto, não do centro.
force=
nome=
boolFalse
Só nos verbos de objeto (mover / girar / escalar, tween, move_along) — a câmera não tem. Se o campo que o verbo precisa mudar está ligado com @=: False (padrão) levanta LinkedAttributeError; True desfaz a ligação (dele e das partes dele, como center.x), mostra um aviso e segue. Recurso de exceção, não o uso do dia a dia. Ver Drivers persistentes.
Não confundir
Parece
É
Não é
time= e duration são a mesma coisa
Um verbo de pose levatime= (segundos da viagem). Um transiente ou uma capa duraduration (segundos em que o efeito fica). sprite.flash(duration) e canvas.flash(intensity) não têm o mesmo primeiro argumento: no sprite é a duração; no canvas, a intensidade.
sinônimos
speed= e h.speed são a mesma coisa
speed= é o pedido: a velocidade média da viagem, convertida em duração na chamada. h.speed é a leitura: quanto o objeto andou neste frame, por segundo. Com ease="linear" os dois coincidem; com uma curva, h.speed começa abaixo e passa do pedido no meio.
o mesmo número sempre
move_to(alvo, speed=) persegue o alvo
O destino é copiado na chamada, como com time=. Se o alvo andar, o objeto chega aonde o alvo estava. Para perseguir um alvo que se move na mesma velocidade, chame approach no update, todo frame: inimigo.move_to(approach(inimigo.center, jogador.center, 120)).
perseguir
time= e speed= juntos
Escolha um: time= fixa a duração e a velocidade sai da distância; speed= fixa a velocidade e a duração sai da distância. Os dois juntos levantam TypeError.
combinação
O Handle de uma viagem escreve .vel
Com time= ou speed=, h.velocity / h.speed / h.heading dizem o que este frame andou, em unidades por segundo — mas não escrevem .vel. Para física: caixa.vel = h.velocity. Quem está em cima da plataforma não precisa: o resolve já o leva junto. Ver Handle.
.vel
move_along
Faz o objeto percorrer uma curva: uma Track, o contorno de qualquer Shape, ou uma forma pronta pelo nome ("eight", "circle"…). time= é quanto dura uma volta; ou passe speed= (unidades por segundo ao longo da trilha) e a volta dura o comprimento ÷ speed. É sempre uma viagem: uma parede de resolve() para o objeto no caminho. Para ir até um ponto em linha reta: move_to(..., time=).
# primeiro passo: string = preset flutuante (loops=1)
ship.move_along("eight", time=2)
ship.move_along("circle", time=4, loops="forever")
# tamanho / trilha própria: Track completo
wasp.move_along(Track.eight(width=90, height=40), time=2.5)
ship.move_along(Track.circle(center=(400, 300), radius=120), time=4, loops="forever")
bug.move_along(hexagon, time=3, phase=0.2)
ship.move_along(Track(vectors=[(80, 0), (0, 50), (-30, 20)], smooth=0), time=1.2)
moon.move_along(orbit, time=6, face=True, loops="forever", yoyo=True)
# mesma velocidade em trilhas de tamanhos diferentes
small.move_along(Track.circle(radius=60), speed=80, loops="forever")
big.move_along(Track.circle(radius=200), speed=80, loops="forever")
A trilha. Uma Track; um Shape (o objeto segue o contorno dele, no lugar onde o Shape está, mesmo que ele se mexa); ou o nome de uma forma pronta, no tamanho padrão e posta onde o objeto está: "circle" · "eight" · "star" · "wave" · "spiral" · "arc" · "line" · "shuttle". "zigzag" levanta erro (use "eight"). Para outro tamanho: Track.eight(width=…). Ponto / (x, y) → TypeError; use move_to(..., time=).
span=
nome=
(u0, u1)(0, 1)
Que trecho da trilha percorrer, de 0 (início) a 1 (fim): (0, 1) é a volta inteira, (0, 0.5) só a primeira metade, (1, 0) percorre ao contrário — só nesta viagem; a trilha não muda.
phase=
nome=
número0.15
Quanto este objeto sai adiantado na trilha, em fração da volta (0 a 1): 0.25 começa a um quarto do caminho. Para uma fila de objetos na mesma trilha, dê a cada um um phase= diferente num for (veja o exemplo abaixo). yoyo= não muda o phase=.
at=
nome=
ponto ou None
Onde o começo do trecho (span) é posto — qualquer ponto, copiado agora. Padrão: onde o objeto está quando a viagem começa. Só vale para trilhas sem lugar fixo (flutuantes: os nomes prontos, um Track.eight(width=…) só com tamanho, ou Track(…, floating=True)). Numa trilha com lugar fixo (criada com center= / start=, ou o contorno de um Shape) levanta TypeError — envolva com Track(fonte, floating=True).
time= ou speed= (um dos dois; com speed=, uma volta dura track.length × o tamanho do span ÷ speed, medido na chamada — phase= não muda a conta) · ease= · loops= (padrão 1) · yoyo= (inverte o span) · face= (tangente) · then= / on_done= (fim das voltas finitas) · force=. Sem about=.
Tema
Regra
Frota
group["wasp"].move_along(…) chama em cada um e devolve uma lista de handles. Fase diferente por membro: o for com phase=. Espera: await parallel(*handles). Ver Agir em massa.
Preset aberto em loop
"wave" / "spiral" / "arc" / "line" com loops="forever": passe yoyo=True — senão a costura do caminho aberto salta. Fechados ("eight", "circle", "shuttle", …) envolvem sozinhos.
Mesma chave de move_to
Um move_along e um move_to(..., time=) no mesmo objeto se cancelam: o último vence.
O Shape seguido some
Se o Shape cujo contorno o objeto segue for destruído, o objeto continua pela última forma que o contorno tinha. A viagem só é cancelada se o Shape sumir antes do primeiro frame dela.
Handle
Reporta o que este frame andou: h.velocity (unidades por segundo), h.speed, h.heading (tangente — a mesma de face=), h.u (parâmetro da trilha) e h.lap (volta começando em 0; loops="forever" continua a contar). Com speed= e ease="linear", h.speed lê a velocidade pedida. Não escreve .vel.
# frota: lista de handles
handles = group["wasp"].move_along(track, time=4)
for i, w in enumerate(group["wasp"]):
w.move_along(track, time=4, loops="forever", phase=i * 0.15)
await parallel(*handles)
Não confundir
Parece
É
Não é
move_along(ponto) / move_along((x, y))
TypeError. Deslizar até um ponto: move_to(..., time=). move_along é curva: Track, contorno de Shape, ou nome de preset.
reta até o ponto
ship["center"] @= lambda: track.cursor viaja
Coloca: escreve o ponto a cada frame e atravessa paredes. Para viajar: move_along.
viagem
confine
Mantém um objeto dentro de uma região (um Box ou um Graphic): quando ele passa da borda, confine o segura nela (exatamente na borda: com o padrão by="center", o centro fica sobre ela), faz quicar ou o leva para o lado oposto (estilo Asteroids). É uma chamada que você faz no update, logo depois de mover. Para paredes que empurram, use resolve; para saber quando algo saiu da área de jogo, @leave_playfield.
PLAY = canvas.game_view.inset(40)
class Ship(Sprite):
def on_update(self, dt):
self.move_by(dx, 0)
sides = self.confine(PLAY) # by="center", action="clamp"
if sides:
canvas.shake(2)
# plataforma: a caixa do sprite fica dentro
enemy.confine(platform_box, by="box")
# bola quica nas bordas (precisa de .vel)
ball.vel = Vec2(220, 140)
ball.move_by(ball.vel.x * dt, ball.vel.y * dt)
ball.confine(arena, action="bounce")
# wrap estilo Asteroids
ship.confine(canvas.game_view, action="wrap")
A região onde o objeto deve ficar. Pode estar girada: os lados ("left" / "right" / "top" / "bottom") giram junto com ela (como em inset / grid / contains_point). Aceita também Rect / Square / Ellipse / Circle / Sprite / Svg (mesmo contrato de box=).
by=
nome=
str"center"
O que é comparado à região. Ver aba by=.
action=
nome=
str"clamp"
O que fazer ao cruzar a borda efetiva. Ver aba action=.
Devolve um frozenset das bordas locais da região em que agiu nesta chamada ("left" / "right" / "top" / "bottom"), ou vazio se nada aconteceu. Em região rotacionada, "left" é a esquerda local — o mesmo contrato de orientação de Grid / Box. Use o retorno para reagir (shake, som, inverter outra coisa); não há callback.
Qual parte de self precisa respeitar a região.
Valor
Significado
"center" (padrão)
Só o centro. Equivale ao padrão escrito à mão (if x < 40: center.x = 40). Num Sprite com Sheet(anchor="bottom"), o center são os pés: é ele que fica na borda.
"box"
O graphic.box fica inteiro dentro. Para isso, encolhe a região pela metade da largura e da altura do objeto, medidas ao longo dos lados da região. Objeto maior que a região: fica preso no centro nesse eixo (resto, não erro) — o mesmo espírito de Box.inset.
"overlap"
O contrário de "box": o objeto pode ficar parcialmente para fora; a ação só dispara quando o .box dele já saiu por completo da região. Para isso, cresce a região pela metade da largura e da altura do objeto.
# patrol numa plataforma: o sprite não “cai” pela metade
enemy.confine(platform, by="box")
# permitir sair até desaparecer, aí wrap / destroy
sides = shot.confine(view, by="overlap", action="wrap")
Para Sprite, graphic.box é o retângulo da imagem desenhada (o tamanho que você pediu com height= / box=), não a silhueta. Silhueta é só hitbox_shape="auto" na colisão. by="box" / "overlap" em Line / Polygon livre / Track / Point levanta o mesmo TypeError de graphic.box.
O que acontece quando a referência cruzou a borda efetiva.
Valor
Significado
"clamp" (padrão)
Para a referência na borda. Não mexe em .vel.
"bounce"
Segura na borda (como "clamp") e inverte o sinal de self.vel no eixo local cruzado (medido ao longo dos lados da região, mesmo girada). Exige self.vel já definido — senão TypeError com dica. A bola volta com a mesma velocidade (não perde força). Para quicar mais fraco, leia o retorno e diminua .vel você mesmo.
"wrap"
Leva o objeto para a borda oposta, mantendo o quanto ele passou (saiu 15 além → entra 15 para dentro do outro lado), sem pulo visível.
Uma string só (action=), não um combo bounce= + wrap= — isto não é resolução de colisão.
O frozenset de bordas é o gancho de reação.
sides = hero.confine(PLAY)
if "left" in sides or "right" in sides:
hero.flash(0.15)
# vazio → não tocou a borda neste frame
Os nomes seguem os lados da própria região. Com angle=30, "top" é a borda “de cima” da região inclinada, não o +y do mundo. Igual a como Grid.cell_at já pensa em Box orientado.
confine
@leave_playfield
resolve
Papel
Segurar / wrap / quicar a pose agora
Avisar quando o centro sai do playfield
Empurrar corpos que colidem
Quando
No seu update / on_update
Passo do frame (depois da colisão)
Passo do frame (colisão)
Registro
Não — só a chamada
Sim (grupo ou método)
Sim (resolve(a, b))
Muda pose?
Sim
Não (só callback)
Sim (para o corpo na parede e o afasta dela)
Não use confine e um tween / move_along / parede resolve() no mesmo eixo — são autoridades separadas sem precedência definida. Limpeza quando o tiro sai do mundo: @leave_playfield.
Contrato
Método
Método
Assinatura
Retorna
Significado
g.confine(region, *, by=, action=)
Parâmetros: region (Box ou Graphic), by (str), action (str)
frozenset
Mantém o objeto na região. Escreve o centro (e .vel se action="bounce"). As bordas devolvidas seguem os lados da região.
Erros
Caso
Erro
by= / action= desconhecido
TypeError com a lista permitida
action="bounce" sem self.vel
TypeError — dica: self.vel = Vec2(…)
by="box" / "overlap" em Graphic sem .box
O mesmo TypeError de graphic.box
Não confundir
Parece
É
Não é
confine(PLAY) uma vez já vale para sempre
Uma chamada que age só naquele frame: chame no update / on_update, depois de mover.
uma regra registrada uma vez (como resolve ou @leave_playfield)
Uma região que encolheu até virar um ponto (por exemplo, um inset maior que ela) não dá erro: o objeto fica no centro dela, como no resto do Box.
tween
tween(campo, valor, time=) leva um campo do valor atual até valor em time segundos. O valor pode ser número, tupla / lista de números, Vec2, Color (o valor muda aos poucos, em linha reta, do atual ao final — como lerp) ou um ponto. Para posição, ângulo e tamanho, prefira os verbos próprios (move_to…). Um tween novo no mesmo campo cancela o anterior. Aceita os parâmetros de tempo dos verbos de mover: time= (obrigatório) / ease= / loops= / yoyo= / then= / on_done= / force=. Só aqui: start= (valor inicial; sem ele, o valor atual). O que fazer no fim: Ao terminar. Cor e glow de um objeto (color, glow) não podem ser alvo — não são campos que se ligam; mude-os com .color = … ou set_color.
tween(ship["radius"], 80, time=0.4, ease="ease_out")
tween(ship.center, (400, 200), time=1.2)
tween(ship.center, (400, 200), time=1.2, start=(0, 0))
ship.tween(ship["hp"], 0, time=0.6)
# AttributeError: cor e glow não podem ser alvo — tween(ship["color"], …) não vale
Valor inicial. Sem: usa o valor atual no momento da chamada. Com: a interpolação começa daí — mesmo formato de to (número, ponto, Color, …). Só em tween, não em move_to / rotate_to / scale_to.
Aqui: campo ligado com @=: False levanta LinkedAttributeError; True desfaz a ligação e avisa. parâmetros de tempo.
name=
str ou None"pulse"
parâmetro
Slot: um tween novo com o mesmo name=, no mesmo objeto (ou solto no mesmo lugar), substitui o anterior em vez de somar outro. Sem name=: cada chamada é um tween novo.
Métodos
Método
Assinatura
Retorna
Significado
tween(target, to, *, time, ease="linear", start=, loops=1, yoyo=False, then=, on_done=, force=False, name=)
Parâmetros: target (alvo, Attribute ou Point), to (mesmo tipo do valor atual), time (número), ease (str), start (mesmo tipo de to), loops, yoyo, then, on_done, force, name
Handle
Alvo: um campo (ship["radius"], ship["hp"]), uma célula (st["t"]; st.t é o número e levanta erro) ou um Point (ship.center). Cor e glow não podem ser alvo. to deve casar com o valor atual — número, tupla/lista plana, Vec2, Color, ponto (Graphic, mouse: copia agora). start= opcional (mesmo formato de to). Vai do início a to aos poucos, em linha reta (como lerp). Tipo incompatível levanta erro na hora. time= obrigatório.
e.tween(target, to, *, time, ease="linear", start=, loops=1, yoyo=False, then=, on_done=, force=False, name=)
Parâmetros: target (alvo, Attribute ou Point), to (mesmo tipo do valor atual), time (número), ease (str), start (mesmo tipo de to), loops, yoyo, then, on_done, force, name
Handle
Igual, neste Entity. Sobrevive a troca de mode; morre no destroy.
Não confundir
Parece
É
Não é
tween(…) no corpo do mode sobrevive à troca
tween(…) solto no corpo do mode pertence ao mode e para quando ele acaba. e.tween pertence ao objeto e continua. Ver o que cancela.
—
tween(shape.center, …)coloca. Escreve o ponto a cada frame, como center =. A parede de resolve() não para no meio. Para viajar (a parede de resolve() para no meio): move_to(..., time=), move_by(..., time=) ou move_along. O mesmo para center @= (incluindo track.cursor).
yoyo=True com loops=1 levanta erro. Nada a saltar. Com loops=N finito e yoyo: pousa no início se N é par, no fim se N é ímpar. Alvo ligado: force=True (aviso + unlink) ou unlink antes.
Fazer algo daqui a N segundos (after) ou a cada N segundos (every), limitar a frequência de uma ação (ready) e lembrar que algo aconteceu há pouco (recently). Para rodar algo quando uma condição vira verdade, veja @when.
after(segundos, função) chama a função uma vez, depois desse tempo; every(segundos, função) chama de novo a cada intervalo — a primeira vez depois de um intervalo, não na hora. Dispara no primeiro frame em que o tempo já passou: com frames de 1/60 s, after(0.5) é o frame 30 e every(0.25) os frames 15, 30, 45… Um frame mais longo que o intervalo dispara as vezes que deve: o ritmo não depende do FPS. O tempo é o do jogo: pausa e câmera lenta valem. Ignore o Handle devolvido, guarde-o para cancelar, ou dê um name= — chamar de novo com o mesmo nome substitui o anterior. Dentro de um mode, after / every soltos param quando o mode acaba; e.after / e.every continuam (escopo e cancelamento).
Numa classe, @after / @every valem para cada objeto: o timer começa quando o objeto é criado e pertence a ele (continua na troca de mode, para no destroy).
Só em after: função a chamar quando o tempo acaba (o mesmo que fn). parâmetros de tempo.
name=
str"spawn"
parâmetro
Slot: um timer novo com o mesmo name=, no mesmo objeto (ou solto no mesmo lugar), substitui o anterior em vez de somar outro. Sem name=: cada chamada é um timer novo.
Métodos
Método
Assinatura
Retorna
Significado
after(delay, fn=None, *, then=, on_done=, name=)
Parâmetros: delay (número, segundos), fn (função), then (função do capítulo), on_done (função), name (str)
Handle
Uma vez, depois de delay segundos: chama fn (ou on_done=, que é o mesmo) ou entra no mode then=.
every(interval, fn, *, name=)
Parâmetros: interval (número), fn (função), name (str)
Handle
Repete. Sem then= e sem on_done= (não termina).
e.after / .every
Parâmetros: segundos (número), fn (função)
Handle
Neste Entity, sempre. Uma fn que pede um parâmetro sem valor padrão recebe o objeto (bola.after(4, sumir) com def sumir(bola)); sem parâmetro, nada. Sobrevive a troca de mode; morre no destroy / cancel_process. Ver escopo.
@after / @every
Decorator — agenda a função
—
No módulo: o timer começa já (conta o tempo do jogo, que anda a partir do run(): @every(0.4) chama pela primeira vez 0,4 s depois do início); o Handle fica em fn.handle. Num método de classe: começa quando cada objeto é criado.
Não confundir
Parece
É
Não é
on_done= e then= juntos
Escolha um dos dois: fn / on_done= chama uma função; then= muda o mode. Os dois juntos levantam erro. after(d, boom) ≡ after(d, on_done=boom). Não misture @every com @collide / @when / @on_mode no mesmo def.
—
every(0, fn) para cada frame
ValueError. Cada frame é on_update (Entity) ou update(dt) (script) — os dois recebem dt. O intervalo tem que ser > 0.
EVERY_FRAME ou intervalo 0
ready
ready(nome, intervalo) é um tempo de recarga: devolve True uma vez por intervalo — “já posso atirar de novo?”. O ritmo é exato na média: a sobra de um frame conta para o próximo, então dois True seguidos podem vir um pouco mais perto que o intervalo quando ele não é um número inteiro de frames. A primeira chamada já devolve True. Conta no tempo do jogo (pausa vale). Chamado todo frame (botão segurado), mantém o ritmo em qualquer taxa de quadros; depois de um tempo sem chamar (soltou o botão, nem que por um frame), volta a dar True uma vez, sem rajada, e o ritmo recomeça desse True.
def update(dt):
if ready("fire", 0.18):
shoot()
if keyp.once("R"):
ready.reset("fire")
Neste ready
ready(name, interval)
Parâmetros: name (nome), interval (intervalo). Retorna True/False.
Método
Assinatura
Retorna
Significado
ready(name, interval)
Parâmetros: name (nome), interval (intervalo)
True/False
True quando o intervalo vence. O nome é o slot. Passar o intervalo de novo atualiza o período.
e.ready(name, interval)
Parâmetros: name (nome), interval (intervalo)
True/False
Mesma leitura, slot desta Entity. Dois inimigos com self.ready("fire", …) não compartilham cooldown. Não é Handle — morre com a instância; destroy / cancel_process não tocam.
ready.reset(name)
Parâmetros: name (nome)
—
Zera: a próxima pergunta devolve True.
ready.disable(name) / .enable(name)
Parâmetros: name (nome)
—
Pausa / retoma o slot. Disable → sempre False.
ready.remove(name)
Parâmetros: name (nome)
—
Apaga o slot. A próxima chamada com intervalo cria de novo (e devolve True).
Rolagem pontual (30% de drop): chance. ready é intervalo, não probabilidade. Callback daqui a N segundos: after / every. Lembrar um booleano por um instante: recently.
recently
recently(nome, valor, hold=) lembra por um instante que algo foi verdade: devolve True se valor foi verdadeiro em algum momento nos últimos hold segundos. Serve para dar folga ao jogador — pular logo depois de sair da beirada (coyote time), apertar o pulo um pouco antes de aterrissar — ou para “foi atingido há pouco”.
def update(dt):
no_chao = "bottom" in heroi.touching_sides
# Coyote: fica "no chão" por 80ms depois de sair da borda.
no_chao_visivel = recently("no_chao", no_chao, hold=0.08)
# Buffer: UP até 120ms antes de aterrissar ainda conta.
if recently("pulo", keyp.once("UP"), hold=0.12) and no_chao_visivel:
heroi.vel.y = PULO
recently.reset("pulo") # pulou: gasta o buffer e o coyote,
recently.reset("no_chao") # senão o impulso volta nos próximos frames
Neste recently
recently(name, value, hold=0.1)
Parâmetros: name (nome), value (valor) (qualquer — conta como verdadeiro se if valor: passaria), hold (número, segundos). Retorna True/False.
Método
Assinatura
Retorna
Significado
recently(name, value, hold=)
Parâmetros: name (nome), value (valor), hold (número)
True/False
True se valor é verdadeiro agora, ou foi nos últimos hold segundos. O nome é o slot. Passar hold de novo atualiza a janela.
e.recently(name, value, hold=)
Parâmetros: name (nome), value (valor), hold (número)
True/False
Mesma leitura, memória desta Entity. Frota com self.recently("hit", …) não colide entre si. Não é Handle — morre com a instância.
recently.reset(name)
Parâmetros: name (nome)
—
Esquece: a próxima pergunta é False até valor voltar a ser verdadeiro.
recently.remove(name)
Parâmetros: name (nome)
—
Apaga o slot. A próxima chamada cria de novo.
recently devolve True em todo frame enquanto a janela dura, não uma vez só. Serve para estados e animação. Uma ação que muda o movimento não se repete sem estrago: pôr vel.y = PULO de novo em cada frame da janela desfaz a gravidade desses frames, e o pulo sobe mais (no exemplo acima, cerca de 150 em vez de 115) — por isso o exemplo chama recently.reset ao pular. Também não serve para o que deve acontecer uma vez só (criar um objeto, tocar um som, somar pontos): use keyp.once / @when. Cooldown periódico: ready.
Ao terminar
Os verbos que levam tempo — after, move_to(…, time=), tween, transientes, play_sound, fades — aceitam um gatilho para quando terminam: on_done= chama uma função; then= troca de mode. Esta seção trata só disso; o resto de cada verbo fica na seção dele.
then= espera um mode: a função do capítulo (leaving, self.diving), sem (). Não é callback.
on_done= espera um callback: uma função para chamar, sem mudar mode.
Chama quando o fade termina. Depois do fade_out a tela fica coberta até você chamar fade_in.
play(clip, next=)
Encadeia clip (min_hold= / resume= no painel Clip do Sprite). Não é este contrato.
then=ship.destroy não destrói: destroy não é mode. Para remover no fim: on_done=ship.destroy, after(0.2, ship.destroy), um mode dying, ou await e depois destroy(). Um transiente novo no mesmo canal substitui o velho e chama o on_done do velho na hora (sem aviso de que foi cortado).
Handle
Um Handle é o objeto que os verbos que levam tempo devolvem — after, every, tween, move_to(…, time=), move_along, play_sound, wait. Ele representa aquela tarefa enquanto ela corre: guarde-o para cancelar (h.cancel()), pausar, ler o progresso, ou esperar o fim com await h num mode async. Se não precisar de nada disso, ignore o retorno. .elapsed / .remaining em segundos de mundo. Em move_along e nos move_to / move_by com time=: também .velocity / .speed / .heading. Em play_sound: .volume / .pan / .pitch / .level.
h = after(2.0, boom)
h.remaining # 2.0, depois desce
h.cancel()
if h.done:
pass
caneta = every(0.03, stamp).pause()
caneta.resume()
@every(1.2, name="drop")
def drop():
spawn()
drop.handle.cancel()
async def diving(e):
await e.move_to(player.center.x, 90, time=0.8)
await wait(0.2)
# herdar a velocidade da viagem (o autor escreve .vel)
h = plataforma.move_along(trilho, time=4, loops="forever")
def solta_caixa():
caixa.vel = h.velocity # a caixa sai na velocidade da plataforma
Neste Handle
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.done
boolTrue
leitura
Já terminou (sucesso ou cancelado).
.cancelled
boolFalse
leitura
Parou via cancel / troca de mode / destroy.
.paused
boolFalse
leitura
Congelado com pause(). Só True enquanto vivo e congelado. Só leitura.
.elapsed
número1.2
leitura
Segundos de jogo desde que a tarefa começou (ou desde o último disparo de every).
.remaining
número0.8
leitura
O contrário de .elapsed: segundos que faltam até o fim (after / wait / tween) ou até o próximo disparo (every). 0 se já terminou ou foi cancelado. Só leitura.
fn.handle
Handledrop.handle
lê / escreve
Decorator de módulo: o Handle daquele @after / @every.
Leitura de movimento
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.velocity
Vec2Vec2(80, 0)
leitura
Unidades por segundo neste frame (mundo); depois do fim, guarda a do último frame (o on_done lê a velocidade de chegada). Só em move_along / move_to(..., time=) / move_by(..., time=). Não escreve Graphic.vel. Só leitura.
.speed
número80
leitura
Módulo de .velocity. Só leitura.
.heading
número90
leitura
Graus, anti-horário a partir de +x (igual a .angle / face=). Na trilha: tangente; no move_to / move_by: direção do delta. Só leitura.
.u
número0.25
leitura
Onde o objeto está na trilha, de 0 (início) a 1 (fim), já contando span= / phase=. Só move_along. Só leitura.
.lap
número inteiro0
leitura
Número da volta atual, começando em 0. loops="forever" continua a contar. Só move_along. Só leitura.
Métodos
Método
Assinatura
Retorna
Significado
await h
Sem parâmetros
Espera (use await)
No mode async. Handle já .done passa na hora. Cancelado levanta Cancelled.
.cancel()
Sem parâmetros
—
Para agora. Seguro duas vezes, inclusive depois do destroy.
.pause()
Sem parâmetros
o próprio Handle
Congela o relógio desta tarefa (every / after / wait / tween / play_sound). Chamar de novo não faz nada. Devolve self. Em until / parallel / race levanta TypeError.
.resume()
Sem parâmetros
o próprio Handle
Descongela. Não revive um Handle cancelado ou já terminado. Chamar de novo não faz nada. Devolve self.
Não confundir
Parece
É
Não é
Handle(...)
quem cria um Handle é o verbo: after, every, tween, wait, move_to(..., time=), play_sound. Guarde o que ele devolve: h = after(2.0, fn)
algo que se constrói à mão — Handle(...) dá TypeError com esse recado
Cancelled é BaseException.except Exception não pega. Não há cancel("spawn") por string: o slot é name=; uma tarefa específica é o Handle dela.
Não escreva h.remaining = ….TypeError. Para renovar a duração, after(2.0, fn, name="tail") (o slot substitui). Para parar, h.cancel().
Não escreva h.paused = ….TypeError. Use h.pause() / h.resume(). Pausar o mundo é clock.paused; estacionar o Entity é e.active = False.
.velocity não escreve .vel da Graphic. Copie se a física precisa: caixa.vel = h.velocity (não para quem está em cima: o resolve já o leva junto). Só bounce= (colisão), confine(…, action="bounce") e o seu código escrevem .vel. after / every / rotate_* / scale_* / tween levantam AttributeError (não têm velocidade). Handle já .done ou cancelado: guarda o último frame (útil no on_done).
Um mode é o capítulo de comportamento em que uma Entity está — em formação, mergulhando, morrendo. Cada mode é uma função; e.mode = e.mergulhando entra nele (a função roda). O que o capítulo inicia (timers, movimentos, sons) para sozinho quando ele acaba — ver escopo e cancelamento. Uma ação que termina e passa a outro mode: then=. Uma sequência de passos com esperas: um mode async.
e.mode é o mode atual (a função) ou None. Escreva para trocar: o mode velho acaba e o novo começa. Compare com ==. Escrever o mesmo mode de novo não faz nada; para recomeçá-lo, use goto_mode.
class Wasp(Sprite):
@on_mode
def entering(self):
self.move_to(self.slot_x, self.slot_y, time=1.0, then=self.formation)
@on_mode
def formation(self):
every(2.0, self.shoot) # some na troca de mode
after(3.0, then=self.diving) # idem
@on_mode
async def diving(self):
await self.move_to(player.center.x, 90, time=0.75)
await wait(0.12)
self.mode = self.formation
wasp.mode = wasp.entering
class Director(Entity):
def setup(self):
self.mode = self.directing
@on_mode
async def directing(self):
while True:
self.start_wave()
await until_empty("wasp")
await wait(1.4)
Neste mode
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
e.mode
mode ou Nonee.mode = e.diving
lê / escreve
A função atual, ou None. Compare com ==. Mesmo mode não faz nada.
e.mode_time
número1.2
leitura
Segundos neste capítulo. Zera ao entrar / reentrar.
Métodos
Método
Assinatura
Retorna
Significado
e.goto_mode(fn)
Parâmetros: fn (função do capítulo)
—
Sempre sai e entra (mesmo mode ou não).
@on_mode
Decorator — só marca
—
Marca a função como um mode, para quem lê o código e para as ferramentas de depuração. Não muda nada ao rodar: e.mode = fn funciona com ou sem ele.
@exit_mode(formation)
Parâmetros: enter_fn (a função do capítulo, ex.: formation)
—
Decorator: a função marcada roda quando a Entity sai do mode formation (por troca de mode, then= ou e.mode = None). Use para aparência e campos próprios — timers e movimentos do mode já param sozinhos. A função é comum (def), não async.
Não confundir
Parece
É
Não é
self.mode = self.diving()
O mode é a função, não a chamada: self.mode = self.diving. Atribuir o mesmo mode não faz nada — force com goto_mode. then= é Ao terminar, não um callback. Sem mode definido, .mode é None.
chamar a função
espera async
Num mode async def, await pausa o capítulo até algo acontecer — um tempo (wait), uma condição (until), um grupo esvaziar (until_empty), outras tarefas terminarem (parallel / race) — e então segue para a próxima linha. Só funciona dentro de um mode async (ou de uma função que ele chama com await); no update ou num @collide, use after.
await wait(0.4)
await until(lambda: ship.hp <= 0)
await until_empty("wasp")
await parallel(h1, h2)
i = await race(h1, h2) # 0 ou 1; cancela o outro
async def pause(s):
await wait(s)
async def diving(e):
await pause(0.4)
Nesta espera
Método
Assinatura
Retorna
Significado
await wait(0.4)
Parâmetros: segundos (número)
Espera (use await)
Pausa. Sem await não bloqueia: erro. Só no async que o mode está rodando, incluindo helpers.
await until(lambda: ship.hp <= 0)
Parâmetros: pred (condição, função)
Espera (use await)
Espera até a função devolver True (o Scena confere a cada frame). Passe uma função (lambda: …) ou uma célula (until(st["acabou"])), não a condição já calculada — True/False direto levanta TypeError, e st.acabou também (é o valor). Mesma regra de script que wait.
await until_empty("wasp")
Parâmetros: group_or_id (nome ou o grupo)
Espera (use await)
Grupo vazio. Aceita o nome, group["wasp"] (vista viva, não uma cópia), ou um grupo fórmula. Não use until(group.empty).
await parallel(h1, h2)
Parâmetros: h1 (Handle), h2 (Handle), …
Espera (use await)
Espera todos. Cancelar cancela o resto. Passe o Handle ao parallel / race antes do próximo await.
await race(h1, h2)
Parâmetros: h1 (Handle), h2 (Handle), …
número inteiro
O primeiro que terminar; cancela os outros. Resultado: índice começando em 0.
set_watchdog(enabled=False)
Parâmetros: enabled (ligado, True/False)
—
Por padrão, um mode async que trava num laço sem await é parado com um erro (o watchdog). set_watchdog(False) desliga essa checagem — só para demos e medições de desempenho.
wait(0.4) sem await não pausa o script. Devolve um Handle e o resto corre. Fora de um mode async (update, on_update, @collide, mode sync) é TypeError. Dentro, sem await (e sem passar a parallel / race) é TypeError com a linha e o await que falta. Use after(…) quando não há um capítulo async.
Só o mode é async. O Scena roda um mode async como script; todo o resto ele só chama. Por isso async def setup, async def on_update, um @every / @when / @collide / on_click / on_done= async, ou after(1, lambda: self.anda()) com anda async são TypeError — o corpo nunca rodaria. Vale também para uma função que o Scena pergunta: um filtro (group["x"].filter / any / all / count_where, nearest(where=)), a condição de until / @when, o alvo de @on_change, resolve(only=) e uma fórmula @=: a resposta de uma função async não é um valor. Ponha as esperas num mode e entre nele: self.mode = self.intro (no setup, num handler ou com then=). Um helper async vale quando o mode o chama com await.
escopo e cancelamento
Um mode é um capítulo de comportamento. Tudo que você inicia enquanto esse capítulo está rodando some quando o capítulo acaba — a menos que você diga o contrário com e.after / e.every / e.tween.
O que é um mode
Um mode é a função em e.mode — o capítulo atual do Entity (formação, mergulho, morrer…). Entrar no capítulo roda o corpo uma vez; sair cancela o trabalho desse capítulo e, se houver, chama @exit_mode. Compare com ==. Uma ação que termina e passa a outro mode: then=. Uma sequência de passos com esperas: um mode async com await.
A regra única
O que o mode começa enquanto está rodando pertence ao mode e some quando você sai. Fora do corpo do mode (setup, on_update, @collide, @when, script solto) não há “capítulo atual” — a tarefa pertence a quem você chama (ship.after: o dono é ship) ou, numa chamada solta, ao jogo todo (se feita no topo do arquivo) ou ao Act atual (se feita depois — ver Atos).
Para uma tarefa sobreviver à troca de mode, use o receptor explícito: ship.after(…), ship.every(…), ship.tween(…). Esses vivem até o destroy ou cancel_process.
O que cancela (e o que não)
Estas chamadas, no corpo do mode, herdam o escopo do capítulo e cancelam na troca:
e.after / e.every / e.tween / e.play_sound — escopo Entity (morrem no destroy / cancel_process; no destroy, um som que acaba sozinho termina de tocar)
timers do script (módulo, sem mode rodando) — esses saem quando o Act muda (change_act, ver Atos) se nasceram depois do topo
transientes (blink, flash, dissolve_out, …) e sprite.play
funções de evento (@collide, @when, on_click)
Onde / como
Escopo
Troca de mode
No corpo do mode: after / every / wait / tween / move_*(time=) / …
"mode"
cancela
e.after / e.every / e.tween
sempre a Entity
não cancela (morre no destroy / cancel_process)
after(…, bomb.destroy) de outro Entity, de dentro do mode
aquele outro Entity
não cancela com este mode
setup / on_update / @collide da Entity: after(0.1, self.atirar) ou uma lambda / def interna que usa self (after(0.1, lambda: self.set_color("red")))
a Entity (self)
não cancela (não há mode ambiente); morre no destroy: um inimigo destruído não atira depois. Exceção: no on_destroy a lambda é do script e roda depois da morte
script solto no topo do arquivo (after(…), play_sound(…), tween(…))
"game" — o jogo todo
não cancela; vale o jogo todo
script solto depois do topo (update, funções de evento, o setup de um Act)
Som é a exceção: quando o dono acaba, um som que acaba sozinho termina de tocar. Vale para a troca de mode, o destroy() e o change_act. play_sound("boom.wav") logo antes de self.destroy() ou de change_act(…) é ouvido inteiro. São cortados só os que nunca acabariam: loops="forever", um slot name= (música, ambiente), um som pausado, o monitor do mic. Um som com fade_out em andamento acaba sozinho — termina o fade. O on_done / then= de um som cujo dono acabou não roda. h.cancel() e cancel_process() cortam tudo. Todo corte tem uma rampa de ~5 ms (sem estalo).
cancel_process() — reinício sem destruir
Mantém a Entity; cancela tudo o que ela tem em andamento — timers, tweens, movimentos com time=, sons (do mode e da própria Entity) — e zera mode sem disparar @exit_mode nem on_destroy. Campos próprios, tags, active, on_update e @when ficam. Transientes: clear_transients().
Quero
Chamo
Pausar um Handle (continua pendente)
h.pause() / h.resume()
Pausar timers do Entity (continuam pendentes)
e.active = False
Cancelar timers, tweens, sons e o mode
e.cancel_process()
Remover de vez
e.destroy()
Sair do mode com exit hooks
e.mode = None
# Reinício: destrói a frota descartável; reinicia o que fica
group["wasp"].destroy()
group["bolt"].destroy()
player.cancel_process()
player.clear_transients()
player.hp = 3
player.active = True
avatar.goto_mode(avatar.playing) # force re-enter; playing é um mode da classe
director.goto_mode(director.directing)
Não existe reset_all global para o autor. Handle que você guardou fica .cancelled (pode ignorar ou cancelar de novo).
Padrões de uso
@on_mode
def formation(self):
every(2.0, self.shoot) # some ao sair de formation
after(3.0, then=self.diving) # idem
self.move_along(track, time=2.4, loops="forever") # idem
@on_mode
def hurt(self):
# i-frames: devem sobreviver se o mode mudar no meio
self.after(0.7, lambda: self.untag("hurt"), name="hurt")
Chamada solta no mode = capítulo. self.after / self.every = Entity. Os dois não são sinônimos.
@exit_mode e try/finally
Timers e movimento do capítulo já cancelam sozinhos. Use @exit_mode para o que não é Handle — aparência (glow, visual…), campos próprios, flags:
@on_mode
def charging(self):
self.glow = True
after(0.5, then=self.diving) # cancela sozinho se sair cedo
@exit_mode(charging)
def charging_exit(self):
self.glow = False # aparência: precisa do @exit_mode
Vale para mode sync ou async. O hook do exit é sempre uma função comum (não async).
Em mode async, try/finally também serve quando a limpeza precisa ficar ao lado do setup (raro — a maior parte dos Handles some sozinha):
@on_mode
async def formation(self):
self.glow = True
try:
every(2.0, self.shoot)
while True:
await wait(5.0)
if chance(0.5):
self.mode = self.diving
return
finally:
self.glow = False # roda também se o mode for cancelado
Prefira @exit_mode para aparência / campos próprios (um lugar, todo caminho de saída). Prefira não cancelar Handle à mão. o escopo do mode já faz isso. destroy() e cancel_process() não disparam @exit_mode; use on_destroy para “a Entity sumiu”, ou e.mode = None antes se precisar dos exit hooks e depois cancel_process() para o que restar.
Um ato é uma parte do jogo — título, fase, fim de jogo — escrita como uma classe class Fase(Act); change_act passa de um para outro, sem você escrever código de limpeza. O que o topo do arquivo cria fica no palco o jogo todo. O que um ato cria sai com ele, e o que ele muda volta ao que era. Atos são opcionais: um jogo sem atos (run()) funciona como sempre.
Um ato é uma classe plana. setup(self) monta; on_update, @when e modes fazem a lógica deste ato. Nada de código de limpeza: tudo o que ele criou sai na troca.
# ---- o palco: topo do arquivo, vale o jogo todo ----
def jogo_novo(): # os dados de um jogo que começa
st.pontos = 0
st.vidas = 3
st.recorde = 0 # vale entre jogos
jogo_novo() # run(Fase) direto também tem st pronto
class Heroi(Sprite): ... # classes são só definições
# ---- atos ----
class Titulo(Act):
def setup(self):
Text("GALAGA", center=(500, 500), font_size=72)
def on_update(self, dt):
if keyp.once("SPACE"):
jogo_novo()
change_act(Fase)
class Fase(Act):
def setup(self):
Heroi(center=(500, 70))
placar = Text("", top_left=(20, 760))
placar["text"] @= lambda: f"Pontos {st.pontos} Vidas {st.vidas}"
@when(lambda self: st.vidas == 0)
def perdeu(self):
self.after(1.5, lambda: change_act(FimDeJogo))
class FimDeJogo(Act):
def setup(self):
Text("FIM DE JOGO", center=(500, 400))
self.after(8, lambda: change_act(Titulo))
run(Titulo)
Memória do ato — só para um objeto que um método posterior usa.
on_destroy(self)
Roda primeiro na troca, com os objetos do ato ainda vivos. Raro: o resto sai e volta sozinho.
Classes planas: class Fase(Act), nunca class Fase1(Fase). O que se repete vai num ato que lê st (st.fase) ou numa função do topo chamada no setup. Sem __init__, sem setup(self, vidas). Um ato não tem corpo: @collide_with, @leave_playfield de classe, on_click e on_collide dão erro na definição da classe. self.destroy(), self.active, self.tag(…), self.visible e self.cancel_process() também — a mensagem diz o que usar.
Na 0.6.0, Scene e change_scene passaram a se chamar Act e change_act; com from scena import * os nomes antigos não existem mais (NameError): troque Scene por Act e change_scene por change_act.
change_act
run(Titulo) começa pelo primeiro ato. change_act(Fase) troca na hora: o ato anterior sai, o novo é montado, e a linha seguinte já está no novo. Sempre um ato novo — change_act(Fase) dentro de Fase recomeça.
class Fase(Act):
def on_update(self, dt):
if keyp.once("R") and st.vidas > 0:
change_act(Fase) # recomeça: sempre uma Fase nova
@on_mode
async def frota(self):
montar_frota()
await until_empty("inimigo")
st.fase += 1
change_act(Fase) # última linha: o mode para aqui
Faça do change_act a última coisa. Depois dele, os objetos do ato anterior estão mortos e o que você criar já é do ato novo (mesmo conselho do destroy()). Vale em qualquer lugar onde uma chamada vale: update, timer, @when, @collide, on_done=, a última linha de um mode async. A mesma tecla não passa para o ato novo: keyp.once é consumido por quem leu primeiro.
Erro
Correção
Fase()
Atos não se constroem à mão: change_act(Fase) — ou run(Fase) para o primeiro.
change_act(Fase()) / change_act("Fase")
Passe a classe: change_act(Fase).
change_act(Fase, vidas=2)
Só o ato. Dados vão em st: st.vidas = 2, depois change_act(Fase).
change_act antes do run
O jogo ainda não começou: run(Titulo).
change_act num jogo que começou com run()
Para usar atos, comece com run(Titulo).
change_act num on_destroy durante uma troca
Erro que nomeia o hook e o ato que saía; a troca termina no ato pedido primeiro. Fluxo de ato fica no ato: um @when do ato.
o palco e o ato
O topo do arquivo monta o palco: fica o jogo todo e funciona em todo ato. Tudo o que é criado depois — por qualquer código, até um tiro de Heroi.on_update — é do ato atual e sai com ele.
Na troca de ato
O quê
Sai
Objetos (Shape, Sprite, Text, SpatialFx, Entity, widgets) — on_destroy roda. Timers, tweens, sons e tweens de câmera sem dono do palco — mas um som que acaba sozinho termina de tocar (sem on_done); loop "forever", name= e pausado cortam. @when / @on_change / @collide / @leave_playfield soltos escritos no setup. Post e background criados no ato. Grupos-fórmula (group["alvos"] @= …) criados no ato. Uma tag de colisão que nenhuma regra restante nomeia deixa de inscrever membros (um objeto que só ela pôs na colisão sai dela; collidable = True escrito por você fica). Um movimento de câmera com time=, de qualquer dono. Slots de ready / recently do script. shake / flash / explosion_at pendentes. Ligações @= entre objetos que saem juntos somem em silêncio.
Volta ao que o topo deixou (ou ao padrão)
Câmera, canvas.background, canvas.effects e o background (lista e .enabled), canvas.playfield, clock.scale / clock.paused, collision.resolve_enabled, canvas.layers.default, set_defaults, os pares de resolve, os grupos-fórmula do topo, a coluna do HUD (o próximo Text ou widget sem y do ato começa sempre no mesmo lugar).
Fica
st. Objetos do palco e os timers deles (radio.play_sound(…)) — menos o que o ato entregou a eles: um método do ato (ou uma função que lê o self do ato) em heroi.every(…), heroi.on_update ou heroi.mode sai com o ato. Um timer do palco cujo name= o ato reusou volta depois da troca. Camadas do palco (uma camada criada no ato sai com ele; recomeçar o ato a cria de novo). Preferências: volume do audio, fx_quality, post_fx_scale, display, framerate, ui.scale / ui.font. O fade (fade_out / fade_in).
Regra de bolso: se deve começar do zero toda vez, crie no setup. Se deve sobreviver a todo ato, crie no topo. Música tocada no topo atravessa os atos; música tocada no setup para na troca.
Preciso de…
Como
Dado que atravessa atos (pontos, recorde, venceu?)
st
Música do ato que some suave na troca
Guarde o Handle e comece o fade no on_destroy do ato: self.musica.fade_out(0.5). Ele termina o fade já no ato novo.
Algo iniciado no meio do jogo que sobrevive ao ato (música ligada por tecla)
Um dono no topo: radio = Entity(), depois radio.play_sound(…) / radio.after(…) de qualquer lugar.
Objeto em todo ato, mostrado só depois
Crie no topo com visible=False; um ato mostra. O estado é dele: a troca não esconde de novo.
Objeto criado dentro de um ato que vire do palco
Não existe: ele some na troca → mova para o topo.
Trabalho sem dono (after(…), play_sound(…), tween(…) soltos) tem h.scope == "game" quando nasce no topo e "act" depois — o "act" sai na troca. O SCENA WARNING de um trabalho sem dono diz <game> / <act> do mesmo jeito.
Um @= que o ato faz num objeto do palco (hud["text"] @= lambda: f"Pontos {st.pontos}" no setup) é do ato: na troca a ligação sai e o campo volta ao que era antes dela — a ligação do palco, se havia, ou o valor (hud.text volta a ""). Recomeçar o ato liga de novo sem aviso, e o próximo ato pode escrever hud.text = "NAVES". Vale também quando a fórmula lia um objeto do ato ou o próprio ato (self.pontos).
dados do jogo
Tudo o que o próximo ato precisa saber vive em st. Um jogo novo é uma função no topo (jogo_novo()), chamada no topo e pelo ato que começa um jogo. A troca de ato nunca limpa st.
st.venceu = True
change_act(FimDeJogo) # FimDeJogo.setup lê st.venceu
class Fase(Act):
def setup(self):
st.pontos_na_entrada = st.pontos # se recomeçar deve desfazer pontos
Recomeçar (R) mantém st como está: vidas e pontos até agora. Quem precisa desfazer guarda na entrada, visível no código.
Entidades se acham por grupo, nunca pelo ato: group["heroi"].nearest(self). O ato não guarda o herói — o HUD e a regra de derrota leem st.
self.inimigos = group["inimigo"] é erro em qualquer Entity: um grupo se procura onde se usa, não se guarda. Campos guardam qualquer valor menos função; um objeto guardado que morreu lê falso (if self.aviso:). Herói no topo do arquivo é do palco: guarda vidas perdidas, tags e aparece no título — crie no setup.
regras do ato
Uma regra (@collide, @when, @on_change…) vale conforme o momento em que é registrada, não o lugar onde está escrita: no topo do arquivo, o jogo todo; numa classe de Entity, aquele objeto; dentro do setup, só este ato.
@collide("tiro", "inimigo") # topo: o jogo todo (inerte onde não há tiros)
def acertou(tiro, inimigo):
tiro.destroy()
class Treino(Act):
def setup(self):
@collide("tiro_inimigo", "heroi") # só neste ato
def de_mentira(tiro, heroi):
tiro.destroy()
update e on_key_down do topo rodam em todo ato. Lógica de um ato vai no on_update dele; teclas e mouse do ato, lá dentro (keyp.once("R"), mouse.press()). Um def update(self, dt) ou def on_key_down(self, key) dentro da classe do ato nunca rodaria: é TypeError na linha da classe, com a grafia do ato.
@when dispara na subida (falso → verdadeiro). Uma condição que já é verdadeira quando o ato começa nunca dispara: recomeçar Fase com st.vidas == 0 deixaria o ato preso. Proteja a troca (keyp.once("R") and st.vidas > 0) ou confira o estado no setup.
Transições
Fades se compõem — não há fade= no change_act. O fade é do palco e atravessa a troca:
canvas.fade_out(0.5, on_done=lambda: change_act(Fase)) # ato anterior
class Fase(Act):
def setup(self):
canvas.fade_in(0.5) # ato novo
Não confundir
Parece
É
Não é
Ato × mode
Mode é um capítulo do comportamento de um objeto (self.mode = self.mergulho; mesmo mode não faz nada; then= entra num mode). Ato é a parte do jogo em que o jogador está (change_act(FimDeJogo); sempre novo; dono de tudo o que nasce nele). Um ato pode ter modes.
Não existe then= para ato, nem pilha de atos, nem ato de pausa — pausa é clock.paused = True.
Tocar um arquivo é play_sound("laser.wav"). O retorno é o mesmo Handle de Agendar — cancelar, await, on_done, then=, name=. O som pertence a quem o tocou, como um timer (escopo e cancelamento): o mode, a Entity, ou o jogo todo / o Act atual — e quando esse dono acaba, um som que acaba sozinho termina de tocar. Sound("x.wav") só carrega o arquivo (como Sheet): não toca nem aparece sozinho. Efeitos (eco, filtro…) se põem no Handle enquanto o som toca; para aplicá-los uma vez e guardar o resultado, use Sound.bake.
play_sound toca um arquivo de som (ou um Sound já carregado, ou o microfone) e devolve um Handle para controlar essa execução: volume, pausa, fade, efeitos. Sound("x.wav") só carrega o som, sem tocar. h.pause() segura o som; h.resume() continua.
import random
play_sound("laser.wav")
play_sound("hit.wav", volume=0.6, pan=-0.2, pitch=1.05)
h = play_sound("laser.wav", on_done=ship.destroy)
play_sound("theme_a.mp3", name="music", loops="forever")
play_sound("theme_b.mp3", name="music", loops="forever", crossfade=1.0)
laser = Sound("laser.wav", volume=0.7)
play_sound(laser, pitch=random.uniform(0.95, 1.05)) # taxa ao vivo (muda a duração)
crunch = laser.bake(Distortion(), transpose=3, pitch=1.1)
play_sound(crunch) # PCM já processado — barato
class Ship(Sprite):
@on_mode
def flying(self):
play_sound("thrust_loop.wav", loops="forever") # some ao sair
@on_mode
def cruising(self):
self.hum = self.play_sound("engine.wav", loops="forever") # sobrevive ao mode
@exit_mode(cruising)
def cruising_exit(self):
self.hum.fade_out(0.3)
Os dois name="music" acima trocam porque rodam no mesmo lugar. O slot é por lugar (slot): solto no setup de um ato e solto dentro de um mode do ato são dois slots, e as duas músicas tocam. Trilha de um ato que troca entre fases: self.play_sound("tema.ogg", name="musica", loops="forever") no ato, sempre.
Neste play_sound
Função / material
Método
Assinatura
Retorna
Significado
play_sound(source)
Parâmetros: source (caminho do arquivo, Sound ou Microphone); volume (1 = original; pode passar de 1), pan (−1 esquerda … 1 direita), pitch (1 = normal; 2 = uma oitava acima e duas vezes mais rápido), loops=1 (int ou "forever"), fade_in (segundos subindo do silêncio), crossfade (com name=: segundos misturando com o som que tocava nesse name=), on_done, then, name
Handle
Toca. A quem pertence: no corpo do mode → ao mode; e.play_sound → à Entity; solto → ao jogo todo (no topo do arquivo) ou ao Act atual (depois — ver Atos). Quando o dono acaba, um som que acaba sozinho termina de tocar; loop "forever" / name= / pausado cortam. Arquivo que falta / corrompido: silêncio de duração nominal + aviso no banner (uma vez por asset). O Handle continua a correr — on_done e loops ainda se comportam. Um clip ainda a gravar levanta RuntimeError — mic.stop() ou await primeiro.
Material. Não toca. Clipes curtos (< 8 s) vão para memória; longos fazem stream. stream= força a escolha. Sem .effects.
Sound.bake(*fx, transpose=0, pitch=1)
efeitos do catálogo; transpose (semitons, mesma duração); pitch (taxa, muda a duração)
Sound
Aplica transpose, pitch e os efeitos uma vez e devolve um Sound novo, já processado — tocar esse Sound não custa nada a mais. Ordem: transpose → pitch → efeitos; a cauda de Echo/Reverb fica incluída no som novo.
Mudam o som na hora, enquanto ele toca. Volume pode passar de 1; o Scena só evita que a soma de todos os sons estoure. Pan −1…1. Pitch 1 = normal (taxa — muda a duração; barato). Em mic monitor, pitch é ignorado.
.fade_to(v, time=)
métodoh.fade_to(0.2, time=0.4)
método
Rampa de volume. Relógio do mundo (respeita clock.scale), como qualquer tween.
.fade_out(time=)
métodoh.fade_out(0.3)
método
Rampa até 0 e para (fim natural: on_done dispara). Não é cancel. Sem cauda de Echo/Reverb depois.
.level
número 0…10.4
leitura
Quão alto este som está agora, de 0 (silêncio) a ~1 (o máximo), já suavizado (audio.level_tau). Para barras: amp_db(.level).
.spectrum(bins=8)
lista[0.1, 0.8, …]
lê / escreve
Divide o som desta voz em bins faixas, dos graves (~40 Hz) aos agudos (~16 kHz); cada valor (0…1) diz quanto som há naquela faixa agora. Suavizado como .level (audio.level_tau). Atributo só-de-som: num Handle que não é de som levanta AttributeError com a dica — o mesmo para .volume, .pan, .pitch, .fade_to, .fade_out, .level, .effects.
Tempo até o fim audível (clip + cauda de Echo/Reverb, se houver). Mesmo contrato do Handle — não é exclusivo de som.
Sound("laser.wav") não toca. Só carrega o arquivo. Use play_sound. Fade nunca é automático: quem quer fade escreve fade_in= / .fade_out() / crossfade=. Ao sair do mode, um som que acaba sozinho termina de tocar; um som com loops="forever", com name= ou pausado é cortado na hora (com uma rampa de ~5 ms, sem estalo).
on_done só no fim natural (inclui cauda de Echo/Reverb). .cancel(), .pause(), troca de mode e name= (corte) não disparam. loops="forever" nunca termina sozinho. pitch= ao vivo ≠ transpose= no bake (este mantém a duração).
audio
audio é o som do jogo como um todo: volume geral, mudo, e medidores do que está tocando (level, spectrum) — para barras que pulam com a música. amp_db converte essas medidas na escala que o ouvido percebe, pronta para a altura de uma barra.
audio.volume = 0.8
audio.level_tau = 0.08
play_sound("theme.mp3", loops="forever")
N = 8
bars = [Rect(width=18, height=4, color="cyan") for _ in range(N)]
def update(dt):
spec = audio.spectrum(bins=N)
for i, bar in enumerate(bars):
bar.height = 8 + 160 * amp_db(spec[i]) # floor=-60 por padrão
bar.center = (80 + i * 28, 120 + bar.height / 2)
Neste audio
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
audio.volume
número0.8
lê / escreve
Ganho mestre. 0 silencia tudo sem cancelar Handles.
audio.muted
boolFalse
lê / escreve
True silencia tudo; False volta ao volume de antes.
audio.level_tau
segundos0.08
lê / escreve
Quanto tempo a suavização de level / spectrum leva para acompanhar o som, em segundos (no mix, em cada som e no mic). Menor = barras mais nervosas. ≤ 0 levanta ValueError.
audio.level
número 0…10.4
leitura
Quão alto está o som do jogo todo agora (todos os sons somados), de 0 (silêncio) a ~1 (o máximo), já suavizado. Só leitura. Para barras: amp_db(audio.level). Liga: barra["width"] @= lambda: 300 * amp_db(audio.level) acompanha o som, uma vez por frame. O h.level de um play_sound liga do mesmo jeito.
audio.spectrum(bins=8)
lista8 números
lê / escreve
Divide o som do jogo em bins faixas, dos graves (~40 Hz) aos agudos (~16 kHz); cada valor (0…1) diz quanto som há naquela faixa agora. Só se escolhe o número de faixas (bins=).
amp_db(x, floor=-60)
número 0…1amp_db(audio.level)
lê / escreve
Converte uma medida 0…1 (level ou um valor de spectrum) para a escala do ouvido (decibéis), também 0…1, pronta para a altura de uma barra: 1 continua 1; o que está floor decibéis abaixo do máximo (padrão −60) ou menos vira 0. Valores ≤ 0 dão 0; acima de 1 ficam em 1.
.level / .spectrum são lineares (0…1). Em barras, use amp_db. Senão o grave engole o resto. A suavização já vem pronta; não precisa de média no update.
Microphone
Microphone() abre o microfone do computador — nada é captado antes disso. Com ele você mede o volume da voz (mic.level), ouve ao vivo (play_sound(mic)) e grava (clip = mic.record(); o clip toca depois de mic.stop() ou await).
mic = Microphone() # entrada padrão do sistema
print(mic.level)
barra = Rect(top_left=(20, 20), width=0, height=10)
barra["width"] @= lambda: 300 * amp_db(mic.level) # segue a voz
play_sound(mic) # ouvir ao vivo (com mic.effects)
mic.effects.add(Echo()) # só no que se ouve ao vivo — a gravação sai sem eco
clip = mic.record() # Sound, ainda a gravar
mic.stop() # clip pronto; o mic continua aberto
play_sound(clip)
h = play_sound(clip)
h.effects.add(Echo()) # efeito na reprodução
play_sound(clip.bake(Distortion()))
clip = await mic.record(seconds=2)
mic.close() # larga o dispositivo; não é stop()
Neste Microphone
Método
Assinatura
Retorna
Significado
Microphone(backends=None)
Parâmetros: backends (lista de miniaudio.Backend; avançado — testes / diagnóstico)
Microphone
Abre a entrada padrão do sistema. Sem dispositivo: um SCENA WARNING e um mic silencioso (.level fica 0). Uma gravação com seconds= ainda termina, cheia de zeros, para o await não ficar preso. Não há escolha de entrada nem de saída.
.record(seconds=None)
seconds (amostras capturadas, não relógio do jogo); on_done, then
Sound
Começa a gravar um clip sem os efeitos de mic.effects (eles só valem para o que você ouve ao vivo). Ainda não toca. seconds= tem de ser finito, > 0 e ≤ 30 (ValueError senão). Sem seconds=, grava até stop() ou o teto de 30 s. No teto: fim normal + um aviso por mic. Já a gravar, ou mic fechado: RuntimeError. A quem pertence: no corpo do mode → ao mode; chamada solta → ao jogo todo (no topo do arquivo) ou ao Act atual (depois — ver Atos). on_done / then= no próximo frame, só num fim natural.
.stop()
Sem parâmetros
—
Termina a gravação e guarda o clip. O mic fica aberto. O clip toca na linha seguinte; on_done espera o próximo frame. Sem gravação a correr: RuntimeError — chame record() primeiro.
.recording
bool
bool
Só leitura. True enquanto está gravando.
.level / .spectrum(bins=8)
bins no spectrum
número / lista
Mesma suavização de audio.level_tau. Linear 0…1 — use amp_db nas barras. Uma fórmula (@=) ou um @when que lê o nível acompanha a voz, uma vez por frame. O mic precisa existir quando a fórmula é ligada: o @= aprende o que segue nessa hora.
.effects
Pilha
—
Pilha ao vivo do monitor. Não entra na gravação. Efeitos do clip: h.effects na reprodução, ou clip.bake(...).
.close()
Sem parâmetros
—
Chamar de novo não faz nada. Larga o dispositivo e zera .level / .spectrum. Se estava gravando, guarda o áudio já capturado e não dispara on_done. Sair da aplicação fecha todos os microfones abertos da mesma forma.
clip.duration
segundos
número
Cresce enquanto grava (amostras ÷ 44100). Fica fixo quando a gravação acaba. Só leitura.
await clip
o Sound devolvido por record()
o mesmo Sound
Espera o fim da gravação e devolve o mesmo clip, já tocável. await num Sound de arquivo levanta TypeError.
Como a gravação acaba
Fim
Guarda o áudio
on_done / then=
mic.stop()
sim — tocável na linha seguinte
no próximo frame
seconds= atingido
sim
no próximo frame
teto de 30 s
sim
no próximo frame, mais um aviso
sair do mode / destroy() / mic.close()
sim
não
Não confundir
Parece
É
Não é
clip logo a seguir a record()
um Sound ainda a ser feito. play_sound / bake levantam RuntimeError.
tocável — mic.stop() ou await primeiro.
mic.stop()
termina a gravação; o mic continua aberto.
mic.close() (larga o dispositivo).
mic.effects vale para tudo
mic.effects muda só o que você ouve ao vivo (play_sound(mic)).
a gravação — o clip sai sempre sem esses efeitos.
pitch= no monitor do mic é ignorado (só faz sentido em arquivos). O monitor é play_sound(mic) ao vivo. O clip, depois de pronto, aceita pitch= e bake.
efeitos
Efeitos de som mudam como um som soa enquanto toca: abafado, de rádio, com eco, numa sala, distorcido. Cada som tocando (o Handle de play_sound) e o monitor do microfone têm uma pilha .effects: .add(…) põe um efeito, .remove() / .clear() tira, .enabled = False desliga sem tirar. Os parâmetros mudam ao vivo (lp.cutoff = 1500).
Para um som que você toca muitas vezes sempre com o mesmo efeito, calcule uma vez com Sound.bake(…): o resultado é um Sound novo, já processado, que toca sem custo extra.
h = play_sound("shoot.wav")
h.effects.add(LowPass())
lp = h.effects.add(HighPass())
lp.cutoff = 1500
lp.enabled = False
lp.remove()
h.effects.clear()
# Uma vez, depois play barato:
crunch = Sound("shoot.wav").bake(Distortion(), Wavefold(), transpose=3, pitch=1.1)
play_sound(crunch)
Neste catálogo
Nome
Parâmetros
O que faz
LowPass(cutoff=300)
cutoff: frequência de corte, em Hz
Abafa: deixa passar os graves e corta os agudos acima de cutoff — som “atrás da parede”, debaixo d'água. Quanto menor o cutoff, mais abafado.
HighPass(cutoff=2000)
cutoff: frequência de corte, em Hz
O contrário: corta os graves abaixo de cutoff — som fino de rádio ou telefone.
Echo(delay=0.25, decay=0.6)
delay: segundos entre as repetições · decay: quanto cada repetição guarda do volume (0 a 0.95)
Eco: o som volta várias vezes, cada vez mais baixo. Depois que o arquivo acaba, o eco continua até sumir.
Reverb(size=1.0, mix=0.6)
size: tamanho da sala (0.05 a 1.5) · mix: quanto da sala se ouve (0 = nada, 1 = só a sala)
Ambiente: o som numa sala, caverna ou salão. Também continua soando um pouco depois do fim.
Distortion(drive=8)
drive: quanto o som é forçado (maior que 0)
Distorção “suja”, de guitarra ou alto-falante estourado. Muda o caráter, não o volume.
Wavefold(drive=3)
drive: quantas dobras por pico (maior que 0)
Som metálico, de fliperama. Mais drive, mais brilhante.
.enabled = False desliga o efeito: soa exatamente como sem ele. .remove() duas vezes não faz nada. cancel e fade_out param o som sem esperar a cauda de Echo/Reverb. Sair do mode não corta um som que acaba sozinho: ele termina de tocar, com a cauda (quando o dono acaba). Sound não tem .effects; use bake.
Rodar código quando algo acontece: uma condição vira verdade (@when), um valor muda (@on_change), uma tecla é apertada (callbacks do script), o mouse clica num objeto, algo sai da área de jogo. Para ler o teclado a cada frame: Frame. Objetos se tocando: Colisão.
@when(condição) roda a função uma vez quando a condição passa de False para True — “quando o HP chegar a 0, morra”. A condição é conferida a cada frame; enquanto continua True, a função não roda de novo.
Expressão Python (player.hp <= 0) vale agora, na hora do decorator. Para o Scena conferir a condição a cada frame, passe uma função: lambda: …. Uma célula bool (st["alive"]) pode ir crua no módulo. Na classe: lambda self: ….
Expressão crua (@when(player.hp <= 0)) avalia uma vez, na hora do decorator — não é conferida de novo. No módulo, uma célula bool também vale: @when(st["alive"]) (com colchetes: st.alive é o valor de agora).
Na classe: lambda self: …. Célula crua (st["hp"]) só no módulo.
class Ship(Sprite):
@when(lambda self: self.hp <= 0)
def on_death(self):
self.destroy()
Contrato
Peça
Significado
@when(cond)
Bool. Precisa de lambda (expressão True/False). Dispara na borda False→True. Enquanto continua True, não dispara de novo. No módulo, célula bool: @when(st["alive"]).
Quando avalia
Todo frame, e depois de gravações em campos próprios / State. flush() roda agora as conferências pendentes, sem esperar o fim do frame.
Método
lambda self: …. Vale para cada objeto da classe, desde que ele é criado.
Vários @when no mesmo def
Ok: as duas expressões disparam o mesmo corpo. Misturar com @on_change é TypeError.
Espera async no Mode. Não é evento. until também precisa de callable: lambda: ship.hp <= 0.
Não combine @when com keyp.once.@when já dispara só na passagem para True. Widget ligado a fórmula: o gesto não faz nada (isso é Reativo, não evento).
@on_change
@on_change(valor) roda a função toda vez que o valor muda, e pode receber o valor novo. Vale para qualquer tipo: número, texto, tupla.
Aceita uma célula (st["wave"], st["pos"]) ou um lambda. Sem colchetes, st.wave é o número de agora e levanta erro. Campo de objeto precisa de lambda: c.radius cru pega o número de agora. Na classe: lambda self: ….
st.pos = (0, 0)
@on_change(st["pos"])
def on_pos(value):
marker.center = value
@on_change(lambda: c.radius)
def on_radius(value):
ring.radius = value
Dispara quando o valor novo é != do anterior. Callback: zero args, ou um arg (o valor novo).
Campo de objeto precisa de lambda. @on_change(c.radius) recebe só o número de agora e levanta TypeError: escreva @on_change(lambda: c.radius).
Callbacks do script
Funções com nome fixo que você define no script; o run() as chama: setup ao abrir a janela, update a cada frame, on_key_down / on_mouse_click… quando há entrada. Para saber se uma tecla está segurada, keyp no update costuma ser mais simples (Frame). Clique num objeto: Clique / arraste. Numa classe: Callbacks da Entity.
O motor manda a tupla completa e corta o que a função não pede. update(dt) e update(dt, canvas) valem os dois. O mesmo para on_mouse_up(x, y, button) vs. com click_x, click_y.
No teclado só chega key — não há um segundo parâmetro mods. Modificadores são teclas próprias: "SHIFT", "CTRL", "ALT", "CMD". Atalho: keyp("CTRL+R"). Nunca key == "CTRL+R".
Assinaturas
Método
Assinatura
Retorna
Significado
setup
Parâmetros: canvas (o canvas)
—
Uma vez, janela aberta. Não é o setup(self) da Entity.
update
Parâmetros: dt (número), canvas (o canvas)
—
Todo frame, depois do input. Pegue só dt se não precisar do canvas. Nome no script é update, não on_update.
on_key_down
Parâmetros: key (str)
—
Tecla apertou. Nome amigável em maiúsculas, o mesmo do keyp (pontuação chega como "K1"…"K12" — tabela). Uma tecla comum só chega sem Ctrl / Alt / Cmd segurado: Ctrl+R não chama on_key_down("R") — use keyp("CTRL+R"). Shift não bloqueia: com Shift segurado, SPACE chama on_key_down("SPACE"). key == "ESCAPE" → ValueError sugerindo "ESC".
on_key_up
Parâmetros: key (str)
—
Tecla soltou. Mesmo key. Só chega para uma tecla cujo on_key_down chegou (um atalho como CMD+S não manda nenhum dos dois para o "S"); chega mesmo se um menu modal abriu no meio ou se o ato mudou com a tecla segurada (andando para a direita até a porta, o on_key_up("RIGHT") chega no ato novo).
on_key
Parâmetros: key (str)
—
Ao apertar e, enquanto a tecla continua segurada, a cada repetição automática. Mesmo key. Uma tecla apertada antes de change_act não repete no ato novo (o on_key_up chega).
on_char
Parâmetros: char (str)
—
Caractere Unicode.
on_mouse_down
Parâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…)
—
Um botão do mouse desceu; x, y no mundo. Quem recebe primeiro: widgets, depois objetos, depois esta função.
on_mouse_up
Parâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…), click_x (número), click_y (número)
—
Soltou o botão; x, y é onde soltou, no mundo. click_x, click_y (opcionais) é onde o botão foi apertado — útil para medir o arraste. Pode omitir os dois e ficar em x, y, button. Chega também quando o ato mudou com o botão apertado; aí não há on_mouse_click nem on_mouse_drag no ato novo.
on_mouse_click
Parâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…)
—
Clique (press+release no mesmo lugar). Mundo. Não é shape.on_click.
on_mouse_double_click
Parâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…)
—
Duplo clique. Mundo.
on_mouse_move
Parâmetros: x (número), y (número)
—
O mouse se moveu; x, y no mundo. Roda junto com os outros eventos de mouse, sem bloqueá-los.
on_mouse_drag
Parâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…)
—
Arraste com botão segurado. Mundo. Não é shape.on_drag_to.
on_mouse_scroll
Parâmetros: xoffset (número), yoffset (número)
—
A roda do mouse girou (yoffset > 0 = para cima). Quem recebe primeiro: o widget sob o ponteiro (fica com a roda; um Slider muda de valor), depois um objeto segurado com wheel_rotate, depois esta função. Um objeto sem wheel_rotate não segura a roda.
Métodos com nome fixo que o Scena chama numa Entity: setup ao nascer, on_update a cada frame, on_destroy ao ser destruída. Valem para toda Entity (Shape, Sprite, SpatialFx e o Entity() sem desenho). Escreva-os como método da classe ou atribua uma função ao objeto (ship.on_update = …). Os nomes do script (setup(canvas), update) são outra coisa — Callbacks do script.
Clicar, arrastar, soltar e passar o mouse por cima de um Shape, Sprite ou Text (e de um Button). Basta declarar a função do gesto: ela liga a propriedade certa (clickable, draggable…) sozinha. Quem recebe o clique, de cima para baixo: widgets → ui → top → outras camadas → script.
Três formas para o mesmo gesto: decorator (@click, @hover, @drag_to…), callback no objeto (shape.on_click = fn — alternativa ao decorator) e só as propriedades (clickable / draggable). Todo Graphic (Shape, Sprite, Text) tem essas propriedades e callbacks; SpatialFx não. A função do gesto pode receber o alvo ou não pedir nada — por isso a mesma função serve um Shape e um Button. Um parâmetro com valor padrão não recebe o alvo, então lambda f=f: escolher(f) num laço funciona.
@click(c1, c2, box)
def on_click(shape):
shape.set_color("gold")
reset = Button(label="Reset")
@click(reset, restart_icon) # Button e Shape: a mesma função
def restart():
st.score = 0
@click(group["click_group"])
def on_group(shape):
shape.set_color("gold")
@drag_to(token)
def on_drag(from_shape, to_shape, pos):
from_shape.center = pos
@drag_over(token)
def on_over(from_shape, to_shape, pos):
zone.set_color("yellow" if to_shape is zone else "gray")
@drop_by(zone)
def landed(from_shape, to_shape, pos):
from_shape.center = zone.center
Um alvo de hover por vez: o mais em cima sob o cursor, entre widgets, ui e mundo. Entrou → @hover; saiu → @hover_exit. Toda entrada tem exatamente uma saída.
Três testes diferentes, cada um responde a uma pergunta — não troque um pelo outro:
if ship.contains_point(mouse):
... # este objeto (pelo desenho), no mundo. Também contains_point(x, y)
def on_update(self, dt):
self.set_color("gold" if self.pointer_hover else "gray") # nível, não evento
hits = collision.query_point(mouse) # sem group=: hitboxes inscritos
A saída é imediata: esconder, destruir, sair de baixo do cursor, clickable = False, widget por cima ou Menu modal. A entrada espera o mouse se mexer: um shape que desliza para baixo do cursor parado só ganha hover no próximo movimento. Shape só com @hover não bloqueia clique: o clique passa para o de baixo.
# roda com o botão segurado no corpo
rect.wheel_rotate = True
rect.show_handles()
rect.show_points()
a1 = Anchor(120, 620)
a2 = Anchor(280, 660, color="dark red")
Line(p1=lambda: a1.center, p2=lambda: a2.center)
a = Circle(center=(900, 150), radius=55, color="red")
b = Circle(center=(930, 170), radius=40, color="green")
# só dentro da mesma camada
b.move_to_front()
a.move_to_back()
hud = Rect(center=(930, 170), width=85, height=65, layer="top", color="navy")
Decorators
Ligam no alvo a propriedade certa (clickable, draggable, droppable) e a função ao gesto. Alvo é um Graphic (Shape, Sprite ou Text), não SpatialFx. @click também aceita Button; @hover / @hover_exit aceitam qualquer widget. Slider / CheckBox / DropDown / TextInput em @click levantam erro: use @on_change(w.data). @click(group["…"]) vê os membros no momento da decoração; um membro recusado levanta erro antes de ligar qualquer um.
Método
Assinatura
Retorna
Significado
@click(c1, c2)
Função recebe: alvo (Shape, Sprite, Text ou Button)
—
Liga clickable. Clique no corpo, com o botão esquerdo (o direito não dispara: use mouse.press(Mouse.BTN_RIGHT) ou on_mouse_down).
@hover(card)
Função recebe: alvo (objeto)
—
O cursor entrou: virou o alvo de hover. Não liga clickable.
@hover_exit(card)
Função recebe: alvo (objeto)
—
Deixou de ser o alvo de hover (saiu, sumiu, destruído, coberto).
@drag_to(shape)
Função recebe: from (objeto), to (objeto ou None), pos (ponto)
—
Soltou o arraste deste shape.
@drag_over(shape)
Função recebe: from (objeto), to (objeto ou None), pos (ponto)
—
Durante o arraste, a cada movimento do mouse (não a cada quadro): to é o alvo sob a peça, ou None fora de qualquer alvo.
@drop_by(zone)
Função recebe: from (objeto), to (objeto ou None), pos (ponto)
—
Liga droppable. Alguém soltou em cima.
from, to e pos nas três de arraste:
Parâmetro
Passar
Tipo (default)
Significado
from
1º
Shapetoken
O shape que está sendo arrastado.
to
2º
Shape ou Nonezone
O droppable mais em cima sob o ponteiro: primeiro os da camada ui, depois os do mundo, venha o arraste de onde vier — a semente do inventário (layer="ui") cai no canteiro do mundo, a pedra do mundo cai na lixeira do HUD. None se não houver.
pos
3º
ponto(mouse.x, mouse.y)
(x, y) onde o ponteiro está (over) ou soltou (to / drop), no espaço do objeto dono da função: em drag_to / drag_over o do arrastado (por isso from_shape.center = pos funciona), em drop_by o do alvo (Planta(center=pos) nasce no canteiro). Mundo para objetos do mundo, tela (view) para layer="ui"; sem câmera os dois coincidem.
@drag_to(token)
def on_drag(from_shape, to_shape, pos):
from_shape.center = pos
if to_shape is zone:
from_shape.set_color("gold")
Callbacks
Alternativa aos decorators. Mesmos gestos, mesma assinatura. Propriedade no objeto: atribuir também liga a flag. Use um ou outro — o último a gravar ganha.
Leitura. True no alvo de hover: o clicável / arrastável / com on_hover mais em cima sob o cursor. Nível, atualizado uma vez por frame. Enfeite sem nada disso fica sempre False (e não bloqueia). Não é contains_point.
pointer_pressed
boolTrue
leitura
Leitura. True enquanto o botão esquerdo, apertado nesta forma, segue segurado e o cursor está sobre ela (soltar ali clicaria). Sai para False quando o cursor sai e volta a True quando ele volta; arrastando fica True. Termina ao soltar e quando a forma some (visible = False, active = False, destroy()), deixa de ser clicável / arrastável, ou um menu modal abre. Bom para o visual de "afundado".
wheel_rotate
boolTrue
lê / escreve
True: com o botão do mouse segurado sobre o objeto, a roda do mouse o gira. Independente de draggable.
Métodos
Método
Assinatura
Retorna
Significado
contains_point(x, y=None)
Parâmetros: x (um ponto, ou o número x com y)
True/False
Este objeto contém o ponto? Geometria de desenho (a mesma forma que o clique testa). Point, tupla, mouse, Graphic. O ponto está no mundo: num objeto da camada "ui" com a câmera em pan / zoom / giro, contains_point(mouse) não é o clique — use pointer_hover. Sprite: alfa do frame atual, não o retângulo do PNG nem o hitbox. Circle: a fatia pintada (pizza ou segmento). Rect corner_radius: o clique segue o canto; a colisão continua o retângulo.
show_handles() / show_points()
Sem parâmetros
—
show_handles() mostra os pontos livres que definem a forma (cantos, centro, pontas) como alças de arraste: bolinhas que você arrasta com o mouse para mudar a forma. show_points() só marca todos os pontos, sem arraste, para conferir a geometria. Uma Anchor já nasce como alça de arraste, na camada top.
move_to_front() / move_to_back()
Sem parâmetros
—
Ordem dentro da camada. Um objeto em layer="top" fica por cima dos da camada "default" (a padrão), seja qual for a ordem.
Invisível não clica. Quem recebe o clique: widgets → objetos na camada ui → mundo. pointer_hover vem de clickable / draggable / on_hover. não há hoverable: @hover já torna alvo. Três testes: contains_point = este desenho; pointer_hover = o alvo mais em cima; collision.query_point sem group= = hitboxes inscritos.
@leave_playfield
@leave_playfield roda uma função quando o centro de um objeto sai da área de jogo (canvas.playfield) — para destruir o tiro que saiu da tela, perder uma vida quando a estrela cai.
@leave_playfield("bullet")
def leftover(shot):
shot.destroy()
canvas.playfield.pad = 0.4
# ou explícito:
canvas.playfield.set(left=-40, bottom=-40, right=1040, top=840)
# ou um Box:
canvas.playfield.set(canvas.game_view.outset(300))
O retângulo é o playfield, fixo no mundo — não segue a câmera. Não é canvas.game_view (a janela). Layout de naves / HUD: Colocar.
Câmera que anda (corredor, flappy): o playfield fica para trás. Faça-o seguir a janela com uma linha por quadro, no update: canvas.playfield.set(canvas.game_view.outset(300)).
Só sai quem esteve dentro: um objeto que nasce fora do playfield e se afasta (o tiro de uma torre que ficou para trás da câmera) nunca dispara @leave_playfield e fica vivo para sempre. Crie-o dentro da área (atire só quando a torre está na tela) ou destrua-o depois de um tempo (tiro.after(3, tiro.destroy)).
Padrão (não precisa definir): o retângulo de design com pad=0.25 em cada lado (1.5×, centrado). Só passa a valer quando algum @leave_playfield está em uso.
Dois @leave_playfield("bullet") rodam os dois, na ordem em que foram escritos (como @collide). Um feito dentro de um ato sai na troca de ato.
Neste canvas.playfield
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
canvas.playfield.pad = 0.4
número0.4
lê / escreve
Fração em cada lado (tamanho × (1+2p)). 0.0 seria limite justo da tela. Negativo encolhe dentro do retângulo de design.
.left.right.bottom.top
número-40
lê / escreve
Bordas. y para cima: top é o maior y. Graváveis uma a uma.
.width.height.center
número ou ponto120
leitura
Derivados, só leitura. .center tem .x / .y.
.pad
número0.4
lê / escreve
Número se o modo é pad; None se as bordas são explícitas. Negativo = área menor que o design; pad <= -0.5 colapsa no centro (não inverte bordas).
Na classe: sem nomes de grupo. Vale para cada objeto da classe, desde que ele é criado. Callback: fn(self).
class Bolt(Sprite):
@leave_playfield
def leftover(self):
self.destroy()
Dispara só depois de o centro ter estado dentro uma vez. Spawn fora não conta. Não misture com @collide / @every no mesmo def. Não precisa destruir: pode estacionar, perder vida, reaparecer do outro lado. Não use o playfield para posicionar a frota — isso é Box / game_view. Para segurar a pose dentro de uma região (clamp / bounce / wrap): confine.
Saber quando dois objetos se tocam (@collide) e fazer um parar no outro — paredes, chão, plataformas (resolve). Só participa quem você inscreve, nomeando-o num @collide ou num resolve(). Shape, Sprite e SpatialFx usam o mesmo sistema. Para saber se algo saiu da área de jogo: @leave_playfield; onde dois contornos se cruzam: intersections.
Função que roda quando dois corpos se tocam → Detectar (@collide / @collide_with / on_collide)
O que a função de colisão recebe → CollisionInfo
Parede, chão, quique → Tornar sólido (resolve / .mass / bounce=)
Está no chão? → Tocando (touching_sides)
Em que bloco estou pisando? → Tocando (touching_contacts)
Vou cair se andar? Borda da plataforma → Chão à frente (floor_below)
Dash, controlador próprio → Resolução manual
Quem está aqui / raio → Consultas collidable / forma do colisor → O que colide
Overlay de depuração → Debug
Detectar
@collide chama uma função sua quando dois objetos (ou grupos) se tocam. Diga quais dois — @collide("bullet", "enemy") — e escreva a função que reage: função no módulo, método da classe ou propriedade no objeto. As três formas têm as mesmas três fases: começou a tocar, continua tocando, deixou de tocar.
Sem um par nomeado (nem on_collide / ANYTHING), nenhuma função de colisão roda. Cada um dos dois lados do par é um nome de grupo (tag), um objeto, group["tag"], ANYTHING ou uma parte de hitbox — ou uma lista deles. Qualquer outra coisa (5, None, um dict, um efeito) dá TypeError na hora, em @collide e em resolve / unresolve. Nomes de grupo veem quem entrar depois; um objeto passado diretamente sai do par quando é destruído (destroy()), e o objeto novo que você criar no lugar dele não entra — prefira o nome do grupo se a Entity morre e volta.
Fase
Quando
Função
Método
Objeto
enter
Primeiro frame de sobreposição.
@collide(a, b)
@collide_with(alvo)
on_collide
stay
Cada frame seguinte ainda sobrepostos.
@collide_stay(a, b)
@collide_with_stay(alvo)
on_collide_stay
exit
Deixaram de se tocar. info.point é None.
@collide_exit(a, b)
@collide_with_exit(alvo)
on_collide_exit
ANYTHING é o coringa: aquele lado × todos os inscritos. Callback: fn(me, other) ou fn(me, other, info) — me é o objeto do 1º argumento de @collide, other o do 2º; info é um CollisionInfo.
Vários handlers no mesmo contato rodam todos, mesmo que um deles destrua um dos dois: os seguintes recebem o objeto já morto (dá para ler; destroy() de novo não faz nada). Assim, um Tiro com @collide_with("inimigos") que faz self.destroy() e um Inimigo com @collide_with("tiros") que se destrói e soma ponto dão o mesmo placar, seja quem for criado primeiro — e o mesmo vale para uma função no módulo junto com um método. O exit desse contato espera os handlers que faltam: num contato você sempre vê o enter, depois o exit (um contador com += 1 no enter e -= 1 no exit volta a zero, seja qual for o handler que destrói). Rodam na hora do destroy(), enquanto ele ainda existe, os ganchos de exit do próprio objeto destruído (o on_collide_exit dele, os métodos @collide_with_exit da classe dele) e os exits dos outros contatos dele. Quem morreu num contato não começa outro no mesmo frame: um tiro acerta um inimigo só. Numa troca de ato no meio do contato, os handlers do ato que saiu não rodam.
Objeto rápido. Um objeto que viaja (move_by, move_to(…, time=), move_along, ou girando em volta de outro ponto com rotate_by(…, about=)) mais da metade da própria espessura num frame é conferido no caminho inteiro do frame, não só onde parou. Um tiro rápido não atravessa o inimigo, em qualquer taxa de quadros. Se o caminho cruza vários, o enter do mais perto roda primeiro: destruir o tiro ali impede os outros. O contato de quem já passou dura um frame (enter agora, exit no próximo); info.point é onde bateu e info.depth é 0. Colocar (center = …, move_to sem time=, @=, tween) não é conferido no caminho — para isso, raycast da posição anterior até a nova. Girar no lugar também não: uma lâmina que gira muito rápido pode pular um objeto fino entre dois frames.
Função no módulo. Os dois lados na chamada. Alcance: o par.
Método de classe. self é o objeto da classe; other é o alvo. O alvo usa as mesmas regras de um lado do @collide (tag, shape, ANYTHING). Alcance: self × o alvo.
class Wasp(Sprite):
@collide_with("bolt")
def on_hit_bolt(self, bolt, info):
self.hp -= 1
@collide_with_stay("acid_pool")
def in_acid(self, pool, info):
self.hp -= clock.dt
Callback: fn(self, other) ou fn(self, other, info).
Propriedade no objeto — Shape, Sprite e SpatialFx. Alcance: este objeto × qualquer outro inscrito.
Atribuir inscreve o objeto e declara interesse ANYTHING: equivale a @collide_with(ANYTHING) (e _stay / _exit). Como método da classe: fn(self, other) ou fn(self, other, info). Não filtra o outro lado — para um par específico, use @collide(a, b). Um só por fase no objeto: o último a gravar ganha.
on_collide = None tira o interesse ANYTHING desse callback: o motor deixa de testar este corpo contra todos os outros, a menos que @collide_with(ANYTHING) ou @collide(…, ANYTHING) ainda declare. As funções de @collide(a, b) no módulo continuam disparando.
Ordem. Dá para combinar as formas. No mesmo contato, a função do objeto (on_collide) roda primeiro, depois a função do par (@collide); se os dois existem, o Scena avisa e os dois disparam.
Não confundir
Parece
É
Não é
@collide e @collide_with no mesmo def
TypeError. Função solta usa @collide(a, b); método usa @collide_with.
duas grafias do mesmo decorator
Coletável / sensor num resolve()
Coletável / sensor é só @collide, nuncaresolve(). Moeda não empurra ninguém. Ver Tornar sólido.
CollisionInfo é o terceiro parâmetro (info) da função de colisão: diz onde os dois objetos se tocaram, em que direção e quanto se sobrepõem, naquele frame. Não se constrói — o Scena entrega. Existe sem resolve(): vem de @collide, on_collide e test_collision.
@collide("bolt", "wasp")
def boom(me, other, info):
canvas.explosion_at(info.point, amount=0.8, shake=False)
# ou: canvas.explosion_at(*info.point, amount=0.8, shake=False)
# ou: canvas.explosion_at(info, amount=0.8, shake=False)
other.destroy()
@collide_stay(player, "ramps")
def on_ramp(me, other, info):
if abs(info.slope_angle) <= 45: # limiar é decisão do script, não do motor
player.vel = info.slide_along(player.vel)
else:
... # íngreme demais — trata como parede comum
info.a é me e info.b é other. info.point é o lugar útil para VFX — o meio dos pontos de contato (info.points). Enter / stay sempre têm ponto (sem um ponto de contato exato, vale um ponto na borda do objeto). Exit não tem ponto (points vazio → point é None — o motor não guarda o último stay).
Neste CollisionInfo
Contato
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
info.a / info.b
Entityinfo.a
leitura
Os dois corpos deste par. No callback: a é me, b é other. Parte de hitbox: aqui vem o Sprite dono.
info.a_hitbox / info.b_hitbox
Hitbox ou Noneinfo.b_hitbox is hurt
leitura
A parte (add_hitbox) de cada lado; None = o corpo. Com parte, push_apart / bounce_apart / resolve dão TypeError (parte só detecta).
info.normal
Vec2info.normal
leitura
De a rumo a b (unitário). Zero se não houver.
info.depth
númeroinfo.depth
leitura
Quanto se sobrepõem. 0 quando um objeto rápido já passou do outro.
info.point
ponto ou Noneinfo.point
leitura
Um (x, y) usável — o meio dos pontos de contato. Enter / stay: sempre uma tupla (no mínimo, um ponto na borda do objeto). None só no exit. Objeto rápido: o lugar do impacto, mesmo que ele já esteja adiante.
info.points
listainfo.points
leitura
Todos os pontos onde os dois se tocam (lista). Vazia só no exit. Para física própria ou uma faísca em cada ponto.
info.side
str ou Noneinfo.side == "bottom"
leitura
Lado de a no contato: top/bottom/left/right.
Rampa
Nome
Tipo / assinatura
Retorna
Significado
info.slope_angle
númeroinfo.slope_angle
leitura
Ângulo da superfície em graus (0 = chão plano). Contínuo — não os 4 baldes de side.
info.tangent
Vec2info.tangent
leitura
Vetor unitário ao longo da superfície (perpendicular a normal).
info.slide_along(velocity)
Parâmetros: velocity (Vec2)
Vec2
Projeta velocidade na tangente — andar/deslizar numa rampa.
Isso é um canto da face. Use info.point. canvas.explosion_at / burst_at / ripple_at / wave_at / bubbles_at / fireworks_at / zap_at / hit_at / debris_at aceitam (x, y) ou um ponto (tupla, Point, Vec2, Graphic, info). None não faz nada — no exit, explosion_at(info.point) é silêncio; *info.point no exit levanta TypeError.
o último contato guardado
Na função de colisão, me.vel / other.vel já refletem o choque
Ainda são as de update(dt): o resolve() corre depois. O info não guarda velocidade — leia nos corpos. info.bounce_apart() / info.resolve(bounce=…) dentro dela escrevem .vel na hora.
velocidade pós-choque
Tiro rápido: info.point é onde o tiro está
É onde ele bateu. O tiro continua onde o frame o levou (o Scena não o puxa de volta): para a explosão, use info.point, não me.center.
a posição do tiro
Tornar sólido
resolve(a, b) inscreve um par para separar sozinho a cada frame: acompanha o caminho do movimento e para no ponto do impacto, depois desliza na superfície. Detecção (@collide) e resolução são inscrições distintas.
wall = Rect(center=(400, 300), width=200, height=30, group="walls")
wall.mass = STATIC
player.mass = 1.0
resolve(player, "walls") # inscreve + detecta + empurra
resolve(player, "walls", bounce=0.0) # + zera velocidade ao longo da normal
# reset: coloca, não viaja — a parede no caminho não para o herói
player.center = nascer
unresolve(player, "walls")
.mass decide quem cede no empurrão. STATIC não cede: chão, parede, plataforma.
Cada frame, o Scena acompanha o caminho do movimento do par inscrito e para no impacto, desliza na superfície e desfaz o que sobrou de sobreposição. Os lados são objetos ou nomes de grupo (quem entrar no grupo depois também entra). Massa padrão 1.0; <= 0 levanta erro. Duas massas iguais se empurram meio a meio.
Peças encostadas raramente ficam perfeitas: uma fileira de blocos girada, uma coordenada arredondada. Quem está no chão e anda contra a lateral de uma peça parada que sobe até 2 acima dos pés sobe esse degrau e continua andando. Degrau maior é parede. No ar não há degrau: bater na quina de lado é parede.
Coletável ou sensor não é sólido: só @collide. Moeda não empurra ninguém:
resolve(hero, ANYTHING) torna sólido tudo que está inscrito, inclusive moeda, tiro e sensor: o herói empurra a moeda em vez de passar por ela (o @collide da moeda continua disparando). Não há como marcar um corpo como “não sólido”: nomeie os grupos sólidos, resolve(hero, ["walls", "crates"]). ANYTHING serve quando tudo no mundo é sólido (um mundo só de blocos e paredes).
Chão que não pode ceder é STATIC. Quando nenhum dos dois corpos é STATIC, o empurrão se divide pela massa, quem quer que tenha sido criado primeiro: uma caixa de massa 1 anda a metade do passo do jogador que a empurra, uma de massa 50 quase não sai do lugar. Isso vale para chão, parede e plataforma também: deixados na massa padrão 1, eles cedem embaixo de um herói de massa 1 (a gravidade dele afunda o chão). Uma plataforma que você move com move_by e que deve carregar quem estiver em cima é STATIC.
Quem está em cima vai junto. Um objeto parado em cima de outro do resolve (o lado "bottom" encostado) anda junto com ele no frame, para os lados e para cima e para baixo, com move_by, time= ou move_along, em qualquer taxa de quadros; o que ele mesmo anda soma (andar em cima do elevador). Uma parede no caminho do elevador segura quem está em cima. Quem pula ou quica deixa de ir junto. Uma caixa empurrada não carrega a de cima: só uma plataforma (STATIC, ou sem .vel) carrega.
Não copie a velocidade do elevador para quem está em cima.heroi.vel.x += elevador_vel (ou heroi.vel = h.velocity) faz o herói andar duas vezes: o resolve já o leva junto.
Um STATIC nunca cede: uma pilha de caixas pesadas não afunda a de baixo no chão, e uma bola leve atingida por uma pesada não atravessa o chão nem uma parede fina. As caixas de uma pilha ainda podem entrar um pouco uma na outra e tremer (o Scena não é um motor de física). Dois objetos que já nascem um dentro do outro se separam pelo lado em que estão sobrepostos, seja qual for a .vel.
bounce= diz o que acontece com .vel no contato. Omitido é outra coisa que 0.
Chamada
Efeito
resolve(a, b)
Só posição: para no impacto; não lê nem escreve .vel.
resolve(a, b, bounce=0.0)
Posição + impulso inelástico na normal: tira o componente que entra e deixa o tangente (andar na rampa).
resolve(a, b, bounce=1.0)
Elástico. > 1 = super-quique. Só corre se o móvel tem .vel.
“Sem bounce” em português costuma significar bounce=0.0; omitir o parâmetro é só posição. .mass decide quem absorve o choque; bounce= decide quanto da velocidade normal sobrevive. Duas massas iguais com bounce=0 terminam juntas — não é bug.
Não há atrito. As superfícies são lisas: bounce=0 tira só a velocidade que entra na parede, e a de lado fica para sempre — a bola escorrega rampa abaixo e uma pilha nunca para. Freie você mesmo no on_update(self, dt): self.vel.x *= 0.5 ** dt (perde metade da velocidade de lado por segundo, igual em qualquer taxa de quadros).
Para zerar a velocidade contra uma parede há dois padrões:
# A — motor zera velocidade normal no contato
resolve(player, "walls", bounce=0.0)
# B — só posição no resolve; autor trata velocidade (mesma receita da rampa)
resolve(player, "walls")
def update(dt):
player.vel.y -= GRAVITY * dt
...
for c in player.touching_contacts:
if c.side == "top" and player.vel.y > 0:
player.vel.y = 0.0 # cabeça no teto — outro está acima
player.move_by(player.vel.x * dt, player.vel.y * dt)
.vel é None até atribuir; o script move com move_by(vel * dt). move_along / move_to(..., time=) / move_by(..., time=) não escrevem .vel — para herdar a taxa da viagem: caixa.vel = h.velocity (Handle). Quem está em cima da plataforma já vai junto; não copie a velocidade dela.
Quebrou no contato. Destruir (ou estacionar) o parceiro no @collide não tira o quique: com resolve(bola, "tijolo", bounce=1) e @collide(bola, "tijolo") que faz tijolo.destroy(), a bola quebra o tijolo e volta. Esse contato ainda dá a resposta de .vel neste frame; o tijolo morto não empurra nada. Só a parte que vem de encontro muda: quem passa rente ou se afasta segue com a mesma velocidade. Com bounce=0, um chão que some quando o herói pousa segura a queda nesse frame, e no próximo ele cai.
Quem estava no chão no início do frame é puxado de volta para um chão ao alcance. É o que mantém o herói grudado ao descer rampa, em frames curtos e num elevador.
bounce=
Grudar no chão
omitido, 0 ou até 0.5 — quem anda
Cola. Só sai do chão quando, no mesmo frame, a velocidade (.vel) aponta para longe do chão e o corpo de fato se afasta dele (como num pulo). Subir numa rampa ou ser carregado por um elevador dá velocidade grande para cima, e mesmo assim o objeto continua no chão.
acima de 0.5 — bolas, projéteis
Não puxa um quique de verdade: o motor confia na velocidade de saída.
Por isso resolve(bola, "parede", bounce=1) quica e resolve(heroi, "chão", bounce=0) fica estável. Não há opção de “grudar” — bounce= já diz a intenção.
Colocar pula direto para o lugar; viajar percorre o caminho. A parede só para o que viaja.
Coloca (atravessa a parede)
Viaja (a parede para no meio)
center =, move_to / rotate_to / scale_to sem time=, tween(shape.center, …), center @= (incluindo track.cursor)
O @collide segue a mesma divisão: quem viaja rápido é conferido no caminho inteiro (Objeto rápido); quem é colocado, só onde parou. Reset / respawn: hero.center = nascer ou move_to na hora — não desligue collision.resolve_enabled para isso. Deslizar até um ponto parando na parede: move_to(..., time=). Ver Mover.
only= diz de que lados de a o par segura. Qualquer outro primeiro contato atravessa até os dois se separarem. Plataforma de pular por baixo é only="bottom".
resolve(heroi, "fina", bounce=0.0, only="bottom") # pula por baixo, pisa em cima
resolve(heroi, "fina", bounce=0.0, only={"bottom", "left", "right"}) # laterais seguram
resolve(heroi, "porta", only="left") # porta de mão única
# descer da plataforma: BAIXO + pulo
def update(dt):
st.descer = recently("descer", keyp("DOWN") and keyp.once("UP"), hold=0.15)
...
resolve(heroi, "fina", bounce=0.0,
only=lambda c: c.side == "bottom" and not st.descer)
Com only="bottom"
Acontece
Pula por baixo
A cabeça bate ("top"): atravessa.
O topo do pulo fica dentro da plataforma
Continua atravessando e cai de volta: não passou do topo.
Passa do topo e cai
Os pés tocam ("bottom"): pisa.
Anda contra a lateral
Atravessa (como no Mario). Para segurar, liste os lados.
Anda sobre blocos encostados com um degrau de até 2
Sobe o degrau e continua em cima (não atravessa). Degrau maior é lateral: atravessa.
O Scena confere o caminho inteiro do frame: pega o topo e não atravessa.
Os lados são os de a, as mesmas palavras de a.touching_sides: "bottom" = pisar. Vale um lado, ou lista / tupla / conjunto (a ordem não importa). Os quatro lados = sem filtro. Uma função recebe o contato (TouchingContact, visto de a: .side, .other, .normal) e devolve True para segurar. Ela roda dentro da colisão, várias vezes por frame: só leia estado. Efeito (tocar som, quebrar a tábua) vai no @collide, que continua disparando enquanto atravessa. O Scena confere: no resolve(), uma função que escreve (st.x = …, heroi.vel.y = 0, destroy(), recently(…), keyp.once(…), chance(…), criar um objeto) levanta TypeError apontando a linha; o que escapa dessa leitura (código sem fonte) levanta RuntimeError ao rodar. Calcule no update(), guarde em st e só leia aqui — como st.descer no exemplo.
O lado é o do primeiro contato, o mesmo que uma plataforma sólida usaria. Cair na diagonal e tocar a quina pela lateral atravessa, como escorregaria na parede de uma plataforma sólida.
Não confundir
Parece
É
Não é
only="top" para plataforma de pular por baixo
only="bottom": o lado é do herói (os pés), não da plataforma.
o lado da plataforma
A mesma plataforma em "plataforma" e "fina", com only= só num dos dois
ValueError: dois registros discordam. Dê às plataformas de mão única um grupo só delas.
o primeiro vence
resolve("caixa", "caixa", only="bottom")
ValueError: um grupo contra ele mesmo vê cada contato dos dois lados. Use {"top", "bottom"}, {"left", "right"} ou uma função.
caixas empilháveis
only= numa rampa de mais de 45°
Os lados são faixas de 45° do mundo. Para rampa íngreme de mão única, use uma função com c.normal.
“em cima” da rampa
Tirar um par: unresolve(a, b), com os mesmos lados do resolve.
unresolve(player, "walls")
collision.resolve_enabled = False # avançado: desliga toda a resolução
collision.resolve_enabled = False desliga, em todos os pares, a parada no caminho, a separação e o grudar no chão. @collide, consultas e raycast continuam; touching_* fica vazio enquanto desligado. Não é o jeito de fazer reset (ver Colocar × viajar).
Neste resolve
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
STATIC
constantewall.mass = STATIC
lê / escreve
Imóvel — chão, parede. float("inf").
shape.mass
número1.0
lê / escreve
Peso no empurrão. Padrão 1.0. <= 0 levanta erro.
shape.vel
Vec2 ou Noneball.vel = Vec2(0, -200)
lê / escreve
Velocidade do corpo. None até atribuir. Lida e escrita por resolve(..., bounce=). Não se liga com @= (o motor escreve nela): para seguir o estado, escreva a cada frame — ship.vel = vec_from_angle(st.a * 30, 10) no on_update. Tweens de pose não escrevem isto — o Handle reporta a taxa (Handle).
collision.resolve_enabled
boolTrue
lê / escreve
Avançado. False: nenhum par para no caminho, se separa ou gruda no chão. @collide, consultas e raycast continuam. touching_* zera enquanto desligado.
Métodos
Método
Assinatura
Retorna
Significado
resolve(a, b, bounce=, only=)
Parâmetros: a (objeto ou nome de grupo), b (objeto ou nome de grupo), bounce (número, opcional), only (lado, lista / conjunto de lados ou função do contato, opcional)
—
Registra o par. Cada frame: para o movimento no impacto, desliza na superfície, desfaz o que sobrou de sobreposição. Sem bounce= → não lê .vel. Com: 0 inelástico, 1 elástico, >1 super-quique. only=: só esses lados de a seguram; o resto atravessa (Mão única). Chamar de novo o mesmo par troca bounce= e only=.
unresolve(a, b)
Parâmetros: a (objeto ou nome de grupo), b (objeto ou nome de grupo)
—
Cancela a inscrição. Mesmos lados que resolve.
Tocando
touching_sides diz quais lados do objeto encostam em algo sólido, conforme a última conferência de colisões (a do frame anterior) — só pares em resolve(), não sensores. É o “está no chão?” do jogo de plataforma.
player["grounded"] @= lambda: "bottom" in player.touching_sides
def update(dt):
if keyp("SPACE") and player.grounded:
player.vel.y += PULO
player.tilt = "ground" # rampa: inclina só o desenho (Orientar sprites)
for c in player.touching_contacts:
if c.side == "bottom":
poeira_em(c.point)
# em que bloco estou pisando? (None no ar)
piso = next((c.other for c in player.touching_contacts if c.side == "bottom"), None)
Neste touching
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
touching_sides
frozenset{"bottom"}
leitura
Lados em contato com par resolve(). Reativo quando uma fórmula depende (ex. grounded @=).
touching_contacts
tuplaplayer.touching_contacts
leitura
Um TouchingContact por par resolve(). Não reativa. Tabela abaixo.
Cada item de touching_contacts (TouchingContact)
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.other
Graphicc.other
leitura
O outro corpo do par.
.side
str ou None"bottom"
leitura
"top" · "bottom" · "left" · "right" — lado deste corpo que toca.
.slope_angle
número22
leitura
Ângulo da superfície em graus (0 = chão plano). Sprite.tilt = c com âncora "bottom" roda nesse ângulo e encosta o desenho no plano só ao longo da normal. tilt = c.slope_angle só gira.
.normal
Vec2c.normal
leitura
Normal unitária (deste corpo → o outro).
.tangent
Vec2c.tangent
leitura
Tangente unitária à superfície.
.point
ponto ou Nonec.point
leitura
Ponto de contato no mundo (o meio dos pontos de contato, ou o ponto de apoio — canto de apoio em vértice-sobre-face). VFX / dust. Pode ser None.
Métodos
Método
Assinatura
Retorna
Significado
.slide_along(velocity)
Parâmetros: velocity (Vec2)
Vec2
Projeta a velocidade na tangente (andar / deslizar na rampa).
Saiu da plataforma ou saltou → o lado some na hora. Coyote time e jump buffer são timers seus em cima (recently). touching_contacts pode ter mais de um item (parede + chão no canto).
Não confundir
Parece
É
Não é
@collide(heroi, "bloco") para saber em que bloco o herói pisa
Leia touching_contacts (o c.other do contato "bottom"): vem da posição já resolvida e é estável em qualquer taxa de frames. @collide num par resolve() não pisca com o corpo parado, mas na emenda entre dois blocos, com frames irregulares, um corpo redondo pode entrar e sair do bloco vizinho por alguns frames.
entrada/saída de @collide por bloco
Sensor / coletável aparece em touching_*
Nunca: @collide sem resolve() detecta sobreposição, mas não vira chão.
contato de sensor
touching_contacts é o contato deste frame
Reflete o frame anterior (igual touching_sides). Para decidir no mesmo frame, use @collide_stay com info.side, antes do auto-resolve.
o frame atual
Na rampa, girar o herói com .angle
Gira o colisor e ele tropeça. Use tilt — só o desenho, com hitbox_anchor="bottom". Ver Inclinar.
.angle
Chão à frente
objeto.floor_below(dx=) devolve o chão em que o objeto estaria depois de andar dx, ou None se ele cairia. É a borda da plataforma do jogo de plataforma: o inimigo que vira antes de cair, o herói que se equilibra na beirada.
resolve(inimigo, "chao", bounce=0.0)
def update(dt):
frente = inimigo.hitbox.box.width * st.dir
if "bottom" in inimigo.touching_sides and not inimigo.floor_below(dx=frente):
st.dir = -st.dir # a ponta do pé chegou na borda: vira
inimigo.move_by(60 * st.dir * dt, 0)
piso = heroi.floor_below() # em que chão estou (ou None no ar)
if not heroi.floor_below(dx=2 * st.dir):
heroi.play("equilibrando") # mais um passo e cai
Neste floor_below
Método
Assinatura
Retorna
Significado
floor_below(dx=0, depth=None, group=None)
Parâmetros (por nome): dx (número com sinal, + = direita), depth (≥ 0), group (tag ou lista)
o objeto que serve de chão, ou None
Anda dx em passos curtos (sobe rampas até 45°, para na parede) e pergunta se há chão embaixo. Só leitura: não move, não gasta o movimento pendente, não inscreve ninguém.
Mesmas regras da física. Quem decide é a hitbox (não o desenho), chão é contato "bottom" (±45°, o mesmo de touching_sides), e uma queda maior que depth é cair. Sem depth, vale o alcance que o resolve() desce sozinho (16 unidades): um degrau menor ele desce andando; maior, o corpo cai. Então None quer dizer “a física vai deixar cair”. depth vale só para degrau e queda vertical: rampa até 45° é chão com qualquer depth. Uma caixa apoiada só na quina ainda está no chão; um círculo passa a rolar quando o contato sai dos 45°.
O que é chão. Sem group=, o que o corpo resolve (resolve(corpo, …), com os only=): moeda, sensor e inimigo só com @collide não contam. group="chao" nomeia o chão — é assim que um bicho sem resolve(), movido por script, pergunta. Sem nenhum dos dois, levanta ValueError com as duas saídas.
O que dx pergunta. Um número só cobre os casos: dx=0 é o chão de agora; dx pequeno é “caio no próximo passo?”; dx = largura é “a ponta do pé passou da borda?” (o inimigo que vira); dx = largura / 2 é “o meio passou?”. Use a largura da hitbox (hitbox.box.width), não a do desenho (box.width inclui a margem transparente do PNG). Parede no caminho não é borda: a resposta é o chão onde a caminhada parou. Paredes são "left" / "right" em touching_sides.
Com show_colliders, cada pergunta aparece onde o passo terminou: magenta = achou chão, vermelho = cairia.
Não confundir
Parece
É
Não é
Comparar x com a borda do bloco em que pisa
floor_below: dois blocos encostados não viram “borda” na emenda, e rampa não vira precipício.
c.other.box.right
Ponto de teste com collision.query_point na frente do pé
Funciona para retângulo em chão plano. floor_below usa a hitbox inteira e a regra de chão da física (rampa, círculo, degrau).
a mesma pergunta
floor_below() para “está no chão?”
"bottom" in touching_sides. floor_below() acha chão até 16 unidades abaixo — também no começo de um pulo.
grounded
Resolução manual
Quando o resolve automático não basta: um dash, um controlador cinemático, uma decisão dentro da função de colisão. Cinco níveis, uma mesma receita — detectar → separar posição → separar velocidade — com os mesmos mass_a/mass_b/bounce=.
Nível
Chamada
Quando
1
resolve(a, b, bounce=)
Registro permanente — degrau de todo dia
2
shape.resolve_now(alvo, bounce=)
Detecta + resolve agora; devolve lista de CollisionInfo
3
info.resolve(bounce=)
Dentro da função de colisão — par já detectado
4
info.push_apart() / info.bounce_apart(bounce=)
Avançado — aplica com massas explícitas, não necessariamente .mass
5
info.mtv_split() / info.velocity_split()
Avançado — só tuplas, autor decide o que "aplicar" significa
O caminho de 95% dos scripts: registra uma vez, ou resolve por conta própria quando o momento pede.
# Declarativo (motor corre sozinho):
resolve(player, "walls", bounce=0.0)
# Manual no update — antes de o Scena conferir as colisões do frame:
for info in player.resolve_now("walls", bounce=0.0):
if info.side == "bottom":
grounded = True
# Manual dentro da função de colisão — resolve condicional:
@collide_stay(player, "walls")
def touch(me, other, info):
if not player.dashing:
info.resolve(bounce=0.0)
A inscrição permanente resolve(a, b) acompanha o caminho do movimento do frame. resolve_now / info.resolve só empurram o par já sobreposto pelo menor caminho que os separa (o MTV) — não olham o caminho.
Recurso de exceção: massas "de mentirinha" (não as .mass reais dos corpos) e efeitos parciais que resolve() não expressa sozinho.
# Nível 4 — aplica na hora, com massas escolhidas no momento:
@collide(player, boulder)
def hit(me, other, info):
info.push_apart(mass_a=1.0, mass_b=10.0) # só posição
info.bounce_apart(mass_a=1.0, mass_b=10.0, bounce=0.3) # + velocidade
# Nível 5 — só tuplas: autor decide o que "aplicar" significa —
# aqui, empurrão pela metade, para um choque mais macio:
@collide(player, cloud)
def hit(me, other, info):
(dxa, dya), (dxb, dyb) = info.mtv_split(mass_a=1.0, mass_b=1.0)
player.move_by(dxa * 0.5, dya * 0.5)
cloud.move_by(dxb * 0.5, dyb * 0.5)
mass_a= / mass_b= aqui não precisam ser a.mass / b.mass: servem para um empurrão só de efeito, sem tocar a massa real. Sem slop= explícito, mtv_split / velocity_split encostam exatamente (sem folga); push_apart / bounce_apart (e resolve) deixam a folga que evita tremido do stay.
Métodos de resolução em CollisionInfo
Método
Assinatura
Retorna
Significado
shape.resolve_now(target, bounce=)
Parâmetros: target (objeto ou nome de grupo), bounce (número, opcional)
lista de CollisionInfo
Detecta e resolve agora, sem esperar a conferência automática do frame.
info.resolve(bounce=)
Parâmetros: bounce (número, opcional)
—
push_apart + (se bounce dado) bounce_apart. Omitido = só posição, igual resolve(a, b) sem bounce=.
info.push_apart(mass_a=, mass_b=)
Parâmetros: mass_a (número), mass_b (número)
—
mtv_split + move_by nos dois corpos. Sem massas explícitas, usa a.mass/b.mass.
Só a tupla de empurrão ao longo do menor caminho de separação — sem move_by. slop: quanto da sobreposição fica sem desfazer (0 = encostam exatamente). Massa <= 0 levanta erro.
Só calcula as velocidades novas dos dois objetos depois do choque — sem escrever .vel. vel_a/vel_b são obrigatórios aqui (não lê .vel sozinho).
info.reflect_velocity(velocity, restitution=1)
Parâmetros: velocity (Vec2), restitution (número)
Vec2
Reflete velocidade sobre normal. Caso mass_b=STATIC de velocity_split — segue existindo, não é o primitivo recomendado para corpo móvel.
Consultas
Perguntar ao sistema na hora, sem esperar uma função de colisão disparar: quem está neste ponto, neste círculo, neste cone, na frente deste raio.
collision já existe depois de from scena import * — como canvas ou clock. Não se constrói. Não é um Shape. O Scena confere as colisões sozinho a cada frame; o script não precisa chamar nada para isso.
hits = collision.query_point(mouse)
near = collision.query_circle(mouse, radius=80)
enemies = collision.query_circle(mouse, radius=80, group="enemy")
seen = collision.query_sector(guard, radius=300, spread=60, group="enemies")
wedge = Circle(center=lambda: guard.center, radius=300, spread=60, use_center=True)
wedge["angle"] @= lambda: guard.angle
hit = collision.raycast(player, p2=mouse)
blocked = collision.raycast(hero, p2=enemy, ignore=enemy)
if hit:
body, t, point = hit
info = test_collision(player, door)
victims = collision.query_shape(explosion, group=["ships", "asteroids"])
nearby = player.collides_with(group="enemy")
Quem uma consulta vê. Sem group= (ou group=ANYTHING): só quem já está inscrito (collidable, @collide, resolve, on_collide) — corpos, não partes. Com group= (tag, group['nome'], lista / tupla / set): os membros desse grupo, inscritos ou não, sem inscrever ninguém. Um Graphic na origem é quem consulta e nunca é hit; ignore= soma a ele (corpo, tag, group['nome'], ou lista).
Parâmetros: x (um ponto, ou o número x com y); radius (número ≥ 0); group, ignore (opcionais)
lista
Os objetos que se sobrepõem ao círculo. query_circle(x, y, radius) ou query_circle(p, radius=80). Depois de um ponto, radius= vai por nome. Raio negativo levanta ValueError.
Parâmetros: origem (ponto); angle (graus, opcional); radius e spread (números ≥ 0); group, ignore (opcionais)
lista
Quem se sobrepõe a um setor (radar). spread=60 é a abertura inteira (±30). angle= omisso usa .angle de um Graphic (no Sprite isso já é para onde a arte aponta). Sem Graphic, angle= é obrigatório. spread=0 vira só a linha do meio do setor: devolve todos os objetos que essa linha toca. spread=360 é query_circle. Acima de 360 levanta ValueError. Ordem do registro, sem ordenar por distância. Visão bloqueada: raycast depois. Desenhe a mesma fatia: Circle(center=, radius=, angle=, spread=) (use_center já nasce True; False é o segmento da corda).
collision.query_shape(shape, group=)
Parâmetros: shape (objeto), group (str ou lista)
lista
Quem se sobrepõe à geometria de shape. group=: tag, lista / set (OR), group['nome'], ANYTHING. A própria sonda não é hit.
Parâmetros: p1 (ponto); um fim: p2 (ponto), ou vec (deslocamento), ou length com angle opcional; group, ignore (opcionais)
tupla, ou None
O primeiro objeto que o segmento atinge. Um segundo argumento sem nome levanta TypeError — o fim vai por nome: p2=, vec=, ou angle= e length=. angle= omisso usa .angle do Graphic. vec= não aceita Point, Graphic nem mouse. O destino conta, salvo ignore=. (objeto, t, ponto) ou None: o objeto atingido; t de 0 a 1, a fração do segmento até o impacto (0 = começa dentro); o ponto do impacto.
O que colide
Quem entra no sistema (collidable) e com que forma. O hitbox não é o desenho.
ball.collidable = True
wasp.collidable = False # opt-out, mesmo com @collide("wasp", …)
collidable é propriedade em Shape, Sprite e SpatialFx — liga o hitbox no registro (consultas, alvo de ANYTHING, contornos de depuração). Não é método de collision: vive no corpo. Padrão False. @collide, resolve e on_collide = fn inscrevem sozinhos. False tira do sistema (e fica fora) — mesmo com @collide("wasp", …) — até voltar a True. Dois corpos só com a flag não são testados um contra o outro a cada frame; nomeie o par ou use on_collide / ANYTHING.
Um ponto de um Shape (c.center, linha.p2, um vértice) não é um objeto que colide: collidable = True, on_collide, @collide e resolve com ele levantam TypeError. Colida com o dono (@collide(linha, parede); info.point diz onde tocou), teste um ponto (parede.contains_point(linha.p2)) ou crie um objeto que o acompanha: ponta = Point(lambda: linha.p2) — esse é seu, destrua-o quando quiser. Consultas (test_collision, collides_with) aceitam o ponto.
Corpo
O que colide
Shape
A geometria do modelo (círculo, retângulo, polígono…).
Track
Uma fileira de retângulos finos ao longo do traço, com a espessura de line_width. Fechado = anel (buraco vazio), não disco. begin/end cortam o traço que colide.
Sprite
O hitbox calculado (o corpo): forma e arte escolhidas por hitbox_shape= / hitbox_source=, congelado ao animar. Ver Hitbox do Sprite.
Parte do Sprite
sprite.add_hitbox(group=…) — colisor extra, só detecção, que segue o desenho e responde pelos grupos que reivindica. Ver Partes.
SpatialFx
O domínio (orbe, área, feixe) — não o glow.
Não existe em Widget, Text, nem no Entity() sem desenho. Camada parallax / ui: TypeError. Ver os contornos: collision.show_colliders() — pares em resolve() em dourado.
Collider
Um Collider junta vários shapes num corpo só para a colisão — um chefe feito de tronco e cabeça, uma nave de várias peças. @collide(body, "bolt") dispara uma vez para o corpo, e o me da função é o Collider, não a peça atingida. As peças continuam se desenhando e se movendo cada uma por si; o Collider só as junta na colisão. Um shape sozinho não precisa de Collider: nomeie-o direto no @collide / resolve.
As peças do corpo. body.shapes lê as que ainda estão vivas (uma peça destruída sai da lista).
on_collide / on_collide_stay / on_collide_exit
—
função ou None
As mesmas três fases de Detectar, no corpo inteiro: body.on_collide = lambda me, other, info: … reage a qualquer objeto inscrito.
Ponto fraco: uma peça que também é um corpo. Uma peça pode ser, além de parte do Collider, um corpo dela mesma — com collidable = True, ou porque uma regra nomeia o grupo dela. São dois contornos separados: a regra do grupo da peça vê a peça, e a do corpo vê o corpo.
torso = Rect(center=(500, 300), width=80, height=120, group="ponto_fraco")
head = Circle(center=(500, 390), radius=30)
chefe = Collider([torso, head])
@collide("bolt", "ponto_fraco") # o tiro no tronco: dano dobrado
def no_ponto_fraco(tiro, peca):
tiro.destroy()
chefe.on_collide = lambda me, other, info: … # o tiro em qualquer parte do chefe
O corpo nunca colide com as próprias peças (nem as empurra num resolve), e duas peças do mesmo Collider que também são corpos (dois pontos fracos) não colidem entre si. Uma regra com ANYTHING vê os dois corpos: um tiro no tronco são dois contatos, um com o tronco e um com o chefe — nomeie o grupo para contar uma vez. Nas consultas, o Collider vem antes das peças que também são corpos: collision.query_point (e query_circle, query_sector, query_shape) lista o chefe primeiro, e collision.raycast devolve o chefe quando o raio entra nos dois no mesmo ponto; raycast(…, group="ponto_fraco") devolve o tronco. A ordem das linhas não muda nada disso.
Contas de geometria: vetores (Vec2), onde dois contornos se cruzam, e funções de pontos — distância, rumo, meio, o mais perto. Ângulos em graus. Contas de valores (remapear, misturar, perseguir, sortear) estão em Utilitários.
Vetor → Vec2
Cruzamento de contornos → Interseções
distance / heading / closest_point → Pontos
Vec2
Um Vec2 é uma seta: um deslocamento (dx, dy) ou uma velocidade, com comprimento e direção. Soma com outro, multiplica por um número, gira. Ângulos em graus (0 = para a direita).
v = Vec2(1, 0)
w = vec_from_angle(90, length=200)
a1 = v + w # outro Vec2
a2 = w - v # outro Vec2
a3 = w * 0.5 # outro Vec2, com metade do comprimento
a4 = w / 2 # outro Vec2 (dividido por 2)
ship.move_by(angle=w.angle, distance=w.length)
Mudar comprimento e rumo de um Vec2 que já existe..length e .angle são calculados a partir de .x / .y: só leitura (v.length = 5 dá erro e mostra a forma certa). Para pôr um comprimento e um rumo, monte a seta com vec_from_angle(rumo, comprimento) e copie-a para dentro de v com <<. O v continua o mesmo objeto, então quem o guardava (outro objeto, uma lista, uma fórmula que lê v) vê os valores novos. Já v = vec_from_angle(…) só faz o nome v apontar para outro Vec2; quem guardava o antigo não vê a mudança. << está em Ligar.
v = Vec2(3, 4) # comprimento 5, rumo ≈ 53°
v << vec_from_angle(30, 10) # rumo 30°, comprimento 10: x ≈ 8.66, y = 5
v << vec_from_angle(v.angle, 2) # muda só o comprimento (rumo fica 30°)
v << vec_from_angle(90, v.length) # muda só o rumo (comprimento fica 2)
Neste Vec2
Propriedades
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.x / .y
número1
lê / escreve
Os componentes: quanto a seta anda em x e em y.
.length / .angle
número200
leitura
Comprimento e rumo em graus (0 = +x), calculados a partir de .x / .y. Para mudar no próprio v: v << vec_from_angle(rumo, comprimento) (só o comprimento: v << vec_from_angle(v.angle, n); só o rumo: v << vec_from_angle(graus, v.length)). Para um Vec2 novo: .normalize() * n (ou limite com .clamp_length) e .rotate(graus).
Graus. 0 = +x. Com reference_vec, o ângulo conta a partir da direção dele (vec_from_angle(90, reference_vec=frente) = 90° à esquerda de frente). Para mover: move_by(angle=, distance=).
+ / -
Parâmetros: other (outro Vec2)
Vec2
Soma / subtrai outro vetor.
* / /
Parâmetros: número
Vec2
Multiplica / divide por um escalar. Vec2 à esquerda: v * 2, v / 2.
.dot(other)
Parâmetros: other (Vec2)
número
Produto escalar: positivo se as setas apontam para o mesmo lado, 0 se são perpendiculares, negativo se opostas.
.normalize()
Sem parâmetros
Vec2
Mesmo rumo, comprimento 1.
.rotate(angle)
Parâmetros: angle (número)
Vec2
Gira em graus (anti-horário).
.lerp(other, t)
Parâmetros: other (Vec2), t (número)
Vec2
Mistura com other: t=0 dá este vetor, t=1 dá other, 0.5 o meio. Fora de 0–1 continua a reta (não trava). Também existe a função lerp.
.distance(other)
Parâmetros: other (Vec2 ou ponto)
número
Distância. Mesma métrica que distance (Point, tupla, Graphic, …).
.reflect(normal)
Parâmetros: normal (Vec2)
Vec2
Reflete sobre a normal. O mesmo que .reflect_about.
.cross(other)
Parâmetros: other (Vec2)
número
Produto vetorial no plano: positivo se other está à esquerda desta seta (anti-horário), negativo se à direita.
.project_onto(onto)
Parâmetros: onto (Vec2)
Vec2
Projeção sobre onto.
.reflect_about(normal)
Parâmetros: normal (Vec2)
Vec2
Quique numa superfície com essa normal: Vec2(1, -1).reflect_about(Vec2(0, 1)) → (1, 1).
Mesmo rumo, comprimento preso entre min_length e max_length: vel.clamp_length(0, 300) limita a velocidade a 300.
.to_tuple()
Sem parâmetros
(x, y)
Par de números. Também x, y = v (cópia de agora). v[0] levanta TypeError: use v.x.
Interseções
Os pontos onde as bordas de duas formas se cruzam, como tuplas (x, y). Não diz se uma forma está dentro da outra (para isso, @collide). Para ligar um ponto a um cruzamento (@= ou lambda: no construtor), use intersection.
Lista agora. Vazia se os contornos não se encontram.
hits = intersections(line, circle)
for x, y in hits:
Circle(center=(x, y), radius=6, color="red")
hit = min(hits, key=lambda p: distance(line.p1, p)) if hits else None
intersection sempre devolve um ponto, então serve num lambda: do construtor — aqui, a corda: o segmento entre os dois cruzamentos. Para esconder a corda quando não há 2 cruzamentos, teste no update (visible não se liga com @=).
Parâmetros: a (shape), b (shape), i (número inteiro)
ponto (x, y)
O cruzamento número i, para @= / construtor. Se não houver nenhum, devolve um ponto próximo das duas formas em vez de dar erro. i < 0 → ValueError.
Quem entra / ordem / o que intersection devolve
Tema
Regra
Permitidos
Line, Circle, Ellipse, Rect, Square, Polygon, RegularPolygon, EquilateralTriangle, Box. Outro tipo (Point, Track, Sprite) → TypeError.
Contorno
Line = segmento p1→p2 (ignora espessura). Circle = circunferência cheia (ignora arco). Ellipse = contorno inteiro, com rotação. Elipse achatada a zero = segmento do eixo maior. Caixa / n-gon = arestas (ignora corner_radius).
Ordem
Com Line: ao longo dela. Dois círculos: esquerda → direita do eixo dos centros. Primeiro é Circle / Ellipse: ao longo dele, a partir do +x local, no sentido da rotação. Senão: ao longo do primeiro.
Quantos
Elipse × círculo ou elipse: até 4 pontos. Tangente conta 1.
O que intersection devolve
hits[i] se existir; senão o último cruzamento (numa tangente, 0 e 1 dão o mesmo ponto); senão um ponto próximo das duas formas.
Círculo dentro de um quadrado, sem tocar a borda → []. Isso não é @collide nem CollisionInfo.points. Não devolve Point. Point / Track levantam TypeError. Ponto no corpo: closest_point.
Pontos
Distância, rumo, meio, item mais perto, projeção no corpo. Tuplas (x, y) — não criam Point. ponto + (dx, dy) devolve um Point novo (colocar, não viajar).
Ponto. Onde a API pede um lugar, um ponto pode ser Point, Vec2, (x, y), um Graphic (o .center) ou mouse. Onde a API espera um lugar — move_to, center=, about=, snapshot, closest_point, pan_to, query_point, raycast, tween de um ponto, move_along(at=), íris center= — qualquer um desses vale. Isso copia a posição agora. Para acompanhar: Circle(center=lambda: mouse) ou center @= mouse. Deslocamento (move_by, pan_by, vec= / vectors= de Track) recusa um lugar. Graphic e SpatialFx não têm .x / .y — a posição é .center.
Tupla (x, y). Point, Vec2, tupla, Graphic ou mouse.
distance(p1, p2)
Parâmetros: p1 (ponto), p2 (ponto)
número
Distância euclidiana (≥ 0). Também p1.distance(p2) no Point (e Vec2).
heading(origin, target)
Parâmetros: origin (ponto), target (ponto)
número
Rumo em graus (0 = +x, −180…180), de origin para target. Também p1.heading(p2) no Point.
nearest(origin, items, *, within=, where=)
Parâmetros: origin (ponto), items (lista), within (número), where (função)
o item, ou None
O item de items mais perto de origin. Detalhes em Neste nearest, abaixo.
closest_point(shape, p)
Parâmetros: shape (shape), p (ponto)
ponto (x, y)
O ponto do corpo preenchido mais perto de p, sempre (x, y). Detalhes em Neste closest_point, abaixo.
ponto + (dx, dy)
(dx, dy) ou Vec2
Point
Translação. Também (dx, dy) + ponto. ponto - (dx, dy) anda para o outro lado. ponto - outro é o Vec2 entre os dois. ponto + outro levanta TypeError — para o meio, use middle_point. O resultado não entra no jogo.
ponto += (dx, dy)
—
erro
TypeError, de propósito: += colocaria o objeto direto no lugar, atravessando paredes. Viajar: move_by. Colocar: center = center + (dx, dy).
ponto == outro
—
True/False
Num Point, == compara o objeto, não a posição: c.center == (400, 120) é sempre False, e dois Points diferentes no mesmo lugar também não são iguais (o Scena avisa nos dois casos). Mesma posição: distance(c.center, (400, 120)) == 0, ou < 1 se o movimento não chega exato. Mesmo objeto: a is b. Num Vec2 ou numa tupla, == compara a posição.
shape.contains_point(x, y=None)
Parâmetros: x (um ponto, ou o número x com y)
True/False
Este objeto contém o ponto (no mundo)? Desenho — Clique / arraste. Não é closest_point nem query_point. Objeto da camada "ui" sob câmera: pointer_hover.
box.contains_point(x, y=None)
Parâmetros: x (um ponto, ou o número x com y)
True/False
True se o ponto está dentro desta caixa. Um Box é só uma área: não recebe cliques. Para saber a casa de um tabuleiro: grid.cell_at — Colocar.
Neste heading
heading(origin, target)
Parâmetros: origin (ponto), target (ponto). Retorna número (graus).
Rumo de origin até target — o ângulo para apontar de origin para target. Atribua a .angle.
hero.angle = heading(hero, enemy_ship)
hero.angle = hero.center.heading(enemy_ship.center) # mesmo valor
Tema
Regra
origin · target
De onde se olha e para onde. A ordem importa: trocar os dois inverte o rumo em 180°.
Quem entra
Point, Vec2, (x, y), Graphic, mouse — os mesmos de distance / nearest.
Coincidente
Mesmo lugar → 0.
Sprite
.angle já é rumo no mundo (art_angle entra no desenho). Não some art_angle outra vez.
Parâmetros: origin (ponto), items (lista), within (número), where (função). Retorna o item, ou None.
Qual item da lista está mais perto da origem. Devolve o próprio item — não um Point novo.
Parâmetro
Passar
Tipo (default)
Significado
origin
1º
ponto(400, 300)
Point, Vec2, (x, y), mouse, ou algo com .center / .x/.y (Shape, Sprite).
items
2º
iterávelgroup["wasp"]
Lista, tupla, group["wasp"], poly.points, … Itens sem posição são ignorados.
within=
nome=
número ou None200
Distância máxima (inclusiva). Omitir = sem limite. Negativo → erro.
where=
nome=
função (True/False) ou Nonelambda w: w.hp > 0
Uma função que recebe cada item e devolve True/False. Só entram os itens com True.
Empate: o primeiro. Vazio / ninguém dentro de within → None. Frota nomeada: preferir group["wasp"].nearest(…) (mesma métrica; aceita within= e where=).
Parâmetros: shape (shape), p (ponto). Retorna sempre um ponto (x, y).
Ponto mais próximo no corpo preenchido. Dentro → o próprio ponto (exceto Line). Seguir o ponteiro: dot["center"] @= lambda: closest_point(planet, (mouse.x, mouse.y)) ou, no update, dot.center << closest_point(…), que copia o ponto para dot.center naquele frame (colocar) — ver Mouse.
dot["center"] @= lambda: closest_point(planet, (mouse.x, mouse.y))
# ou no update:
def update(dt):
dot.center << closest_point(planet, (mouse.x, mouse.y))
d = distance(player.center, closest_point(wall, player.center))
Tema
Regra
shape · p
O Shape onde procurar e o ponto de referência. Shapes aceitos: os mesmos de Interseções; outro tipo → TypeError.
Dentro
Disco / caixa / polígono → p. Centro do círculo → (cx + r, cy).
heading não é Handle.heading (o que o tween andou neste frame) nem Vec2.angle_to (ângulo entre duas setas). nearest devolve um item (alvo, vértice, …). closest_point devolve um ponto no shape. contains_point é True/False no desenho (clique), não no hitbox — Clique / arraste. Ranking de itens: sorted(itens, key=lambda p: distance(origem, p)).
Controles de interface — botões, sliders, caixas de marcar e de texto, listas, menus — presos à janela, por cima do jogo e fora do alcance da câmera. Sem posição, eles se empilham numa coluna junto com os Text de HUD.
Todos os widgets → Widgets
Empilhar em coluna → Coluna
Button / Slider / CheckBox / TextInput / DropDown / Menu → as seções seguintes
Folga na pilha → ui.gap
Próxima coluna → ui.stack_column
Widgets
Widgets são os controles de interface — Slider, Button, CheckBox, TextInput, DropDown — desenhados na janela, por cima do jogo, sem acompanhar a câmera. Servem para ajustes e ferramentas (volume, opções, nome do jogador).
Cada widget fica ligado a um valor: o 1º argumento, uma célula de st com colchetes (Slider(st["volume"])). Sem colchetes, st.volume é o número de agora, e o widget levanta erro com a correção. Os dois andam juntos nos dois sentidos — mexer no controle grava o valor, e gravar o valor no código (st.volume = 0.2) move o controle. Sem top_left=, os widgets se empilham numa coluna no canto da janela, junto com os Text de HUD. ui.scale e ui.font mudam o tamanho e a fonte de todos. Menu é outra coisa: uma pergunta no centro da tela, fora da coluna. Botão feito da sua arte: SpriteButton.
Defina antes de criar os widgets; mudar depois não refaz a coluna. ui.font é família instalada ou arquivo .ttf / .otf (mesma busca que imagem). Fonte ausente: aviso e a padrão.
ui.min_label_space
número50
lê / escreve
Largura mínima da coluna de rótulo do Slider, em unidades de design com ui.scale=1 (o tema multiplica por ui.scale). Antes de construir: só os próximos widgets pegam o valor. Negativo: ValueError.
O valor que o widget mostra e edita. Os dois ficam ligados nos dois sentidos: o gesto grava na célula; gravar no valor (st.volume = 0.2) move o controle. É a própria célula, com colchetes (st["volume"]), não uma fórmula @=; st.volume sem colchetes é o número e levanta erro.
top_left=
nome=
ponto(20, 300)
Canto superior esquerdo visual, espaço da janela. Omita para empilhar (mesma coluna que Text HUD); com ele, ainda avança a coluna. Cópia: lambda levanta erro (widget não segue fórmula). Não existe x1= / y1=.
initial=
nome=
qualquer0.5
Valor de partida. Se vier junto com um Attribute, grava este valor nele.
width= / height=
nome=
número260 / 28
Tamanho do controle, em unidades de design. Omitido: o do tema (260 × 28, vezes ui.scale). Slider só tem width=; CheckBox usa size=.
Neste widget
Propriedade
Tipo (valor típico)
Leitura/Escrita
Significado
.data / .value
Attribute ou númeroslider.data
lê / escreve
.data é o Attribute ligado; .value é o valor dele agora. Para mostrar o valor num Text, ligue ao Attribute: hud["text"] @= lambda: f"{st.volume:.2f}".
visible
boolFalse
lê / escreve
False esconde: não desenha e não recebe cliques. Para mostrar desligado (esmaecido, sem responder): enabled = False.
pointer_hover
boolTrue
lê
Este widget é o alvo de hover (um widget ganha de um Shape ou Sprite embaixo dele). Ver Hover.
pointer_pressed
boolTrue
lê
Apertado neste widget, ainda segurado e com o cursor em cima. Mesma regra dos Shapes e Sprites; um widget desligado nunca lê True.
on_hover / on_hover_exit
função ou Nonelambda w: …
lê / escreve
Entrou / saiu do hover. fn(widget) ou fn(). Mesmo que @hover / @hover_exit.
group=
str"opts"
construtor
Widgets são Entity. group["opts"].visible / .destroy() vale para widgets e Text HUD juntos.
destroy()
método
—
Tira do jogo. O último item vivo da coluna atual devolve o cursor ao topo dessa coluna.
.box
Boxslider.box.top_right
lê
Cópia do retângulo do widget na janela. Para colocar algo ao lado: ui.stack_origin(slider.box.top_right + (16, 0)).
Se o Attribute do widget estiver ligado a uma fórmula (st["volume"] @= …), quem manda é a fórmula: arrastar ou clicar não muda nada. Mudar ui.scale depois de criar os widgets não rearranja a coluna. Ver Coluna.
Coluna
Sem top_left=, widgets e Text de HUD se empilham numa coluna, de cima para baixo, a partir do canto superior esquerdo da janela. ui.stack_origin / stack_from / stack_reset mudam onde a coluna continua; ui.gap() abre um espaço; ui.stack_column() começa outra coluna ao lado. A posição é calculada na criação de cada item.
Layout na construção. O próximo item criado sem top_left= usa a coluna; um top_left= explícito ainda avança o cursor.
Método
Assinatura
Retorna
Significado
ui.stack_reset()
Sem parâmetros
—
O próximo item criado sem top_left= volta à origem padrão: (20, height − 40), com o height do init. Zera ui.gap pendente.
ui.stack_origin(x, y)
Parâmetros: x, y (números) ou um ponto
—
Canto superior esquerdo visual do próximo item criado sem top_left= (como move_to: (x, y) ou um ponto, ex. slider.box.top_right + (16, 0)). Sem vão antes. Começa outra coluna neste ponto. Zera ui.gap pendente.
ui.stack_from(item)
Parâmetros: item (widget ou Text)
—
Continua abaixo da caixa do item. Aplica o vão do tema. Zera ui.gap pendente.
Text(..., center=) e stack=False não ocupam a coluna (FPS / SCENA WARNING / Menu). Destruir o último item vivo da coluna atual devolve o cursor ao topo. Um destroy parcial não. ui.scale depois de construir não rearranja.
Button
Botão de interface com uma legenda: clicar chama a função on_click — no soltar, com o ponteiro ainda em cima; soltar fora do botão cancela o clique. Para ligar / desligar uma opção, use CheckBox; para um botão feito da sua arte, SpriteButton.
A função pode não pedir nada, ou receber o próprio botão — ela o recebe quando pede um parâmetro sem valor padrão. Por isso a mesma função serve um botão e um Shape (@click(botao, shape)), e lambda f=f: … ou salvar(depois=None) não recebem o botão. Opcional: ligado a um Attribute, o botão grava value_on enquanto está apertado e value_off ao soltar — um "segure para atirar".
st.firing = False
Button(label="Reset", on_click=reset)
Button(st["firing"], label="Fire") # st.firing é True enquanto o botão está pressionado
Opcional (omitida: sem célula; None levanta). Segue o botão: value_on enquanto pressionado, value_off ao soltar (não é toggle — para liga/desliga use CheckBox). Destruído com o botão do mouse segurado (ou change_act no meio), também grava value_off, sem clique: st.firing não fica preso em True. Ex.: Button(st["firing"], …).
label
nome=
str"Button"
Texto escrito no botão.
on_click
nome=
função ou None
Callback: fn() ou fn(botao). Ex.: on_click=reset. Também botao.on_click = fn ou @click(botao). Só Button tem on_click.
value_on / value_off
nome=
qualquer ou None
Valores escritos no cell ao pressionar / soltar. Padrão True / False.
width
nome=
número260
Largura do botão, em unidades de design. Omitida: a do tema (260 × ui.scale).
height
nome=
número28
Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enabled
nome=
boolTrue
False: desenhado esmaecido e ignora cliques e teclas (o clique não passa para o jogo; on_hover ainda dispara). Também w.enabled = False. Esconder é visible = False.
SpriteButton
Botão de jogo feito da sua arte (PNG / JPG / Sheet). Já vem com o comportamento: cresce e clareia no hover, encolhe e escurece no aperto, clica ao soltar em cima, fica cinza quando desligado. É um Sprite (camada ui por padrão): play, .glow, fade_out… funcionam. Para botões de ferramenta (ajustes, depuração), use Button.
st.moedas = 0
jogar = SpriteButton("botao.png", center=(400, 300), label="Jogar",
on_click=comecar, click_sound="clique.wav")
loja = SpriteButton("botao.png", center=(400, 200), label="Loja", breathe=True)
loja["enabled"] @= lambda: st.moedas >= 10 # cinza até ter 10 moedas
jogar.label_text.glow = True # o Text do rótulo: controle fino
# Arte por estado (kits de UI): troca a imagem em vez do efeito padrão
sair = SpriteButton("sair.png", hover="sair_hover.png", pressed="sair_down.png",
center=(400, 100), label="Sair", label_inset=(0, 0, 0, 6))
Mais os parâmetros do Sprite (width=, height=, size=, frame=, layer=, box=, …).
Parâmetro
Passar
Tipo (default)
Significado
source
1º
arquivo ou Sheet
A arte do botão. Uma Sheet com um clipe anima como num Sprite.
label
nome=
str, número, célula ou função
Texto centralizado sobre a arte. Encolhe para caber (só no desenho: font_size continua o seu). Uma função ou uma célula liga: label=lambda: f"Vidas {st.vidas}", label=st["vidas"].
Margem da caixa do rótulo dentro da arte — para borda ou "degrau" 3D embaixo. O rótulo centraliza e encaixa nessa caixa.
on_click
nome=
função ou None
Ao soltar em cima: fn() ou fn(botao). Também botao.on_click = fn ou @click(botao).
enabled
nome=
bool ou funçãoTrue
False: cinza e esmaecido, não clica, sem crescer. O aperto não passa para o que está embaixo (nem para mouse.press, como em qualquer SpriteButton: o aperto é do botão); on_hover ainda roda. Uma função liga: enabled=lambda: st.moedas >= 10 (ou botao["enabled"] @= …).
hover / pressed / disabled
nome=
arquivo, quadro ou clipeNone
Arte do estado. Com source arquivo: outro arquivo. Com uma Sheet: nome / índice de quadro ou nome de clipe dela. Substitui o efeito padrão daquele estado (sem crescer / escurecer / cinza por cima). A área de clique não muda.
hover_sound / click_sound
nome=
arquivo, Sound ou None
Som ao entrar o hover / a cada clique. O som do clique continua tocando se o clique chama change_act.
hover_scale / press_scale
nome=
número1.06 / 0.94
Tamanho desenhado no hover / no aperto. 1 desliga. Só no desenho: scale continua o seu.
breathe
nome=
bool ou númeroFalse
Pulsa devagar enquanto parado: True = ±5 %, ou a amplitude (0.03). Pausa no hover, no aperto e desligado.
breathe_time
nome=
segundos1.6
Duração de uma respiração.
Neste SpriteButton
Propriedade
Tipo
Leitura/Escrita
Significado
label
str ou None"Jogar"
lê / escreve
O texto mostrado. Escreva uma string ou número. Para ligar: botao.label_text["text"] @= lambda: ….
label_text
Text ou None
leitura
O Text do rótulo, para controle fino: font_size, set_color(Gradient(…)), glow. A pose dele é relativa ao centro da caixa do rótulo (label_text.move_to(0, 4) sobe 4). Anda, gira e some junto com o botão; não tem camada nem destroy() próprios.
label é o atalho de string (como no Button); label_text é o objeto. A área de clique é o contorno convexo da arte em repouso: cantos transparentes não clicam, buracos dentro da arte clicam, e o crescer/encolher do hover não mexe na área. O efeito é só desenho — botao.scale, width e o hitbox de colisão não mudam.
Slider
Barra com um puxador para escolher um número dentro de uma faixa. Ligada a um Attribute numérico: arrastar grava o valor, e mudar o valor move o puxador.
A faixa vem de min_value= / max_value= e o padrão é 0 a 100 — um volume de 0 a 1 precisa de max_value=1. slider.percentage lê a posição do puxador na faixa, de 0 a 1. A roda do mouse sobre o Slider também muda o valor: 1 por passo com only_int=True, 2% da faixa sem. O Slider não corrige o Attribute: um valor que já está fora da faixa (ou não inteiro com only_int=True) fica como está — o puxador para na ponta — até o arraste ou a roda gravarem um valor novo, sempre dentro da faixa.
Legenda à esquerda da barra. Omitida: o nome do campo (Slider(st["volume"]) mostra st.volume).
min_value / max_value
nome=
número0 / 100
Os dois extremos da barra. Iguais, ou mínimo maior que o máximo, levantam ValueError.
initial
nome=
número ou NoneNone
Valor de partida; gravado no Attribute. Sem ele e sem Attribute: o meio da faixa.
only_int
nome=
boolFalse
True: o puxador anda de inteiro em inteiro (vidas, nível).
show_value
nome=
boolTrue
Mostra o valor atual à direita da barra.
width
nome=
número260
Largura de tudo (legenda + barra + número), em unidades de design. Omitida: a do tema (260 × ui.scale).
enabled
nome=
boolTrue
False: desenhado esmaecido e ignora cliques e teclas (o clique não passa para o jogo; on_hover ainda dispara). Também w.enabled = False. Esconder é visible = False.
CheckBox
Caixa de marcar: liga e desliga um Attribute booleano (True / False). Clicar inverte o valor; gravar o valor marca ou desmarca a caixa. .checked lê o mesmo valor que .data.
Lado da caixa, em unidades de design. Omitido: o do tema (18 × ui.scale).
enabled
nome=
boolTrue
False: desenhado esmaecido e ignora cliques e teclas (o clique não passa para o jogo; on_hover ainda dispara). Também w.enabled = False. Esconder é visible = False.
TextInput
Caixa de texto de uma linha — nome do jogador, semente, um console. Ligada a um Attribute de texto ou de número: Enter, Tab ou clicar fora gravam o que foi digitado; Esc desfaz a edição. Enquanto a caixa tem o foco (o cursor piscando nela), as teclas de texto são dela, não do jogo.
st.player_name = "" # st.name é reservado (é o nome do State)
st.seed = ""
TextInput(st["player_name"], placeholder="Nome", label="Jogador", max_length=12)
TextInput(st["seed"], width=200, top_left=(20, 300))
def run(cmd): # console: Enter roda e continua digitando
print(cmd)
console.text = ""
console.focus()
console = TextInput(on_submit=run, width=400)
Célula de texto ou número. O texto é gravado nela no Enter, no Tab ou ao perder o foco. Célula bool levanta erro: use CheckBox. Ex.: TextInput(st["player_name"], …).
placeholder
nome=
str""
Dica cinza com a caixa vazia
label
nome=
str ou NoneNone
Legenda à esquerda. width inclui a legenda. Empilhados, Sliders e TextInputs com legenda dividem a mesma coluna de legendas.
max_length
nome=
int ou NoneNone
Máximo de caracteres ao digitar ou colar. Um valor maior vindo do código aparece inteiro.
on_submit
nome=
função ou None
Roda no Enter, depois de gravar o texto e de a caixa perder o foco: fn() ou fn(valor) (o valor já convertido). Roda mesmo sem mudança no texto; não roda no clique fora, no Esc, nem quando o número não converte. Também caixa.on_submit = fn. Para continuar digitando: caixa.focus() dentro da função.
initial
nome=
str ou númeroNone
Texto de partida; gravado no Attribute.
width
nome=
número260
Largura da caixa, legenda incluída, em unidades de design. Omitida: a do tema (260 × ui.scale).
height
nome=
número28
Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enabled
nome=
boolTrue
False: esmaecida, não recebe foco, Tab pula. Desligar uma caixa focada grava o texto.
.text
—
str
O texto que está na caixa agora — pode ainda não ter sido gravado no campo. Quando o campo muda, a caixa passa a mostrar o valor novo. Escrever muda o que aparece; Enter ou perder o foco grava como se fosse digitado.
.is_valid
—
boolTrue
Só leitura. False quando o último texto gravado não converteu para o tipo do Attribute (ex.: "abc" num cell int); o cell fica com o valor anterior, o texto digitado continua na caixa com fundo de inválido até você editar, apertar Esc ou o cell mudar.
.focus()
—
método
Dá o teclado à caixa, com o texto selecionado (tela de nome, console). Levanta erro se a caixa estiver escondida, desligada, destruída ou com um Menu modal aberto.
.focused
—
bool
Só leitura. True enquanto a caixa tem o teclado.
Teclas: setas, Home / End, Backspace / Del; Shift seleciona; Cmd (macOS) ou Ctrl + A / C / X / V seleciona tudo e usa a área de transferência; Option (macOS) ou Ctrl + setas / Backspace andam por palavra. Arrastar seleciona; clique duplo seleciona a palavra. Esc volta ao valor do cell sem gravar e solta o foco. Tab / Shift+Tab vão para o próximo / anterior TextInput (ordem de criação). Texto maior que a caixa rola.
Clicar e sair sem editar nunca grava: 0.125 continua 0.125. Enquanto focado, keyp e on_key_* não veem as teclas de texto e de edição, nem Cmd/Ctrl + A / C / X / V / Z / Y (Z e Y não fazem nada: a caixa não tem desfazer). Cmd/Ctrl + outra tecla, F1–F12 e Page Up / Down passam para o jogo, depois de a caixa gravar o texto, como um Enter, mas o foco fica e on_submit não roda.
DropDown
Caixa que abre uma lista ao clicar e deixa escolher uma opção. Ligada a um Attribute: a escolha é gravada nele — o próprio item da lista, não o texto mostrado. dropdown.selected_index lê a posição da opção atual (0 = a primeira).
A lista abre para baixo; perto da borda de baixo da janela, abre para cima. Ela não rola: uma lista longa demais passa da tela. Para uma pergunta que pausa o jogo, use Menu.
Célula da seleção. Ex.: DropDown(st["palette"], …).
options
nome=
lista["night", "day"]
As escolhas. O texto mostrado é str() de cada item. Sem options=: três opções de exemplo ("Option 1"…). Ex.: options=["night", "sunset", "day"].
initial
nome=
um item de options"laser"
Opção marcada no começo; gravada no Attribute. Fora da lista: a primeira.
width
nome=
número260
Largura da caixa fechada, em unidades de design. Omitida: a do tema (260 × ui.scale).
height
nome=
número28
Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enabled
nome=
boolTrue
False: desenhado esmaecido e ignora cliques e teclas (o clique não passa para o jogo; on_hover ainda dispara). Também w.enabled = False. Esconder é visible = False.
.text
—
str
O texto da opção atual, como aparece na caixa. Só para ler; a escolha mesmo está no Attribute.
Menu
Um Menu é uma pergunta no centro da janela — pausa, escolher nave, continuar ou sair. Aparece ao ser criado; um clique numa opção escolhe e fecha. Três jeitos de receber a escolha: um Attribute, a função on_choose ou await num mode async. Escolha com o mouse; fica fora da coluna de widgets.
options= é o único obrigatório. cell é opcional. center= e box= não vêm juntos.
Parâmetro
Passar
Tipo (default)
Significado
cell
1º
célula
Opcional. Célula que recebe a opção escolhida. Ex.: Menu(st["nave"], …). None levanta (seria st.nave ainda vazio, sem colchetes).
options
nome=
lista["Jogar", "Sair"]
Escolhas. O rótulo é str(). O valor escolhido é o item, não o texto.
on_choose
nome=
funçãoescolher
escolher(opcao). Roda em todo clique, inclusive a mesma opção de novo. Não roda em destroy().
title
nome=
str
Faixa no topo. Omitir = só as opções.
cols
nome=
int1
Quantas colunas de opções: 1 = lista vertical; len(options) = uma linha só; outro número = grade.
modal
nome=
boolTrue
Escurece o jogo. Só um modal por vez; o segundo levanta erro com o nome do aberto. False: sem véu; um novo substitui o aberto (o antigo fecha com None).
pause
nome=
boolFalse
True pausa o jogo (clock.paused) enquanto o menu está aberto; ao fechar, clock.paused volta ao que era antes. Vale com modal=False.
center
nome=
ponto ou função
Centro na janela (espaço da vista), valor ou lambda. Acompanha todo frame. O painel fica inteiro na tela. Não segue a câmera.
box
nome=
Box
Preenche a caixa. A fonte encolhe até um mínimo legível; abaixo disso corta e avisa.
font / font_size
nome=
str / número"pixel.ttf"
Família ou arquivo .ttf / .otf. Omitir font usa ui.font.
color
nome=
cor"white"
Cor dos rótulos. O destaque da opção sob o mouse sai dessa cor quando ela é dada.
background
nome=
cor, Gradient ou TextureGradient("red", "gold")
Fundo do painel. Gradient e Texture se ajustam ao painel como no preenchimento de uma forma: o degradê vai até a borda do painel, e o size= do Gradient é medido a partir do centro do painel. A faixa do título fica um pouco mais clara. Sem color, o destaque da opção sob o mouse sai do fundo (mean_color() do Gradient, color da Texture). Preenchimento animado (WaterFill …) ou valor que não é cor levanta erro na criação.
icons
nome=
lista de arquivos
Uma imagem por opção, acima do rótulo. O valor escolhido continua sendo a opção. Número de ícones diferente do número de opções levanta erro. Arquivo ausente: xadrez e aviso.
Neste Menu
Tema
Regra
Ao escolher
Fecha, devolve a pausa, escreve o Attribute, completa o await, destrói, depois chama on_choose.
await menu
A escolha, ou None se fechou sem escolher. Nunca Cancelled. Não precisa de await. Sair do mode destrói o menu.
destroy()
Fecha sem escolha. Quem espera recebe None. on_choose não roda. .choice é só leitura.
Teclas
Com modal, keyp / mouse.press leem falso. O clique que escolhe fica escondido até soltar o botão. Abra a pausa com keyp.once("ESC") — uma tecla segurada durante o menu não conta como toque novo ao fechar.
Onde
Sem center / box: tamanho do texto, centro da vista (não a coluna do HUD). center=lambda: heroi.center só acompanha o herói se a câmera não se move.
Não confundir
Parece
É
Não é
DropDown
Menu fecha ao escolher e bloqueia o jogo. DropDown fica na coluna e não pausa.
o mesmo widget
Button para cada opção
Menu é uma pergunta só. Vários botões ficam na tela.
um menu
ui.gap
ui.gap() abre um espaço extra entre o último item da coluna e o próximo. Cada chamada soma um espaço; nada é desenhado.
O x novo é a borda direita do item mais largo da coluna, mais o vão. O y novo é o topo visual desta coluna. Sem gap=, o vão é o do tema (12 em ui.scale=1).
Método
Assinatura
Retorna
Significado
ui.stack_column()
Parâmetros: gap= (número, opcional)
—
O próximo item criado sem top_left= começa outra coluna à direita, no mesmo topo.
gap=
número
—
Vão horizontal. Omita: mesmo vão do tema entre itens.
Largura da coluna
—
—
Até a borda direita do item mais largo que ocupou a coluna (Text HUD e widgets). Não o último item.
Coluna vazia
—
—
Avança a largura padrão do widget + vão. Duas chamadas seguidas pulam uma coluna vazia.
Aplica em
—
—
O próximo Text de HUD ou widget criado sem top_left=.
Não confundir
Parece
É
Não é
ui.stack_column() leva a um ponto fixo da tela
ui.stack_column() deriva x e y da coluna que acabou de construir: x à direita dela, y no topo dela. Zera ui.gap pendente (a folga vertical não vira vão entre colunas). Os rótulos dos Sliders da coluna nova se alinham entre si, não com os da coluna anterior.
Pequenas contas que aparecem em todo jogo: converter um valor de uma faixa para outra (remap), misturar dois valores (lerp), perseguir um alvo suavemente (chase) ou em velocidade constante (approach), dar folga a uma câmera (deadzone) e sortear sim ou não (chance). Contas com pontos e vetores: Geometria.
Remapear intervalo → remap
Misturar dois valores → lerp
Perseguir com atraso → chase
Perseguir a velocidade constante → approach
Folga de câmera → deadzone
Sim/não → chance
remap
Leva um valor de um intervalo para outro. Opcional: curva no meio (modifier=).
x = remap(hp, 0, 100, 8, 40)
# hp 0 → 8 · hp 100 → 40 · hp 50 → 24
x = remap(hp, 0, 100, 8, 40, modifier="ease_in")
# normaliza → curva → destino; sem modifier= é linear
Intervalo de origem. in_min == in_max devolve out_min.
out_min, out_max
4º, 5º
número (0, 1)0, 255
Intervalo de destino.
modifier=
nome=
nome ou função"ease_in"
Curva em [0,1]→[0,1] depois da normalização. Sem: linear. Mesmos nomes de ease=, mais spring · wobble (só aqui: começam e terminam em 0.5, ou seja, balançam em volta do meio do intervalo de destino). Ou um callable. Nome desconhecido → ValueError com a lista.
Pipeline: normaliza value em [0,1] com in_min…in_max → aplica modifier → escala para out_min…out_max.
Curvas como bounce / elastic / spring podem sair de [out_min, out_max] ou não ser monótonas. Para curva custom: modifier=lambda t: ….
lerp
Interpola entre dois valores do mesmo tipo. Número, tupla/lista plana de números, Vec2, Color, ou dois Point (devolve (x, y)). t não é limitado — eases tipo elastic passam de 0–1.
lerp(0, 100, 0.5) # 50
lerp((0, 0), (100, 40), 0.25) # (25.0, 10.0)
lerp(Color("red"), Color("blue"), 0.5) # mistura em OKLAB
lerp(ship.center, target.center, 0.5) # (x, y) — não cria um Point
Assinatura
lerp(a, b, t)
Parâmetros: a, b (mesmo tipo / forma); t (número). Retorna o mesmo tipo de a (ou (x, y) para Point).
Parâmetro
Passar
Tipo (default)
Significado
a, b
1º, 2º
mesmo tipoColor("red")
Extremos. Número · tupla/lista · Vec2 · Color · Point.
t
3º
número0.5
Quanto andar de a para b: 0 = a, 1 = b, 0.5 = o meio. Valores fora de 0–1 não são cortados (passam de a ou de b).
É a mesma conta que o tween usa para animar um campo. Tuplas e listas valem só se forem planas: números, não tuplas dentro de tuplas.
Tipos ou comprimentos incompatíveis levantam erro na hora. Não a meio da animação. lerp em dois Points devolve tupla, não um Point novo (Point é objeto do jogo).
chase
chase aproxima um valor de um alvo, um pouco a cada frame: anda depressa quando está longe e devagar quando está perto, sem passar do alvo — e no mesmo ritmo em qualquer taxa de frames. Bom para câmera que segue, barra de HP que desce suave. Aceita os mesmos tipos que lerp.
Parâmetros: current, target (mesmo tipo / forma); rate (número); dt (número, opcional). Retorna o mesmo tipo que lerp(current, target, t).
Parâmetro
Passar
Tipo (default)
Significado
current, target
1º, 2º
mesmo tipo(100, 80)
Extremos. Número · tupla · Vec2 · Color · Point (devolve (x, y)).
rate
3º
número6
Rapidez da aproximação, por segundo: a cada 1/rate segundo, a distância que falta cai para cerca de um terço. Com rate=6, em 0,5 s já percorreu ~95% do caminho. Maior = chega mais rápido.
dt
4º ou nome=
númeroclock.dt
Opcional. Omitido = passo do mundo (clock.dt). Passe dt= no on_update se a Entity tem time_scale, ou para um passo feito à mão. dt == 0 (jogo pausado) ou rate == 0 devolve current sem mudar nada; dt ou rate negativo → ValueError.
É lerp(current, target, 1 - exp(-rate * dt)). Chame você no update. Não registra, não corre depois, não disputa com tween. Receita de câmera: Câmera.
Não confundir
Parece
É
Não é
canvas.follow(hero) faz a câmera seguir
esse verbo não existe. Perseguir é chase + pan_to todo frame. pan_to(..., time=) (ou speed=) congela o destino no momento da chamada.
pan_to(..., time=) sozinho
chase anda sempre na mesma velocidade
não: freia perto do alvo e, em tese, nunca chega exato. Velocidade constante e chegada exata é approach.
approach
approach
approach (aproximar) anda de current até target sempre na mesma velocidade, speed unidades por segundo, e chega exato: quando falta um passo ou menos, devolve o próprio alvo. Chamada a cada frame com um alvo que se move, persegue em velocidade constante. É a irmã de chase, que freia ao chegar.
Parâmetros: current, target (dois números ou dois pontos); speed (número); dt (número, opcional). Retorna número, Vec2 (se current é Vec2) ou (x, y).
Parâmetro
Passar
Tipo (default)
Significado
current, target
1º, 2º
número ou pontohero.center
De onde e para onde. Dois números, ou dois pontos: Point, Vec2, (x, y), mouse ou o .center de uma forma. Color → TypeError (use chase).
speed
3º
número120
Velocidade, em unidades por segundo. Cada chamada anda speed * dt. Negativo → ValueError.
dt
4º ou nome=
númeroclock.dt
Opcional. Omitido = passo do mundo (clock.dt, o mesmo dt que o update(dt) recebe). Passe dt= no on_update se a Entity tem time_scale. dt == 0 (jogo pausado) ou speed == 0 devolve current; dt negativo → ValueError.
Um ponto anda em linha reta até o alvo; na diagonal, na mesma velocidade que na horizontal. Nunca passa do alvo nem treme em volta dele, e a chegada é exata: o valor devolvido é igual ao alvo (dá para testar com ==, como pos == (400, 120)), e move_to(pos) põe o centro exatamente ali. Compare o valor devolvido, não elevador.center == (400, 120): um Point nunca é igual a uma tupla. Chame você no update. Não registra, não corre depois, não disputa com tween. Combina com deadzone para um pet na coleira.
Passe o ponto inteiro, não x e y separados.approach(bola.center.x, mouse.x, 300) e outra chamada para o y fazem a diagonal andar mais rápido (√2 vezes) e chegam num eixo antes do outro. Com chase separar os eixos dá no mesmo; com approach, não. Ângulos também não dão a volta curta: de 350 para 10, approach passa por 180.
Não confundir
Parece
É
Não é
approach e chase fazem o mesmo
chase freia perto do alvo e não chega exato; approach anda em velocidade constante e chega exato.
duas grafias da mesma conta
move_to(alvo, time=2) persegue
não: time= (e também speed=) lê o destino uma vez, na chamada. Para seguir um alvo que se move, approach no update, todo frame.
move_to(..., time=)
deadzone
deadzone deixa o alvo se mexer livre dentro de uma caixa de tamanho size em volta de current; só quando ele sai da caixa o resultado anda, o bastante para trazê-lo de volta à borda. Para uma câmera que não treme a cada passo do herói.
look = (canvas.pan_x, canvas.pan_y)
tx, ty = deadzone(look, hero.center, size=(160, 90))
if hero.grounded:
ty = canvas.pan_y # trava Y no chão — regra de jogo de plataforma, escrita por você
canvas.pan_to(chase(look, (tx, ty), rate=6.0))
Largura e altura totais da caixa, em unidades de design, centrada em current (size=160 → 80 para cada lado). Número = quadrado (160 ≡ (160, 160)). float("inf") num eixo trava esse eixo (o retorno fica no current).
Combina com chase para câmera com folga. Também serve fora da câmera (mira, pet na coleira). size inválido → TypeError.
Dentro da caixa o resultado é current. Não oscila em repouso. Regras como “não seguir o y enquanto o herói está no chão” (comum em jogo de plataforma) ficam num if seu, não dentro de deadzone.
chance
chance(p) sorteia: devolve True com probabilidade p, de 0 a 1. chance(0.30) é verdade em 30% das vezes.
if chance(0.30):
drop_loot()
Assinatura
chance(p)
Parâmetros: p (número 0 a 1). Retorna True/False.
Tema
Regra
p
0 a 1. 0 nunca, 1 sempre. Fora disso → erro.
Uso
Uma rolagem. Não é intervalo — para cadência use ready.
chance no update rola todo frame. 0.30 a 60 fps ≈ 18 vezes por segundo. Cadência: ready. Número em intervalo: from random import uniform.
Ferramentas de depuração em execução: métodos de overlay e atalhos de teclado. Não são API de lógica de jogo — nada aqui participa de @collide, resolve ou do resultado do jogo.
Desenha hitbox inscrito (dourado = resolve; ciano = parte add_hitbox; verde = só detecção). O retângulo de quem anda (dourado) e as partes (ciano) aparecem juntos. Ctrl+Shift+7. Consultas: Consultas. Quem entra no sistema: O que colide.
show_tracks(enabled=True)
Parâmetros: enabled (ligado, True/False)
—
Overlay magenta das viagens ativas de move_along e dos Tracks parados numa camada. Não liga Track.visible. Também collision.show_tracks (mesmo método, outro nome). Ctrl+Shift+7 (o zoom de inspeção do Ctrl+Shift+8 não mostra as trilhas). Desligado: zero trabalho extra no frame. Ver Track.
Atalhos de teclado
Atalhos que funcionam em qualquer jogo Scena enquanto ele roda: todos são Ctrl+Shift + um dígito ou letra, no Mac e no PC (as teclas F1–F7 costumam ser do sistema).
Atalho
Faz
Ctrl+Shift+1
Liga / desliga o HUD de sistema (FPS, objs, mouse xy no mundo). Começa desligado.
Ctrl+Shift+2
Liga / desliga o ajuste automático de qualidade (FX Auto): quando o jogo não cabe no frame, o Scena baixa a qualidade dos efeitos (high → medium → low) e, por último, a taxa (60 → 30 fps). Começa ligado, em high a 60 fps.
Overlay: colisores + quadro de design + cruz da origem + trilhas move_along. Com @leave_playfield, também o playfield. O mesmo desenho: collision.show_colliders(enabled=True) / show_tracks(enabled=True).
Ctrl+Shift+8
Zoom de inspeção: 100% → 50% → 25%. Não muda canvas.zoom. Quadro de design / playfield; sem trilhas.
Ctrl+Shift+E
Fecha o banner SCENA WARNING (placa acima do FPS; ex.: som que falta). Erro no seu código não vira banner: sobe com o traceback.
Ctrl+Shift+P
Tempo: roda → pausa → ¼ → roda. Devolve o clock.scale do script ao completar o ciclo.
Ctrl+Shift+F
Tela cheia: cobre o monitor (sem borda, sem taskbar). Preserva o aspecto com faixas pretas. Se a janela estava maximizada, apertar F de novo sai da tela cheia e volta a maximizada. O mesmo que canvas.display = "fullscreen" (o atalho é toggle).
Ctrl+Shift+M
Maximiza na área de trabalho (taskbar fica). Conteúdo com faixas pretas. O botão Maximizar da barra de título é o mesmo modo. De fullscreen, M sai de F e maximiza. O mesmo que canvas.display = "maximized" (o atalho é toggle).
Ctrl+Shift+Z
Sai.
Ctrl+Shift+5 / 6 não fazem nada. As alças de arraste de um Shape aparecem com shape.show_handles() — é por objeto, não há atalho global. M e F são toggles; no script use canvas.display = … (absoluto). Não existe toggle_fullscreen().