Guia de referência

Começar

Como um script Scena começa — o import, a janela, o loop do jogo — e as regras e palavras que o resto do Guia usa.

Script mínimo / init → Script mínimo
Exceção → causa → correção → Catálogo de erros
Regras → Regras
Unidades, Entity, ponto, frame… → Vocabulário
Arquivo PNG / SVG → Arquivos de imagem
st / canvas / clock… → Objetos globais únicos
setup / update / on_key → Callbacks do script

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.

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.

from scena import *
init(width=1200, height=800)

SkyGradient("night")
Stars()
Vignette()
t = EquilateralTriangle(center=(400, 300), radius=50, color="cyan")

def update(dt):
    t.rotate_by(30 * dt)

run()
init

init(width=2000, height=1200, title="Scena", window_scale=0.8, display="windowed", ui_scale=, ui_font=)

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âmetroPassarTipo (default)Significado
widthnome=número2000Largura do espaço de design (canvas.width). Não é a janela OS em pixels.
heightnome=número1200Altura do espaço de design (canvas.height). y-up, origem no canto inferior esquerdo.
titlenome=str"Scena"Título da janela OS.
window_scalenome=número0.8Fraçã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.
displaynome=str"windowed"Atalho para canvas.display. Só "windowed" · "maximized" · "fullscreen". A janela windowed é criada primeiro (escala, centro); depois entra no modo.
ui_scalenome=númeroatualEscreve ui.scale. Omitir deixa o valor atual (padrão de import: 1).
ui_fontnome=stratualEscreve 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.

  1. Construtor coloca. Circle() já aparece. Rain(), Vignette(), Slider() também.
  2. 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.
  3. = copia agora. @= liga ao vivo. c.radius = 10 · c["radius"] @= lambda: player.hp * 10 — ver Reativo.
  4. Propriedade é estado. Método é ação. c.visible = False · c.move_by(20, 0)
  5. 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.

TermoO 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.
frameUma 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).
EntityTodo 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().
GraphicUma 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.
poseOnde 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.
ShapeUm Graphic geométrico: Circle, Rect, Square, Line, Ellipse, Polygon, RegularPolygon, EquilateralTriangle, Point, Track.
pontoOnde 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 / AttributeUm 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.
HandleO 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.
modeO 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 × viajarColocar 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 apertokeyp("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.
canalNos Transientes, a vaga de um tipo de efeito: um flash novo substitui o flash em curso, mas convive com um pop.
HUDTextos 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.

  1. Caminho absoluto. Só aquele arquivo. Existe ou falta.
  2. Pasta do script. Ao lado do .py que você rodou.
  3. media/ ao lado do script. Sprite("nave.png") acha media/nave.png.
  4. 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.

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
stStatelê / escreveOs seus valores do jogo (placar, vidas, fase), acompanhados pelo Scena. Guardar valores.
canvasCanvaslê / escreveA tela: câmera, fundo, efeitos de tela inteira, camadas. Mundo.
clockClocklê / escreveO tempo do jogo: dt, time, pausa e câmera lenta. clock.
keypKeyPressedlêTeclado: que tecla está segurada, qual acabou de ser apertada. Teclado.
mouseMouselêPonteiro: posição no mundo e botões. Mouse.
audiomixerlê / escreveO som como um todo: volume geral, mudo, medidores. audio.
uipilhalê / escreveTamanho e fonte dos widgets e a coluna onde eles se empilham. UI.
groupgruposlê / escreveAcha objetos pela tag: group["inimigo"]. Grupos.
collisionconsultaslê / escrevePerguntas 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.

def setup(canvas):
    canvas.fx_quality = "high"

def update(dt, canvas):
    if keyp("RIGHT"):
        player.move_by(200 * dt, 0)

def on_key_down(key):
    if key == "ESC":
        canvas.reset_view()

def on_mouse_scroll(xoffset, yoffset):
    factor = 1.1 if yoffset > 0 else 1 / 1.1
    canvas.zoom_by(factor, about=(mouse.x, mouse.y))

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.

Onde mais procurar — Reativo · Desenhar · Frame · Callbacks do script

Reativo

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.hp liga: 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.

Ligar um campo / substituir conteúdo (<<) → Ligar
O que pode ficar à direita → O que pode ser ligado
Lambda num for → Lambda
Lambda no construtor → Fórmulas no construtor
Placar do app (st) / HP na Entity → Guardar valores
Quem mais controla um campo → Drivers persistentes

Ligar

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çãoSignificado
=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.
ConstrutorArgumento cru = cópia. Callable = ligação. Detalhe: Fórmulas no construtor.
ship.campo @=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.
WidgetO 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 acompanhaTypeError — 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 << BCopia 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))

A regra geral, no dia a dia:

objeto["campo"] @= lambda: obj1.campo_a + obj2.campo_b

Exemplos concretos:

c["radius"] @= lambda: ship.hp * 0.1
hud["text"] @= lambda: f"Wave {st.wave}"
cursor["center"] @= mouse
twin["angle"] @= (lambda h=host: h.angle)

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 comumVale: 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órmulaO 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 (@property def 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:

c["radius"] @= lambda: player.hp * 10
cursor["center"] @= lambda: mouse

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:

# errado — SyntaxError (lambda não aceita atribuição)
# after(1.0, lambda: player.active = True)

after(1.0, lambda: setattr(player, "active", True))
ship.dissolve_out(0.4, on_done=lambda: setattr(ship, "active", False))

Se quiser a grafia player.active = True

def wake():
    player.active = True
after(1.0, wake)

Fórmulas nos construtores

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.

ConstrutorParâmetros que ligamNão ligam (cópia / recusam)
Shape — Circlecenter, radius, perimeter_point, p1, p2, angle, spreadcolor, layer, group, box, use_center
Shape — Rect / Square / Ellipsegeometria do modelo: center, width, height, angle, top_left, p1, p2, vec, length, …color, corner_radius (não liga), layer / group, box, anchor
Shape — Linecenter, angle, length, p1, p2, vec, dx, dy, distcolor, layer / group; relações (parallel_to=, circle=…) já observam sozinhas — não são lambda do autor
Shape — Polygoncada vértice em points= (pode misturar: uns lambda, uns copiados)quantidade de vértices (trava na construção)
Shape — RegularPolygon / EquilateralTrianglecenter, radius, side, vertex, anglen= (lados: inteiro, trava na construção)
Pointo ponto inteiro: Point(lambda: …); um eixo: Point(lambda: mouse.x, 0); par: Point((lambda: src.x, 10))Point(outro_ponto) copia (não acompanha)
Vec2cada 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 / Svgsó 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"] @=
Textcenter=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: …
Trackcenter, radius, begin, end; cada ponto em points=stroke, smooth, floating
SpatialFx (Fire, Rain, …)center, radius; no fogo dirigido também width, length, anglecolor, estilo, layer / group
Boxos 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 playercopia 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.

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
st.score = 0valorst.score = 0lê / escreveCria (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"])leituraA 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ê / escreveLiga 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 / boolleituraLista os campos; diz se um já foi criado.
.namestr"st"leituraRó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çãoSignificado
st.keyLê 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.
Campos da Entity: criar, ler e ligar
SituaçãoSignificado
ship.hp = 100Cria (ou grava) o campo. Já é fonte reativa.
ship["hp"] @= lambda: …Liga. Cria o campo se faltar (default 0).
ship.hp @= …TypeError do Python — use colchetes. Ver Regras.
print(ship.hpt)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 = 10Graphic 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.

DriverO que reivindicaSe você @= por cimaComo soltar
@= (fórmula)o próprio campo ligado—.unlink() / unlink_all()
draggable / on_drag_topartes que o move_to do arraste escreve (ex. center)LinkedAttributeError na horadraggable = False (ou limpar os callbacks)
wheel_rotatepartes do rotate_by (inclui angle)LinkedAttributeErrorwheel_rotate = False
tween / move_to(time=) / move_alongo alvo pelo tempo do HandleLinkedAttributeError 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 ladoLinkedAttributeError no @= ou no próprio resolveunresolve(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á erroOK — quem lê o center não disputa com o arraste; só quem escreve nele disputaligar para dentro de um center arrastável
radius ligado + draggableOK — o arraste não escreve radiusbloquear todo o Circle
move_to / confine no seu on_updatechamada pontual do autor — não é driver persistenteo mesmo que draggable / tween

Onde mais procurar — Mover (force=) · Colisão (resolve) · Handle

Onde mais procurar — Frame · Reagir · Mover · UI

Desenhar

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.

Circle / Rect / Line… → Shapes
Imagem / atlas → Sprite
Curva → Track
Texto → Text
Fogo / chuva no mundo → SpatialFx
Objeto sem desenho → Entity

Todo objeto na tela

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âmetroPassarTipo (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 NoneTag, 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.centerponto(400, 300)lê / escreveO 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).
visibleboolFalselê / escreveEsconde o desenho. Continua na camada.
activeboolFalselê / escreveEstaciona: 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_aliveboolTrueleituraTrue 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.
.layerstr, int ou None"default"lê / escreveCamada atual. Escreva para mudar (número cria na hora).
time_scalenúmero1lê / escreveMultiplicador extra neste Entity (timers, tweens, wait, on_update). Padrão 1. Ver clock.

Métodos

MétodoAssinaturaRetornaSignificado
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âmetrosselfCancela 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 / taggedParâmetros: names (nomes); tagged usa name (nome)tag/untag não retornam valor; tagged retorna True/Falsetag 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_updateParâmetros: e (objeto), dt (número)—fn(e, dt) a cada frame, inclusive durante await. Ver Frame.
on_destroyParâ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 copiadoCó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 = removervisible=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 = FalseAttributeError. 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=.

c = Circle(center=(400, 300), radius=50, color="cyan")
c.set_color("tomato")
c.visual = Neon()
c.show_handles()
Construtor (todo Shape)

Todo Shape (Circle, Rect, Line, Polygon…) aceita estes, além de layer= e group= (Todo objeto na tela):

ParâmetroPassarTipo (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.5Espessura 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.

set_defaults(visual=Neon())
set_defaults("coral", line_width=2.0, dash="dot")

set_defaults(visual=None, line_width=, dash=, line_cap=, line_join=, line=, fill=, blend=, line_filter=, fill_filter=)

ParâmetroPassarTipo (default)Significado
visual1ºcor, Gradient, Texture ou Visual"coral"A aparência básica: uma cor ("coral"), um Gradient / Texture ou um visual (Neon()).
line_width · dash · line_cap · line_joinnome=número / str2.0 / "dot"O contorno: espessura, tracejado, pontas e cantos — os mesmos valores das propriedades de Neste Shape.
fill · linenome=cor, Gradient ou TextureMaterial do interior / do contorno, como .fill / .line.
blendnome=str"add"Como misturam com o que já está desenhado. Ver Blend.
fill_filter · line_filternome=filtroBlur(sigma=2)Filtro de pixels no interior / no contorno. Ver Filtros.
Neste Shape

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.colorcor"cyan"lê / escreveCor 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.
.visualVisual ou NoneNeon()lê / escreveVisual principal (um por objeto). None tira o visual; complementos ficam.
.glow / .shadow / .tailbool ou cor ou GlowTruelê / escreveComplementos. Sprite também .hue e .gray. Atalho: True / cor / número / objeto. Ler devolve o objeto de ajuste (Glow, Shadow…) ou None.
.fill / .linecor ou GradientGradient("sunset")lê / escreveO 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.
.blendstr"add"lê / escreveComo 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_widthnúmero1.5lê / escreveEspessura 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.
.dashstr"dash"lê / escrevePadrão do traço. Padrão "solid". "solid" · "dash" · "dot" · "dot-dash". Com um visual posto, como .line_width.
.line_capstr"round"lê / escreveExtremidade do traço. Padrão "round". "round" · "square" · "project".
.line_joinstr"miter"lê / escreveCanto do traço. Padrão "miter". "miter" · "bevel" · "round".
.is_collapsedboolFalseleituraSó 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étodoAssinaturaRetornaSignificado
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 copiadoCó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()
c.move_by(200 * dt, 0)
c.move_by(angle=90, distance=40)
c.move_by(distance=120 * dt)
c.rotate_by(30 * dt)
Construtor

Circle(center=, radius=, color=, layer=, group=)

Nome com =: passe pelo nome. (Circle não tem argumento só na ordem.)

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designPonto (x, y). Ex.: center=(400, 300).
radiusnome=númerotamanho médioRaio em unidades de design. Ex.: radius=50.
boxnome=BoxcellUma região (Box): o círculo fica dentro dela, com raio = metade do lado menor. Com radius=, só centra nela. Ver box=.
colornome=corpadrão do jogoPreenchimento + traço de contraste. Ex.: color="cyan".
layernome=str ou None"default"Camada de desenho
groupnome=str ou lista ou NoneTag, ou lista de tags
Modelos de construção
ChamadaSignificado
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_centerPadrão True: pizza. False: segmento (corda + arco). O clique e a colisão seguem essa área; spread=360 continua o disco.
box=Inscreve. Ver Colocar
Neste Circle

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.center .radiusponto ou número(400, 300)lê / escreveGeometria. Leia e escreva.
.angle .spreadnúmero0 / 360lê / escreveArco, 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 .p2ponto(0, 0)lê / escrevePontas 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.
.visualVisual ou NoneNeon()lê / escreveVisual principal. Neon(), Halo(), … (não Glow — use .glow).
.glow / .shadow / .tailbool ou cor ou GlowTruelê / escreveComplementos. Ver Complementos.

Métodos

MétodoAssinaturaRetornaSignificado
set_color(…)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)PointPonto 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 imagemImagem não entra no Circle. Use Sprite, ou c.fill = Texture("…").—
Arco do Circle é só desenhoClique 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.

Rect()
Rect(center=(400, 300), width=120, height=60, color="navy")
Rect(top_left=(80, 200), width=160, height=80)
Rect(center=(400, 200), width=140, height=70, corner_radius=16)
Rect(p1=(100, 300), p2=(300, 300), height=40, anchor="square")
Construtor

Rect(center=, width=, height=, angle=, corner_radius=, color=, layer=, group=)

Nome com =: passe pelo nome.

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designPonto
width / heightnome=númerotamanho médioLados
boxnome=BoxcellUma região (Box): o retângulo ocupa a região inteira (lugar, tamanho e ângulo dela). Ver box=.
anglenome=número0Graus, anti-horário
corner_radiusnome=número0Cantos arredondados (unidades de design), só um número. O clique segue o preenchimento; a colisão continua o retângulo.
colornome=corpadrão do jogoPreenchimento
Modelos de construção
ChamadaSignificado
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=))
p1=, p2=, height=, anchor=Eixo p1→p2 + espessura; ver âncoras abaixo
box=Preenche. Ver Colocar
Neste Rect
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.center .width .height .angleponto ou número(400, 300)lê / escreveGeometria
.corner_radiusnúmero8lê / escreveCantos 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_rightponto(80, 200)lê / escreveCantos, 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.
.top_center .bottom_center .left_center .right_centerponto(400, 240)leituraMeios das arestas (anchor="cap" usa left_center/right_center)
.left_square_center .right_square_centerponto(120, 300)leituraCentros dos quadrados de lado height nas pontas (anchor="square")
.boxBoxpanel.boxleituraUma 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ãoO 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 caixaangle= 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 caixaEspelha: 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.

Square()
Square(center=(400, 300), length=80, color="gold")
Square(top_left=(100, 200), length=60)
Square(center=(400, 200), length=70, corner_radius=12)
Construtor

Square(center=, length=, angle=, corner_radius=, color=, layer=, group=)

Nome com =: passe pelo nome.

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designPonto
lengthnome=númerotamanho médioComprimento do lado, em unidades de design.
top_leftnome=ponto(100, 200)Põe o quadrado pelo canto de cima à esquerda, no lugar de center=.
boxnome=BoxcellInscreve (lado menor). Com length=: só centra. box=.
anglenome=número0Graus
corner_radiusnome=número0Cantos arredondados. O clique segue o preenchimento; a colisão continua o quadrado. Herdado do Rect.
Neste Square
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.lengthnúmero80lê / escreveLado. Escreva aqui.
.width / .heightnúmero120leituraEspelhos. Escrever levanta erro.
.corner_radiusnúmero8lê / escreveCantos 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.

Line()
Line(center=(240, 160), length=200, angle=30, color="cyan")
Line(p1=(80, 80), p2=(400, 240), color="cyan", line_width=5)
ln.visual = Laser()
Construtor

Line(center=, length=, angle=, color=, line_width=, layer=, group=)

Nome com =: passe pelo nome. Ou o caminho com pontas: p1=, p2=.

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designMeio do segmento
length / anglenome=númerotamanho médio / 0Par: comprimento e graus
p1 / p2nome=pontoPontas (caminho completo)
colornome=corpadrão do jogoTraço
line_widthnome=número1.5Espessura do traço. Omissão mantém 1.5.
Modelos de construção
ChamadaSignificado
Line() / center=Horizontal no centro, tamanho médio
length=, angle=Tamanho + orientação no centro do design
center=, length=, angle=Centro, comprimento e rumo
p1=, p2=Pontas (ambas obrigatórias)
p1=, angle=, length=Polar a partir de uma ponta
p1=, vec= / p1=, dx=, dy=Vetor
p1=, parallel_to=, length=Paralela a outra Line
dist=, parallel_to=Cópia paralela afastada dist (positivo = à esquerda do sentido p1 → p2)
p1=, perpendicular_to=, length=Perpendicular a outra Line, saindo de p1 (à esquerda do sentido dela)
circle=, at_angle=, length=Tangente: at_angle é a posição no círculo; o rumo angle é at_angle + 90
circle=, from_point=, side=Tangente saindo de um ponto de fora até tocar o círculo. side="left" / "right" escolhe qual das duas
Neste Line
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.p1 .p2 .length .angleponto ou número(0, 0)lê / escreveGeometria
.centerponto(400, 300)lê / escreveMeio do segmento

Não confundir

PareceÉNão é
Line(width=…)Comprimento é length=, não width=. Espessura do traço é line_width.—
Visual de área (Neon, Gel…) numa LineVisual de área no Line some no traço. Prefira Laser / set_color.—

Uma elipse (oval), com largura, altura e giro. Pode nascer inscrita num Rect e acompanhá-lo: from_rect=.

Ellipse()
Ellipse(center=(400, 300), width=140, height=70)
Ellipse(from_rect=box, padding=8)
Construtor

Ellipse(center=, width=, height=, angle=, color=, layer=, group=)

Nome com =: passe pelo nome.

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designPonto
width / heightnome=númerotamanho médioLargura e altura totais (os eixos inteiros, não a metade), antes do giro.
boxnome=BoxcellPreenche a região uma vez. Não é from_rect= (ligação ao vivo). box=.
from_rect / paddingnome=Rect / número— / 0A 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.
anglenome=número0Graus
Modelos de construção
ChamadaSignificado
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=, padding=Pose ligada a um Rect
box=Preenche um Box (uma vez). Ver Colocar

Não confundir

PareceÉNão é
Ellipse(from_rect=…) copia o Rectfrom_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.

Polygon()
Polygon(points=[(100, 80), (220, 90), (200, 180), (90, 160)], color="coral")
Polygon(points=[(0, 0), (80, 0), (40, 60)], visual=Neon())
Construtor

Polygon(points=, color=, visual=, layer=, group=)

Nome com =: passe pelo nome. Use color= ou visual=, não os dois.

ParâmetroPassarTipo (default)Significado
pointsnome=listaquadrilátero médioLista de (x, y), um por vértice. O número de vértices fica fixo depois de criado. Ex.: points=[(0, 0), (40, 10), (20, 50)].
colornome=corpadrão do jogoPreenchimento + traço de contraste. Ex.: color="coral".
visualnome=VisualNeon()Visual no construtor (alternativa a color=).
layernome=str ou None"default"Camada de desenho
groupnome=str ou lista ou NoneTag, ou lista de tags
Neste Polygon
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.pointslista[(0, 0), (40, 10), (20, 50)]leituraVértices (um Point cada; a quantidade é fixa). Ligar um vértice: tri.points[1] @= lambda: a.center. Mover um: tri.points[1].move_to(x, y). tri.points[1] = … e tri["points"] @= … levantam erro com essas formas.
.centerponto(400, 300)leituraO centro do polígono: a média dos vértices. Só leitura; para mover, use move_to / move_by.

Não confundir

PareceÉNão é
Mudar o número de vértices depoisO número de vértices não muda depois. Para N regular, use RegularPolygon.—

Um polígono de lados iguais — pentágono, hexágono… n= é o número de lados (padrão 6) e não muda depois.

RegularPolygon()
RegularPolygon(n=5, center=(400, 300), radius=50, color="cyan")
Construtor

RegularPolygon(n=, center=, radius=, angle=, color=, layer=, group=)

Nome com =: passe pelo nome.

ParâmetroPassarTipo (default)Significado
nnome=número inteiro6Número de lados, ≥ 3. Fixo depois de criado.
centernome=pontocentro do designPonto
radiusnome=númerotamanho médioCircunraio (centro → vértice)
anglenome=número0Rumo, em graus, do centro até o primeiro vértice (0 = um vértice apontando para a direita).
boxnome=BoxcellInscreve (circunraio = metade do lado menor). box=.
Modelos de construção
ChamadaSignificado
n=, center=, radius=Circunraio
n=, center=, side=Comprimento da aresta
n=, vertex=, side=Põe o polígono pelo primeiro vértice (vertex=), em vez do centro, com lados de tamanho side=.
box=Inscreve. Ver Colocar

Não confundir

PareceÉNão é
flip_x renumera os vérticesEspelha 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.

EquilateralTriangle()
EquilateralTriangle(center=(400, 300), radius=50, color="cyan")
t.rotate_by(30 * dt)
Construtor

EquilateralTriangle(center=, radius=, angle=, color=, layer=, group=)

Nome com =: passe pelo nome.

ParâmetroPassarTipo (default)Significado
centernome=pontocentro do designPonto
radiusnome=númerotamanho médioDistância do centro a cada vértice (circunraio).
sidenome=número60Ou o tamanho pelo lado, no lugar de radius=.
vertexnome=ponto(0, 0)Com side=: põe o triângulo por um vértice (o ápice) em vez do centro.
boxnome=BoxcellInscreve. box=.
anglenome=número0Para 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âmetroPassarTipo (default)Significado
x1ºnúmero0Posição. Só x → y=0.
y2ºnúmero0Posição
ponto1º (só)pontobox.centerCópia: Point, Vec2, (x, y), Graphic (centro), mouse. Callable → ligação do Point inteiro (como center=).
colornome=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.
visiblenome=boolFalseMostra / esconde. True mostra o Point (na camada "top" se não houver layer=). Vale mais que o efeito de color=.
layernome=str ou NoneNoneCamada. Omitido = fora da lista de desenho até mostrar.
groupnome=str ou lista ou NoneTag, ou lista de tags
Neste Point

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.x .ynúmero400lê / escreveCoordenadas
x, y = pdois númerosx, y = ship.centerleituraDesempacota 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).
.centerpontopleituraO 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.
.visibleboolFalselê / escreveGuia na tela. Sem handle.
.handleboolTruelê / escreveTrue: o Point aparece e pode ser arrastado com o mouse. Num Point ligado com @=, levanta LinkedAttributeError.

