
Vamos fazer um jogo de MSX – Estrutura e Organização
- Por oddbitmachine
Algumas semanas atrás, começamos o design do conceito do jogo e adicionamos alguns códigos básicos que resultaram no carregamento do nosso sprite na tela do modo gráfico, o que foi legal e divertido. O problema é que nosso código parece apressado, com muitos endereços de memória codificados e comandos repetidos em todo o lugar. Hoje vamos organizar tudo e garantir que as futuras adições sempre respeitem um padrão de estrutura mínima que nos será familiar nos próximos artigos e facilitará a manutenção do programa quando alguma atualização ou correção for necessária.
Temos que ser inclusivos, sempre
A maioria dos compiladores de montagem suporta o uso da instrução include
para adicionar código externo ao arquivo de programa principal, e o IDE 8bitworkshop não é diferente em relação a esse recurso. Para adicionar um arquivo externo ao nosso código, basta escolher onde queremos que a instrução seja adicionada ao código, ir ao menu IDE, selecionar File e Add Include File, depois inserir o nome do arquivo que queremos usar, e finalmente clicar em OK. Vou criar meu primeiro nome de arquivo incluído System_BIOSCalls.asm
e, em seguida, mova as rotinas de BIOS existentes para este novo arquivo:

Notaremos que a instrução include é adicionada automaticamente ao código e, no menu do lado esquerdo, encontraremos o novo nome do arquivo exibido na lista:

Clicar no nome do arquivo no menu mostrará uma janela de código vazia, então agora podemos recortar as entradas de rotina do BIOS do arquivo TheMeteorGame.asm para este novo:

Observe que se você deixar o código rodando no IDE você terá muitos erros após recortar o código do arquivo principal, mas tudo é corrigido novamente após colar as linhas no novo arquivo. Isso é importante observar, pois queremos que o código se comporte da mesma maneira que quando todas as linhas de código estavam localizadas no mesmo arquivo.
Também aproveitei a oportunidade e criei o arquivo System_SystemVariables.asm
, e em seguida movi as entradas referentes às variáveis do código principal para este outro lugar, e também o System_CartridgeHeader.asm para remover o cabeçalho do cartucho do código principal e manter as coisas ajustadas. Observe que a instrução org ainda precisa ser posicionada antes da instrução header, caso contrário o programa não funcionará corretamente:

