ALAN JAHNEL / Engenharia de software

Artigos

GBA em Assembly — 7. Movendo um quadrado

Una controles, posição na RAM e desenho para mover um quadrado, respeitar as bordas e apagar a imagem anterior sem deixar rastros.

Já sabemos ler os botões e desenhar em um ritmo regular. Vamos unir essas duas coisas: um quadrado branco de oito por oito pixels vai andar pelo fundo preto usando o direcional.

O objetivo ainda não é criar um personagem completo. Vamos estudar posição na RAM, atualização por quadro, limites da tela e a necessidade de apagar a imagem anterior. Use movimento.s, disponível no pacote dos exemplos.

Roteiro deste capítulo

Antes de começar: você deve conseguir fazer A mudar a cor do quadrado no capítulo 6. Este capítulo usa o direcional.

  1. Salve movimento.s completo com os arquivos de apoio, construa com os quatro comandos abaixo e abra movimento.gba.
  2. Confira o mapeamento das quatro direções no emulador. O quadrado branco inicial tem oito por oito pixels e começa em (16,16).
  3. Segure Direita, solte e depois segure Baixo. Confira que o quadrado só anda enquanto uma direção está pressionada e deixa um fundo preto atrás de si.
  4. Teste cada borda separadamente. Relacione a parada visível com o limite de X ou Y explicado abaixo e acompanhe a sequência de apagar, atualizar e desenhar.

Ponto de conferência: o quadrado para nas quatro bordas e não deixa rastro. Se deixar rastro, investigue o apagamento antes de mudar os controles. Se nunca andar, repita a conferência de entrada do capítulo 6. Aumentar a velocidade ou mudar o tamanho é uma experiência opcional, não um requisito para o próximo capítulo.

A posição do objeto pertence ao programa

No Mode 3, o hardware não guarda um objeto chamado “quadrado branco” com propriedades X e Y. Ele guarda cores de pixels. Por isso, nosso programa precisa manter sua própria posição e decidir quais pixels escrever.

Reservamos duas palavras na RAM: hero_x e hero_y. O boot coloca 16 em cada uma. Elas representam o canto superior esquerdo do quadrado, cuja largura e altura são oito pixels.

É útil separar duas perguntas:

PerguntaQuem responde
Onde o quadrado deveria estar?As variáveis e regras do programa
Que imagem está visível agora?O conteúdo da VRAM interpretado pelo vídeo

Alterar hero_x sozinho não move um pixel. Precisamos usar a nova posição ao desenhar. Da mesma forma, desenhar em outra posição não altera automaticamente hero_x.

Traduzir os botões em uma mudança de posição

Os comandos são detalhados em cargas e armazenamentos, ADD e SUB e condições.

A rotina que atualiza as duas coordenadas é:

<!-- source: movimento.s#movement -->
Assembly ARM
update_position:
    ldr r3, =hero_x
    ldr r0, [r3]
    ldr r3, =hero_y
    ldr r1, [r3]
    ldr r3, =keys
    ldr r2, [r3]
    tst r2, #16
    addne r0, r0, #1
    tst r2, #32
    subne r0, r0, #1
    tst r2, #64
    subne r1, r1, #1
    tst r2, #128
    addne r1, r1, #1
    cmp r0, #0
    movlt r0, #0
    cmp r0, #232
    movgt r0, #232
    cmp r1, #0
    movlt r1, #0
    cmp r1, #152
    movgt r1, #152
    ldr r3, =hero_x
    str r0, [r3]
    ldr r3, =hero_y
    str r1, [r3]
    bx lr

Carregamos X, Y e a fotografia dos botões. Testamos cada direção e fazemos uma operação simples: direita soma um a X; esquerda subtrai um; cima subtrai um de Y; baixo soma um.

As instruções condicionais usam as flags do tst imediatamente anterior. addne significa “some se o teste não deu zero”. Sem o sufixo s, essa soma não altera as flags.

No final, gravamos as coordenadas de volta na RAM. Se esquecêssemos essa gravação, a próxima atualização partiria outra vez da posição antiga.

O exemplo aceita duas direções ao mesmo tempo. Direita e baixo alteram ambos os eixos. Direita e esquerda se anulam antes de aplicar os limites. Um jogo maior poderia escolher outra regra, mas ela precisaria estar explícita no programa.