Métodos

MétodoAssinaturaRetornaSignificado
.distance(other)Parâmetros: other (ponto)númeroDistância euclidiana. Também distance(p1, p2).
.heading(other)Parâmetros: other (ponto)númeroRumo até other, em graus (0 = +x, −180…180). Também heading(p1, p2).
set_color(…)Parâmetros: cores (nome, Color ou RGB; posicionais)selfRecolorir 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)PointEfê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âmetroPassarTipo (default)Significado
x, y / pontoposiçãonúmero ou pontoIgual a Point (cópia; sem fórmula).
colornome=corvermelho (auto)Cor do marcador. Omitido: a cor automática dos marcadores arrastáveis.
groupnome=str ou listaTag(s)

Não confundir

PareceÉNão é
Point(canvas.game_view.center) acompanha a vistaCopia 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 arrastadocolor= 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 naveDois 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")
TipoPapelEntra no jogo?
SheetCatá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.
SpriteO 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.

Tenho estes arquivos → Carregar sprites
Clips, play, tamanhos diferentes por clip → Animar sprites
Virar, apontar, espelhar, inclinar na rampa → Orientar sprites
Forma do colisor, hurtbox, ponto fraco → Hitbox do Sprite
Clone, classe com padrões, Svg, aparência, clique → Recursos adicionais

Construtor Sprite

Sprite(source, height=, width=, size=, center=, box=, frame=, offset=, hitbox_shape=, hitbox_source=, hitbox_anchor=, hitbox_scale=, flip_x=, flip_y=, art_angle=, angle=, visual=, shield_scale=, group=, layer=)

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âmetroPassarTipo (default)Significado
source1ºstr ou SheetUm 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 / sizenome=númerotamanho do PNGUnidades 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.
centernome=pontocentro do design(x, y). É o pivô do frame (com Sheet(anchor="bottom"), os pés).
boxnome=BoxcellEncaixa (aspecto do PNG) ou só centra se vier height= / width= / size=. Não misture com center=. box=.
framenome=número inteiro ou str0A célula mostrada parada: índice ou nome no atlas. Com um clip no Sheet, frame= explícito vence o autoplay.
offsetnome=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=).
hitbox_shape / hitbox_source / hitbox_anchor / hitbox_scalenome=str, número, tupla ou par"auto"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.
flip_x / flip_y / art_anglenome=bool / númeroFalse / 0Espelho e “para onde o PNG aponta”. Ver Orientar sprites.
anglenome=númerorest_headingRumo inicial no mundo, em graus. Omitido: o PNG aparece como foi desenhado. Ver Orientar sprites.
visual / shield_scalenome=Visual / númeroNone / 1.4Visual no construtor. shield_scale é o tamanho do Shield em volta da silhueta. Ver Aparência e clique.
group / layernome=str—Os mesmos de Todo objeto na tela.
Neste Sprite

Propriedades — imagem e tamanho

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
framenúmero inteiro ou str0lê / escreveCélula atual. Índice ou nome no atlas. Escrever um valor para a animação e mostra essa célula.
sheetSheetleituraCatá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 / sizenúmero64lê / escreveTamanho do frame atual, em unidades de design. play / frame= mudam a leitura quando o frame novo tem outro tamanho. Escrever reescala todos os clips.
offsetpar(10, 0)lê / escreveDeslocamento do desenho e do colisor em relação ao center, em unidades de design.
.boxBoxnave.boxleituraA 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_boxBoxnave.art_boxleituraSó os pixels visíveis do frame na tela (sem a margem). Anda com a animação. Medir.
.is_collapsedboolFalseleituraTrue 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
flip_x / flip_yboolTruelê / escreveEspelha o desenho e o colisor. Reescreve .angle. Espelhar.
art_anglenúmero90lê / escrevePara onde o nariz do PNG aponta, em graus (0 = +x, 90 = +y), antes do flip. Apontar.
rest_headingnúmero90leituraRumo em que o PNG aparece como desenhado: art_angle depois do flip. reset_rotation() volta aqui.
tiltnúmero · contato · "ground""ground"lê / escreveInclina só o desenho, em volta do hitbox.anchor. Não gira o corpo. Inclinar.

Propriedades — colisão

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
hitboxHitboxhero.hitboxleituraA hitbox principal (pisa, bate, resolve): .shape / .source / .anchor / .scale (escrever em jogo vale), .active, .box. Hitbox.
hitboxestupla de Hitboxhero.hitboxesleituraTodas: a principal primeiro, depois as extras de add_hitbox.

Propriedades — aparência

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.visualVisual ou NoneHalo()lê / escreveVisual principal. None tira o visual; complementos ficam.
.alphanúmero0.5lê / escreveOpacidade do corpo, 0–1. Vale só para a imagem: visuais que envolvem o Sprite (Shield, Electric…) continuam com a opacidade deles.
.blendstr"add"lê / escreveComo a imagem se mistura com o que já está desenhado. Mesmos nomes que Shape. Padrão "normal". Blend.
.glow / .shadow / .hue / .gray / .tailbool ou cor ou GlowTruelê / escreveComplementos. .gray tira a cor (0–1). Ver Complementos.
shield_scalenúmero1.4lê / escreveTamanho da oval do Shield em relação à silhueta.

Métodos

MétodoAssinaturaRetornaSignificado
play(clip, loops=, next=, min_hold=, resume=)Parâmetros: clip (str), loops (número), next (str), min_hold (número), resume (True/False)selfToca um clip do Sheet. Visual, não mode. Tabela completa logo abaixo.
add_hitbox(group=, shape=, source=, anchor=, scale=, active=, px_rect=, px_circle=)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)HitboxHitbox 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)boolO frame na tela é um destes? Vai pelo frame (acerta com yoyo, next=, golpe interrompido). Caixas de ataque.
remove_hitbox(part)Parâmetros: part (Hitbox)selfTira 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)o objeto copiadoNovo Sprite com a aparência deste. Clone.
set_alpha(alpha)Parâmetros: alpha (número 0–1)selfIgual a .alpha = alpha. Não é o Shape.set_alpha (esse recolore fill/line).
look_like(outro)Parâmetros: other (outro)—Copia a aparência de outro objeto (cor, 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.
prepare_fx(Halo, Aura)Parâmetros: visuals (Visual)—Calcula antes a silhueta de todos os frames (evita um engasgo na primeira troca de visual). Sem args: o visual atual, se precisar.
play(clip, …)

sprite.play(clip, *, loops=, next=, min_hold=, resume=)

ParâmetroPassarTipo (default)Significado
clip1ºstrNome registrado com sheet.clip / strip / fps= no Sheet. Ex.: sprite.play("walk") ou sprite.play(clip="walk").
loops=nome=número inteiro ou Noneloops=1 toca uma vez e segura o último frame (ou o primeiro quando yoyo=True), salvo next=. Omitido: para sempre.
next=nome=str ou NoneClip a seguir após loops. Não é then=. Ex.: next="idle".
min_hold=nome=número0Segundos. Enquanto o clip atual não tiver rodado esse tempo, pedidos de outro clip são ignorados.
resume=nome=boolFalseAo 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.

Sheet(source, *files, cols=, rows=, cell=, atlas=, fps=, name=, yoyo=, anchor=, pixel_size=)

ParâmetroPassarTipo (default)Significado
source1ºstr, lista ou NonePNG, 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 extra2º…strAtalho: 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 / rowsnome=número inteiro ou NoneGrade regular. Ex.: cols=4. Juntos: cols=4, rows=3. rows= sem cols=: TypeError (faixa vertical é cols=1).
cellnome=par de números ou NoneTamanho da célula em pixels. Ex.: cell=(32, 32). Não misture com cols= / atlas=.
atlasnome=str ou NoneJSON/XML, com o PNG em source. Ex.: atlas="sheet.json".
fpsnome=número ou NoneRegistra um clip sobre todas as células já carregadas. Sem name=: "default". Grade e lista. Atlas empacotado: TypeError.
namenome=str ou NoneNome desse clip. Exige fps=. No 5 Strip, name="idle" no primeiro arquivo casa com os strip seguintes.
yoyonome=boolFalseVai-e-volta no clip do construtor. Exige fps=.
anchornome=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_sizenome=número > 0 ou NoneNoneTamanho 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
nframesnúmero inteiro8leituraQuantas células o catálogo tem agora.
namestupla de strleituraNomes das células (atlas / vários arquivos). Grade regular costuma usar índices.
anchorstr ou par"center"leituraO anchor= do construtor.
pixel_sizenúmero ou None3.0leituraO pixel_size= do construtor (None se não foi dado).

Métodos

MétodoAssinaturaRetornaSignificado
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 SheetNomeia 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).
strip(name, source, cols=, rows=, cell=, fps=, yoyo=, offset=)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 SheetAcrescenta 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 inteiroResolve í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.

Você temForma
Um PNG parado (poção, tiro, nave)1 PNG — sem Sheet
Um PNG por frame (Run (1).png, Run (2).png…)2 Arquivos
Um PNG empacotado + XML/JSON, com animações3 Atlas
Um PNG em grade regular4 Grade
Um PNG de faixa por animação (idle.png, walk.png), ou um pacote de PNGs soltos com várias animações (Idle__000.png…, Run__000.png…)5 Strip
Um atlas de itens / naves; cada Sprite fica num frame6 Imagem parada

Um arquivo, uma imagem parada. Sem Sheet, sem clip, sem play. O construtor já coloca o Sprite na tela.

Sprite("potion.png", height=48, center=(x, y), group="potion")
Sprite(source="shot.png", height=18, center=(x, y))

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=.

space = Sheet("space_sheet.png", atlas="space_sheet.xml")
space.clip("green", ["enemyGreen1.png", "enemyGreen2.png"], fps=6)
enemy = Sprite(space, height=40, center=(x, y))
enemy.play("green")

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.

hero = Sheet("idle.png", cols=4, fps=6, name="idle")
hero.strip("walk", "walk.png", cols=8, fps=12)
hero.strip("jump", "jump.png", cols=4, fps=10)
# nomeado: hero.strip(name="walk", source="walk.png", cols=8, fps=12)
s = Sprite(hero, height=48, center=(x, y))
s.play("walk")

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.

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=(x, y))
heroi.play("correndo")

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.

space = Sheet("space_sheet.png", atlas="space_sheet.xml")
Sprite(space, frame="playerShip1_blue.png", height=48, center=(x, y))
Sprite(space, frame="powerup_bolt.png", height=24, center=(x2, y2))

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")
  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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 temSprite(sheet) começa
0 clipsParado em frame (padrão 0).
1 clipTocando esse clip (autoplay).
2+ clipsParado no primeiro frame do primeiro clip registrado, até .play.
frame= explícitoParado 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
RegraO 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 Spriteheight= / 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 atlasUm 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)
CampoSignificado
art_angleGraus, 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.
.angleRumo no mundo: para onde a frente pintada aponta. O construtor começa em rest_heading, então o PNG aparece como desenhado.
rest_headingSó 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
ValorEfeito
númeroGira o desenho esse ângulo. Não desloca.
"ground" / AUTO_GROUNDLê a cada frame o slope_angle de um contato "bottom" (touching). No ar: 0.
um contato cUsa 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 + ânguloGira 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 porO construtor: Sprite(hitbox_shape=…, hitbox_scale=…)sprite.add_hitbox(group=…, scale=…)
QuantasUma (todo Graphic tem; num Shape é a própria geometria)Várias
Serve pararesolve (pisar, bater), @collide que nomeia o SpriteSó detecção, pelos grupos que ela reivindica
GruposOs do Sprite (sprite.tag(…))Os que ela reivindica (group=)
source padrãoFrame do construtor: estável enquanto anima"each": segue o frame atual
Desligar só elasprite.hitbox.active = False (as extras continuam)extra.active = False
Medirsprite.hitbox.boxextra.box
Segue o tiltNão (fica em pé)Sim
Frame sem arteCai na caixa do desenhoSem colisor nesse frame
Forma à mão—px_rect= / px_circle=, em pixels do frame (Caixas de ataque)
Liga nos frames—parte["active"] @= lambda: sprite.showing(…)

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.

ParâmetroPerguntaValoresPadrão
hitbox_shape= · .shapeQual forma?"auto" · "rect" · "ellipse" · "circle" · "hull""auto"
hitbox_source= · .sourceDe qual arte?frame · clip · tupla de clips · "all" · "clip" · "each"frame do construtor (parte: "each")
hitbox_anchor= · .anchorQue ponto fica parado?"center" · "bottom" · "top" · "left" · "right" · (ax, ay)"center"
hitbox_scale= · .scaleQuanto encolher / esticar?número ou (sx, sy)(1.0, 1.0)

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

LeiaMedeQual frame
sprite.width / heightO tamanho que você deu (o ajuste; escrever reescala).fixo
sprite.boxA célula desenhada inteira, com a margem transparente, onde é desenhada.o da tela
sprite.hitbox.boxA hitbox principal, como a colisão a usa. É esta para floor_below(dx=).o do source (padrão: fixo)
extra.boxAquela hitbox extra (None num frame sem colisor).o do source (padrão: o da tela)
sprite.art_boxSó 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).

Sprite("nave.png")                          # "auto": escolhe a forma sozinho
Sprite("bola.png", hitbox_shape="circle")
Sprite("espadachim.png", hitbox_shape="hull")

folha = Sheet("heroi.png", cols=4, rows=3)
folha.clip("parado", [0, 1], fps=6)
folha.clip("correndo", [2, 3, 4, 5], fps=12)
folha.clip("morto", [9, 10, 11], fps=8)
heroi = Sprite(folha, hitbox_shape="rect", hitbox_source=("parado", "correndo"))

shape — qual forma

ValorEncaixeQuando usar
"auto"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).

ValorQual arteCalcula
omitidoO 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))
ÂncoraParPonto 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:

  1. Fixa a escala. (0.55, 1.0) + "bottom" estreita sem levantar os pés.
  2. 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.
  3. É 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.

EM_PE = ("parado", "correndo", "pulando")

heroi = Sprite(ninja, group="heroi", hitbox_shape="rect", hitbox_source=EM_PE,
               hitbox_anchor="bottom", hitbox_scale=(0.55, 1.0))

def entrar_na_agua():
    heroi.hitbox.source = ("nadando", "boiando")   # união dos dois clips
    heroi.hitbox.anchor = "center"
    heroi.hitbox.scale = (1.0, 0.55)             # pose horizontal: achata a altura
    heroi.play("nadando")

def sair_da_agua():
    heroi.hitbox.source = EM_PE
    heroi.hitbox.anchor = "bottom"
    heroi.hitbox.scale = (0.55, 1.0)
    heroi.play("parado")

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".

TemaRegra
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 paredeSe a forma nova invade um sólido que estava livre, o resolve empurra de volta por aquela face. Ver resolve.
Nome erradoChecado 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ê escreveQuem colide
@collide(bala, heroi) · ANYTHING · on_collideA 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ãoRecebe 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 parteLevanta erro. normal, point e depth continuam legíveis.
touching_sides / touching_contactsSó 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

