Desenvolvimento de jogos para ZX Spectrum em C

Esse post bobo é dedicado ao desenvolvimento de um jogo para o antigo computador ZX Spectrum em C. Vamos dar uma olhada no bonitão:

Iniciou a produção em 1982 e foi produzido até 1992. Características técnicas da máquina: processador Z80 de 8 bits, 16-128kb de memória e outras extensões, como o chip de som AY-3-8910.

Como parte da competição Yandex Retro Games Battle 2019 para esta máquina, escrevi um jogo chamado Interceptor 2020. Como não tive tempo de aprender linguagem assembly para o Z80, resolvi desenvolvê-lo em C. Como conjunto de ferramentas, escolhi um conjunto pronto – z88dk, que contém compiladores C e diversas bibliotecas auxiliares para agilizar a implementação de aplicações para o Spectrum. Ele também suporta muitas outras máquinas Z80, como MSX e calculadoras Texas Instruments.

A seguir, descreverei meu voo superficial sobre a arquitetura do computador, o conjunto de ferramentas z88dk, e mostrarei como consegui implementar a abordagem OOP e usar padrões de design.

Recursos de instalação

A instalação do z88dk deve ser realizada de acordo com o manual do repositório, porém, para usuários do Ubuntu, gostaria de destacar um recurso – Se você já possui compiladores para Z80 instalados a partir de pacotes deb, então você deve removê-los, já que z88dk os acessará da pasta bin por padrão devido à incompatibilidade das versões do compilador do conjunto de ferramentas, você provavelmente não conseguirá compilar nada.< /p>

Olá, mundo

Escrever Hello World é muito fácil:

#include 

void main()
{
    printf("Hello World");
}

É ainda mais fácil compilar um arquivo tap:

zcc +zx -lndos -create-app -o helloworld helloworld.c

Para rodar, use qualquer emulador ZX Spectrum com suporte a arquivo tap, por exemplo online:
http://jsspeccy.zxdemo.org/

Desenhe na imagem em tela cheia

tl;dr As imagens são desenhadas em blocos, blocos de tamanho 8×8 pixels, os próprios blocos são incorporados na fonte Spectrum e, em seguida, a imagem é impressa como uma linha a partir dos índices.

A biblioteca de saída de sprites e blocos sp1 produz blocos usando UDG. A imagem é traduzida em um conjunto de UDGs (blocos) individuais e depois montados na tela usando índices. Deve ser lembrado que UDG é usado para exibir texto, e se sua imagem contiver um conjunto muito grande de blocos (por exemplo, mais de 128 blocos), você terá que ir além dos limites do conjunto e apagar o Spectrum padrão fonte. Para contornar essa limitação, utilizei uma base de 128 – 255 simplificando as imagens, deixando a fonte original no lugar. Sobre simplificar as imagens abaixo.

Para desenhar imagens em tela cheia você precisa se munir de três utilitários:
Gimp
img2spec
png2c-z88dk

Existe uma maneira para verdadeiros homens ZX, verdadeiros guerreiros retrô, é abrir um editor gráfico usando a paleta Spectrum, conhecendo os recursos de saída da imagem, prepará-la manualmente e carregá-la usando png2c-z88dk ou png2scr.< /p>

A maneira mais fácil – pegue uma imagem de 32 bits, mude o número de cores para 3-4 no Gimp, edite-a levemente e importe-a para img2spec para não trabalhar com restrições de cores manualmente, exporte png e converta-a em um array C usando png2c- z88dk.

Lembre-se de que, para uma exportação bem-sucedida, cada bloco não pode conter mais de duas cores.

Como resultado, você receberá um arquivo h que contém o número de blocos únicos, se houver mais de ~128, simplifique a imagem no Gimp (aumente a repetibilidade) e execute o procedimento de exportação sobre um novo .

Após a exportação, você literalmente carrega a “fonte” dos blocos e imprime o “texto” dos índices dos blocos na tela. Abaixo está um exemplo da “classe” de renderização:

// грузим шрифт в память
    unsigned char *pt = fullscreenImage->tiles;

    for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
            sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
    }

    // ставим курсор в 0,0
    sp1_SetPrintPos(&ps0, 0, 0);

    // печатаем строку
    sp1_PrintString(&ps0, fullscreenImage->ptiles);

Desenhando sprites na tela

A seguir descreverei um método para desenhar sprites de 16×16 pixels na tela. Não cheguei na animação e na mudança de cores, porque… É trivial que já nesta fase, presumo, tenha ficado sem memória. Portanto, o jogo contém apenas sprites monocromáticos transparentes.

Desenhamos uma imagem png monocromática 16×16 no Gimp, depois usando png2sp1sprite a traduzimos em um arquivo assembly asm, em código C declaramos arrays do arquivo assembly e adicionamos o arquivo na fase de montagem.< /p>