Por que o limite direito é 232?

A diferença entre comparações com e sem sinal está na tabela de condições LT, GT, LO e HS.

O maior X possível de um pixel é 239. Mas estamos posicionando o canto de um objeto com oito pixels de largura. Para que ele caiba inteiro:

Texto simples
X máximo = 240 - 8 = 232
Y máximo = 160 - 8 = 152

Se o canto estiver em X igual a 232, o último pixel ficará em 239. Colocá-lo em 239 faria os pixels seguintes ultrapassarem a linha desejada.

As comparações depois dos botões limitam X à faixa de 0 a 232 e Y à faixa de 0 a 152. Usamos condições com sinal porque uma subtração na borda pode produzir -1. Esses valores pequenos e controlados cabem confortavelmente nessa interpretação.

Esse cuidado protege a rotina fill_rect, que não recorta áreas fora da tela. Os limites são parte das regras do chamador, não uma proteção automática oferecida pelo hardware.

Mover significa apagar e desenhar de novo

A distinção entre chamada com bl e encaminhamento com b é explicada em BL e BX.

O laço completo que combina as etapas é:

<!-- source: movimento.s#moving-loop -->
Assembly ARM
main_loop:
    bl wait_vblank
    bl tick
    bl read_keys
    mov r4, #0
    bl paint_hero
    bl update_position
    ldr r4, =0x7fff
    bl paint_hero
    b main_loop

Primeiro desenhamos preto na posição antiga, usando paint_hero. Depois mudamos a posição e desenhamos branco no destino. A rotina paint_hero lê as duas variáveis, prepara largura e altura oito e encaminha para fill_rect.

Ela termina com b fill_rect, sem bl. É um encaminhamento final: conserva o lr que recebeu do chamador, e fill_rect retorna diretamente a esse chamador. Não é obrigatório usar essa forma; ela apenas evita uma chamada e um retorno intermediários.

Se retirarmos o desenho preto, o quadrado deixará um rastro. A VRAM conserva as cores antigas até alguém sobrescrevê-las. Não existe uma limpeza automática entre quadros.

Em um fundo preto, apagar com preto funciona. Em uma estrada ou cenário detalhado, isso abriria um buraco. Precisaríamos restaurar o cenário sob o objeto ou usar outra organização gráfica. No artigo de Tower Defense, a estrada é restaurada antes de desenhar o inimigo atual.

A velocidade agora tem uma unidade

Cada atualização muda a posição em um pixel por eixo pressionado. Como esperamos um novo quadro a cada volta, a velocidade fica próxima de 59,73 pixels por segundo em cada eixo, quando o programa acompanha todas as atualizações.

Mudar o incremento de um para dois duplica essa velocidade. Porém, também altera como o objeto atravessa a tela: ele passa a visitar posições de dois em dois pixels. Para movimentos menores que um pixel por quadro, uma evolução possível é guardar a posição com uma parte fracionária, usando números de ponto fixo.

Mover em dois eixos ao mesmo tempo produz uma distância diagonal maior que mover em apenas um. Vamos aceitar isso neste exercício. Corrigir a velocidade diagonal seria outra regra de movimento, não uma capacidade que o direcional aplica sozinho.

Executar e testar as bordas

Shell
arm-none-eabi-as -mcpu=arm7tdmi -g -o movimento.o movimento.s
arm-none-eabi-ld -T gba.ld -o movimento.elf movimento.o
arm-none-eabi-objcopy -O binary movimento.elf movimento.gba
python3 rom.py movimento.gba

Abra movimento.gba no mGBA. O quadrado deve aparecer em (16,16). Segure cada direção e confira que ele para nas bordas sem desaparecer, voltar pelo outro lado ou deixar um rastro branco.

Como exercício, transforme o objeto em um retângulo de 16 por oito. Será necessário alterar a largura em paint_hero e o limite de X para 224. A altura e o limite de Y continuam iguais. Mudar só o desenho deixa a regra de limite incorreta.

Depois, experimente retirar temporariamente a chamada que apaga a posição antiga e observe o rastro. Recoloque-a antes de continuar. Uma alteração pequena com efeito previsível ajuda a entender qual parte do programa responde por cada comportamento.

Anterior: ler os botões. Próximo: acrescentar um alvo e uma condição de vitória. Índice da série.

Voltar ao início