QueroEscrevaEfeito
Invencível por um tempoheroi.untag("dor") … heroi.tag("dor")O Sprite sai do grupo; a parte continua dona de "dor" e volta com o tag.
Desligar só a extrador.active = FalseSai de jogo (dispara as saídas). True religa.
Desligar só a principalheroi.hitbox.active = FalseAtravessa chão e paredes (fantasma); as extras continuam detectando. True religa. Tudo: collidable = False.
Desfazer a possedor.remove_from_group("dor")A parte larga o grupo e o Sprite sai dele.
Remover a parteheroi.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_alive False, host None).
  • 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âmetroNú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[...] @=.

QueroEscrevaEfeito
Caixa só nos frames do golpeespada["active"] @= lambda: heroi.showing(GOLPE)Entra no primeiro frame do golpe, sai depois do último.
Soltar a fórmulaespada["active"].unlink()Fica o último valor; .active = … volta a valer.
Invencível por 1,5 sdor.active = False e after(1.5, religar)O tempo decide, não o desenho: escrita simples.
Escrever com a fórmula ligadaespada.active = TrueLinkedAttributeError: 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
shape / source / anchor / scalestr, par ou tupla"each"lê / escreveOs quatro parâmetros. Na extra, source padrão "each".
.boxBox ou Noneheroi.hitbox.box.widthleituraEsta hitbox como está agora, alinhada à tela. None num frame sem colisor. Mede mesmo desligada. Medir.
px_rect / px_circletupla ou None(430, 180, 90, 80)lê / escreveSó extras. Forma à mão em pixels do frame (y para baixo). Escrever uma limpa a outra; None volta para a arte. Caixas de ataque.
.activeboolTruelê / escreveLiga / desliga só esta hitbox (na principal, as extras continuam; collidable desliga todas). Pode ser ligado a uma fórmula (abaixo); aí escrever levanta LinkedAttributeError.
.groupfrozenset{"dor"}leituraExtra: 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).
.hostSprite ou NoneleituraO dono. None depois de removida ou com o dono destruído.
.is_aliveboolTrueleituraTrue até remove_hitbox ou o destroy() do dono.

Métodos

MétodoAssinaturaRetornaSignificado
add_to_group(groupid) / remove_from_group(groupid)Parâmetros: groupid (str)a parteSó 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 / boolMesma 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 hitboxsprite.hitbox.box.width. A caixa do desenho inclui a margem transparente do PNG.o colisor
sprite.hitbox.active = False = collidable = FalseDesliga só a principal; as extras continuam. collidable = False desliga todas.desligar o Sprite
Track(sprite) como hurtboxTrack(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 inteiroUnião dos clips registrados. Um atlas com lasers e naves não entra inteiro.toda célula
px_rect com y para cimaPixels 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á tocandoOs 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 golpeparte["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 hitboxO 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_anglesetup() 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())
visiblePosiçã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.

bussola = Svg("compass.svg", height=48, center=(x, y))
Svg("<svg …>", height=48)
bussola.svg = lib.render()   # recarrega (markup ou arquivo)

Svg(source, height=, width=, size=, center=, box=, hitbox_shape=, hitbox_source=, hitbox_anchor=, hitbox_scale=, group=, layer=)

ParâmetroPassarTipo (default)Significado
source1ºstrTexto <svg …> ou nome de arquivo. Sem < é arquivo. Busca: Arquivos de imagem. Ex.: Svg("compass.svg").
height / width / sizenome=número ou NoneUnidades de design. Ex.: height=48.
centernome=pontocentro do design(x, y).
boxnome=BoxcellIgual ao Sprite. box=.
hitboxnome=—Os mesmos parâmetros do Sprite; "all" / "each" / "clip" dão no mesmo (um frame). add_hitbox também vale.
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.svgstr"<svg …>"lê / escreveMarkup 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).
.boxBoxicon.boxleituraUma 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).

orbit = Track.circle(center=lambda: earth.center, radius=120)
moon.move_along(orbit, time=4, loops="forever")
wasp.move_along(Track.eight(width=90, height=40), time=2.5)
track = Track(points=[(80, 200), (240, 380), (520, 160)], color="gold")
t1 = Track(sprite1)

Ancorada ou flutuante. É a decisão que muda o comportamento de uma trilha:

AncoradaFlutuante
Onde ficaNum 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).
QuandoPassou 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.

wall = Track.arc(
    center=(400, 300), radius=80, from_angle=0, to_angle=90,
    color="gold", line_width=8, group="wall",
)
resolve(player, "wall", bounce=0.0)

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).

Construtor

Track() · Track(points=, smooth=, closed=, quality=, line_width=, color=, begin=, end=, floating=, angle=) · Track(vectors=, start=, smooth=, closed=, floating=) · Track(shape, floating=) · Track(center=, radius=, floating=)

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âmetroPassarTipo (default)Significado
source1ºShape ou SpriteTrack(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.
pointsnome=lista de pontosPosiçõ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.
vectorsnome=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=.
smoothnome=número ou False10 / False = polilinha. 1 = curva. >1 = ultrapassa os pontos.
closednome=boolLoop. Padrão: Track() (= eight) fechado; points= / vectors= aberto.
qualitynome="high" ou "low""high"Amostras por segmento. Só se pedir — nunca escolhido sozinho por ter ligação.
center / radiusnome=ponto / númeroCom 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=.
startnome=pontoSó em vectors= (e no preset Track.wave). Planta u=0. Omitido → flutua.
floatingnome=bool ou omitidoExplí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.
anglenome=graus0Rumo 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 / visualnome=cor ou VisualEstilo. 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_widthnome=númeroEspessura do traço. Também conta como estilo. Não é width= — isso é tamanho do Track.eight.
begin / endnome=número 0…10 / 1Janela 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.
visiblenome=boolFalseComo Point: True sobe para a camada. Sem estilo e sem isto, fica fora da lista de desenho.
layer / groupnome=como todo objeto na telalayer= explícita também registra o Track no canvas.
Modelos de construção
ChamadaSignificado
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=0Polilinha.
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.

ChamadaSignificado
Track.line(length=, angle=, p1=) · Track.line(vec=, p1=) · Track.line(p1=, p2=)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.
Track.arc(radius=, from_angle=, to_angle=, center=)Arco aberto. Ângulos em graus. Padrão 0 → 180. Sem center= flutua. Parede curva: color= + line_width= + group= / @collide.
Track.eight(width=90, height=40, center=)Oito fechado. u=0 no cruzamento (= center). Sem center= flutua. Track() é isto.
Track.star(points=5, radius=, inner=, center=)Estrela fechada. points ≥ 3 (número de pontas, não lista). inner omisso = 40% do raio. Sem center= flutua.
Track.spiral(turns=3, radius=, center=)Espiral aberta. u=0 no ponto mais interno (= center). Sem center= flutua. radius omisso = tamanho médio.
Track.wave(length=200, amplitude=30, waves=3, start=)Onda aberta. start= planta u=0. Sem start= flutua. Não use center=.
Track.shuttle(length=, angle=, center=) · Track.shuttle(vec=, center=)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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.centerpontolê / escreveMeio 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.
.radiusnúmerolê / escreveSó no modelo círculo / arco.
.lengthnúmeroleituraComprimento total da curva, em unidades de design. Só leitura: para mudar, mude a forma (radius=, points=…).
.unúmero0lê / escreveParâmetro 0…1. Escrever move o cursor.
.begin / .endnúmero 0…10 / 1lê / escreveJanela 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.
.cursorPointleituraPonto em u; só é calculado quando você o lê ou liga. ["center"] @= lambda: cursor coloca — não varre.
.closedboolleituraLoop. Só leitura.
.is_floatingboollê / escreveSe 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 / .qualitynúmero / "high" ou "low"lê / escreveLeia e escreva. Escrever recalcula os pontos da curva.
.visibleboolFalselê / escreveComo Point: True promove à camada; False tira da lista de desenho.

Métodos

MétodoAssinaturaRetornaSignificado
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)grausRumo 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âmetrosTrack (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")selfOndula no próprio objeto. clone() se quiser a cópia lisa.
orbit(radius=20, laps=8)Parâmetros: radius (número), laps (número)selfEnrola em torno da curva. No próprio objeto.
wander(amount=6, seed=0)Parâmetros: amount (número), seed (número inteiro)selfDesvio aleatório estável. No próprio objeto.
clone(floating=)Parâmetros: floating (bool, omitido copia o original)TrackCongela a vista atual (inclui reversed e modificadores) numa polilinha. Copia is_floating; clone(floating=True) desliga a pose no clone.

Funções

MétodoAssinaturaRetornaSignificado
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 TerraNã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.

dots = History(max_points=40, on_leave=lambda dot: dot.dissolve_out(0.3))

def drop_dot():
    dot = dots.add(Circle(center=tip.center, radius=4))
    dot.visual = Halo("cyan")

every(0.05, drop_dot)
Construtor History

History(*, max_points=, duration=, on_leave=, destroy_on_leave=True)

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âmetroPassarTipo (default)Significado
max_points=nome=int ou NoneNoneCapacidade. Ao estourar, o mais antigo sai. Crescer o rastro: h.max_points += 1.
duration=nome=número ou NoneNoneTempo 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=boolTruePadrã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
NomeAssinaturaRetornoSignificado
add(thing)Parâmetros: thingthingGuarda 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, yPointGuarda uma cópia da posição, que não acompanha nada. Nada a destruir.
history[i] / [a:b]índice / fatiavalor / lista0 = mais antigo, -1 = mais novo. Fatia é cópia (não vista viva).
.first / .lastqualquerleituraAtalhos de [0] e [-1].
len(history) / for v in history——Do mais antigo ao mais novo.
pop(index=-1)Parâmetros: index (int)valorTira vivo. Sem on_leave, sem destroy — passa a ser seu.
discard(value)Parâmetros: valueboolRemove 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).
.max_points / .durationint / float / Nonelê / escreveLimites ao vivo. Baixar expulsa na hora.
segments = History(max_points=100, duration=2.0)

def sample():
    start = segments.last.p2 if segments else tip.center
    segment = segments.add(Line(p1=start, p2=tip.center))
    segment.visual = Laser("red")

every(0.05, sample)

Não confundir

PareceÉNão é
add(tip) ou add(tip.center) guarda a poseadd(tip) passa a dono de tip e o destrói na saída. add(tip.center) levanta TypeError — a parte pertence a tip. Posição: snapshot(tip).cópia automática
History desenha o rastrosó guarda. Traço contínuo: Contrail. Círculos / segmentos: add(Circle(...)) / add(Line(...)).Track / MotionTrail / .tail
on_leave= numa subclasse com def on_leaveTypeError — callback e método não empilham.os dois juntos
on_leave cancela o destroyé 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)
Construtor Contrail

Contrail(*, max_points=, duration=, fade=, ease=, color=, visual=, line_width=, line_join=, smooth=, on_leave=)

Mesmos limites que History. Traço privado (não é Track). smooth=1 por padrão (cúbica); smooth=0 = polilinha. Sem stroke= — use color=.

ParâmetroPassarTipo (default)Significado
max_points= / duration=nome=int / número / NoneIguais a History (ao vivo; baixar expulsa na hora).
fade=nome=boolFalseTrue: alpha 0 na cauda → 1 na cabeça (multiplicador sobre o alpha da cor).
ease=nome=nome ou função"linear"Curva do esmaecimento. Só com fade=True. Padrão "linear". Mesmo catálogo de tween.
color=nome=corpadrão do jogoCor do traço; ex.: Color("cyan").with_alpha(0.5). Alpha = 4º canal — não há alpha=.
visual=nome=VisualNoneVisual do traço (ex.: Laser("red")). Mutuamente exclusivo com color= no construtor, como em Shape.
line_width=nome=número1.5Espessura do traço, em unidades de design. Também contrail.line_width = … depois.
line_join=nome=str"round"Canto do traço (útil com smooth=0). Padrão "round". Facetado: "miter". Não inventa a curva.
smooth=nome=número ou False10 / False = polilinha. 1 = curva Catmull-Rom (cúbicas no path). >1 = ultrapassa os pontos. Igual a Track.smooth.
on_leave=nome=função ou NoneEfeito colateral quando a cauda recua (faísca, som). O traço sempre se atualiza sozinho.
Métodos e propriedades
NomeAssinaturaRetornoSignificado
snapshot(point) / snapshot(x, y)Parâmetros: Point / (x, y) / GraphicPointSempre guarda um Point desligado. contrail.snapshot(dst) basta. add levanta TypeError.
len / índice / iterate / .first / .last——Herdados de History (valores = Points).
.color / .visual / .line_width / .line_join / .line_cap—lê / escreveLeem e mudam o traço do rastro, como num Shape.
.smoothnúmerolê / escreveReconstrói o path (cúbica ou polilinha).
.fade / .easebool / callablelê / escreveReconstroem o traço. ease= exige fade=True.
.max_points / .duration—lê / escreveIguais a History.
pop / discard / clear——Iguais a History; o traço acompanha.
destroy()Sem parâmetros—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)—TrackCongela 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 rastrosó arredonda o canto da polilinha (smooth=0). A curva é smooth=1.Catmull-Rom
fade= pinta opaco na cabeçamultiplicador sobre o alpha da cor — cabeça = alpha do autor, cauda → 0.rampa absoluta até 1.0
Amostragem no construtordst.every(0.05, lambda: trail.snapshot(dst)) — cadência à parte do armazenamento. O timer é de quem é lido.sample(...) / record(every=) / Graphic.contrail(...)
every do módulo amostra o heróiheroi.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çorastro.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ídaDestroyedError. Cancele o handle ao destruir o Contrail; amarre o timer na fonte com e.every.nada acontece / último ponto
Círculos / segmentos no Contrailuse 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 comOnde 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.

score = Text("", top_right=(980, 780), font_size=20)
score["text"] @= lambda: f"{st.score}"
Text("READY", top_left=(20, 720), width=160, background="maroon")
Text("Kills 3\nTime 1:02", top_right=(980, 700), align="left")
Text("HP 100/100", center=ship.center, width=120, align="center")

# seguir outro objeto: lambda no ponto (ou num eixo)
panel = Rect(center=(500, 300), width=300, height=120, layer="ui")
Text("Menu", top_left=lambda: panel.top_left)
Text("x", top_left=(lambda: mouse.x, 760))
Construtor Text

Text(text, top_left=, top_center=, top_right=, center=, box=, width=, align=, layer=, font=, font_size=, color=, background=, glow=, stack=, group=, angle=)

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âmetroPassarTipo (default)Significado
text1º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úmero120Caixa 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=boolTrueFalse: 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= / .fontnome=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úmero13Altura 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úmero0Giro 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= / .glownome=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_rightPontos 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.heightCópia da caixa de layout (medir, colocar ao lado).

Não confundir

PareceÉNão é
Text(…, layer="top") fica preso à janelaA camada "top" ainda anda com a câmera. HUD: layer="ui", ou omita center=.HUD
align="right" prende a borda direitaalign= só alinha as linhas dentro da caixa. Para prender a borda direita: top_right=.âncora
Text para o jogador digitarText só exibe. Para digitar: TextInput. O rótulo de um Slider continua sendo label= (uma string). Coluna automática: ui.stack_reset / stack_origin / stack_from / stack_column.campo de entrada

SpatialFx

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.

FamíliaEfeitosColocação
PontoFire, Sparks, GlowSpot, Orb, Sunburst, Starburst, Warp, Portalcenter= e o tamanho em radius= (o Fire de jato usa width=)
ÁreaRainUm retângulo: p1= / p2= (cantos opostos) ou center=, width=, height=
FeixeJetUm 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).

Fire(center=(200, 150))
Warp(center=(400, 220), radius=90, color="cyan")
Rain(p1=(300, 250), p2=(700, 550))

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âmetroPassarTipo (default)Significado
layernome=str ou NoneNoneNone = fora da lista de desenho (o usual aqui).
groupnome=str ou lista ou NoneTag, 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.layer / .groupstr"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.
activeboolFalselê / escreveEstaciona: esconde e pausa. Sem desenho, visible quase não importa. Para remover: destroy().
modemode ou Nonee.mode = e.divinglê / escreveCapítulo de comportamento. Ver Mode. Mesmo mode não faz nada.
campos própriosAttributeself.hp = 3lê / escreveAtributo criado no setup (self.hp = 3) vira Attribute. Guardar valores.

Métodos

MétodoAssinaturaRetornaSignificado
destroy()Sem parâmetros—Remove. Sem desenho, visible quase não importa. .active vira False.
cancel_process()Sem parâmetrosselfCancela 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 / everyParâmetros: segundos (número), fn (função)HandleTimers neste Entity — sobrevivem a troca de mode; morrem no destroy / cancel_process. Ver escopo.
tweenParâmetros: target (alvo, Attribute ou Point), to (mesmo tipo do valor atual)HandleInterpola 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.
on_update(self, dt)Parâmetros: self (este objeto), dt (número)—A cada frame, nesta Entity. Ver Frame.
on_destroy(self)Parâmetros: self (este 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)).

class Bolt(Circle, radius=8, color="gold", group="bolt"):
    def on_update(self, dt):
        self.move_by(0, 560 * dt)
Bolt(center=(x, y))

class Nave(Sprite, source="playerShip1_blue.png", height=48, group="nave"):
    def setup(self):
        self.hp = 3
Nave(center=(x, y))
Entra na classeNão entra
color visual groupcenter layer
Tamanho: radius width/height length/anglePose por instância (center=)
Sprite: source= frame= height=class Bolt(Circle): radius = 8 — TypeError na classe: vai na linha class
Text: font_size font color background glow align width groupO texto e a posição (center top_left … box layer stack angle)
—Widgets, Entity, Endless: sem parâmetros de classe (erro com a correção); passe na instância
SpatialFx: os parâmetros do tipo (radius amount color length…), group soft_edgePontos (center p1 p2 origin); style= de Fire — escolhe outra classe: escreva Fire(style=…)

Não confundir

PareceÉNão é
class Bolt(Circle): radius = 8Parâ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 subclasseSubstitui 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.—

Onde mais procurar — Colocar · Aparência · Mover · Geometria

Aparência

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.

Cor / Gradient / Texture → Cor / Gradient
Escolher cores (rampa, paleta) → Paletas e rampas
Blend → Blend
.glow / .shadow / .tail → Complementos
Neon / Halo / … → Visual
Morte / hit / knock → Transientes
Blur / DropShadow → Filtros

Cor / Gradient / Texture

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:

FormaExemplo
Nome"coral" · "dark red" · "sky blue"
Hex"#33ccff" · "#RRGGBBAA"
RGB 0–1(0.2, 0.4, 0.8) · (0.2, 0.4, 0.8, 0.5)
ColorColor("cyan") · Color(0.2, 0.4, 0.8)
c = Circle(color="coral")
c.set_color("tomato")
c.fill = Gradient("sunset", angle=-90)
c.fill = Texture("tex2.png", fit="tile", tile_size=40)
Color("cyan")
Color(0.2, 0.4, 0.8)
Color("#33ccff")
Color.from_hsv(0.55, 0.8, 0.9)
Color.random()
Color("red").jitter()
c.set_color("navy")
c.set_alpha(0.8)
gold = Color("gold").lighter(0.15)

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.rgbtupla(1, 0.8, 0, 1)leituraTupla (r, g, b, a) em 0–1.
.alphanúmero0.8leituraAlfa 0–1. Outro alfa: with_alpha(a) (Color novo) ou forma.set_alpha(a).
.hsl / .hsv / .brightnesstupla ou número(0.5, 0.8, 0.6)leituraLeitura. HSL/HSV: (h, s, l|v, a).

