Vamos fazer um jogo de MSX – Conceito de jogo

Qualquer criação de jogo deve começar com uma história, uma premissa, uma razão que explique por que as coisas estão acontecendo ou qualquer coisa que justifique os eventos que testemunharemos se desenvolverem em nossas telas. Não precisa ser um roteiro shakespeariano ou mesmo fazer algum sentido, desde que possa ser traduzido em um game jogável.

Tentei imaginar um conceito de jogo que pudesse ser simples de criar, mas ao mesmo tempo atraente para ser jogado, algo como Flappy Bird e Angry Birds (mas sem pássaros desta vez), e também não exigiria muito esforço para projetar gráficos e bens para isso. Como meu nível de sanidade não é tão alto, não demorou muito para ter uma ideia maluca para um jogo, afinal:

Você é um meteoro, viajando bem pelo espaço sideral sem muito o que fazer ou nada de novo para ver. De repente, você sente uma pequena entidade de um planeta azul pálido convocando você, desafiando sua força de vontade, zombando de sua simples existência, e isso simplesmente o deixa muito, muito louco!


Feito com Storyboard That

O objetivo do jogo

Como um poderoso meteoro, seu objetivo é atingir a superfície do planeta Terra e atingir o cidadão desafiador na cabeça, apenas para mostrar a esses humanos insignificantes que nenhuma má ação deve passar sem consequências.

Mas atravessar as várias camadas do escudo da Terra não é uma tarefa fácil. Normalmente, um meteoro que penetra na atmosfera do planeta é reduzido a pedaços muito pequenos durante a entrada e nem chega ao solo no final. Mas você é um meteoro esperto e sabe que passar as camadas com cuidado pode evitar o destino vergonhoso de se tornar poeira espacial, e o cara lá embaixo vai se livrar da sua ira. Inaceitável!

Mas outras vantagens de bônus podem tornar seu sacrifício final mais agradável: aviões, pára-quedas e outros objetos voadores feitos por humanos podem ser encontrados no caminho para o seu alvo, então destruí-los lhe dará aquela sensação especial de vingança, juntamente com alguns pontos de bônus e até mesmo alguma ajuda bem merecida! Então, acertar tudo o que você encontra em seu caminho que merece ser aterrado é uma obrigação.

Então este é o negócio: descer dos céus, evitar ser queimado pelas camadas protetoras da Terra, acertar alguns bypassers pelo caminho e, finalmente, esmagar aquele mentiroso imundo com a força que ele merece.

Mecânica do jogo

Então, aqui estão as regras e restrições nas quais o jogo deve se basear:

  1. O jogo começa com o meteoro no espaço sideral, começando a descer na Terra através do cinco camadas da atmosfera do planeta:
    • Exosfera: Este é o ponto de partida no espaço sideral, e não haverá elementos perigosos aqui. Podemos adicionar alguns satélites como alvos aqui apenas por diversão.
    • Ionosfera: devido aos íons eletrificados existentes nesta camada, alguns perigos podem afetar o meteoro de alguma forma.
    • Termosfera: Com temperaturas chegando a 1500 graus Celsius, é uma boa ideia desacelerar a descida por essa camada.
    • Mesosfera: É aqui que um meteoroide se torna meteorito real ou simplesmente se transforma em poeira espacial. O jogador precisa evitar os perigos neste nível o máximo possível para ter sucesso na próxima camada.
    • Estratosfera: Objetos voadores feitos por humanos começarão a aparecer neste nível, mas as correntes de jato podem divergir a direção do personagem e errar o alvo final.
    • Troposfera: Agora é hora de acelerar na direção do alvo antes que a vítima fuja do local. Acertar objetos voadores pode adicionar pontos ou causar riscos ao nosso personagem, e a poluição também pode tornar as coisas mais difíceis.
    • Crosta terrestre: acerte o alvo ou falhe miseravelmente em sua missão.
  2. O jogador precisa controlar a descida e evitar os perigos durante a viagem:
    • Sofrer danos diminuirá o tamanho do meteorito a um ponto em que ele se transformará em pó, perdendo uma vida
    • Aquecer demais pode fazer com que o meteorito derreta e perca massa.
    • A queda muito rápida fará com que o meteorito se quebre, perdendo pedaços de seu corpo e diminuindo de tamanho
    • Atingir objetos pelo caminho pode recuperar alguma massa do meteorito junto com pontos de bônus
    • Errar o alvo no final da descida diminuirá a vida útil e reduzirá o total de pontos obtidos.

