使用 C 语言进行 ZX Spectrum 游戏开发

这篇废话帖子专门为老ZX Spectrum电脑用C语言开发一个游戏,来看看帅哥吧:

1982年开始生产,一直生产到1992年。该机的技术特点:8位Z80处理器,16-128kb内存和其他扩展,例如AY-3-8910声音芯片。

作为Yandex Retro Games Battle 2019竞赛的一部分,我为这台机器编写了一个游戏叫做Interceptor 2020。由于我没有时间学习Z80的汇编语言,所以我决定用C来开发它。作为工具链,我选择了一套现成的工具链—— z88dk,其中包含 C 编译器和许多辅助库,可加快 Spectrum 应用程序的执行速度。它还支持许多其他 Z80 机器,例如 MSX、德州仪器计算器。

接下来,我将描述我对计算机体系结构、z88dk 工具链的浅薄了解,并展示我如何设法实现 OOP 方法和使用设计模式。

安装功能

z88dk 的安装应该根据存储库中的手册进行,但是,对于 Ubuntu 用户,我想注意一个功能:如果您已经从 deb 包安装了 Z80 编译器,那么您应该删除它们,因为 z88dk 默认情况下将从 bin 文件夹访问它们;由于工具链编译器版本不兼容,您很可能无法编译任何内容。 /p>

你好世界

编写 Hello World 非常简单:

#include 

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

编译 Tap 文件更加容易:

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

要运行,请使用任何支持 Tap 文件的 ZX Spectrum 模拟器,例如在线模拟器:
http://jsspeccy.zxdemo.org/

全屏在图片上绘图

tl;dr 图片以图块形式绘制,图块大小为 8×8 像素,图块本身内置于 Spectrum 字体中,然后将图片打印为索引中的一条线。

精灵和图块输出库 sp1 使用 UDG 输出图块。图片被转换为一组单独的 UDG(图块),然后使用索引在屏幕上组装。应该记住,UDG 用于显示文本,如果您的图片包含非常大的图块集(例如,超过 128 个图块),那么您将不得不超出该集的边界并删除默认的 Spectrum字体。为了解决这个限制,我使用了 128 – 的基数。 255,通过简化图像同时保留原始字体。关于简化下面的图片。

要绘制全屏图像,您需要使用三个实用程序来武装自己:
瘸子
img2spec
png2c-z88dk

真正的 ZX 男人、真正的复古战士有一种方法,那就是使用 Spectrum 调色板打开图形编辑器,了解图像输出的特征,手动准备并使用 png2c-z88dk 或 png2scr 上传。< /p>

更简单的方法–拍摄一张32位图像,在Gimp中将颜色数量切换为3-4,稍微编辑一下,然后将其导入到img2spec中,以免手动处理颜色限制,导出png并使用png2c-将其转换为C数组z88dk。

您应该记住,为了成功导出,每个图块不能包含超过两种颜色。

因此,您将收到一个包含唯一图块数量的 h 文件,如果超过 ~128 个,则在 Gimp 中简化图像(增加重复性)并在新图块上执行导出过程.

导出后,您可以从字面上加载图块中的“字体”,并将图块索引中的“文本”打印到屏幕上。下面是渲染“类”的示例:

// грузим шрифт в память
    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);

在屏幕上绘制精灵

接下来我将描述一种在屏幕上绘制 16×16 像素精灵的方法。我没有开始动画和改变颜色,因为……正如我所假设的,在这个阶段我已经耗尽了内存,这是微不足道的。因此,游戏仅包含透明的单色精灵。

我们在Gimp中绘制一个单色png图像16×16,然后使用png2sp1sprite将其转换为asm汇编文件,在C代码中我们从汇编文件中声明数组,并在汇编阶段添加该文件。< /p>

在声明精灵资源阶段之后,必须将其添加到屏幕上所需的位置,下面是游戏对象“类”的代码示例:

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

从函数名称就可以大致明白–的含义。为精灵分配内存,添加两个 8×8 列,为精灵添加颜色。

每帧中都会指示精灵的位置:

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

模拟 OOP

C 中没有 OOP 语法,如果你真的想要的话该怎么办?您需要连接您的思想并受到以下想法的启发:不存在 OOP 设备之类的东西;一切最终都属于一种机器架构,其中根本不存在对象的概念以及与之相关的其他抽象。 /p>

这个事实让我在很长一段时间里都无法理解为什么需要 OOP,如果最终一切都变成了机器代码,为什么有必要使用它。

但是,在从事产品开发之后,我发现了这种编程范式的好处,当然主要是开发灵活性、代码保护机制、采用正确的方法、减少熵、简化团队工作。所有上述好处都源于三大支柱——多态、封装、继承。

还值得注意的是解决与应用程序架构相关的问题的简化,因为 80% 的架构问题是在上个世纪由计算机科学家解决的,并在设计模式的文献中进行了描述。接下来,我将描述向 C 添加类似 OOP 的语法的方法。

以C结构体作为存储类实例数据的基础更为方便。当然,您可以使用字节缓冲区,为类、方法创建自己的结构,但为什么要重新发明轮子呢?毕竟,我们已经在重新发明语法。

类数据

游戏对象“类”数据字段示例:

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;

将我们的类保存为“GameObject.h”,在正确的位置执行#include“GameObject.h”并使用它。

类方法

考虑到 Objective-C 语言开发人员的经验,类方法的签名将是全局范围内的函数,第一个参数始终是数据结构,后面是方法参数。下面是“类”GameObject 的“方法”示例:

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

方法调用如下所示:

GameObject_hide(gameObject);

构造函数和析构函数的实现方式相同。可以将构造函数实现为分配器和字段初始值设定项,但我更喜欢单独控制对象分配和初始化。

使用内存

使用 new 和 delete 宏中包含的 malloc 和 free 来手动管理表单内存以匹配 C++:

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

对于同时被多个类使用的对象,半手动内存管理是基于引用计数实现的,类似于旧的 Objective-C Runtime ARC 机制:

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);
    }
}

因此,每个类必须使用retain声明共享对象的使用,通过release释放所有权。现代版本的 ARC 使用自动保留/释放调用。

声音!

Spectrum 有一个能够再现 1 位音乐的高音扬声器;当时的作曲家能够同时再现多达 4 个声道。

Spectrum 128k包含一个独立的声音芯片AY-3-8910,可以播放跟踪器音乐。

提供了一个用于在 z88dk 中使用高音扬声器的库

还有什么需要学习

我有兴趣熟悉 Spectrum、使用 z88dk 实现游戏并学习很多有趣的东西。我还有很多东西需要学习,例如 Z80 汇编器,因为它使我能够充分利用 Spectrum 的功能、使用内存库以及使用 AY-3-8910 声音芯片。希望明年还能参加比赛!

链接

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

源代码

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

Leave a Comment

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