Métodos

MétodoAssinaturaRetornaSignificado
Color(nome) / Color(r, g, b[, a]) / Color("#RRGGBB")Parâmetros: r, g, b, a (números 0–1), ou um nome de cor / "#RRGGBB"ColorConstrutor. Color(None) é “sem cor” (falso em if).
Color.from_hsl(h, s, l[, a]) / Color.from_hsv(h, s, v[, a])Parâmetros: h, s, l|v, a (números 0–1)ColorFábricas. Matiz h dá a volta (wrap); S / L / V / alfa clampam. HSV: saturação e brilho separados — bom para cores vivas.
Color.from_rgb(r, g, b[, a]) / Color.from_name(name[, a])Parâmetros: r, g, b, a (canais 0–1) · name, aColorAtalhos equivalentes a Color(...).
ColorNames.is_known(name) / ColorNames.get(name)Parâmetros: name (nome, str)True/False · (r, g, b)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)), alphaColorMatiz aleatória em HSV. Padrão vivo: S=0.65, V=0.85. Intervalo: saturation=(0.3, 0.9).
jitter(hue=, saturation=, value=)Parâmetros: hue / saturation / value (número, ± metade)ColorCor próxima (HSV). Padrão hue=0.04 (~±14°) — vermelhos continuam vermelhos. Amplie para “tons quentes”. Um cinza continua cinza (só o valor varia).
with_alpha(alpha)Parâmetros: alpha (número 0–1)ColorCópia com outro alfa.
lighter(amount=0.1) / darker(amount=0.1)Parâmetros: amount (número)ColorCópia com a luminosidade (HSL) amount mais alta / mais baixa; matiz e saturação ficam. amount padrão 0.1.
saturate(amount=0.1) / desaturate(amount=0.1) / hue_cycle(amount=0.1)Parâmetros: amount (número)ColorCópia. amount padrão 0.1. saturate num cinza devolve o mesmo cinza (cinza não tem matiz).
vibrant_version(amount=0.5) / whiter_version(amount=0.4)Parâmetros: amount (número)ColorMais neon (HSV; um cinza continua cinza, só mais claro) · mistura rumo ao branco. Padrões 0.5 / 0.4.
Color.interpolate(color1, color2, t, color_space=)Parâmetros: color1 (Color), color2 (Color), t (número), color_space (ColorSpace)ColorMistura. 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)ColorAtalho de instância para Color.interpolate (OKLAB). Usado por lerp / tween.
complementary()Sem parâmetrosColorCor 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âmetroPassarTipo (default)Significado
*cores1º, 2º…str ou Color"rdyl"Um nome de preset, ou a rampa. Uma cor: escuro → claro.
typenome=str"linear"linear · radial · sweep.
anglenome=número0Graus. 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.
sizenome=número ou Nonecabe no objetoEm unidades de design. Linear: a distância do centro até cada ponta do degradê. Radial: o raio. Sweep ignora.

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.typestr"linear"leituraSó leitura. "linear" · "radial" · "sweep". O mesmo valor passado em type=.

Métodos

MétodoAssinaturaRetornaSignificado
darker(amount=0.1)Parâmetros: amount (número)GradientNova 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)GradientNova 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âmetrosColorMé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)GradientAlfa 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.

Texture("tex2.png")
Texture("tex2.png", fit="cover")
Texture("tex2.png", fit="tile", tile_size=40)
Texture("tex2.png", shift=HueShift(amount=0.33))
Neste Texture

Texture(source, filename=, image=, fit="cover", tile_mode=, scale=1, tile_size=, angle=0, color=, shift=)

Nome sem =: 1º na ordem (arquivo ou imagem). O resto pelo nome. Busca: Arquivos de imagem.

ParâmetroPassarTipo (default)Significado
source1ºstr"tex2.png"O arquivo de imagem (PNG / JPG). Busca: Arquivos de imagem.
filename / imagenome=str / imagem SkiaNoneNo lugar de source: o caminho pelo nome, ou uma imagem Skia já carregada. Raro — o comum é o 1º argumento.
fitnome=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_sizenome=número ou (l, a)40Com fit="tile": o tamanho de cada ladrilho, em unidades de design. Com outro fit é TypeError (não mudaria nada).
scalenome=número1Zoom extra na imagem (maior que 0): 2 mostra a imagem com o dobro do tamanho.
anglenome=número0Gira 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.
shiftnome=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.
colornome=cormédia da imagemNã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_modenome=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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.fitstrleituraSó leitura. O mesmo valor passado em fit=.
.tile_modestrleituraSó 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.

FábricaAssinaturaRetornaSignificado
WaterFill(kind="side", amount=1.0, speed=1.0)Parâmetros: kind ("side" / "top"), amount (número), speed (número)ProceduralFillÁgua. "side" = plataforma vista de lado (superfície em cima); "top" = lago visto de cima.
FlameFill(amount=1.0, speed=1.2)Parâmetros: amount (número), speed (número)ProceduralFillFogo subindo dentro da forma.
SwellFill(amount=1.0, speed=1.0)Parâmetros: amount (número), speed (número)ProceduralFillFluxo abstrato (não é água de lado).
WaveFill(color=None, amount=1.0, speed=1.0)Parâmetros: color (cor), amount (número), speed (número)ProceduralFillVolume de líquido de qualquer cor.
AuroraFill(color=None, amount=1.0, speed=0.9)Parâmetros: color (cor), amount (número), speed (número)ProceduralFillFaixas verticais de aurora, de qualquer cor.
ProceduralFill(kind=, amount=1.0, speed=1.0, seed=0.0, color=None)Parâmetros: kind (nome do shader), amount, speed, seed (números), color (cor)ProceduralFillA 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.

ClasseAssinaturaRetornaSignificado
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)PaintPreenchimento 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)FillSó preenchimento. Gradient / Texture / visual levantam TypeError — use Paint(Gradient(…)).
Stroke(color="black", width=None)Parâmetros: color (cor sólida), width (número)StrokeSó 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 preenchimentoNuma 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.

tints = Palette("plasma")              # 6 cores do preset
hud.fill = tints[wave_number]          # índice cíclico

ramp = Colormap.from_colors("red", "blue")
cor = ramp.get_color(0.5)              # meio-termo (OKLAB)
Colormap

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.

ramp = Colormap.from_colors("red", "blue")
ramp.get_color(0.0)          # vermelho
ramp.get_color(0.5)          # meio-termo (OKLAB)
ramp.random()                # cor aleatória na rampa

Colormap.from_colors("red", "yellow", "blue")
Colormap.from_preset("plasma")
Colormap([(0.0, "navy"), (0.4, "cyan"), (1.0, "white")])
MétodoAssinaturaRetornaSignificado
Colormap.from_colors(*cores, color_space=)Parâmetros: cores (nome / Color / RGB), color_space (ColorSpace.OKLAB)ColormapRampa com cores espaçadas igualmente (2 → extremos; 3+ → passos iguais). Padrão OKLAB.
Colormap.from_preset(preset, color_space=)Parâmetros: preset (nome), color_space (ColorSpace.OKLAB)ColormapMesmos 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)ColormapParadas explícitas. Padrão OKLAB.
get_color(t)Parâmetros: t (número 0–1)ColorCor na posição t da rampa.
random()Sem parâmetrosColorAtalho 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.

Palette(*fonte, steps=6, color_space=ColorSpace.OKLAB)

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étodoAssinaturaRetornaSignificado
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_spacePaletteAmostra o preset (mesma tabela de Gradient). Sem argumentos: "plasma".
Palette(["red", "yellow", "blue"])Parâmetros: lista ou *cores, steps, color_spacePaletteRampa com essas cores, depois steps amostras. Também: paradas [(t, cor), …], um Colormap ou um Gradient.
palette[i]Parâmetros: i (int)ColorAmostra no índice. Dá a volta: palette[6] == palette[0] se steps=6. Fatia (palette[:3]) não dá a volta.
get_next()Sem parâmetrosColorPró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)PaletteReposiciona o cursor de get_next (módulo). Encadeável.
random()Sem parâmetrosColorUma das amostras, ao acaso (discreto). Colormap.random() amostra o contínuo.
len(palette) / .colorsSem parâmetrosint / listaQuantas amostras. .colors devolve uma cópia.

Não confundir

PareceÉNão é
Color.random() · ramp.random() · palette.random() · color.jitter()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

NomeFaz
"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.
"multiply"Multiplica. Escurece. Sombra, tinta, vidro colorido.
"screen"Inverso do multiply. Clareia. Fumaça, glow suave (estoura menos que add).

Contraste

NomeFaz
"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

NomeFaz
"darken"Fica o canal mais escuro de fonte e destino.
"lighten"Fica o canal mais claro.

Comparação

NomeFaz
"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

NomeFaz
"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)

NomeFaz
"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.

PropriedadeO que fazEmAtalhos
.glowHalo de luz em volta do corpo.Shape, Sprite, TextTrue = brilho na cor do objeto · uma cor ("cyan") · Glow(…) / CheapGlow(…) para ajustar
.shadowSombra deslocada para baixo e à direita.Shape, SpriteTrue · uma cor · Shadow(…)
.tailCópias fantasmas atrás do objeto quando ele anda.Shape, SpriteTrue · uma cor · Tail(…)
.hueGira as cores do PNG no círculo de matiz (vermelho → verde → azul).Spriteum número de voltas (0.33 = um terço: vermelho vira verde) · True = 0.33 · HueShift(…)
.grayTira a cor (botão desligado, personagem petrificado).SpriteTrue = 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.

ship.glow = True
ship.glow = "cyan"
sting.glow = CheapGlow("tomato", radius=8)
ship.shadow = Shadow("navy")
sprite.hue = 0.33
sprite.gray = True          # cinza (botão desligado); 0.5 = meio
bolt.tail = True
# sem angle=: segue o movimento
ship.tail = Tail(color="gold", size=1.4, amount=1.15)
bolt.tail = Tail(angle=180)
ship.tail = Tail(
    angle=lambda: ship.angle + 180,
    size=lambda: 1.4,
    amount=lambda: 1.15,
)
Text("GO", glow=True)

tail = True e Tail(…) sem angle= 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).

ship.save_visual()
ship.visual = Neon("tomato")
ship.color = "lime"
ship.glow = True
after(0.4, ship.restore_visual, name="bonus")

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.

ship.flash(0.12)
ship.pop(scale=1.2)
ship.knock(distance=16, angle=30)
ship.dissolve_out(0.4, style="vortex", on_done=ship.destroy)
ship.dissolve_out(0.45, style="wipe", angle=0)
ship.dissolve_out(0.35, style="fade")

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.

CanalMétodosAo terminar
lifedissolve_out burn_out fade_out implode dissolve_in fade_inmorte: visible=False; spawn: mostra no começo; on_done
flashflash blinklimpa; on_done
textflash_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
pulsepulse_outlinelimpa; on_done
poppoplimpa; on_done
knockknocklimpa; on_done
sparkspark_burstlimpa; nunca esconde; on_done
—clear_transientscancela 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.

c.fill_filter = Blur(sigma=4)
c.fill_filter = DropShadow(offset=12, sigma=8)
c.fill_filter = None

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.

Onde mais procurar — Desenhar · Mundo · Som

Mundo

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.

Fundo → Background
Vignette / Bloom → Post · flash / explosion_at / fade_out → Impactos
Zoom / pan / frame_to → Câmera
FPS / qualidade → Apresentação
layer= → Camadas
group["wasp"] → Grupos

Background

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.

canvas.background = "navy"
canvas.background = (0.1, 0.1, 0.15)
canvas.background = Color("black")
canvas.background = Gradient("sunset", angle=-90)
Wallpaper("bg1.jpg", fit="cover")
canvas.background.base = Color(0.05, 0.05, 0.08)

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
canvas.backgroundpilhacanvas.backgroundlê / escreveLer 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.
.baseWallpaperColor("navy")lê / escreveO papel de parede. Sempre tem um.
.enabledboolFalselê / escreveEsconde só os extras. A base ainda desenha.

Métodos

MétodoAssinaturaRetornaSignificado
.add(field)Parâmetros: field (um fundo: Stars, Clouds…)o fundoEmpilha 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)listaSó 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).

Wallpaper(source, filename=, image=, fit="cover", tile_mode=, scale=1, tile_size=, underlay=, pan_response=0, zoom_response=0, rotation_response=0, scroll=(0, 0), enabled=True)

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.

Wallpaper("navy")
Wallpaper(Gradient("black", "navy", angle=90), pan_response=0.05)
Wallpaper("space.jpg", tile_mode="mirror", pan_response=0.15, zoom_response=0.3)
Wallpaper("bg2.png", fit="contain")
Wallpaper(path, fit="tile", tile_size=200, tile_mode="mirror")
ParâmetroPassarTipo (default)Significado
source1ºcor ou Gradient ou arquivo ou TexturepretoO 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").
filenamenome=strNoneEm vez de source: caminho do arquivo.
imagenome=ImageNoneEm vez de source: skia.Image já carregada.
fitnome=str"cover"Só imagem: cover · contain · stretch · tile.
tile_modenome=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".
scalenome=número1Só imagem: zoom extra. Tem que ser > 0.
tile_sizenome=número ou par de números¼ do lado menorSó imagem / fit="tile": célula em unidades de design. Com outro fit é TypeError.
underlaynome=corpretoSó 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_responsenome=número0Resposta ao pan da câmera. 0 = colado na tela. 1 = 1:1 com o mundo.
zoom_responsenome=número0Quanto o zoom da câmera afeta o padrão.
rotation_responsenome=número0Fração da rotação da câmera.
scrollnome=ponto(0, 0)Deriva em unidades de design / s, independente da câmera.
enablednome=boolTrueEsconde o desenho sem tirar da pilha.
fitComportamento
coverEscala uniforme até cobrir; recorta o resto.
containEscala uniforme até caber; letterbox com underlay.
stretchEstica nos dois eixos.
tileRepete 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.

SkyGradient(palette="day", top=, middle=, bottom=, horizon=0, fade=0.003, pan_response=0, zoom_response=0, rotation_response=0, scroll=(0, 0), enabled=True)

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.

SkyGradient("night")
SkyGradient("sunset")
SkyGradient("day", horizon=0.42)
SkyGradient(top="navy", middle="sky blue", bottom="navy")
ParâmetroPassarTipo (default)Significado
palette1ºstr"day"day · dawn · sunset · dusk · night · space · overcast · desert · forest · toxic · ember · ice · underwater. Qualquer top / middle / bottom pula o preset e completa com day.
topnome=cordo presetCor do zênite (topo da tela).
middlenome=cordo presetCor da atmosfera, no horizonte.
bottomnome=cordo presetCor abaixo do horizonte (sólido). Com horizon=0 não entra.
horizonnome=número0Linha do horizonte (0–1, y-up desde a base). 0 = céu até embaixo (middle na base). Alinhe com Ground.
fadenome=número0.003Mistura middle → bottom abaixo da linha. 0 = corte seco. Mesmo parâmetro do Ground.
pan_responsenome=número0Resposta ao pan da câmera. 0 = colado na tela. 1 = 1:1 com o mundo.
zoom_responsenome=número0Quanto o zoom da câmera afeta o padrão.
rotation_responsenome=número0Fração da rotação da câmera.
scrollnome=ponto(0, 0)Deriva em unidades de design / s, independente da câmera.
enablednome=boolTrueEsconde 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.

Vignette()
bloom = Bloom(intensity=0.8)
bloom.intensity = 1.0
bloom.enabled = False
grain = Grain()
grain.remove()
canvas.effects.remove(grain)   # igual
canvas.effects.clear()
canvas.effects

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
canvas.effectspilhacanvas.effectslêA lista dos Posts ligados. Criar um Post já o põe aqui; não atribua canvas.effects = ….
.enabledboolFalselê / escreveFalse desliga todos os Posts de uma vez. Cada efeito também tem .enabled.

Métodos

MétodoAssinaturaRetornaSignificado
.add(effect)Parâmetros: effect (post)o efeitoEmpilha. 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)listaOrdem 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.

canvas.flash(0.6)
canvas.shake(10)
canvas.explosion_at(x, y)
canvas.fireworks_at(heroi)
canvas.zap_at(info.point, style="fork")
canvas.hit_at(info, angle=90)
canvas.debris_at(x, y, color="peru")
canvas.fade_out(0.5, on_done=load_next)
canvas.fade_in(0.5)

*_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):

def update(dt):
    look = (canvas.pan_x, canvas.pan_y)
    tx, ty = deadzone(look, hero.center, size=(160, 90))
    canvas.pan_to(chase(look, (tx, ty), rate=6.0))

Tamanho do espaço de design (canvas.width / height) e canvas.playfield: Colocar › Pais.

Neste canvas (câmera)

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
canvas.zoomnúmero1leituraEscala uniforme. 1 = identidade.
canvas.pan_x / .pan_ynúmero80leituraPonto do mundo agora no centro da janela.
canvas.rotationnúmero1leituraRotação da vista, em graus.
canvas.game_viewBoxcanvas.game_viewleituraO 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_viewBoxcanvas.ui_viewleituraA 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_offscreenboolTruelê / escreveNão desenha o que está fora da vista. Padrão True. Os objetos continuam rodando update e colidindo. False desenha tudo: Apresentação.

Métodos

MétodoAssinaturaRetornaSignificado
set_origin(where, *, time=None, ease="linear", loops=1, yoyo=False, then=, on_done=)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_byParâ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_tox, 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_bydx, 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_byParâ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=.
frame_to(alvo, *, pad=0, rotate=False, time=None, ease="linear", loops=1, yoyo=False, then=, on_done=)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=.
reset_view(*, time=None, ease="linear", loops=1, yoyo=False, then=, on_done=)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.

MovimentoQuem usa
panpan_to / pan_by / set_origin. Também zoom/rotação quando há about=.
zoomzoom_to / zoom_by.
rotaçãorotate_to / rotate_by.
os trêsframe_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óiO 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".

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
"windowed"str"windowed"lê / escrevePadrã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ê / escrevePreenche 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ê / escreveCobre 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).

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
"vsync60"str"vsync60"lê / escrevePadrã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ê / escrevePreso 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ê / escreveUm 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úmeronúmero30lê / escreveReló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ê / escreveSem 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).

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
"high"str"high"lê / escrevePadrão. Efeitos de tela (Post) em meia resolução; efeitos nos objetos completos.
"medium"str"medium"lê / escreveEfeitos de tela em meia resolução; SpatialFx e efeitos pesados em versão mais simples. Também: "med".
"low"str"low"lê / escreveEfeitos de tela em ¼ da resolução; versões leves de tudo.
"max"str"max"lê / escreveTudo 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).

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
TrueboolTruelê / escrevePadrã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.
FalseboolFalselê / escreveDesenha 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.

set_fx_auto

set_fx_auto(enabled=True, refresh_hz=None, params=None)

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âmetroPassarTipo (default)Significado
enabled1ºboolTrueFalse desliga o ajuste: qualidade e fps ficam onde você os puser.
refresh_hznome=número ou NoneNoneA taxa do monitor para o plano (60, 120…). None mede sozinho.
paramsnome=objeto de ajusteNoneRecurso 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.