Alguns gramas de inspiração, uma tonelada de transpiração

Agora que temos um conceito básico de como será o jogo, vamos trabalhar duro. Como de costume, estou usando o Jannone's TinySprite para criar uma maquete para nosso personagem de meteoro mortal, que será usado como está por enquanto, mas pode melhorar no futuro.

Nosso protagonista não tão feliz, olhando para seu alvo inconsciente.

Exportar o sprint como ASM Hexadecimal resulta nos dois conjuntos de sprites abaixo, um para cada cor:

; --- Slot 0
; color 1
DB $07,$18,$21,$42,$44,$81,$81,$81
DB $80,$80,$80,$40,$41,$20,$18,$07
DB $E0,$18,$E4,$12,$E2,$11,$75,$72
DB $E1,$07,$70,$8A,$02,$04,$18,$E0
; color 11
DB $00,$07,$1E,$3D,$3B,$7E,$7E,$7E
DB $7F,$7F,$7F,$3F,$3E,$1F,$07,$00
DB $00,$E0,$18,$EC,$1C,$0E,$0A,$0D
DB $1E,$F8,$8F,$74,$FC,$F8,$E0,$00
; color 15
DB $00,$00,$00,$00,$00,$00,$00,$00
DB $00,$00,$00,$00,$00,$00,$00,$00
DB $00,$00,$00,$00,$00,$E0,$80,$80
DB $00,$00,$00,$00,$00,$00,$00,$00

Vamos carregar os sprites na VRAM e então posicionar o sprite 1 em cima do sprite 0, para que possamos terminar com um caractere de 2 cores. Observe que isso consumirá 50% dos possíveis sprites por linha que o TMS99918 é capaz de mostrar, mas como a ação do jogo ocorrerá de cima para baixo a probabilidade de ter mais de 4 sprites na mesma linha é mínima neste caso. Também podemos usar ladrilhos para os outros objetos ou até mesmo tentar definir a segunda cor como um ladrilho em vez de um sprite e descobrir como manter os ladrilhos na mesma posição do sprite, e acho que ambos os cenários podem ser tentados em os próximos artigos como um exercício para nossas habilidades de codificação.

Codificação e estruturação inicial do jogo

Embora eu não seja um desenvolvedor de jogos profissional e apenas um programador amador em geral, existem algumas estruturas básicas de codificação que eu gostaria de aplicar neste projeto para facilitar minha vida no futuro. Aproveitando a possibilidade de adicionar arquivos separados para o programa fornecido pelo 8BitWorkshop IDE, vou separar o código em arquivos diferentes dependendo da tarefa executada pelas rotinas. Coisas como chamada de BIOS e variáveis de sistema serão adicionadas aos seus respectivos arquivos, e farei isso lentamente durante a série de artigos para que qualquer pessoa possa entender qual é o conteúdo de cada arquivo e não fique sobrecarregado por enfrentar uma lista enorme de loucos nomes e endereços de memória sem maiores explicações.

Para tornar as coisas mais didáticas vou iniciar o código do jogo em um único arquivo, então no próximo artigo, mostrarei como transferir o conteúdo para novos arquivos mantendo o programa funcionando e facilitando a visualização da lógica do código – espero que sim assim!

Prosseguindo para o 8BitWorkshop IDE, vou criar um novo projeto com o nome do jogo selecionando “New Project” no menu do IDE. Por enquanto, vou chamá-lo de “O Jogo do Meteoro” e então podemos criar um nome melhor como “Vem, Meteoro!” ou alguma outra coisa.

Este será o arquivo principal para o nosso código do jogo