Lembra-se daquela rotina maluca Set16x16Sprites que adicionamos no último artigo para fazer o VDP entender que 16x16 pixels é o tamanho do sprite que queremos usar e não o tamanho 8×8 que é o padrão para o TMS9918, por que não também mover essas linhas para outro arquivo específico para rotinas gráficas? eu criei o Function_SetScreen.asm
arquivo para armazenar qualquer código relacionado às configurações de tela e, em seguida, movi o código para lá.
Trabalhe de forma inteligente, não dura
Mover o código é fácil, mas há um limite de quão útil isso pode ser. Estou almejando ter um código reutilizável e evitar ter que repetir os mesmos comandos através do programa, e uma forma de conseguir isso que encontrei foi usar endereços de memória como variáveis, depois chamar as rotinas após definir os valores das variáveis. Muito complicado? Deixe-me demonstrar e tornar isso mais fácil de entender.
Vou pegar as linhas que alteram os valores das cores de primeiro plano, plano de fundo e borda e mover toda esta seção para o Function_SetScreen.asm
e fazê-lo usar os registradores B,C e D para definir os valores de cor:
; Define as cores de primeiro plano, plano de fundo e borda, chamando INITGRP em seguida InitGraphicMode: ld hl,FORCLR ; Carrega a variável sys FORCLR em HL ld (hl),b ; Carrega o valor de B na posição apontada por HL ld hl,BAKCLR ; Carrega a variável sys BAKCLR em HL ld (hl),c ; Carrega o valor de B na posição apontada por HL ld hl,BDRCLR ; Carrega a variável sys BDCLR em HL ld (hl),d ; Carrega o valor de B na posição apontada por HL call INIGRP ; Inicializa o modo gráfico VDP ret ; Retornar à rotina do chamador
Em seguida, precisamos substituir o código do arquivo principal pelas linhas abaixo, que carrega os registradores com os valores de cor desejados e então chama a rotina InitGraphicMode que criamos no arquivo Function_SetScreen.asm:
Init: ;Vamos inicializar as cores da tela ld b,15 ; Cor do primeiro plano ld c,13 ; Cor de fundo ld d,13 ; Cor da borda chamada InitGraphicMode ; Inicializa o modo gráfico com a cor em BCD
Basicamente, substituímos as 7 linhas de código do código original por um total de 14 linhas no novo código, o que não parece ser uma melhoria, mas lembre-se de que provavelmente mudaremos as cores novamente no futuro durante o desenvolvimento do jogo, o que eventualmente justificará essa mudança.
Código é uma coisa difícil quando as coisas são codificadas de forma fixa.
No artigo anterior, usei os endereços VRAM para carregar os padrões e atributos de cada sprite na memória. Isso exigia algum cálculo manual para descobrir onde os próximos valores deveriam ser carregados e também tornava o código bastante inflexível e difícil de ler e manter.
Felizmente, o MSX BIOS possui duas rotinas que calculam o endereço de memória do sprite com base em seu ID carregado em A, e retornam o valor no registrador HL, facilitando a compreensão e alteração do código:
CALPAT: equ 00084h ; Função: Retorna o endereço da tabela de padrões de sprite ; Entrada: A-ID do Sprite ; Saída: HL - Para o endereço ; Registros: AF, DE, HL CALATR: equ: 00087h ; Função: Retorna o endereço da tabela de atributos do sprite ; Entrada: A - Número do Sprite ; Saída: HL - Para o endereço ; Registros: AF, DE, HL
CALPAT carregará em HL a posição da VRAM relacionada ao padrão do sprite cujo ID é carregado em A antes de chamar a rotina, e CALATR fará o mesmo, mas desta vez para os atributos do sprite. Observe que há um pequeno problema aqui porque o próximo passo é usar LDIRVM para mover os dados de padrão e atributos de RAM para VRAM, mas a rotina requer que o endereço do padrão seja carregado em HL e o endereço VRAM em DE, e CALPAT/CALATR retornará o endereço de destino em HL!
LDIRVM: equ 0005Ch ; Função: Transferência em blocos para VRAM da memória ; Entrada: BC - Comprimento do bloco ; DE - Endereço inicial da VRAM ; HL - Endereço inicial da memória
Como não há nenhum comando que carrega HL diretamente no DE, há uma maneira rápida de fazer isso: PUSH-ando o conteúdo do HL para a pilha e, em seguida, POP-indo de volta em DE. Aqui está o trecho de código que carrega o sprite com id 0 na VRAM:
ld a,0 ; Carregue o sprite ID 0 em A call CALPAT ; Retorna o endereço VRAM do padrão 0 do sprite em HL push hl ; Enviar dados HL para o stack pop de ; então retorne novamente em DE ld bc,64 ; Nosso padrão de sprite usa 64 bytes ld hl,MeteorIdleSprite01 ; Carregue o endereço padrão na chamada HL call LDIRVM; Copie os blocos BC de dados do endereço RAM HL para o endereço VRAM DE ld a,0 ; Carregue o sprite ID 0 em A call CALATR ; Retorna o endereço VRAM do atributo sprite 0 em HL push hl ; Enviar dados HL para o stack pop de ; então retorne novamente em DE ld bc,4 ; Nosso atributo sprite usa 4 bytes ld hl,MeteorIdleSprite01_attrib ; Carregue o endereço do atributo na chamada HL call LDIRVM; Copie blocos BC de dados do endereço RAM HL para o endereço VRAM DE
Para carregar os próximos 3 sprites tudo o que precisamos fazer é duplicar essas linhas de código substituindo o valor carregado em A e os rótulos relacionados aos padrões e atributos do sprite. Mais uma vez, essa mudança não está reduzindo o tamanho do código nem evitando a repetição do código, mas é mais fácil de entender e manter, além de facilitar a melhoria da rotina no futuro, o que faremos no próximo artigo.
Um toque final antes de encerrar o dia
Existe outra seção do código que podemos mover para outro arquivo: os padrões e atributos do sprite. Para isso, criei o arquivo de inclusão “Asset_MeteorData.asm”, depois movi os rótulos com os valores hexadecimais do código principal para este arquivo, que agora está assim:
; The Meteor Game include "System_BIOSCalls.asm" include "System_SystemVariables.asm" org 0x4000 include "System_CartridgeHeader.asm" ; initialize the whole thing here Init: ;Let's initialize the screen colors ld b,15 ; Foreground color ld c,13 ; Background color ld d,13 ; Border color call InitGraphicMode ; Initializes graphic mode with the color at BCD call Set16x16Sprites ; Set the sprite size to 16x16 pixels ld a,0 ; Load the sprite ID 0 into A call CALPAT ; Return the sprite 0 pattern VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE LD bc,64 ; Our sprite pattern uses 64 bytes LD hl,MeteorIdleSprite01 ; Load the pattern address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ld a,0 ; Load the sprite ID 0 into A call CALATR ; Return the sprite 0 attribute VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE ld bc,4 ; Our sprite attribute uses 4 bytes LD hl,MeteorIdleSprite01_attrib ; Load the attribute address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ld a,1 ; Load the sprite ID 1 into A call CALPAT ; Return the sprite 1 pattern VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE LD bc,64 ; Our sprite pattern uses 64 bytes LD hl,MeteorIdleSprite02 ; Load the pattern address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ld a,1 ; Load the sprite ID 1 into A call CALATR ; Return the sprite 1 attribute VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE ld bc,4 ; Our sprite attribute uses 4 bytes LD hl,MeteorIdleSprite02_attrib ; Load the attribute address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ld a,2 ; Load the sprite ID 2 into A call CALPAT ; Return the sprite 2 pattern VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE LD bc,64 ; Our sprite pattern uses 64 bytes LD hl,MeteorIdleSprite03 ; Load the pattern address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ld a,2 ; Load the sprite ID 2 into A call CALATR ; Return the sprite 2 attribute VRAM address in HL push hl ; Send HL data to he stack pop de ; then return it bacl in DE ld bc,4 ; Our sprite attribute uses 4 bytes LD hl,MeteorIdleSprite03_attrib ; Load the attribute address in HL call LDIRVM ; Copy BC blocks of data from RAM HL address into VRAM DE address ForeverLoop: jr ForeverLoop ; Repeats the loop indefinitely include "Function_SetScreen.asm" include "Asset_MeteorData.asm" end Init
É tudo por agora
Mudamos e mudamos bastante nosso código, e pode ser de alguma forma decepcionante que no final não tenha havido nenhuma melhoria no jogo real e ainda estejamos vendo o cara maluco do meteoro preso na mesma posição na tela, mas acredite em mim que esta foi realmente uma excelente melhoria feita através deste artigo e será muito útil no futuro. O código completo está disponível em https://github.com/oddbitmachine/TheMeteorGame, então fique à vontade para conferir o progresso feito até agora. Voltaremos em breve para melhorar a rotina de carregamento de sprites, então fique atento!
Relacionado
Algumas semanas atrás, iniciamos o design do conceito do jogo e adicionamos alguns códigos básicos que resultaram no carregamento do nosso sprite na tela do modo gráfico, o que foi legal e divertido. O problema é que nosso código parece apressado, com muitos endereços de memória codificados e comandos repetidos em todo o…