ChamadaTipoSignificado
below("default")int-1Um passo abaixo da camada âncora.
above("default")int1Um passo acima.
above(10)int11Âncora numérica também vale (depois que a camada 10 existe).
canvas.layers.create

canvas.layers.create(name, z=, pan_response=, zoom_response=, rotation_response=, scroll=, below=, above=)

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âmetroPassarTipo (default)Significado
name1ºstrNome da camada. Ex.: canvas.layers.create("spawn"). Int → TypeError.
pan_responsenome=número10 = colada na tela. 1 = acompanha o mundo. >1 = primeiro plano.
zoom_responsenome=número1Quanto o zoom da câmera afeta esta camada.
rotation_responsenome=número1Fração da rotação da câmera.
scrollnome=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).
znome=número inteiro0Ordem. Ou below= / above= — um só. z ≤ 100.
below / abovenome=str/int ou NoneOrdem 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").
visiblenome=boolTrueCamada visível ao criar. Depois: canvas.layers["name"].visible.
canvas.layers

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.defaultstr"default"lê / escreveCamada de construção quando layer= é omitido (Shape/Sprite; Text sem center= usa ui).
"far" in canvas.layersbool"far" in canvas.layersleituraA camada existe? Também: 10 in canvas.layers.
canvas.layers["far"]camadacanvas.layers["far"]leituraO objeto da camada — ver caixa abaixo. Também: canvas.layers[10].

Métodos

MétodoAssinaturaRetornaSignificado
create(name, *, z=, above=, below=, visible=True, pan_response=1, zoom_response=1, rotation_response=1, scroll=(0, 0))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 camadaNova 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 withDentro do with, .default vira essa camada.
list()Sem parâmetroslistaLista {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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.visibleboolcanvas.layers["far"].visible = Falselê / escreveMostra ou esconde a camada inteira.
.znúmero inteirocanvas.layers["far"].z = -1lê / escreveReordena. Não vale em "top" / "ui" / camada numérica. z ≤ 100.
.post_fxboolcanvas.layers["top"].post_fx = Falselê / escreveFalse: a camada top não recebe os Posts (Bloom etc.) e fica nítida. Só vale em "top"; a ui nunca recebe Posts.
.scrollpontonuvens.scroll = (0, 0)lê / escreveVelocidade 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.
.viewBoxmontes.view.rightleituraA 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.
.namestr ou intcanvas.layers["far"].nameleituraNome (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étodoAssinaturaRetornaSignificado
.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
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
layer="default" / "top" / "ui"str"ui"lê / escreveCamadas 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=10int10lê / escrevePosição. Cria na hora; z congelado. Também: layer=below("default").
shape.layerstr, int ou None"top"lê / escreveMuda 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).

Neste Endless

Endless(layer, pieces, axis=, step=, gap=, start=, seed=, bottom=, left=, group=, height=, width=, size=)

ParâmetroPassarTipo (default)Significado
layer1ºstr ou intUma 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).
pieces2ºcaminho, lista de caminhos ou funçãoO que vai em cada vaga. Lista mista (imagem + função) levanta erro: varie dentro da função com slot.rng.choice([...]).
axisnome="x" / "y" / "xy""x"Horizontal (corredor), vertical (nave subindo) ou grade (mapa visto de cima).
stepnome=número, par ou funçãoNoneTamanho 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.
gapnome=número ou (mín, máx)0Espaço depois de cada imagem; um par sorteia por vaga. Só para imagens sem step.
startnome=número ou parNoneOnde começa a vaga 0. Padrão: a borda da tela quando o Endless nasce (a primeira tela já vem cheia).
seednome=int ou NoneNoneMesmo número = mesmo mundo. None = mundo novo a cada partida.
bottomnome=númeroNoneImagens com axis="x": o y da base. Padrão: a base da tela.
leftnome=númeroNoneImagens com axis="y": o x da esquerda. Padrão: a esquerda da tela.
groupnome=str ou listaNoneGrupo das imagens. A função dá grupo às suas peças.
height · width · sizenome=númeroNoneTamanho 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

MembroRetornaSignificado
.pieceslistaAs 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.
.seedintO 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)

CampoRetornaSignificado
slot.i · slot.jintÍndice: 0, 1, 2… à frente de start; −1, −2… atrás. j só em "xy".
slot.left · slot.right · slot.bottom · slot.topnúmeroBordas no eixo infinito, em coordenadas do mundo (left/right em "x", bottom/top em "y", as quatro em "xy"). O outro eixo é seu.
slot.boxBoxA célula ("xy").
slot.rngrandom.RandomO sorteio desta vaga: slot.rng.uniform(…), slot.rng.choice(…), slot.rng.randint(…).
slot.chance(p)boolComo 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 andaA câmera fica parada
Gêneroscorredor, plataforma, flappy, nave com cavernanave vertical com ondas de inimigos
Coisas do mundomontadas uma vez nas coordenadas do slot; não se movemcarona: group["solo"].move_by(0, -v * dt)
Coisas da telacarona: group["tela"].move_by(dx, 0)tela = mundo: Tracks com pontos fixos funcionam
Parallaxsozinho (pan_response)scroll da camada (layer.scroll muda a velocidade)
Endlessjogo e decoraçãosó decoração; o jogo chega por tempo (every)
Sumir o que saicanvas.playfield.set(canvas.game_view.outset(m)) por quadroo playfield padrão basta
Parar no fim de jogopare a câmera: tudo paranuvens.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:

pegas = set()

def trecho(slot):
    pecas = [Rect(top_left=(slot.left, 40), width=800, height=40, group="chao")]
    if slot.i not in pegas:
        moeda = Circle(center=(slot.left + 400, 120), radius=12, group="moeda")
        moeda.trecho = slot.i
        pecas.append(moeda)
    return pecas

@collide("heroi", "moeda")
def pegou(heroi, moeda):
    pegas.add(moeda.trecho)
    moeda.destroy()

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)

NomeAssinaturaRetornaSignificado
group=Parâmetros: str ou lista—No construtor. Tag, ou lista de tags. Ex.: group="wasp".
tag(*names)Parâmetros: names (nomes)o próprio objetoEntra no grupo.
untag(*names)Parâmetros: names (nomes)o próprio objetoSai do grupo.
tagged(name)Parâmetros: name (nome)True/FalseEste Entity está no grupo?
clone(group=)Parâmetros: group (str, lista ou None)o objeto copiadoOmitir copia as tags atuais. Passar substitui (não mistura). group=None = nenhum.

Ler e percorrer

NomeTipo (valor típico)Retorna / lêSignificado
group["wasp"]Parâmetros: name (nome)o grupoQuem 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 inteiro3leituraQuantos membros agora. Tag: O(1). Grupo fórmula: reconta (O(n)).
.emptyboolTrueleituraTrue se não tem ninguém. Não use em until() — use until_empty.
w in group["wasp"]boolTrueleituraEste Entity está no grupo? Uma string ("nave" in group["wasp"]) levanta TypeError: para testar uma tag, w.tagged("wasp").
.first / .lastobjetowleituraO primeiro que entrou / quem está no fim agora. O mesmo que [0] / [-1]. Grupo vazio: erro (não None).
group["wasp"][0]objetowleituraO 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]leituraFatia: 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 inteiroPosiçã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

NomeAssinaturaRetornaSignificado
group["wasp"].metodo(…)Os argumentos do métodoo grupo, ou listaChama em cada membro. Ver a regra do retorno em Agir em massa.
group["wasp"].hp = 3atribuiçã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 grupoPõe / tira a tag de todos os membros atuais.
.destroy()Sem parâmetroso grupoDestroy em todos os membros atuais.

Buscar

NomeAssinaturaRetornaSignificado
.nearest(point, where=, within=)Parâmetros: point (ponto), where (função), within (número)objeto ou NoneO 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ópiaCó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/FalseAlgum / todos passam no teste. all num grupo vazio: True.
.count_where(pred)Parâmetros: pred (função)número inteiroQuantos passam no teste.
.centroidponto ou NoneleituraPonto médio dos membros. Vazio: None.
.boxBox ou NoneleituraBox 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)

NomeTipoRetornaSignificado
group["a"] | group["b"]cópia do grupogrupo (cópia)União. Também .union().
group["a"] & group["b"]cópia do grupogrupo (cópia)Interseção. Também .intersection().
group["a"] ^ group["b"]cópia do grupogrupo (cópia)Só num dos dois. Também .symmetric_difference().
group["a"] - group["b"]cópia do grupogrupo (cópia)Diferença. Também .difference().
group["a"] <= group["b"] / .issubset(other)boolTrueTrue/FalseSubconjunto: 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)boolTrueTrue/FalseSuperconjunto: a contém todos de b (iguais também). Aceita o grupo ou o nome. Não é cópia.
.as_set()Sem parâmetrossetCongela a vista num set Python dos membros de agora.
.is_liveboolTrueleituraA vista acompanha o grupo? True numa tag ou grupo fórmula; False numa cópia.

Grupo vivo nomeado e espera

NomeAssinaturaRetornaSignificado
group["enemies"] @= lambda: …Parâmetros: lambda, grupo vivo ou nome (str)o grupoNomeia 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_linkedboolTrueleituraEste nome tem fórmula?
.unlink()Sem parâmetroso grupoTira 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 objetoSome no destroy(). Nomes de grupo no @collide veem quem entrar depois — prefira-os se a Entity morre e volta.o mesmo que o nome do grupo

Onde mais procurar — Colocar · Aparência · Frame · Mover

Colocar

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.

zone = Box(center=(400, 300), width=800, height=220)
inner = zone.inset(24)
halo = zone.outset(24)
hud = zone.strip("top", 40)                 # faixa interna
sidebar = zone.beside("right", width=120)   # vizinho externo
cell = canvas.game_view.inset(40).grid(4, 3).cell(0, 0)
Rect(box=cell, color="navy")

# os mesmos modelos do Rect:
Box(top_left=(80, 640), width=160, height=80)
Box(p1=(100, 100), p2=(500, 300))
Box(p1=(100, 300), p2=(500, 300), height=40, anchor="square")
Neste Box

Box(center=, width=, height=, angle=) · Box(source) · Box.from_graphic(graphic)

Construtores

ChamadaSignificado
center=, width=, height=O caminho usual. angle= opcional.
top_left=, width=, height=Canto + tamanho
p1=, p2=Cantos opostos
p1=, vec=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étodoAssinaturaRetornaSignificado
group["wasp"].boxsem argumentosBox ou NoneCaixa alinhada aos eixos que cobre todos os membros (um Box novo), ou None se vazio. Ângulo 0. Não é construtor de Box.

Leitura

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.center .width .height .angleponto ou número(400, 300)lê / escreveGeometria. 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 .topnúmero80leituraBordas. y para cima: top é o maior y. Só leitura: para mudar a área, crie outro Box (ex.: box.outset(40)).
.col_index / .row_indexint ou None0lê / escreveNa célula de uma grade. None num Box solto.
.is_collapsedboolFalseleituraSó leitura. True se largura ou altura for 0 (linha ou ponto). Mesma regra de “sem interior” que contains_point. Sem aviso.

Métodos

MétodoAssinaturaRetornaSignificado
inset(margin)Parâmetros: margin (número)BoxRecuo 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)BoxRecuo 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)BoxFaixa 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)BoxVizinho 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)GridFatia. gap é o vão entre células. Ver Grid.
contains_point(x, y=None)Parâmetros: x (um ponto, ou o número x com y)True/FalseGeometria. Não inscreve clique. Tabuleiro: g.cell_at(mouse.x, mouse.y).
rotate_to / rotate_byvalor (número); about= (ponto, opcional); force=Falseo 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étodoAssinaturaRetornaSignificado
for cell in giterável—Linha 0 primeiro, esquerda → direita.
g.cell(col, row)col (int), row (int)BoxColuna primeiro, como (x, y).
g.cell(n)n (int)BoxPlano: col = n % col_count, row = n // col_count.
.rows / .colslistas—Listas de listas. Contagens: .row_count / .col_count.
.cell_at(x, y)ponto ou x, yBox ou NoneInverso. 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_indexint—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
Tipobox= sozinhoCom tamanho explícito
Rect / EllipsePreenche (pose + tamanho + ângulo)Misturar levanta erro
Circle / Square / N-gon regularInscreve (lado menor). Circle não herda o ânguloSó centra (radius= / length= / side=)
Sprite / SvgCabe (aspecto do PNG; herda ângulo)Só centra (height= / width= / size=)
Textcenter= o centro da região (camada ui); align="center"; width da região se omitido; fora da colunaNã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
Neste canvas
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
canvas.width / .heightnúmero2000leituraTamanho do espaço de design. Defina em init(width=…, height=…) antes de run(). A janela OS é outra coisa: window_scale e canvas.display.
canvas.aspect_rationúmero1.67leiturawidth / height.
canvas.game_viewBox novocanvas.game_viewleituraJanela 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_viewBox novocanvas.ui_viewleituraJanela na vista [0, W] × [0, H]. HUD / layer="ui" / Text de vista. Não se mexe com a câmera.
canvas.playfieldplayfieldcanvas.playfieldleituraÁ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

MembroRetornaSignificado
Level(path)LevelLê o arquivo .json salvo pelo editor. path é o nome do arquivo, procurado como as imagens.
level["nome"]LevelItemO 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 LevelItemOs 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 · .groupstupla de nomesTodos os nomes; todos os grupos usados (ordem do primeiro uso).
.size · .box(largura, altura) · BoxA área da fase, de (0, 0) até size. .box é um Box novo a cada leitura.
.pathtextoCaminho absoluto do arquivo lido.

LevelItem — todo item

MembroRetornaSignificado
.kindtexto"rect" · "ellipse" · "circle" · "line" · "polygon" · "regular_polygon" · "track" · "point" · "image"
.name · .groupstexto ou None · tuplaNome e grupos dados no editor. Um item pode estar em vários grupos.
.centerVec2Centro. Linha: o meio; polígono e caminho: o meio da caixa que os envolve; ponto: o próprio ponto.
make(**kwargs)o shape novoCria 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

TipoCampos.box
rect · ellipse.width .height .angleBox orientado (com o giro)
image.image (caminho absoluto) .width .height .angleBox orientado
circle.radiuso 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
pointsó .centernã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.boxmake() = 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 faseRó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 editorOs 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.

Onde mais procurar — Desenhar · Mundo · Geometria

Frame

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.

def update(dt):
    ship.move_by(0, 200 * clock.dt)

ship.on_update = lambda e, dt: e.rotate_by(40 * dt)
clock.paused = True
Neste clock

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
clock.dt / .timenúmero0.016leituraTempo 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_dtnúmero0.016leituraSegundos 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.scalenúmero1lê / escreve1 normal, 0 pausa, 0.5 câmera lenta.
clock.pausedboolFalselê / escreveAtalho de scale == 0. False devolve o scale anterior.
e.time_scalenúmero1lê / escreveMultiplicador extra neste Entity (timers, tweens, wait, on_update). Padrão 1.

Métodos

MétodoAssinaturaRetornaSignificado
e.on_updateParâ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))
PodeNão pode
campo @= lambda: … clock.time … (também .dt, .frame, .scale, …)campo @= clock.time — número cru → TypeError; use lambda
@when(lambda: clock.time > 5) (o Scena confere a condição a cada frame)clock["time"] @= … — time/dt não são campos que se ligam com @=
Só clock.dt no update — sem custo extraship.angle = lambda: clock.time * 40 — use ship["angle"] @= lambda: …

Não confundir

PareceÉNão é
Fórmula com clock.time só atualiza quando algo mudaTodo 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étodoAssinaturaRetornaSignificado
keyp("A")Parâmetros: key (tecla)True/FalseA 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/FalseA segurada; modificadores ignorados.
keyp.once("A")Parâmetros: key (tecla)True/FalseTrue 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/Falsekeyp("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/FalseAtalho: 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/FalseBotã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)
NomeOnde ficaTecla no USTecla 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()
PodeNão pode
campo @= lambda: … if keyp("D") … / held / only / atalhokeyp.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 keypkeyp["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 andkeyp.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).

def update(dt):
    if mouse.press(Mouse.BTN_LEFT):
        marker.move_to(mouse)

def on_mouse_scroll(xoffset, yoffset):
    factor = 1.1 if yoffset > 0 else 1 / 1.1
    canvas.zoom_by(factor, about=mouse)

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
mouse.x / .ynúmero300leituraPosiçã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 / .dynúmero0leituraQuanto o ponteiro andou desde o frame anterior, em unidades.

Métodos

MétodoAssinaturaRetornaSignificado
mouse.press()Sem parâmetrosTrue/FalseAlgum 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/FalseAquele 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)
PodeNão pode
["center"] @= mouse / campo @= lambda: mouse.x / (mouse.x, mouse.y) / mouse.press(…)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 mousemouse["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 cliquePress/scroll: widgets → shapes (top, depois o resto) → callback do script. on_mouse_move corre junto, não é exclusivo.exclusivo

Onde mais procurar — Reativo · Reagir · Mover

Mover

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.

move_to / rotate / scale → move / rotate / scale
Parâmetros que levam tempo (time= / ease= / loops=…) → Parâmetros de tempo (time=)
Viajar por uma curva → move_along
Segurar na região → confine
Interpolar Attribute → tween

move / rotate / scale

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)
Movermove_to(x, y) — Vec2 / Point, about=move_by(dx, dy) — Vec2, polar angle= / distance=, só distance= (para a frente)
Girarrotate_to(θ) — arco curtorotate_by(dθ) — como escrito
Escalarscale_to(s)scale_by(f)
Por uma curvamove_along(track, time=) ou move_along(track, speed=)

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

move_to(x, y, *, time=None, speed=None, ease="linear", loops=1, yoyo=False, face=False, then=, on_done=, about=, force=False) · move_to(ponto, *, …)

ParâmetroPassarTipo (default)Significado
x, y1º · 2ºnúmero400Destino em duas coordenadas.
ponto1ºpontoDestino 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).
about=nome=ponto ou NoneO ponto que chega exatamente no destino (não o que fica fixo). Padrão: centro do shape. Com face=True, a rotação é em torno desse ponto.
parâmetros de temponome=—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

move_by(dx, dy, *, time=None, speed=None, ease="linear", loops=1, yoyo=False, face=False, then=, on_done=, force=False) · move_by(Vec2, *, …) · move_by(angle=, distance=, *, …) · move_by(distance=, *, …)

ParâmetroPassarTipo (default)Significado
dx, dy1º · 2ºnúmero20Deslocamento cartesiano.
Vec21ºVec2Delta num único argumento. Point levanta TypeError — use move_to. Tupla (dx, dy) não entra.
angle=, distance=nome=número90Polar. 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.
parâmetros de temponome=—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

rotate_to(θ, *, time=None, speed=None, ease="linear", loops=1, yoyo=False, then=, on_done=, about=, force=False)

ParâmetroPassarTipo (default)Significado
θ1ºnúmero45Ângulo absoluto.
about=nome=ponto ou NoneO ponto em torno do qual o objeto gira; ele fica parado. Padrão: o centro do objeto.
parâmetros de temponome=—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