Após iniciar um novo projeto, notamos que o código anterior ainda estará rodando no IDE, e podemos reutilizar alguns dos códigos disponíveis, como o cabeçalho do cartucho MSX que é necessário para que o MSX reconheça a ROM corretamente. Este é o código mais básico para gerar um arquivo ROM válido e, embora compile corretamente com alguns avisos, não fará nada:

; The Meteor Game

	org 0x4000
        
; MSX cartridge header @ 0x4000 - 0x400f
	dw 0x4241
        dw Init
        dw Init
        dw 0
        dw 0
        dw 0
        dw 0
        dw 0

; initialize the whole thing here
Init:
	ret
	end Init

Até agora, tudo o que temos é o código hexadecimal do nosso protagonista do jogo e nada mais, e o que quero fazer agora é ver o pequeno meteoro aparecendo na tela. Para esta tarefa simples, precisamos fazer as seguintes coisas em nosso código:

  1. Definir as cores de primeiro plano, plano de fundo e borda
  2. Diga ao VDP que queremos usar o Modo gráfico, também conhecido como Tela 2 em BASIC
  3. Queremos mostrar o meteoro como um sprite na tela, então precisamos copiá-lo da memória para a VRAM
  4. O personagem tem três cores diferentes, o que exigirá três sprites sobrepostos.

Vamos manter esta pequena lista de tarefas por enquanto porque isso já exigirá algum esforço.

Configurando as cores na tela

O MSX variáveis do sistema são bastante poderosos, além de uma ou outra coisa que falta, vamos notar em breve. Para configurar as três variáveis de cores em nosso código assembly, usaremos as seguintes variáveis:

Nome variávelEndereço de memóriaTamanho em bytesEndereço de memória
FORCLR#F3E91Cor do primeiro plano
BAKCLR#F3EA1Cor de fundo
BDRCLR#F3EB1Cor da borda
Variáveis do sistema para as cores da tela

O que significam todos esses valores? O nome da variável é simplesmente uma convenção definida para a arquitetura MSX, então você pode usar qualquer outro nome desde que se lembre no futuro o que eles significam ou tenha paciência suficiente para percorrer todo o código para descobrir seu significado. O endereço de memória é o local na RAM do sistema onde essas informações serão armazenadas, portanto, as chamadas do BIOS que referenciam os valores consultarão esse local específico da memória para executar o que for suposto.

Agora, precisamos saber como alteramos os valores de qualquer variável no sistema. Em BASIC, basta adicionar algo como LET FORCLR=15 para definir a variável para o valor correspondente à cor branca, mas isso não é tão simples na montagem, e precisamos primeiro carregar a posição de memória da variável em um dos Z80 registradores, então carregue o valor na posição apontada pelo registrador. Complexo? Não muito:

Vamos primeiro definir as variáveis e suas respectivas posições de memória em nosso código:

;System Variables
FORCLR:	equ 0F3E9h
BAKCLR:	equ 0F3EAh
BDRCLR:	equ 0F3EBh

Eu costumo adicionar esta seção antes da instrução de origem (org) e depois da seção de chamadas do BIOS. Em breve, moveremos toda esta seção para um arquivo separado para manter o código principal limpo.

Em seguida, precisamos alterar os valores do primeiro plano, plano de fundo e borda para as cores que queremos ter na tela. Vou usar um foreground branco (valor 15), fundo preto (valor 1) e borda roxa (valor 13) por enquanto, então é assim que definimos as variáveis em assembly:

; initialize the whole thing here
Init:
	ld hl,FORCLR	; Load the foreground variable position in HL
        ld (hl),15	; Changes the value for FORCLR to 15 (White)
        ld hl,BAKCLR	; Load the background variable position in HL
        ld (hl),1	; Changes the value for BAKCLR to 1 (black)
        ld hl,BDRCLR	; Load the border variable position in HL
        ld (hl),13	; Changes the value for BDRCLR to 13 (purple)
        call INIGRP	; Calls the routine that initializes the screen mode
	
ForeverLoop:
	jr ForeverLoop ; Repeats the loop indefinitely
        
        end Init

