RetroGameCoders 5 月 24 日发了一篇 Commodore 64 BASIC v2 开发笔记。主题很小:做一个类似《Ultima III》的俯视地图窗口。

小到什么程度?完整地图在那里,屏幕每次只画玩家周围 11×11 个格子。

这件事有意思的地方不在“BASIC 也能画地图”。真正卡人的,是 C64 BASIC v2 太慢。乘法慢,二维数组访问慢,FOR/NEXT 有成本,逐字符 POKE 也贵。一个 11×11 视窗只有 121 个格子,但如果每帧都在里面重复算地址、查数组、跑循环,手感很快会从游戏变成翻页。

我更在意的是这篇示例背后的思路:先把世界坐标和屏幕坐标分家,再把热路径里能挪走的计算尽量挪走。

视窗不是地图,摄像机也不总在正中

示例里的地图是完整 world map。玩家坐标用 PX/PY 表示,意思是玩家在整个世界地图里的位置。

屏幕只负责显示一块切片。窗口宽高是 11,半径就是 5。正常情况下,摄像机左上角可以从 PX-5PY-5 算出来。

但这里有一个新手很容易写错的点:靠近地图边缘时,摄像机不能继续往外取。CX/CY 必须 clamp,防止读到地图外面。

这也意味着,玩家不是永远绝对固定在屏幕中心。到了边缘,玩家会在 11×11 视窗里偏移。

这个模型看着朴素,其实很关键。世界地图是一套坐标,屏幕显示是另一套坐标。两者只在“摄像机取哪一块”这一步发生关系。

对复古 RPG 开发者来说,第一步不该是写更复杂的绘制循环,而是先检查数据结构:

问题容易写成更稳的做法直接影响
地图和屏幕关系把屏幕当成地图本身完整 world map + 11×11 viewport后续滚动、边缘处理更清楚
摄像机位置永远让玩家居中PX/PY 减半径后再 clamp避免越界,也承认边缘偏移
玩家移动每次重画全世界只处理可见切片把问题压到 121 格以内

这不是“架构感”好听不好听的问题。慢机器上,坐标关系一旦绑死,后面每一处优化都在补锅。

优化链路很直接:把帧内成本搬走

C64 BASIC v2 的痛点很具体。它不是现代语言里那种“可能有点慢”。在解释执行环境里,乘法、数组索引、循环控制,都会实打实进入每一帧。

原文的优化链路也因此很清楚:少算,少查,少绕。

热点朴素写法优化方向代价
屏幕地址每格算 (Y*40)+X屏幕行 LUT启动时要先生成表
地图访问二维数组 M(X,Y)1D 扁平数组代码可读性下降一点
地图行位置绘制时临时计算地图行 LUT初始化变慢
外层绘制双层 FOR/NEXT展开 11 行代码更长、更重复
字符输出每格逐字符 POKE后续可试 PRINT 或汇编拷贝BASIC 里仍难彻底解决

这里最值得学的是 LUT 的取舍。查表不是免费性能,它只是把成本从“每帧”挪到“启动”。

原文也提到,初始化会慢到需要加载进度输出。不然用户可能以为程序卡死了。

所以,原文里约 3–5 倍的改善,只能当经验判断。它没有给严格基准。更稳妥的说法是:这些改法减少了 121 个格子绘制时反复发生的高成本操作。

如果你正在学 C64 BASIC,建议动作很具体:先写一个最朴素的 11×11 绘制;再只加屏幕行 LUT;然后换成 1D 地图数组;最后再考虑展开循环。每次只改一个变量,自己计时或观察输入延迟。

不要一上来就把所有技巧塞进去。那样代码能跑,但你不知道到底是哪一步救了帧率。

还没到“可玩引擎”,下一步看四个瓶颈

这篇示例不该被理解成完整游戏引擎。它更像一张路线图:告诉你在 C64 BASIC 里,viewport 绘制的成本藏在哪里。

现实限制也摆在这里。最终方案仍偏慢,逐字符 POKE 还是重。循环展开能省掉一部分 BASIC 管理开销,但它不能改变解释器本身的天花板。

接下来最该看的不是“还能不能再省一次乘法”,而是路线是否切到更接近游戏成品的方向:

后续方向解决什么判断标准
PRINT + 光标控制尝试替代逐字符 POKE是否比 POKE 更顺滑
汇编拷贝把热路径搬出 BASICBASIC 是否只负责逻辑调度
局部重绘玩家移动一格时少画内容是否只更新新出现的一行/一列
VIC-II 硬件滚动利用机器特性做平滑移动是否从字符刷新转向屏幕滚动

对复古游戏开发者来说,接下来的选择会很实际:如果目标只是教学,BASIC 版本够用了;如果目标是做一个能玩的 8 位 RPG 原型,就要考虑把显示路径交给 PRINT 技巧、汇编例程或硬件滚动。

对学习者来说,也别急着追“最快写法”。更有价值的练习是分清三件事:哪些成本来自数据模型,哪些成本来自 BASIC 解释器,哪些成本来自 C64 的显示方式。

回到开头那个 11×11 视窗。它只有 121 个格子,却足够暴露一条老机器上的硬道理:坐标没拆清,后面每一帧都在还债;热路径不减负,再小的地图也会慢。