rotate_by(dθ, *, time=None, speed=None, ease="linear", loops=1, yoyo=False, then=, on_done=, about=, force=False)

ParâmetroPassarTipo (default)Significado
dθ1ºnúmero30Graus a somar.
about=nome=ponto ou NoneO ponto em torno do qual o objeto gira; ele fica parado. Padrão: o centro do objeto.
parâmetros de temponome=—time= ou speed= (graus por segundo; o tamanho de dθ ÷ speed, também com about=) · ease= · loops= · yoyo= · then= · on_done= · force=.

Muda o tamanho do objeto para value vezes o tamanho original (1 = original, 2 = dobro, 0.5 = metade), igual na largura e na altura.

c.scale_to(1.5)
c.scale_to(0.5, time=0.3, ease="ease_out")
c.scale_to(1.05, time=0.45, ease="steps", loops="forever", yoyo=True)

scale_to(value, *, time=None, ease="linear", loops=1, yoyo=False, then=, on_done=, about=, force=False)

ParâmetroPassarTipo (default)Significado
value1ºnúmero1.5Fator absoluto (1 = tamanho original).
about=nome=ponto ou NoneO ponto a partir do qual o objeto cresce ou encolhe; ele fica parado. Padrão: o centro do objeto.
parâmetros de temponome=—time= · ease= · loops= · yoyo= · then= · on_done= · force=. Sem speed=: escala é razão, não distância.

Multiplica a escala atual. Com time=, o fator congela no início.

c.scale_by(2)
c.scale_by(0.5, time=0.25)
c.scale_by(1.1, time=0.3, loops="forever", yoyo=True)

scale_by(factor, *, time=None, ease="linear", loops=1, yoyo=False, then=, on_done=, about=, force=False)

ParâmetroPassarTipo (default)Significado
factor1ºnúmero1.2Fator relativo (2 = dobro do atual).
about=nome=ponto ou NoneO ponto a partir do qual o objeto cresce ou encolhe; ele fica parado. Padrão: o centro do objeto.
parâmetros de temponome=—time= · ease= · loops= · yoyo= · then= · on_done= · force=. Sem speed=: escala é razão, não distância.
.angle / .scale / reset_rotation
c.angle = 45
c.scale = 1.5
c.reset_rotation()
turret["angle"] @= lambda: dial.value
ship["scale"] @= lambda: 1.0 + 0.15 * st.pulse

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.angle / .scalenúmero15lê / escreveEstado 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_headingnúmero90leituraO â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.
.tiltnúmero · contato · "ground""ground"lê / escreveSprite. Inclina só o desenho, sem girar o colisor. Ver Inclinar.

Métodos

MétodoAssinaturaRetornaSignificado
reset_rotation(*, time=None, speed=None, ease="linear", then=, on_done=, about=, force=False)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 PointTypeError. 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 andaTypeError. 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 centroGira 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 pontoA 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âmetroPassarTipo (default)Significado
time=nome=número ou NoneNoneSegundos 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 NoneNoneVelocidade 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"1Mesma grafia de move_along / play_sound. Padrão 1. "forever" nunca dispara then= / on_done= — pare com handle.cancel() ou destroy.
yoyo=nome=boolFalseVoltas í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=boolFalseSó 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ítuloMode 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 NoneNoneCallback 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 NoneNoneUm 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=boolFalseSó 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 coisaUm verbo de pose leva time= (segundos da viagem). Um transiente ou uma capa dura duration (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 coisaspeed= é 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 alvoO 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= juntosEscolha 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 .velCom 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")

move_along(target, *, time=None, speed=None, ease="linear", span=(0, 1), phase=0, loops=1, yoyo=False, face=False, at=None, then=, on_done=, force=False)

ParâmetroPassarTipo (default)Significado
target1ºTrack, Shape ou strA 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.15Quanto 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 NoneOnde 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).
parâmetros de temponome=—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=.
TemaRegra
Frotagroup["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_toUm move_along e um move_to(..., time=) no mesmo objeto se cancelam: o último vence.
O Shape seguido someSe 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.
HandleReporta 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 viajaColoca: 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")

g.confine(region, *, by="center", action="clamp") → frozenset

ParâmetroPassarTipo (default)Significado
region1ºBox ou GraphicPLAYA 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.

ValorSignificado
"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.

ValorSignificado
"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.
ball.vel = Vec2(220, 140)

def on_update(self, dt):
    self.move_by(self.vel.x * dt, self.vel.y * dt)
    sides = self.confine(arena, action="bounce")
    if "bottom" in sides:
        play_sound("thud")

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_playfieldresolve
PapelSegurar / wrap / quicar a pose agoraAvisar quando o centro sai do playfieldEmpurrar corpos que colidem
QuandoNo seu update / on_updatePasso do frame (depois da colisão)Passo do frame (colisão)
RegistroNão — só a chamadaSim (grupo ou método)Sim (resolve(a, b))
Muda pose?SimNã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étodoAssinaturaRetornaSignificado
g.confine(region, *, by=, action=)Parâmetros: region (Box ou Graphic), by (str), action (str)frozensetMantém o objeto na região. Escreve o centro (e .vel se action="bounce"). As bordas devolvidas seguem os lados da região.

Erros

CasoErro
by= / action= desconhecidoTypeError com a lista permitida
action="bounce" sem self.velTypeError — dica: self.vel = Vec2(…)
by="box" / "overlap" em Graphic sem .boxO mesmo TypeError de graphic.box

Não confundir

PareceÉNão é
confine(PLAY) uma vez já vale para sempreUma 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
Neste tween

Opções

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
time=número ou NoneNoneparâmetroAqui: segundos. Sem: na hora. Com: interpola. Regra completa: parâmetros de tempo.
ease=str ou função"linear"parâmetroAqui: curva do tween. parâmetros de tempo.
start=mesmo tipo de to(0, 0)parâmetroValor 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.
loops=1, N ou "forever"1parâmetroAqui: voltas. "forever" nunca dispara then= / on_done=. Exige time=. parâmetros de tempo.
yoyo=boolFalseparâmetroAqui: voltas ímpares invertem a direção. Precisa loops > 1 ou "forever". parâmetros de tempo.
then=função do capítuloparâmetroAqui: mode a entrar no fim. parâmetros de tempo.
on_done=função ou NoneparâmetroAqui: callback no fim. parâmetros de tempo.
force=boolFalseparâmetroAqui: campo ligado com @=: False levanta LinkedAttributeError; True desfaz a ligação e avisa. parâmetros de tempo.
name=str ou None"pulse"parâmetroSlot: 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étodoAssinaturaRetornaSignificado
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, nameHandleAlvo: 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, nameHandleIgual, neste Entity. Sobrevive a troca de mode; morre no destroy.

Não confundir

PareceÉNão é
tween(…) no corpo do mode sobrevive à trocatween(…) 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.

Onde mais procurar — Agendar · Colisão · Desenhar · Parâmetros de tempo (time=)

Agendar

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 / every → after / every
Cooldown → ready
Janela de tempo → recently
then= / on_done= → Ao terminar
O retorno do tween → Handle

after / every

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).

after(2.0, boom)
every(1.2, drop, name="spawn")
ship.after(0.4, then=diving)
h = after(3.0, then=leaving)
h.cancel()
h.pause()
h.resume()

No módulo. O Handle vive em fn.handle.

@after(2.0)
def boom():
    explode()
# boom.handle.cancel()

@every(1.2, name="drop")
def drop():
    spawn()

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).

class Wasp(Sprite):
    @every(2.4, name="fire")
    def volley(self):
        shoot(self)

    @after(0.4)
    def armed(self):
        self.mode = self.diving
Neste after / every

Opções

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
then=função do capítuloparâmetroSó em after: mode a entrar quando o tempo acaba. parâmetros de tempo.
on_done=função ou NoneparâmetroSó em after: função a chamar quando o tempo acaba (o mesmo que fn). parâmetros de tempo.
name=str"spawn"parâmetroSlot: 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étodoAssinaturaRetornaSignificado
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)HandleUma 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)HandleRepete. Sem then= e sem on_done= (não termina).
e.after / .everyParâmetros: segundos (número), fn (função)HandleNeste 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 / @everyDecorator — 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= juntosEscolha 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 frameValueError. 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étodoAssinaturaRetornaSignificado
ready(name, interval)Parâmetros: name (nome), interval (intervalo)True/FalseTrue 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/FalseMesma 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étodoAssinaturaRetornaSignificado
recently(name, value, hold=)Parâmetros: name (nome), value (valor), hold (número)True/FalseTrue 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/FalseMesma 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.

after(0.4, then=diving)
ship.move_to(400, 90, time=0.8, then=leaving)
ship.tween(ship["radius"], 80, time=0.4, on_done=boom)
ship.dissolve_out(0.4, on_done=ship.destroy)
canvas.fade_out(0.5, on_done=load_next)
Ondethen= (mode)on_done= (callback)
afterEntra nesse mode.Chama a função. O mesmo que passar fn.
pose / tween com time=Entra nesse mode (voltas finitas). loops="forever" nunca dispara.Chama a função (voltas finitas). loops="forever" nunca dispara.
transienteNão tem.Chama quando o efeito acaba — também quando um efeito novo do mesmo canal o substitui no meio.
play_soundEntra nesse mode.Chama no fim natural (não no cancel, nem depois que o dono acabou).
canvas.fade_out / fade_inNão tem.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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.doneboolTrueleituraJá terminou (sucesso ou cancelado).
.cancelledboolFalseleituraParou via cancel / troca de mode / destroy.
.pausedboolFalseleituraCongelado com pause(). Só True enquanto vivo e congelado. Só leitura.
.elapsednúmero1.2leituraSegundos de jogo desde que a tarefa começou (ou desde o último disparo de every).
.remainingnúmero0.8leituraO 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.handleHandledrop.handlelê / escreveDecorator de módulo: o Handle daquele @after / @every.

Leitura de movimento

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.velocityVec2Vec2(80, 0)leituraUnidades 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.
.speednúmero80leituraMódulo de .velocity. Só leitura.
.headingnúmero90leituraGraus, 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.
.unúmero0.25leituraOnde o objeto está na trilha, de 0 (início) a 1 (fim), já contando span= / phase=. Só move_along. Só leitura.
.lapnúmero inteiro0leituraNúmero da volta atual, começando em 0. loops="forever" continua a contar. Só move_along. Só leitura.

Métodos

MétodoAssinaturaRetornaSignificado
await hSem parâmetrosEspera (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âmetroso próprio HandleCongela 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âmetroso próprio HandleDescongela. 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).

Onde mais procurar — Mover · Mode · Reagir · Som

Mode

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.

Entrar num capítulo → mode
wait / parallel / race → espera async
Cancelar o capítulo → escopo e cancelamento

mode

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.

@on_mode
async def diving(e):
    await e.move_to(player.center.x, 90, time=0.8, ease="ease_in")
    await wait(0.2)
    e.mode = leaving

wasp.mode = diving
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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
e.modemode ou Nonee.mode = e.divinglê / escreveA função atual, ou None. Compare com ==. Mesmo mode não faz nada.
e.mode_timenúmero1.2leituraSegundos neste capítulo. Zera ao entrar / reentrar.

Métodos

MétodoAssinaturaRetornaSignificado
e.goto_mode(fn)Parâmetros: fn (função do capítulo)—Sempre sai e entra (mesmo mode ou não).
@on_modeDecorator — 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étodoAssinaturaRetornaSignificado
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 inteiroO 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:

  • after / every (chamada solta)
  • wait / until / until_empty / parallel / race
  • tween(…) (sem o e.)
  • move_to / move_by / move_along / rotate_* / scale_* com time=
  • play_sound (chamada solta)
  • o script async do mode

Não cancelam na troca de mode:

  • 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 / comoEscopoTroca de mode
No corpo do mode: after / every / wait / tween / move_*(time=) / …"mode"cancela
e.after / e.every / e.tweensempre a Entitynão cancela (morre no destroy / cancel_process)
after(…, bomb.destroy) de outro Entity, de dentro do modeaquele outro Entitynã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 todonão cancela; vale o jogo todo
script solto depois do topo (update, funções de evento, o setup de um Act)"act" — o Act atualnão cancela; sai no change_act
transientes / playfora do schedulernão cancela

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().

QueroChamo
Pausar um Handle (continua pendente)h.pause() / h.resume()
Pausar timers do Entity (continuam pendentes)e.active = False
Cancelar timers, tweens, sons e o modee.cancel_process()
Remover de veze.destroy()
Sair do mode com exit hookse.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.

Onde mais procurar — Agendar · Entity · Reagir

Atos

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.

Escrever um ato → Act
Começar / trocar / recomeçar → change_act
O que sai, o que volta, o que fica → o palco e o ato
Pontos, vidas, fase → dados do jogo
@when, @collide e fades num ato → regras do ato

Act

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)
Numa classe de ato
EscreveSignifica
setup(self)Monta o ato. Sem parâmetros — dados vêm de st.
on_update(self, dt)Lógica por frame deste ato.
@every / @after / @when / @on_changeRegras do ato: ligam ao entrar, somem ao sair.
self.mode = self.frotaUm mode do ato (capítulos dentro de um ato).
self.after(…) / self.play_sound(…)Do ato: param na troca (um som que acaba sozinho termina de tocar).
self.aviso = Text(…)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.

ErroCorreçã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 runO 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 trocaErro 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 atoO quê
SaiObjetos (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).
Ficast. 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 trocaGuarde 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ó depoisCrie 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 palcoNã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 × modeMode é 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.

Onde mais procurar — Mode · escopo e cancelamento · st · Câmera

Som

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.

Tocar / material / bake → play_sound
Volume mestre, nível, espectro, amp_db → audio
Captura / gravar → Microphone · mic.record
Filtros / eco / distorção → efeitos

play_sound

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étodoAssinaturaRetornaSignificado
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, nameHandleToca. 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.
Sound(path)Parâmetros: path (str); volume (padrão); stream (True/False, opcional)SoundMaterial. 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)SoundAplica 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.
e.play_sound(src)Iguais a play_soundHandleSempre escopo Entity: sobrevive à troca de mode; para no destroy (um som que acaba sozinho termina de tocar).

No Handle de som

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.volume / .pan / .pitchnúmero0.6lê / escreveMudam 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étodoRampa de volume. Relógio do mundo (respeita clock.scale), como qualquer tween.
.fade_out(time=)métodoh.fade_out(0.3)métodoRampa até 0 e para (fim natural: on_done dispara). Não é cancel. Sem cauda de Echo/Reverb depois.
.levelnúmero 0…10.4leituraQuã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ê / escreveDivide 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.
.effectspilhah.effects.add(LowPass())leituraVer efeitos. Só no Handle / mic — não no Sound.

Do Handle genérico

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.remainingnúmero0.4leituraTempo 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
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
audio.volumenúmero0.8lê / escreveGanho mestre. 0 silencia tudo sem cancelar Handles.
audio.mutedboolFalselê / escreveTrue silencia tudo; False volta ao volume de antes.
audio.level_tausegundos0.08lê / escreveQuanto 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.levelnúmero 0…10.4leituraQuã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úmeroslê / escreveDivide 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ê / escreveConverte 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étodoAssinaturaRetornaSignificado
Microphone(backends=None)Parâmetros: backends (lista de miniaudio.Backend; avançado — testes / diagnóstico)MicrophoneAbre 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, thenSoundComeç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.
.recordingboolboolSó leitura. True enquanto está gravando.
.level / .spectrum(bins=8)bins no spectrumnúmero / listaMesma 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.
.effectsPilha—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.durationsegundosnúmeroCresce enquanto grava (amostras ÷ 44100). Fica fixo quando a gravação acaba. Só leitura.
await clipo Sound devolvido por record()o mesmo SoundEspera 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

FimGuarda o áudioon_done / then=
mic.stop()sim — tocável na linha seguinteno próximo frame
seconds= atingidosimno próximo frame
teto de 30 ssimno próximo frame, mais um aviso
sair do mode / destroy() / mic.close()simnã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 tudomic.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
NomeParâmetrosO que faz
LowPass(cutoff=300)cutoff: frequência de corte, em HzAbafa: 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 HzO 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.

Onde mais procurar — Handle · Mode · Reativo

Reagir

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.

Bool virou True → @when
Valor mudou → @on_change
Tecla / mouse / setup / update → Callbacks do script
setup / on_update / on_destroy da Entity → Callbacks da Entity
Clique num objeto → Clique / arraste
Saiu do playfield → @leave_playfield
Corpos se tocam → Colisão
Segurando / todo frame → Frame
Daqui a N segundos → Agendar
Esta Entity está neste capítulo → Mode
Widget (Button.on_click, valor ligado) → UI

@when

@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: ….

st.hp = 3
@when(lambda: st.hp <= 0)
def on_death():
    player.destroy()

@when(lambda: keyp("Y"))
def on_y():
    restart()

unregister_event(on_death)

Precisa de lambda. A expressão tem que devolver True ou False — senão TypeError.

Dispara na borda False→True. Enquanto continua True, não dispara de novo.

@when(lambda: player.hp <= 0)
def on_death():
    player.destroy()

@when(lambda: keyp("Y"))
def on_y():
    restart()

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çaSignificado
@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 avaliaTodo 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étodolambda self: …. Vale para cada objeto da classe, desde que ele é criado.
Vários @when no mesmo defOk: as duas expressões disparam o mesmo corpo. Misturar com @on_change é TypeError.
Misturar com @every / @collide / @on_modeTypeError.
unregister_event(fn)Tira todos os @when / @on_change daquela função.
until / wait / parallelEspera 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).

st.wave = 1
@on_change(st["wave"])
def on_wave(value):
    spawn(value)

@on_change(lambda: c.radius)
def on_radius(value):
    ring.radius = value

Na classe: lambda self: …. Célula crua (st["hp"]) só no módulo. Callback: def fn(self) ou def fn(self, value).

class Ship(Sprite):
    @on_change(lambda self: self.hp)
    def on_hp(self, value):
        self.flash(0.12)
Contrato
PeçaSignificado
@on_change(alvo)Qualquer valor. Algo reativo do Scena (Attribute) ou lambda. Dispara quando o valor novo é != do anterior. Callback 0 ou 1 arg (o valor novo).
Quando avaliaTodo 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étodolambda self: …. Vale para cada objeto da classe, desde que ele é criado. Callback: def fn(self) ou def fn(self, value).
Vários @on_change no mesmo defOk: as duas expressões disparam o mesmo corpo. Misturar com @when é TypeError.
Misturar com @every / @collide / @on_modeTypeError.
unregister_event(fn)Tira todos os @when / @on_change daquela função.

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.

def setup(canvas):
    canvas.fx_quality = "high"

def update(dt, canvas):
    if keyp("RIGHT"):
        player.move_by(200 * dt, 0)

def on_key_down(key):
    if key == "ESC":
        canvas.reset_view()

def on_mouse_click(x, y, button):
    marker.move_to(x, y)