Observe que temos um novo rótulo chamado “ForeverLoop” e depois disso uma única instrução que é um JR (Jump Relative) para o mesmo local, que é um loop sem saída. Isso se deve ao C-BIOS não ter uma ROM BASIC para retornar se um programa terminar com um simples RET, e também porque queremos ver os resultados antes de sair do código.

Este código é bom e tudo, mas mais uma vez, este programa não faz nada. A simples configuração de variáveis não resulta em nenhuma mudança de comportamento do VDP, a menos que você chame a rotina do BIOS que instrui o VDP a entrar em um novo (ou mesmo o mesmo!) modo gráfico.

Alterando o modo de tela e definindo as novas cores

Agora vamos usar um dos Chamadas de BIOS MSX para informar ao VDP que desejamos que seja configurado um novo modo de tela, juntamente com os valores de cores que definimos nas variáveis. Existem alguns chamados diferentes para fazer a mesma ação, mas o que queremos neste momento é o INIGRP:

INIGRP
Address  : #0072
Function : Switches to SCREEN 2 (high resolution screen with 256×192 pixels)
Input    : GRPNAM, GRPCGP, GRPCOL, GRPATR, GRPPAT
Registers: All

O INIGRP mora no endereço 00072f da ROM, e tem a função de mudar a tela para o modo gráfico junto com a inicialização de outras variáveis do sistema relacionadas às tabelas de nomes, cores e padrões, que usaremos futuramente para saber onde pode armazenar nossos gráficos na VRAM sem precisar calcular todas as posições manualmente. É útil, confie em mim.

Mais uma vez, precisamos definir esse rótulo e o endereço em nosso código para que possamos usá-lo no futuro. Poderíamos simplesmente pular isso e chamar a posição real da memória, mas isso tornaria nosso código difícil de ler e entender:

; The Meteor Game

; MSX BIOS Calls
INIGRP:	equ 00072h
	; Function : Switches to SCREEN 2 (high resolution screen with 256×192 pixels)
	; Input    : GRPNAM, GRPCGP, GRPCOL, GRPATR, GRPPAT
	; Registers: All

Eu gosto de deixar os comentários com uma explicação do que a chamada faz para que eu possa consultá-la rapidamente se tiver alguma dúvida sobre quais registros são usados e afetados. Sabemos agora que chamar INIGRP modificará todos os registradores, portanto, não podemos confiar em nenhum valor definido para eles após a chamada e precisamos garantir que enviaremos para a pilha qualquer valor de registrador que possamos precisar novamente no futuro.

Agora nosso código finalmente fará algo visível: mudar as cores da tela!

; initialize the whole thing here
Init:
	ld hl,FORCLR	; Load the foreground variable position in HL
        ld (hl),15	; Changes the value for FORCLR to 15 (White)
        ld hl,BAKCLR	; Load the background variable position in HL
        ld (hl),1	; Changes the value for BAKCLR to 1 (black)
        ld hl,BDRCLR	; Load the border variable position in HL
        ld (hl),13	; Changes the value for BDRCLR to 13 (purple)
        call INIGRP	; Calls the routine that initializes the screen mode

E voilá! Ainda temos nada, mas em cores diferentes!

Dando ao nosso meteoro um céu para chamar de seu

Colocando nosso meteoro no espaço sideral

Agora é um bom momento para adicionar o código obtido do TinySprite ao nosso jogo. Eu gosto de adicionar os padrões no final do código, mas mais uma vez eles serão alterados no futuro, então é apenas uma questão de boa visualização para todo o código no momento:

ForeverLoop:
	jr ForeverLoop	; Repeats the loop indefinitely

MeteorIdleSprite01:
; color 1
        DB $07,$18,$21,$42,$44,$81,$81,$81
        DB $80,$80,$80,$40,$41,$20,$18,$07
        DB $E0,$18,$E4,$12,$E2,$11,$75,$72
        DB $E1,$07,$70,$8A,$02,$04,$18,$E0