Após a etapa de declaração do recurso sprite, ele deve ser adicionado na tela na posição desejada, segue abaixo um exemplo do código da “classe” do objeto do jogo:

    struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2,    SP1_TYPE_2BYTE, col2-col1, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB,  SP1_TYPE_2BYTE, 0, 0);
    sp1_IterateSprChar(bubble_sprite, initialiseColour);

A partir dos nomes das funções você pode entender aproximadamente o significado de – aloque memória para o sprite, adicione duas colunas 8×8, adicione uma cor para o sprite.

A posição do sprite é indicada em cada quadro:

sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);

Emulando OOP

Não existe sintaxe para OOP em C. O que você deve fazer se ainda quiser? Você precisa conectar sua mente e ser iluminado pela ideia de que não existe equipamento OOP; em última análise, tudo se resume a uma das arquiteturas de máquina, na qual simplesmente não existe o conceito de objeto e outras abstrações associadas a ele.< /p>

Esse fato me impediu por muito tempo de entender por que OOP é necessário, por que é necessário usá-lo se no final tudo se trata de código de máquina.

Porém, após trabalhar no desenvolvimento de produtos, descobri as delícias desse paradigma de programação, principalmente, é claro, flexibilidade de desenvolvimento, mecanismos de proteção de código, com abordagem correta, reduzindo entropia, simplificando o trabalho em equipe. Todos os benefícios acima decorrem de três pilares – polimorfismo, encapsulamento, herança.

Vale ressaltar também a simplificação da resolução de questões relacionadas à arquitetura de aplicações, pois 80% dos problemas de arquitetura foram resolvidos por cientistas da computação no século passado e descritos na literatura sobre padrões de projeto. A seguir, descreverei maneiras de adicionar sintaxe semelhante a OOP em C.

É mais conveniente tomar estruturas C como base para armazenar dados de uma instância de classe. Claro, você pode usar um buffer de bytes, criar sua própria estrutura para classes, métodos, mas por que reinventar a roda? Afinal, já estamos reinventando a sintaxe.

Dados da turma

Exemplo de campos de dados de “classe” do GameObject:

struct GameObjectStruct {
    struct sp1_ss *gameObjectSprite;
    unsigned char *sprite_col;
    unsigned char x;
    unsigned char y;
    unsigned char referenceCount;
    unsigned char beforeHideX;
    unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;

Salve nossa classe como “GameObject.h”, faça #include “GameObject.h” no lugar certo e use-a.

Métodos de classe

Levando em consideração a experiência dos desenvolvedores da linguagem Objective-C, a assinatura de um método de classe serão funções em escopo global, o primeiro argumento será sempre a estrutura de dados, seguido dos argumentos do método. Abaixo está um exemplo de “método” da “classe” GameObject:

void GameObject_hide(GameObject *gameObject) {
    gameObject->beforeHideX = gameObject->x;
    gameObject->beforeHideY = gameObject->y;
    gameObject->y = 200;
}

A chamada do método é semelhante a esta:

GameObject_hide(gameObject);

Construtores e destruidores são implementados da mesma maneira. É possível implementar um construtor como alocador e inicializador de campo, mas prefiro controlar a alocação e inicialização de objetos separadamente.

Trabalhando com memória

Gerenciamento manual de memória do formulário usando malloc e free agrupados em macros novas e de exclusão para corresponder ao C++:

#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)

Para objetos que são usados ​​por várias classes ao mesmo tempo, o gerenciamento de memória semi-manual é implementado com base na contagem de referências, à imagem e semelhança do antigo mecanismo ARC do Objective-C Runtime:

void GameObject_retain(GameObject *gameObject) {
    gameObject->referenceCount++;
}

void GameObject_release(GameObject *gameObject) {
    gameObject->referenceCount--;

    if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
        sp1_DeleteSpr(gameObject->gameObjectSprite);
        delete(gameObject);
    }
}

Assim, cada classe deve declarar o uso de um objeto compartilhado usando reter, liberando propriedade por meio de release. A versão moderna do ARC usa chamadas automáticas de retenção/liberação.

Som!

O Spectrum possui um tweeter capaz de reproduzir música de 1 bit; os compositores da época conseguiam reproduzir nele até 4 canais de som simultaneamente.

O Spectrum 128k contém um chip de som separado AY-3-8910, que pode reproduzir música rastreada.

Uma biblioteca é oferecida para usar o tweeter no z88dk

O que resta aprender

Eu estava interessado em conhecer o Spectrum, implementar o jogo usando o z88dk e aprender muitas coisas interessantes. Ainda tenho muito que aprender, por exemplo, o montador Z80, pois ele me permite usar toda a potência do Spectrum, trabalhar com bancos de memória e trabalhar com o chip de som AY-3-8910. Espero participar da competição no próximo ano!

Links

https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/

Código fonte

https://gitlab.com/demensdeum/ zx-projects/tree/master/interceptor2020

Leave a Comment

Your email address will not be published. Required fields are marked *