def on_mouse_scroll(xoffset, yoffset):
    factor = 1.1 if yoffset > 0 else 1 / 1.1
    canvas.zoom_by(factor, about=(mouse.x, mouse.y))

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étodoAssinaturaRetornaSignificado
setupParâmetros: canvas (o canvas)—Uma vez, janela aberta. Não é o setup(self) da Entity.
updateParâ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_downParâ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_upParâ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_keyParâ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_charParâmetros: char (str)—Caractere Unicode.
on_mouse_downParâ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_upParâ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_clickParâ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_clickParâmetros: x (número), y (número), button (número: Mouse.BTN_LEFT, BTN_RIGHT…)—Duplo clique. Mundo.
on_mouse_moveParâ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_dragParâ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_scrollParâ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.
post_drawParâmetros: skia_canvas (canvas Skia), scene_image (imagem)—Recurso de exceção: você desenha a imagem final da tela, no lugar de toda a lista de efeitos de pós-processamento (Post). Não é evento.

Um atalho não é nome de evento: nunca key == "CTRL+R". TextInput focado: ver Teclado. Button.on_click (recebe o botão, ou nada) é UI.

Não há outro canal de evento no script além dos desta seção e do mapa no topo: @when / @on_change, clique num objeto, @leave_playfield, colisão, leitura a cada frame em Frame, @= em Reativo, timers em Agendar, @on_mode em Mode, widgets em UI.

Callbacks da Entity

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.

class Ship(Sprite):
    def setup(self):
        self.hp = 3

    def on_update(self, dt):
        self.rotate_by(40 * dt)

    def on_destroy(self):
        canvas.burst_at(self)

ship.on_update = lambda e, dt: e.rotate_by(40 * dt)
Assinaturas
MétodoAssinaturaRetornaSignificado
setupParâmetros: self (este objeto)—Uma vez, depois do construtor. Aqui nascem os campos próprios: self.hp = 3. Não é o setup(canvas) do script.
on_updateParâmetros: self (este objeto), dt (número)—A cada frame, nesta Entity, inclusive durante await. Instância: fn(e, dt). Ver Frame.
on_destroyParâmetros: self (este objeto)—Uma vez, de dentro de destroy(). Não dispara em troca de mode.
on_click / on_drag_* / on_hover / on_hover_exitParâmetros: ver Clique / arraste—Shape, Sprite e Text (hover também em widget). Ver Clique / arraste.
on_collide / on_collide_stay / on_collide_exitParâmetros: me (objeto), other (objeto), info (CollisionInfo)—Shape, Sprite e SpatialFx. Ver Colisão.

Clique / arraste / hover

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.

@hover(group["cards"])
def lit(card):
    card.set_color("gold")

@hover_exit(group["cards"])
def unlit(card):
    card.set_color("gray")

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étodoAssinaturaRetornaSignificado
@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âmetroPassarTipo (default)Significado
from1ºShapetokenO shape que está sendo arrastado.
to2ºShape ou NonezoneO 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.
pos3º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.

token = Circle(center=(200, 200), radius=24, color="coral")
token.clickable = True
token.on_click = lambda shape: shape.set_color("gold")

token.draggable = True
def on_drag(from_shape, to_shape, pos):
    from_shape.center = pos
token.on_drag_to = on_drag
MétodoAssinaturaRetornaSignificado
on_clickParâmetros: shape (objeto)—Equivale a @click(shape). shape.on_click = fn ou método na classe.
on_drag_toParâmetros: from (objeto), to (objeto ou None), pos (ponto)—Equivale a @drag_to(shape). No shape arrastado, no soltar.
on_drag_overParâmetros: from (objeto), to (objeto ou None), pos (ponto)—Equivale a @drag_over(shape). No shape arrastado, durante.
on_drop_byParâmetros: from (objeto), to (objeto ou None), pos (ponto)—Equivale a @drop_by(zone). No alvo droppable, no soltar.
on_hoverParâmetros: shape (objeto)—Equivale a @hover(shape). Torna o shape alvo de hover sem torná-lo clicável.
on_hover_exitParâmetros: shape (objeto)—Equivale a @hover_exit(shape).
Em todo Graphic

Todo Graphic (Shape, Sprite, Text) tem estes métodos e propriedades. SpatialFx não — não é Graphic. Widget é UI.

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
clickableboolTruelê / escreveAceita clique. Decorator / callback ligam sozinhos.
draggableboolTruelê / escreveArrasta o corpo inteiro.
droppableboolTruelê / escreveAlvo de drop. @drop_by liga.
pointer_hoverboolTrueleituraLeitura. 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_pressedboolTrueleituraLeitura. 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_rotateboolTruelê / escreveTrue: com o botão do mouse segurado sobre o objeto, a roda do mouse o gira. Independente de draggable.

Métodos

MétodoAssinaturaRetornaSignificado
contains_point(x, y=None)Parâmetros: x (um ponto, ou o número x com y)True/FalseEste 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.
token = Circle(center=(200, 200), radius=24, color="coral")
token.clickable = True
token.draggable = True
token.on_click = lambda shape: shape.set_color("gold")

zone = Rect(center=(500, 300), width=120, height=80, color="navy")
zone.droppable = True

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
canvas.playfield.pad = 0.4número0.4lê / escreveFraçã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 .topnúmero-40lê / escreveBordas. y para cima: top é o maior y. Graváveis uma a uma.
.width .height .centernúmero ou ponto120leituraDerivados, só leitura. .center tem .x / .y.
.padnúmero0.4lê / escreveNú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).

Métodos

MétodoAssinaturaRetornaSignificado
canvas.playfield.set(box) / canvas.playfield.set(left=, bottom=, right=, top=)Parâmetros: box (Box) — ou left (número), bottom (número), right (número), top (número)—Retângulo alinhado aos eixos, no mundo: um Box (lido uma vez) ou as quatro bordas, todas obrigatórias. Os dois juntos: erro.
.inset(margin) / .outset(margin) / .grid(…) / .beside(…) / .strip(…)iguais ao BoxBox / GridCópias para layout. Não escrevem .pad. Ver Colocar.

Ctrl+Shift+7 desenha o playfield (ciano) quando algum @leave_playfield está em uso. Alias: canvas.bounds.

No módulo: um ou mais nomes de grupo. Quem entrar no grupo depois também passa a ser vigiado. Callback: fn(item).

@leave_playfield("bolt", "sting")
def leftover(shot):
    shot.destroy()

@leave_playfield("star")
def missed(star):
    star.destroy()
    st.lives -= 1

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.

Onde mais procurar — Frame · Colisão · Mode · Reativo

Colisão

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.

@collide("bullet", "enemy")
def on_hit(me, other, info):
    other.destroy()

@collide_stay("zone", "player")
def inside(me, other, info):
    other.hp -= clock.dt

@collide(player, ANYTHING)
def bump(me, other, info):
    other.flash(0.1)

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.

FaseQuandoFunçãoMétodoObjeto
enterPrimeiro frame de sobreposição.@collide(a, b)@collide_with(alvo)on_collide
stayCada frame seguinte ainda sobrepostos.@collide_stay(a, b)@collide_with_stay(alvo)on_collide_stay
exitDeixaram 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.

@collide("bullet", "enemy")
def on_hit(me, other, info):
    other.destroy()

@collide_stay("zone", "player")
def inside(me, other, info):
    other.hp -= clock.dt

@collide_exit("player", "door")
def left_door(me, other, info):
    me.set_color("cyan")

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.

ball.on_collide = lambda me, other, info: me.set_color("yellow")
def drain(me, other, info):
    other.hp -= clock.dt                 # lambda não aceita -=; use def
ball.on_collide_stay = drain
ball.on_collide_exit = lambda me, other, info: me.set_color("cyan")

class Probe(Circle):
    def on_collide(self, other, info):
        self.flash(0.1)

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 defTypeError. 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, nunca resolve(). Moeda não empurra ninguém. Ver Tornar sólido.sólido
Contornos que se cruzamDetectar é sobreposição de corpos. Cruzamento de contornos: intersections. Sair do playfield: @leave_playfield.@collide

CollisionInfo

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
info.a / info.bEntityinfo.aleituraOs dois corpos deste par. No callback: a é me, b é other. Parte de hitbox: aqui vem o Sprite dono.
info.a_hitbox / info.b_hitboxHitbox ou Noneinfo.b_hitbox is hurtleituraA parte (add_hitbox) de cada lado; None = o corpo. Com parte, push_apart / bounce_apart / resolve dão TypeError (parte só detecta).
info.normalVec2info.normalleituraDe a rumo a b (unitário). Zero se não houver.
info.depthnúmeroinfo.depthleituraQuanto se sobrepõem. 0 quando um objeto rápido já passou do outro.
info.pointponto ou Noneinfo.pointleituraUm (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.pointslistainfo.pointsleituraTodos 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.sidestr ou Noneinfo.side == "bottom"leituraLado de a no contato: top/bottom/left/right.

Rampa

NomeTipo / assinaturaRetornaSignificado
info.slope_anglenúmeroinfo.slope_angleleituraÂngulo da superfície em graus (0 = chão plano). Contínuo — não os 4 baldes de side.
info.tangentVec2info.tangentleituraVetor unitário ao longo da superfície (perpendicular a normal).
info.slide_along(velocity)Parâmetros: velocity (Vec2)Vec2Projeta velocidade na tangente — andar/deslizar numa rampa.

Métodos que empurram (info.resolve, push_apart, bounce_apart, mtv_split, velocity_split, reflect_velocity): Resolução manual.

Não confundir

PareceÉNão é
VFX em info.points[0]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 choqueAinda 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:

@collide("hero", "star")
def caught(me, star, info):
    star.destroy()
    st.score += 1

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.

ChamadaEfeito
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).

ball = Circle(center=(200, 200), radius=15)
ball.mass = 1.0
ball.vel = Vec2(180, 0)

floor.mass = STATIC
resolve(ball, floor, bounce=1.0)   # elástico
resolve(ball, wall, bounce=0.0)    # inelástico — zera componente normal na .vel

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 andaCola. 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éteisNã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)move_by, move_to(..., time=), move_by(..., time=), move_along

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 baixoA cabeça bate ("top"): atravessa.
O topo do pulo fica dentro da plataformaContinua atravessando e cai de volta: não passou do topo.
Passa do topo e caiOs pés tocam ("bottom"): pisa.
Anda contra a lateralAtravessa (como no Mario). Para segurar, liste os lados.
Anda sobre blocos encostados com um degrau de até 2Sobe o degrau e continua em cima (não atravessa). Degrau maior é lateral: atravessa.
Parado em cimaChão normal: touching_sides, coyote, tilt = "ground", elevador.
Cai muito rápidoO 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 baixoonly="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 doisValueError: 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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
STATICconstantewall.mass = STATIClê / escreveImóvel — chão, parede. float("inf").
shape.massnúmero1.0lê / escrevePeso no empurrão. Padrão 1.0. <= 0 levanta erro.
shape.velVec2 ou Noneball.vel = Vec2(0, -200)lê / escreveVelocidade 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_enabledboolTruelê / escreveAvanç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étodoAssinaturaRetornaSignificado
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
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
touching_sidesfrozenset{"bottom"}leituraLados em contato com par resolve(). Reativo quando uma fórmula depende (ex. grounded @=).
touching_contactstuplaplayer.touching_contactsleituraUm TouchingContact por par resolve(). Não reativa. Tabela abaixo.

Cada item de touching_contacts (TouchingContact)

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.otherGraphicc.otherleituraO outro corpo do par.
.sidestr ou None"bottom"leitura"top" · "bottom" · "left" · "right" — lado deste corpo que toca.
.slope_anglenúmero22leituraÂ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.
.normalVec2c.normalleituraNormal unitária (deste corpo → o outro).
.tangentVec2c.tangentleituraTangente unitária à superfície.
.pointponto ou Nonec.pointleituraPonto 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étodoAssinaturaRetornaSignificado
.slide_along(velocity)Parâmetros: velocity (Vec2)Vec2Projeta 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 pisaLeia 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 frameReflete 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 .angleGira 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étodoAssinaturaRetornaSignificado
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 NoneAnda 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 pisafloor_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ívelChamadaQuando
1resolve(a, b, bounce=)Registro permanente — degrau de todo dia
2shape.resolve_now(alvo, bounce=)Detecta + resolve agora; devolve lista de CollisionInfo
3info.resolve(bounce=)Dentro da função de colisão — par já detectado
4info.push_apart() / info.bounce_apart(bounce=)Avançado — aplica com massas explícitas, não necessariamente .mass
5info.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étodoAssinaturaRetornaSignificado
shape.resolve_now(target, bounce=)Parâmetros: target (objeto ou nome de grupo), bounce (número, opcional)lista de CollisionInfoDetecta 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.
info.bounce_apart(mass_a=, mass_b=, bounce=1.0)Parâmetros: mass_a (número), mass_b (número), bounce (número)—velocity_split + escreve .vel. bounce aqui já vale 1.0 por padrão (diferente de resolve(), que pula a velocidade quando omitido).
info.mtv_split(mass_a=1, mass_b=1, slop=0)Parâmetros: mass_a (número), mass_b (número), slop (número)dois deslocamentos (dx, dy)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.
info.velocity_split(mass_a=, mass_b=, bounce=, vel_a=, vel_b=)Parâmetros: mass_a (número), mass_b (número), bounce (número), vel_a (Vec2), vel_b (Vec2)dois Vec2Só 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)Vec2Reflete 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).

Neste collision
MétodoAssinaturaRetornaSignificado
collision.query_point(x, y=None, *, group=, ignore=)Parâmetros: x (um ponto, ou o número x com y); group, ignore (opcionais)listaOs objetos cujo hitbox contém o ponto. Um ponto (query_point(mouse)) ou dois números. Não é contains_point (desenho / clique).
collision.query_circle(x, y=None, radius=None, *, group=, ignore=)Parâmetros: x (um ponto, ou o número x com y); radius (número ≥ 0); group, ignore (opcionais)listaOs 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.
collision.query_sector(origem, angle=, radius=, spread=, group=, ignore=)Parâmetros: origem (ponto); angle (graus, opcional); radius e spread (números ≥ 0); group, ignore (opcionais)listaQuem 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)listaQuem se sobrepõe à geometria de shape. group=: tag, lista / set (OR), group['nome'], ANYTHING. A própria sonda não é hit.
shape.collides_with(group=)Parâmetros: group (str ou lista)listaAtalho: query_shape(self, group=).
test_collision(a, b)Parâmetros: a (objeto), b (objeto)CollisionInfo, ou NoneTeste pontual entre dois alvos.
collision.raycast(p1, p2= / vec= / angle= length=)Parâmetros: p1 (ponto); um fim: p2 (ponto), ou vec (deslocamento), ou length com angle opcional; group, ignore (opcionais)tupla, ou NoneO 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.

CorpoO que colide
ShapeA geometria do modelo (círculo, retângulo, polígono…).
TrackUma 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.
SpriteO hitbox calculado (o corpo): forma e arte escolhidas por hitbox_shape= / hitbox_source=, congelado ao animar. Ver Hitbox do Sprite.
Parte do Spritesprite.add_hitbox(group=…) — colisor extra, só detecção, que segue o desenho e responde pelos grupos que reivindica. Ver Partes.
SpatialFxO 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.

torso = Rect(center=(500, 300), width=80, height=120)
head = Circle(center=(500, 390), radius=30)
body = Collider([torso, head])

@collide(body, "bolt")
def on_hit(me, other, info):
    other.destroy()

Collider(shapes)

Parâmetro / campoPassarTipoSignificado
shapes1ºlista de Shapes[torso, head]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 NoneAs 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.

Onde mais procurar — Mover · Reagir · Geometria

Geometria

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

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.x / .ynúmero1lê / escreveOs componentes: quanto a seta anda em x e em y.
.length / .anglenúmero200leituraComprimento 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).

Métodos

MétodoAssinaturaRetornaSignificado
Vec2(x, y)Parâmetros: x (número), y (número)Vec2Construtor.
vec_from_angle(angle, length=1, reference_vec=None)Parâmetros: angle (número), length (número), reference_vec (Vec2)Vec2Graus. 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)Vec2Soma / subtrai outro vetor.
* / /Parâmetros: númeroVec2Multiplica / divide por um escalar. Vec2 à esquerda: v * 2, v / 2.
.dot(other)Parâmetros: other (Vec2)númeroProduto escalar: positivo se as setas apontam para o mesmo lado, 0 se são perpendiculares, negativo se opostas.
.normalize()Sem parâmetrosVec2Mesmo rumo, comprimento 1.
.rotate(angle)Parâmetros: angle (número)Vec2Gira em graus (anti-horário).
.lerp(other, t)Parâmetros: other (Vec2), t (número)Vec2Mistura 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úmeroDistância. Mesma métrica que distance (Point, tupla, Graphic, …).
.reflect(normal)Parâmetros: normal (Vec2)Vec2Reflete sobre a normal. O mesmo que .reflect_about.
.cross(other)Parâmetros: other (Vec2)númeroProduto vetorial no plano: positivo se other está à esquerda desta seta (anti-horário), negativo se à direita.
.project_onto(onto)Parâmetros: onto (Vec2)Vec2Projeção sobre onto.
.reflect_about(normal)Parâmetros: normal (Vec2)Vec2Quique numa superfície com essa normal: Vec2(1, -1).reflect_about(Vec2(0, 1)) → (1, 1).
.perp_left() / .perp_right()Sem parâmetrosVec2Perpendicular à esquerda / à direita.
.clamp_length(min_length, max_length)Parâmetros: min_length (número), max_length (número)Vec2Mesmo 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 @=).

c1 = Circle(center=(400, 400), radius=120)
c2 = Circle(center=(520, 400), radius=90)

chord = Line(
    p1=lambda: intersection(c1, c2, 0),
    p2=lambda: intersection(c1, c2, 1),
)

def update(dt):
    chord.visible = len(intersections(c1, c2)) == 2
Assinaturas
MétodoAssinaturaRetornaSignificado
intersections(a, b)Parâmetros: a (shape), b (shape)lista de pontosCruzamentos dos contornos agora. [] se nenhum.
intersection(a, b, i=0)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
TemaRegra
PermitidosLine, Circle, Ellipse, Rect, Square, Polygon, RegularPolygon, EquilateralTriangle, Box. Outro tipo (Point, Track, Sprite) → TypeError.
ContornoLine = 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).
OrdemCom 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.
QuantosElipse × círculo ou elipse: até 4 pontos. Tangente conta 1.
O que intersection devolvehits[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).

p = middle_point(a, b)
d = distance(hero, enemy_ship)
hero.angle = heading(hero, enemy_ship)
target = nearest(player, group["wasp"])
dot.center << closest_point(planet, mouse)
tiro = Sprite("laser.png", center=nave.center + (0, 28))
abaixo = nave.center - (0, 20)

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.

Helpers
MétodoAssinaturaRetornaSignificado
middle_point(p1, p2, weight=0.5)Parâmetros: p1 (ponto), p2 (ponto), weight (número)ponto (x, y)Tupla (x, y). Point, Vec2, tupla, Graphic ou mouse.
distance(p1, p2)Parâmetros: p1 (ponto), p2 (ponto)númeroDistância euclidiana (≥ 0). Também p1.distance(p2) no Point (e Vec2).
heading(origin, target)Parâmetros: origin (ponto), target (ponto)númeroRumo 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 NoneO 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 Vec2PointTranslaçã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)—erroTypeError, de propósito: += colocaria o objeto direto no lugar, atravessando paredes. Viajar: move_by. Colocar: center = center + (dx, dy).
ponto == outro—True/FalseNum 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/FalseEste 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/FalseTrue 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
TemaRegra
origin · targetDe onde se olha e para onde. A ordem importa: trocar os dois inverte o rumo em 180°.
Quem entraPoint, Vec2, (x, y), Graphic, mouse — os mesmos de distance / nearest.
CoincidenteMesmo lugar → 0.
Sprite.angle já é rumo no mundo (art_angle entra no desenho). Não some art_angle outra vez.
Neste nearest