MeteorIdleSprite02
; color 11
        DB $00,$07,$1E,$3D,$3B,$7E,$7E,$7E
        DB $7F,$7F,$7F,$3F,$3E,$1F,$07,$00
        DB $00,$E0,$18,$EC,$1C,$0E,$0A,$0D
        DB $1E,$F8,$8F,$74,$FC,$F8,$E0,$00

MeteorIdleSprite03:        
; color 15
        DB $00,$00,$00,$00,$00,$00,$00,$00
        DB $00,$00,$00,$00,$00,$00,$00,$00
        DB $00,$00,$00,$00,$00,$E0,$80,$80
        DB $00,$00,$00,$00,$00,$00,$00,$00

        end Init

Definir um sprite na tela requer três etapas simples:

  1. Copie os dados padrão da RAM para a VRAM na posição definida pela variável GRPPAT
  2. Copie os atributos da RAM para a VRAM na posição definida pela variável GRPATR
  3. Veja o sprite aparecendo na tela como você esperava!

Os valores armazenados pelo GRPPAT e GRPATR são as posições reais na VRAM onde os sprites são armazenados, e o ID do sprite é definido pelo “slot” onde o padrão é alocado. Os valores de memória para cada variável são os abaixo:

F3C7H GRPNAM: DEFW 1800H   ; Name Table Base
F3C9H GRPCOL: DEFW 2000H   ; Colour Table Base
F3CBH GRPCGP: DEFW 0000H   ; Character Pattern Base
F3CDH GRPATR: DEFW 1B00H   ; Sprite Attribute Base
F3CFH GRPPAT: DEFW 3800H   ; Sprite Pattern Base

Para entender melhor a coisa do slot x sprite ID, considere que enviar um padrão de dados para a VRAM no endereço 3800H resultará na criação do sprite com ID 0, então enviar outro padrão de dados para o endereço 3808H criará o sprite com ID 1 , e assim sucessivamente até atingir o limite de 32 sprites que o TMS9918 pode manipular.

Os dados padrão usados para criar um sprite podem ser definidos usando valores binários ou hexadecimais, dependendo de como você criou o sprite. Para sprites de 8×8 pixels, é necessário copiar 32 bytes de dados relacionados ao padrão de sprite para a VRAM, e para sprites de 16×16 pixels, o valor é de 64 bytes. Quanto aos valores de atributo, é composto por apenas 4 bytes:

-> Posição vertical, de zero a 191
-> Posição horizontal, de zero a 251
-> Número do Sprite, de zero a 31. Há uma ressalva aqui que será explicada posteriormente
-> Cores de primeiro e segundo plano

Então, para colocar nosso meteoro no canto superior esquerdo da tela, mas a alguns pixels da borda, nossos dados de atributo serão algo como:

MeteorIdleSprite01_attrib:
	DB $05,$5,$00,$01	; Line 05, column 05, sprite ID 0, 
        			; transparent(0) foreground and black (1) background

MeteorIdleSprite02_attrib:
	DB $05,$05,$04*1,$0B	; Line 05, column 05, sprite ID 1, 
        			; transparent(0) foreground and black (1) background
        
MeteorIdleSprite03_attrib:
	DB $05,$05,$04*2,$0F	; Line 05, column 05, sprite ID 2, 
        			; transparent(0) foreground and black (1) background

A ressalva mencionada anteriormente pode ser vista aqui, que é o ID do sprite sendo multiplicado por 4 no 3º byte do atributo. Mas por que? Lembre-se que por padrão o tamanho do sprite é 8×8 pixels, mas estamos usando sprites de 16×16 pixels neste caso, e isso significa que o atributo precisa ser deslocado 4 blocos de sprites de 8×8 pixels para frente para encontrar o próximo ID do sprite . Isso soa esmagador para envolver sua mente? Basta seguir o padrão de multiplicar o valor do ID por 4 por enquanto e tudo funcionará, mas simplificaremos tudo em breve nos próximos artigos.

Com o padrão e os atributos carregados na memória do MSX, agora podemos começar a mover os dados entre a RAM e a VRAM e, para isso, usaremos a chamada do BIOS chamada LDIRVM:

LDIRVM
Address  : #005C
Function : Block transfer to VRAM from memory
Input    : BC - Block length
           DE - Start address of VRAM
           HL - Start address of memory
Registers: All

Esta chamada Carrega os dados do endereço RAM apontado por HL na VRAM no endereço apontado por DE, então incrementa tanto HL quanto DE e diminui BC, então repete o loop até BC chegar a 0, copiando efetivamente os dados entre a RAM e a VRAM . Você consegue adivinhar o que a chamada LDIRMV faz?

Agora que sabemos o que precisamos copiar para qual posição na VRAM e qual comando usar para isso, por que agora ir em frente e criar nosso primeiro sprite? Considere isso feito!

        ld de,03800h	; Load in DE the VRAM position for sprite with ID 0
        LD bc,64	; The amount of data to be transferred: 4 x 16 bytes
        LD hl,MeteorIdleSprite01	; Load the MeteorIdleSprite01 pattern in HL
        
        call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        
        ld de,01B00h	; Load in DE the VRAM position for the attributes
        ld bc,4		; The amount of data to be transferred: 4 bytes
        LD hl,MeteorIdleSprite01_attrib	; Load the MeteorIdleSprite01_attrib attribute in HL
        
        call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE
        
        ld de,03820h	; Load in DE the VRAM position for sprite with ID 1
        ld BC,64	; The amount of data to be transferred: 4 x 16 bytes
        ld HL,MeteorIdleSprite02	; Load the MeteorIdleSprite02 pattern in HL
        
        call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        
        ld de,01B04h	; Load in DE the VRAM position for the attributes of the sprite with ID 1
        ld BC,4		; The amount of data to be transferred: 4 bytes
        ld HL,MeteorIdleSprite02_attrib
       
       	call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        
        ld de,03840h	; Load in DE the VRAM position for sprite with ID 2
        ld BC,64	; The amount of data to be transferred: 4 x 16 bytes
        ld HL,MeteorIdleSprite03	; Load the MeteorIdleSprite03 pattern in HL
        
        call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        
        ld de,01B08h	; Load in DE the VRAM position for the attributes of the sprite with ID 2
        ld BC,4		; The amount of data to be transferred: 4 bytes
        ld HL,MeteorIdleSprite03_attrib
       
       	call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        	
ForeverLoop:
	jr ForeverLoop	; Repeats the loop indefinitely

Como a cor do Sprite 0 é preta, alterei o valor BAKCLR de 1 (preto) para 13 (roxo) veja que consegui ver os sprites corretamente, e esse foi o resultado do novo código:

Onde está o resto do meteoro?

Então o que aconteceu aqui? Podemos ver que os três sprites e atributos estão corretos, mas por que só podemos ver o lado superior esquerdo do nosso sprite?

Isso ocorre porque o comportamento padrão do VDP é usar sprites de 8×8 pixels não ampliados. Os outros modos possíveis são 8×8 pixels ampliados, 16×16 pixels não ampliados e finalmente 16×16 pixels ampliados. Como queremos usar sprites não ampliados 16×16, precisamos informar ao VDP o que queremos.

Ok, tudo o que precisamos fazer é usar a chamada do BIOS para alterar o tamanho do sprite, você dirá. Bem, desculpe, mas, ao contrário do comando BASIC Screen que aceita um segundo parâmetro para definir o tamanho e a ampliação do sprite, não há chamada do BIOS para isso. Que tal uma variável de sistema para isso? Humm... não, nada disso aqui? Então, como configuramos essa opção? Modificando os valores do registrador #1 do VDP para o modo desejado.

Primeiro, vamos adicionar a chamada do BIOS que modificou o registro VDP à nossa lista de rotinas:

WRTVDP:	equ 00047h
	; Function : Write data in the VDP-register
	; Input    : B  - Data to write
	;	     C  - Number of the register
	; Registers: AF, BC

Também precisamos adicionar a variável de sistema onde os valores do registrador #1 do VDP podem ser recuperados:

RG1SAV: equ 0F3E0h

Em seguida, adicionaremos a rotina que verifica o valor atual definido no registrador #1 e modifica apenas os bits relacionados ao tamanho do sprite:

Set16x16Sprites:
	ld a,(RG1SAV)	; Load into A the current value of VDP's register 1
        and $FC		; AND A with $FC (11111100) resulting in xxxxxx00 (x=no change)
        or $02		; OR A with 00000010 resulting in xxxxxx10
        di		; Disable interruptions before calling the VDP 
        ld b, a		; Load value from A into B to be used by WRTVDP
	ld c, $01	; Load the register value (#1) into C
	call WRTVDP	; Send to VDP register set at C (#1) the data stored in B
	ei		; Enable interruptions
        ret		; Return to the caller routine

Agora, tudo o que precisamos fazer é chamar essa rotina logo após definir o modo de tela, e o meteoro aparecerá completo no céu roxo:

Init:
	ld hl,FORCLR	; Load the foreground variable position in HL
        ld (hl),15	; Changes the value for FORCLR to 15 (White)
        ld hl,BAKCLR	; Load the background variable position in HL
        ld (hl),13	; Changes the value for BAKCLR to 13 (purple)
        ld hl,BDRCLR	; Load the border variable position in HL
        ld (hl),13	; Changes the value for BDRCLR to 13 (purple)
        call INIGRP	; Calls the routine that initializes the screen mode
	
        call Set16x16Sprites

Agora está muito melhor!

Olha, aí está nosso amiguinho pronto para a ação!

O código até agora

Aqui está o código completo que acabamos de criar para mostrar nosso protagonista na tela:

; The Meteor Game

; MSX BIOS Calls
INIGRP:	equ 00072h
	; Function : Switches to SCREEN 2 (high resolution screen with 256×192 pixels)
	; Input    : GRPNAM, GRPCGP, GRPCOL, GRPATR, GRPPAT
	; Registers: All
LDIRVM:	equ 0005Ch
	; Function : Block transfer to VRAM from memory
	; Input    : BC - Block length
        ;            DE - Start address of VRAM
        ;            HL - Start address of memory
	; Registers: All
WRTVDP:	equ 00047h
	; Function : Write data in the VDP-register
	; Input    : B  - Data to write
	;	     C  - Number of the register
	; Registers: AF, BC

; System Variables
FORCLR:	equ 0F3E9h
BAKCLR:	equ 0F3EAh
BDRCLR:	equ 0F3EBh
GRPATR:	equ 0F3CDh
GRPPAT:	equ 0F3CFh
RG0SAV:	equ 0F3DFh
RG1SAV:	equ 0F3E0h

	org 0x4000
        
; MSX cartridge header @ 0x4000 - 0x400f
	dw 0x4241
        dw Init
        dw Init
        dw 0
        dw 0
        dw 0
        dw 0
        dw 0

; initialize the whole thing here
Init:
	ld hl,FORCLR	; Load the foreground variable position in HL
        ld (hl),15	; Changes the value for FORCLR to 15 (White)
        ld hl,BAKCLR	; Load the background variable position in HL
        ld (hl),13	; Changes the value for BAKCLR to 13 (purple)
        ld hl,BDRCLR	; Load the border variable position in HL
        ld (hl),13	; Changes the value for BDRCLR to 13 (purple)
        call INIGRP	; Calls the routine that initializes the screen mode
	
        call Set16x16Sprites
        
        ld de,03800h	; Load in DE the VRAM position for sprite with ID 0
        LD bc,64	; The amount of data to be transferred: 4 x 16 bytes
        LD hl,MeteorIdleSprite01	; Load the MeteorIdleSprite01 pattern in HL
        
        call LDIRVM	; Copy BC amount of bytes of the content pointed by HL into the VRAM at address loaded in DE 
        
        ld de,01B00h	; Load in DE the VRAM position for the attributes
        ld bc,4		; The amount of data to be transferred: 4 bytes
        LD hl,MeteorIdleSprite01_attrib	; Load the MeteorIdleSprite01_attrib attribute in HL
        
        call LDIRVM
        
        ld de,03820h
        ld BC,64
        ld HL,MeteorIdleSprite02
        
        call LDIRVM
        
        ld de,01B04h
        ld BC,4
        ld HL,MeteorIdleSprite02_attrib
       
       	call LDIRVM
        
        ld de,03840h
        ld BC,64
        ld HL,MeteorIdleSprite03
        
        call LDIRVM
        
        ld de,01B08h
        ld BC,4
        ld HL,MeteorIdleSprite03_attrib
       
       	call LDIRVM
        	
ForeverLoop:
	jr ForeverLoop	; Repeats the loop indefinitely
        
Set16x16Sprites:
	ld a,(RG1SAV)	; Load into A the current value of VDP's register 1
        and $FC		; AND A with $FC (11111100) resulting in xxxxxx00 (x=no change)
        or $02		; OR A with 00000010 resulting in xxxxxx10
        di		; Disable interruptions before calling the VDP 
        ld b, a		; Load value from A into B to be used by WRTVDP
	ld c, $01	; Load the register value (#1) into C
	call WRTVDP	; Send to VDP register set at C (#1) the data stored in B
	ei		; Enable interruptions
        ret		; Return to the caller routine
        

MeteorIdleSprite01:
; color 1
        DB $07,$18,$21,$42,$44,$81,$81,$81
        DB $80,$80,$80,$40,$41,$20,$18,$07
        DB $E0,$18,$E4,$12,$E2,$11,$75,$72
        DB $E1,$07,$70,$8A,$02,$04,$18,$E0

MeteorIdleSprite01_attrib:
	DB $05,$5,$00,$01	; Line 05, column 05, sprite ID 0, 
        			; transparent(0) foreground and black (1) background

MeteorIdleSprite02
; color 11
        DB $00,$07,$1E,$3D,$3B,$7E,$7E,$7E
        DB $7F,$7F,$7F,$3F,$3E,$1F,$07,$00
        DB $00,$E0,$18,$EC,$1C,$0E,$0A,$0D
        DB $1E,$F8,$8F,$74,$FC,$F8,$E0,$00

MeteorIdleSprite02_attrib:
	DB $05,$05,$04*1,$0B	; Line 05, column 05, sprite ID 1 (4*1), 
        			; transparent(0) foreground and black (1) background


MeteorIdleSprite03:        
; color 15
        DB $00,$00,$00,$00,$00,$00,$00,$00
        DB $00,$00,$00,$00,$00,$00,$00,$00
        DB $00,$00,$00,$00,$00,$E0,$80,$80
        DB $00,$00,$00,$00,$00,$00,$00,$00
        
MeteorIdleSprite03_attrib:
	DB $05,$05,$04*2,$0F	; Line 05, column 05, sprite ID 2 ($4*2), 
        			; transparent(0) foreground and black (1) background

        
        end Init

Como mencionei antes, esse código já é longo o suficiente para dificultar a leitura, então no próximo artigo, vamos mover as coisas do arquivo principal para outros lugares, criar alguns métodos para permitir valores menos codificados tornando o código mais flexível, e adicione uma chamada que retorne o endereço VRAM de cada sprite com base em seu ID, liberando-nos da necessidade de calcular cada valor hexadecimal manualmente.

Se você gosta de ver como o projeto fica depois de publicado no GitHub, basta verificar meu repo TheMeteorGame.

Voltaremos em breve, então fique ligado!

Qualquer criação de jogo deve começar com uma história, uma premissa, uma razão que explique por que as coisas estão acontecendo ou qualquer coisa que justifique os eventos que testemunharemos se desenvolverem em nossas telas. Não precisa ser um roteiro shakespeariano nem fazer sentido algum, desde que possa ser traduzido para um jogo jogável…

2 Comentários

  1. Eu pensei que o MSX pode lidar com várias cores em um único sprite, duas por scanline. (mesmo com os fundos)
    Ou o TMS9918 teve um recurso como esse em algum momento.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

pt_BRPortuguês do Brasil