nearest(origin, items, *, within=None, where=None)

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âmetroPassarTipo (default)Significado
origin1ºponto(400, 300)Point, Vec2, (x, y), mouse, ou algo com .center / .x/.y (Shape, Sprite).
items2ºiterávelgroup["wasp"]Lista, tupla, group["wasp"], poly.points, … Itens sem posição são ignorados.
within=nome=número ou None200Distância máxima (inclusiva). Omitir = sem limite. Negativo → erro.
where=nome=função (True/False) ou Nonelambda w: w.hp > 0Uma 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=).

target = nearest(player.center, group["wasp"])
snap = nearest(mouse, poly.points)
coin = nearest(player.center, coins, within=40)
foe = group["wasp"].nearest(player.center, within=200, where=lambda w: w.hp > 0)
Neste closest_point

closest_point(shape, p)

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))
TemaRegra
shape · pO Shape onde procurar e o ponto de referência. Shapes aceitos: os mesmos de Interseções; outro tipo → TypeError.
DentroDisco / 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)).

Onde mais procurar — Colocar · Colisão · Desenhar · Utilitários

UI

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.

ui.scale = 0.85
ui.font = "Menlo"
st.volume = 0.5
st.mute = False
Text("Volume")
Slider(st["volume"], label="Volume", max_value=1)   # faixa 0…1
CheckBox(st["mute"], label="Mute")
Button(label="Reset", on_click=reset)
Neste ui

Propriedades

PropriedadeTipo (valor típico)Leitura/EscritaSignificado
ui.scale / ui.fontnúmero ou str0.85lê / escreveDefina 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_spacenúmero50lê / escreveLargura 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.

Métodos

MétodoAssinaturaRetornaSignificado
ui.stack_reset() / stack_origin / stack_from / stack_columnParâmetros: ver Coluna—Cursor da coluna. Ver Coluna.
ui.gap()Sem parâmetros—Vão extra. Ver ui.gap.
ui.stack_column()Parâmetros: gap= opcional—Próxima coluna, mesmo topo. Ver ui.stack_column.
Construtor (todo widget)
ParâmetroPassarTipo (default)Significado
1º argumento (cell)1ºcélulast["volume"]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.5Valor de partida. Se vier junto com um Attribute, grava este valor nele.
width= / height=nome=número260 / 28Tamanho do controle, em unidades de design. Omitido: o do tema (260 × 28, vezes ui.scale). Slider só tem width=; CheckBox usa size=.
Neste widget
PropriedadeTipo (valor típico)Leitura/EscritaSignificado
.data / .valueAttribute ou númeroslider.datalê / 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}".
visibleboolFalselê / escreveFalse esconde: não desenha e não recebe cliques. Para mostrar desligado (esmaecido, sem responder): enabled = False.
pointer_hoverboolTruelêEste widget é o alvo de hover (um widget ganha de um Shape ou Sprite embaixo dele). Ver Hover.
pointer_pressedboolTruelê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_exitfunção ou Nonelambda w: …lê / escreveEntrou / saiu do hover. fn(widget) ou fn(). Mesmo que @hover / @hover_exit.
group=str"opts"construtorWidgets 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.
.boxBoxslider.box.top_rightlê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.

st.volume = 0.5
st.mute = False
Text("Volume")
Slider(st["volume"], label="Volume", max_value=1)

ui.stack_column()
Button(label="Outra coluna")

ui.stack_origin(400, 700)
Button(label="Coluna absoluta")

ui.stack_from(hud)
CheckBox(st["mute"], label="Mute")

ui.stack_reset()
Text("De novo no topo")
Métodos em ui

ui.stack_reset() · ui.stack_origin(x, y) · ui.stack_from(item) · ui.stack_column()

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étodoAssinaturaRetornaSignificado
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.
ui.stack_column()Parâmetros: gap= opcional—Mesmo topo visual, x depois da coluna. Ver ui.stack_column.
ui.gap()Sem parâmetros—Soma um vão extra do tema. Ver ui.gap.

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
Construtor

Button(cell, label=, on_click=, value_on=, value_off=, width=, height=, enabled=)

Nome sem =: 1º na ordem. Nome com =: pelo nome.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaOpcional (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"], …).
labelnome=str"Button"Texto escrito no botão.
on_clicknome=função ou NoneCallback: 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_offnome=qualquer ou NoneValores escritos no cell ao pressionar / soltar. Padrão True / False.
widthnome=número260Largura do botão, em unidades de design. Omitida: a do tema (260 × ui.scale).
heightnome=número28Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enablednome=boolTrueFalse: 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))
Construtor

SpriteButton(source, center=, label=, font=, font_size=, label_color=, label_inset=, on_click=, enabled=, hover=, pressed=, disabled=, hover_sound=, click_sound=, hover_scale=, press_scale=, breathe=, breathe_time=, …)

Mais os parâmetros do Sprite (width=, height=, size=, frame=, layer=, box=, …).

ParâmetroPassarTipo (default)Significado
source1ºarquivo ou SheetA arte do botão. Uma Sheet com um clipe anima como num Sprite.
labelnome=str, número, célula ou funçãoTexto 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"].
font / font_sizenome=nome ou arquivo / número40 % da alturaFonte e tamanho do rótulo, como em Text.
label_colornome=cor, Gradient ou TexturebrancoCor do rótulo.
label_insetnome=número ou (esq, cima, dir, baixo)0Margem da caixa do rótulo dentro da arte — para borda ou "degrau" 3D embaixo. O rótulo centraliza e encaixa nessa caixa.
on_clicknome=função ou NoneAo soltar em cima: fn() ou fn(botao). Também botao.on_click = fn ou @click(botao).
enablednome=bool ou funçãoTrueFalse: 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 / disablednome=arquivo, quadro ou clipeNoneArte 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_soundnome=arquivo, Sound ou NoneSom ao entrar o hover / a cada clique. O som do clique continua tocando se o clique chama change_act.
hover_scale / press_scalenome=número1.06 / 0.94Tamanho desenhado no hover / no aperto. 1 desliga. Só no desenho: scale continua o seu.
breathenome=bool ou númeroFalsePulsa devagar enquanto parado: True = ±5 %, ou a amplitude (0.03). Pausa no hover, no aperto e desligado.
breathe_timenome=segundos1.6Duração de uma respiração.
Neste SpriteButton
PropriedadeTipoLeitura/EscritaSignificado
labelstr ou None"Jogar"lê / escreveO texto mostrado. Escreva uma string ou número. Para ligar: botao.label_text["text"] @= lambda: ….
label_textText ou NoneleituraO 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_inset(esq, cima, dir, baixo)lê / escreveMargem da caixa do rótulo.
enabledboolTruelê / escreveLigado. Ligável: botao["enabled"] @= lambda: ….
on_clickfunção ou Nonelê / escreveO clique.
hover_sound / click_soundarquivo, Sound ou Nonelê / escreveSons.
hover_scale / press_scalenúmero1.06lê / escreveTamanho desenhado no hover / aperto.
breathe / breathe_timenúmero0.05lê / escreveAmplitude (0 = parado) e duração da respiração.
pointer_hover / pointer_pressedboolleituraEm hover / apertado agora (ver Clique / arraste).

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.

st.volume = 0.5
st.vidas = 3
Slider(st["volume"], label="Volume", max_value=1)
Slider(st["vidas"], label="Vidas", min_value=1, max_value=9, only_int=True)
Construtor

Slider(cell, label=, min_value=, max_value=, initial=, show_value=, only_int=, width=, enabled=)

Nome sem =: 1º na ordem. Nome com =: pelo nome.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaCélula numérica. Ex.: Slider(st["volume"], …).
labelnome=str ou NoneLegenda à esquerda da barra. Omitida: o nome do campo (Slider(st["volume"]) mostra st.volume).
min_value / max_valuenome=número0 / 100Os dois extremos da barra. Iguais, ou mínimo maior que o máximo, levantam ValueError.
initialnome=número ou NoneNoneValor de partida; gravado no Attribute. Sem ele e sem Attribute: o meio da faixa.
only_intnome=boolFalseTrue: o puxador anda de inteiro em inteiro (vidas, nível).
show_valuenome=boolTrueMostra o valor atual à direita da barra.
widthnome=número260Largura de tudo (legenda + barra + número), em unidades de design. Omitida: a do tema (260 × ui.scale).
enablednome=boolTrueFalse: 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.

st.mute = False
st.debug = False
CheckBox(st["mute"], label="Mute")
CheckBox(st["debug"], label="Colliders", initial=False)
Construtor

CheckBox(cell, label=, initial=, size=, enabled=)

Nome sem =: 1º na ordem. Nome com =: pelo nome.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaCélula bool. Ex.: CheckBox(st["mute"], …).
labelnome=str"CheckBox"Legenda à direita da caixa.
initialnome=boolFalseMarcada ou não no começo; gravado no Attribute.
sizenome=número18Lado da caixa, em unidades de design. Omitido: o do tema (18 × ui.scale).
enablednome=boolTrueFalse: 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)
Construtor

TextInput(cell, placeholder=, label=, max_length=, on_submit=, width=, height=, initial=, top_left=, enabled=)

Nome sem =: 1º na ordem. Nome com =: pelo nome.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaCé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"], …).
placeholdernome=str""Dica cinza com a caixa vazia
labelnome=str ou NoneNoneLegenda à esquerda. width inclui a legenda. Empilhados, Sliders e TextInputs com legenda dividem a mesma coluna de legendas.
max_lengthnome=int ou NoneNoneMáximo de caracteres ao digitar ou colar. Um valor maior vindo do código aparece inteiro.
on_submitnome=função ou NoneRoda 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.
initialnome=str ou númeroNoneTexto de partida; gravado no Attribute.
widthnome=número260Largura da caixa, legenda incluída, em unidades de design. Omitida: a do tema (260 × ui.scale).
heightnome=número28Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enablednome=boolTrueFalse: esmaecida, não recebe foco, Tab pula. Desligar uma caixa focada grava o texto.
.text—strO 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—boolTrueSó 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étodoDá 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—boolSó 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.

st.palette = "night"
st.weapon = "laser"
DropDown(st["palette"], options=["night", "sunset", "day"])
DropDown(st["weapon"], options=["laser", "rocket"], initial="laser")
Construtor

DropDown(cell, options=, initial=, width=, height=, enabled=)

Nome sem =: 1º na ordem. Nome com =: pelo nome.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaCélula da seleção. Ex.: DropDown(st["palette"], …).
optionsnome=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"].
initialnome=um item de options"laser"Opção marcada no começo; gravada no Attribute. Fora da lista: a primeira.
widthnome=número260Largura da caixa fechada, em unidades de design. Omitida: a do tema (260 × ui.scale).
heightnome=número28Altura, em unidades de design. Omitida: a do tema (28 × ui.scale).
enablednome=boolTrueFalse: 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—strO 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.

st.nave = "Azul"

def escolher(opcao):
    pass

Menu(st["nave"], options=["Azul", "Vermelha"], on_choose=escolher, title="Sua nave")

async def modo(e):
    escolha = await Menu(options=["Continuar", "Sair"], title="Pausa", pause=True)

def update(dt):
    if keyp.once("ESC"):
        Menu(options=["Continuar", "Título"], title="Pausa", pause=True)
Construtor

Menu(cell, options=, on_choose=, title=, cols=1, modal=True, pause=False, center=, box=, font=, font_size=, color=, background=, icons=, group=)

options= é o único obrigatório. cell é opcional. center= e box= não vêm juntos.

ParâmetroPassarTipo (default)Significado
cell1ºcélulaOpcional. Célula que recebe a opção escolhida. Ex.: Menu(st["nave"], …). None levanta (seria st.nave ainda vazio, sem colchetes).
optionsnome=lista["Jogar", "Sair"]Escolhas. O rótulo é str(). O valor escolhido é o item, não o texto.
on_choosenome=funçãoescolherescolher(opcao). Roda em todo clique, inclusive a mesma opção de novo. Não roda em destroy().
titlenome=strFaixa no topo. Omitir = só as opções.
colsnome=int1Quantas colunas de opções: 1 = lista vertical; len(options) = uma linha só; outro número = grade.
modalnome=boolTrueEscurece 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).
pausenome=boolFalseTrue pausa o jogo (clock.paused) enquanto o menu está aberto; ao fechar, clock.paused volta ao que era antes. Vale com modal=False.
centernome=ponto ou funçãoCentro na janela (espaço da vista), valor ou lambda. Acompanha todo frame. O painel fica inteiro na tela. Não segue a câmera.
boxnome=BoxPreenche a caixa. A fonte encolhe até um mínimo legível; abaixo disso corta e avisa.
font / font_sizenome=str / número"pixel.ttf"Família ou arquivo .ttf / .otf. Omitir font usa ui.font.
colornome=cor"white"Cor dos rótulos. O destaque da opção sob o mouse sai dessa cor quando ela é dada.
backgroundnome=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.
iconsnome=lista de arquivosUma 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
TemaRegra
Ao escolherFecha, devolve a pausa, escreve o Attribute, completa o await, destrói, depois chama on_choose.
await menuA 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.
TeclasCom 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.
OndeSem 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 é
DropDownMenu fecha ao escolher e bloqueia o jogo. DropDown fica na coluna e não pausa.o mesmo widget
Button para cada opçãoMenu é 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.

st.volume = 0.5
st.palette = "night"
Text("Volume")
Slider(st["volume"], label="Volume", max_value=1)
ui.gap()
Button(label="Reset", on_click=reset)

ui.gap()
ui.gap()
DropDown(st["palette"], options=["night", "day"])
Chamada

ui.gap()

Sem argumentos. Sem objeto. O vão extra é o mesmo do tema entre itens (12 em ui.scale=1).

MétodoAssinaturaRetornaSignificado
ui.gap()Sem parâmetros—Soma um vão extra. O vão normal entre itens continua.
Aplica em——O próximo Text de HUD ou widget criado sem top_left=.
Coluna vazia——O 1º item ignora a folga; ela fica para o 2º.

ui.stack_reset / stack_origin / stack_from / stack_column zeram folgas pendentes. Item com top_left= explícito não consome. Não é um Box. Ver Coluna.

ui.stack_column

ui.stack_column() faz o próximo item começar uma nova coluna, à direita da atual e na mesma altura do topo dela. Nada é desenhado.

st.volume = 0.5
st.pan = 0.0
st.mute = False
Slider(st["volume"], label="Volume", max_value=1)
Slider(st["pan"], label="Pan", min_value=-1, max_value=1)

ui.stack_column()
CheckBox(st["mute"], label="Mute")
Button(label="Reset", on_click=reset)

ui.stack_column(gap=24)
Text("Mais uma")
Chamada

ui.stack_column() · ui.stack_column(gap=24)

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étodoAssinaturaRetornaSignificado
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 telaui.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.um ponto absoluto — isso é ui.stack_origin(x, y)

Onde mais procurar — Reativo · Mundo · ui_view

Utilitários

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
Assinatura

remap(value, in_min=0, in_max=1, out_min=0, out_max=1, modifier=None)

Parâmetros: value, in_min, in_max, out_min, out_max (números); modifier (nome ou função). Retorna número.

ParâmetroPassarTipo (default)Significado
value1ºnúmero0.5Valor de entrada.
in_min, in_max2º, 3ºnúmero (0, 1)0, 100Intervalo de origem. in_min == in_max devolve out_min.
out_min, out_max4º, 5ºnúmero (0, 1)0, 255Intervalo 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âmetroPassarTipo (default)Significado
a, b1º, 2ºmesmo tipoColor("red")Extremos. Número · tupla/lista · Vec2 · Color · Point.
t3ºnúmero0.5Quanto 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.

bar.width = chase(bar.width, hp, rate=8.0)
look = chase((canvas.pan_x, canvas.pan_y), hero.center, rate=6.0)
canvas.pan_to(look)
Assinatura

chase(current, target, rate, dt=None)

Parâmetros: current, target (mesmo tipo / forma); rate (número); dt (número, opcional). Retorna o mesmo tipo que lerp(current, target, t).

ParâmetroPassarTipo (default)Significado
current, target1º, 2ºmesmo tipo(100, 80)Extremos. Número · tupla · Vec2 · Color · Point (devolve (x, y)).
rate3ºnúmero6Rapidez 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.
dt4º ou nome=númeroclock.dtOpcional. 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 seguiresse 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 velocidadenã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.

inimigo.move_to(approach(inimigo.center, jogador.center, 120))
bar.width = approach(bar.width, hp * 2, 80)

pos = approach(elevador.center, (400, 120), 90)
elevador.move_to(pos)
if pos == (400, 120):
    print("chegou")
Assinatura

approach(current, target, speed, dt=None)

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âmetroPassarTipo (default)Significado
current, target1º, 2ºnúmero ou pontohero.centerDe 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).
speed3ºnúmero120Velocidade, em unidades por segundo. Cada chamada anda speed * dt. Negativo → ValueError.
dt4º ou nome=númeroclock.dtOpcional. 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 mesmochase 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) perseguenã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))
Assinatura

deadzone(current, target, size)

Parâmetros: current, target (ponto); size (número ou par). Retorna (x, y).

ParâmetroPassarTipo (default)Significado
current, target1º, 2ºpontohero.centerPoint, Vec2, (x, y) ou Graphic (centro).
size3ºnúmero ou par(160, 90)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.

TemaRegra
p0 a 1. 0 nunca, 1 sempre. Fora disso → erro.
UsoUma 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.

Onde mais procurar — Mover · Geometria · Frame

Debug

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.

show_colliders / show_tracks → Métodos
HUD / overlay / pausa / tela cheia → Atalhos de teclado

Métodos

Funções que só desenham overlays de depuração por cima da tela. Zero efeito em colisão, física ou lógica de jogo.

collision.show_colliders(enabled=True)
show_tracks(enabled=True)
Neste Debug
MétodoAssinaturaRetornaSignificado
collision.show_colliders(enabled=True)Parâmetros: enabled (ligado, True/False)—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).

AtalhoFaz
Ctrl+Shift+1Liga / desliga o HUD de sistema (FPS, objs, mouse xy no mundo). Começa desligado.
Ctrl+Shift+2Liga / 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.
Ctrl+Shift+3Cicla perfil de render: low → medium → high.
Ctrl+Shift+4Cicla framerate: vsync → vsync60 → vsync30 → 60 → 30 → max.
Ctrl+Shift+7Overlay: 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+8Zoom de inspeção: 100% → 50% → 25%. Não muda canvas.zoom. Quadro de design / playfield; sem trilhas.
Ctrl+Shift+EFecha 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+PTempo: roda → pausa → ¼ → roda. Devolve o clock.scale do script ao completar o ciclo.
Ctrl+Shift+FTela 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+MMaximiza 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+ZSai.

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().

Onde mais procurar — Colisão / Auxiliares · Apresentação · Frame