C64 Basic:游戏地图俯视“摄像机视角”

Hacker News Top 工具

摘要

本教程讲解如何在C64 BASIC中实现游戏地图的俯视摄像机视图,并介绍了优化技巧。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/26 15:55

# C64 BASIC:游戏地图俯视"摄像机视角" 来源:https://retrogamecoders.com/overhead-camera-view/ 深度解析如何创建《创世纪》风格的地图视图,以及由此引发的 C64 BASIC 优化之旅 像《创世纪》这类游戏使用的经典俯视摄像机视角,与我之前展示的移动角色视角(参见我的复古 roguelike 教程 (https://retrogamecoders.com/roguelike-multiplatform/))有何不同?这种视角又是如何实现的? Jay 在 Commodore 64 Ultimate Development & Modifications Facebook 群组 (https://www.facebook.com/groups/754519687678324/?multi_permalinks=874168462380112&hoisted_section_header_type=recently_seen&__cft__[0]=AZYb8kGPXn9ElKM-LpSGVJhipT0rb1zdd7qJw7JtIPoPEWj9LCWX1ezzO53q4ajrUJ3F0kLuZKbiZKpVhHH6snrX0hAtQEC2c6CBY9FsHkW3kTH5LvfPNb2Nzcfv-BIWNa1P852__TfZ2tOSMFQHuhsx&__tn__=%2CO%2CP-R) 中提出了一个问题 (https://www.facebook.com/groups/754519687678324/?multi_permalinks=874168462380112&hoisted_section_header_type=recently_seen&__cft__[0]=AZYb8kGPXn9ElKM-LpSGVJhipT0rb1zdd7qJw7JtIPoPEWj9LCWX1ezzO53q4ajrUJ3F0kLuZKbiZKpVhHH6snrX0hAtQEC2c6CBY9FsHkW3kTH5LvfPNb2Nzcfv-BIWNa1P852__TfZ2tOSMFQHuhsx&__tn__=%2CO%2CP-R): > 角色移动 vs 地图移动。希望这说得通。我有一个 40x24 的地图。我知道如何编码角色在地图上移动,这部分很简单。我想要的是让角色保持在屏幕中心,然后移动地图来响应角色移动。就像《创世纪3》那样,地图窗口是 11x11,角色永远不离开中心。当你移动时,地图会随之滚动。有人能分享一个不错的示例程序源码吗?我在这里学习。 以下是我的回答(事后看来,这个回答其实并不够有帮助): > 在 C64 字符模式下实现的方法是:地图定义了整个潜在区域,而"摄像机视角"是地图上从 (x, y) 位置开始的一个切片。如果地图是 100x100,那么你需要绘制 x 到 x+11、y 到 y+11 的范围。我相信有人已经有现成的代码了,如果没有,我之后也可以提供一些。 我不想就这样留下一个不完整的回答,我觉得有必要给出一个更好的解决方案,而且这个话题本身也是一个很适合写成博客的挑战,所以就有了这篇文章。 ## 视口 vs 地图 正如我在回答中简要提到的,核心思想在于区分"世界地图"和可见部分。我们是在模拟一个进入完整世界的视口或窗口,解决方案需要从大地图中切出正确的部分并将其粘贴到游戏屏幕上。 - 我们的玩家拥有 `X` 和 `Y` 坐标,代表玩家在世界中的水平(左)和垂直(顶)位置。 - 世界地图是**整个潜在区域**,存储在内存中,与当前屏幕上的人物或物体无关。 - 屏幕上的可玩区域只是一个固定大小的**摄像机视图**,它是从世界地图中某个 `(x, y)` 位置开始的一个切片。 - 对于 100×100 的地图,你只绘制 `x 到 x+10` 列和 `y 到 y+10` 行(即 11 个方块,形成 11×11 的窗口)。 另一个难点是,无论我们如何绘制,我们都希望将游戏焦点集中在玩家角色上,因此还需要从左上角增加一个额外的偏移量,确保精灵或其他元素在屏幕的水平和垂直中心,而不是始终位于游戏屏幕的左上角。 [](https://ide.retrogamecoders.com/?file=yt_hello.bas&platform=c64)你现在可以跟随教程,在 Online Retro IDE (https://ide.retrogamecoders.com/?file=yt_hello.bas&platform=c64) 中边看边编辑代码。 – 无需下载、配置等操作,而且完全免费! ## 初稿:未经优化 我们的原始初稿完全未经优化,几乎只是伪代码的简单实现: 1. 定义一个二维地图数组 `M(x, y)`,其大小等于完整世界的尺寸。 2. 单独跟踪玩家的**世界位置** `(PX, PY)` 和其**屏幕位置**。 3. 每次游戏循环: - 计算摄像机左上角:`CX = PX - 5`,`CY = PY - 5`(11×11 视口的一半) - 将摄像机位置夹紧,确保不会读取到地图边界之外(《塞尔达传说》风格) - 对于 11×11 视口中的每个单元格 `(I, J)`,将 `M(CX+I, CY+J)` 复制到屏幕 RAM 的 `(OX+I, OY+J)` 位置 - 在屏幕中心(或如果摄像机被夹紧则偏移)绘制玩家 - 等待输入,更新 `(PX, PY)`,重新绘制 显然,这非常非常慢,但作为概念验证,它帮助我们确定了基本的实现框架。 ### 第一级代码 → 获取最终代码的可编辑副本,并在在线编辑器中运行 (https://ide.retrogamecoders.com/?share=TmEQZCu8pi-jJC36)。 `` 10 REM ---- 大地图 / 小摄像机 演示 ---- 20 REM JAY 的问题:让玩家居中, 30 REM 地图围绕玩家移动(《创世纪》风格) 40 REM 第一级:极其缓慢的未优化初稿 50 MW=40 : MH=24 60 VW=11 : VH=11 70 HX=INT(VW/2) : HY=INT(VH/2) 80 OX=14 : OY=6 90 SC=1024 100 DIM M(MW-1,MH-1) 110 GOSUB 600 : REM 构建地图 120 PX=20 : PY=12 130 PRINT CHR$(147) 140 GOSUB 300 : REM 绘制视口 150 GOSUB 500 : REM 绘制玩家 160 GET K$ : IF K$="" THEN 160 170 DX=0 : DY=0 180 IF K$="W" THEN DY=-1 190 IF K$="S" THEN DY=1 200 IF K$="A" THEN DX=-1 210 IF K$="D" THEN DX=1 220 IF K$="Q" THEN PRINT CHR$(147) : END 230 NX=PX+DX : NY=PY+DY 240 IF NX<0 OR NX>MW-1 OR NY<0 OR NY>MH-1 THEN 160 250 PX=NX : PY=NY 260 GOSUB 300 : GOSUB 500 : GOTO 160 270 REM 300 REM ---- 绘制视口(初级方法) ---- 310 CX=PX-HX : CY=PY-HY 320 IF CX<0 THEN CX=0 330 IF CY<0 THEN CY=0 340 IF CX>MW-VW THEN CX=MW-VW 350 IF CY>MH-VH THEN CY=MH-VH 360 FOR J=0 TO VH-1 370 FOR I=0 TO VW-1 380 POKE SC+(OY+J)*40+(OX+I),M(CX+I,CY+J) 390 NEXT I 400 NEXT J 410 RETURN 420 REM 500 REM ---- 绘制玩家(初级方法) ---- 510 SX=OX+(PX-CX) : SY=OY+(PY-CY) 520 POKE SC+SY*40+SX,81 530 RETURN 540 REM 600 REM ---- 构建测试地图 ---- 610 FOR Y=0 TO MH-1 620 FOR X=0 TO MW-1 630 T=46 640 IF X=0 OR Y=0 OR X=MW-1 OR Y=MH-1 THEN T=160 650 IF (X=10 AND Y>4 AND Y<15) THEN T=160 660 IF (Y=8 AND X>14 AND X<25) THEN T=87 670 M(X,Y)=T 680 NEXT X 690 NEXT Y 700 RETURN `` ## 第二阶段:屏幕查找表 (LUT) 我们可以止步于上述代码,但它的动画效果更像幻灯片而不是游戏,而且你可能会因为启动速度极慢而认为它已经崩溃了。我们先来调整显示逻辑。 在这个阶段,我们能做的最重要的优化是替换昂贵的乘法运算 `(OY+J)*40`,改用预计算的查找表。我们的 8 位 6510 处理器在乘法运算上并不快,在浮点 BASIC (https://www.youtube.com/watch?v=wo14rDnGUbY) 中更是如此。因此我们添加 `DIM R(24)`,并在启动时一次性填充:`FOR Y = 0 TO 24 : R(Y) = Y*40 : NEXT Y`。 显示循环变成了一次查找,没有乘法: `RO = SC + R(OY+J) + OX` `POKE RO+I, M(CX+I, CY+J)` 消除了 121 次浮点乘法!速度提升约 3–5 倍。 不过这会使启动速度变得更慢。 ## 第三阶段:双重查找表 为什么还是慢?BASIC v2 中访问二维数组每次读取仍然隐藏了一次乘法。 那么,我们把地图改为扁平的一维数组:`DIM M(MW*MH - 1)`。 由于这一改动,我们还需要添加一个地图行查找表:`DIM MR(MH-1)`,并在启动时用 `MR(Y) = Y * MW` 填充该表。 现在,我们的视口循环只剩下加法操作,这是微处理器更擅长的: - `RO = SC + R(OY+J) + OX`(该行的屏幕基址) - `MO = MR(CY+J) + CX`(该行的地图基址) - `POKE RO+I, M(MO+I)`(零次乘法) 我们再次用初始化延迟换取了运行速度——我们在启动时增加了约 24 次乘法,但每显示一帧消除了约 121 次乘法。 ## 第四阶段:初始化进度指示器 我们一开始的启动就慢,现在更慢了,如果不显示程序正在运行,看起来肯定像是卡住了。 我们只需要在每个初始化循环内部打印一些内容。讽刺的是,这些打印语句会进一步降低启动速度——C64 BASIC 的打印可一点也不快。 幸运的是,在实际游戏中,我们会将 LUT 和地图编码为 `DATA` 语句并用 `READ` 读取,或者更好的方法是直接从磁盘加载。 ## 第五阶段:循环展开 优化的最后一步是找到下一个"热点路径"并进行优化。 > "热点路径"是程序中执行最频繁的部分。优化这些部分能带来最显著的性能提升。 在本例中,`FOR J = 0 TO 10 ... NEXT J` 每次重绘都会执行,且循环边界是固定的常量。 BASIC 的 `FOR/NEXT` 每次迭代的开销很大(压栈、变量查找、比较、跳转)。 因此,我们可以将计算移到另一个 LUT 中:`DIM VR(10)` 在启动时填充 `VR(J) = SC + R(OY+J) + OX`,然后"展开"其中一个循环。我们用 11 条显式行替换外层的 `FOR` 循环,每行负责输出一行: `RO = VR(0) : MO = MR(CY) + CX : GOSUB 460` `RO = VR(1) : MO = MR(CY+1) + CX : GOSUB 460` 是的,我们仍然有一个 `FOR`,但每显示一帧消除了 10 个 `NEXT`,这已经相当不错了。 每次按下 WASD 键时,感觉响应快多了,这很重要。 ## 最终代码(暂定) → 获取最终代码的可编辑副本,并在在线编辑器中运行 (https://ide.retrogamecoders.com/?share=TmEQZCu8pi-jJC36)。 后续还有进一步的优化思路,但目前这个版本已经可以运行,虽然仍然有点慢。 `` 10 REM ---- 大地图 / 小摄像机 演示 ---- 20 REM JAY 的问题:让玩家居中, 30 REM 地图围绕玩家移动(《创世纪》风格) 40 REM 第三级:展开的视口 + 预计算 50 MW=40 : MH=24 60 VW=11 : VH=11 70 HX=INT(VW/2) : HY=INT(VH/2) 80 OX=14 : OY=6 90 SC=1024 95 PRINT CHR$(147) : PRINT "LOADING"; 100 DIM M(MW*MH-1) 105 DIM R(24) 106 DIM MR(MH-1) 107 DIM VR(10) : REM 预烘焙的视口行屏幕基址 110 FOR Y=0 TO 24 : R(Y)=Y*40 : PRINT "."; : NEXT Y 111 FOR Y=0 TO MH-1 : MR(Y)=Y*MW : PRINT "."; : NEXT Y 112 FOR J=0 TO 10 : VR(J)=SC+R(OY+J)+OX : NEXT J 113 PRINT : PRINT "BUILDING MAP"; 114 GOSUB 600 115 PRINT : PRINT "READY" 120 PX=20 : PY=12 130 PRINT CHR$(147) 140 GOSUB 300 150 GOSUB 500 160 GET K$ : IF K$="" THEN 160 170 DX=0 : DY=0 180 IF K$="W" THEN DY=-1 190 IF K$="S" THEN DY=1 200 IF K$="A" THEN DX=-1 210 IF K$="D" THEN DX=1 220 IF K$="Q" THEN PRINT CHR$(147) : END 230 NX=PX+DX : NY=PY+DY 240 IF NX<0 OR NX>MW-1 OR NY<0 OR NY>MH-1 THEN 160 250 PX=NX : PY=NY 260 GOSUB 300 : GOSUB 500 : GOTO 160 270 REM 300 REM ---- 绘制视口(展开 + LUT) ---- 310 CX=PX-HX : CY=PY-HY 320 IF CX<0 THEN CX=0 330 IF CY<0 THEN CY=0 340 IF CX>MW-VW THEN CX=MW-VW 350 IF CY>MH-VH THEN CY=MH-VH 360 RO=VR(0) : MO=MR(CY)+CX : GOSUB 460 361 RO=VR(1) : MO=MR(CY+1)+CX : GOSUB 460 362 RO=VR(2) : MO=MR(CY+2)+CX : GOSUB 460 363 RO=VR(3) : MO=MR(CY+3)+CX : GOSUB 460 364 RO=VR(4) : MO=MR(CY+4)+CX : GOSUB 460 365 RO=VR(5) : MO=MR(CY+5)+CX : GOSUB 460 366 RO=VR(6) : MO=MR(CY+6)+CX : GOSUB 460 367 RO=VR(7) : MO=MR(CY+7)+CX : GOSUB 460 368 RO=VR(8) : MO=MR(CY+8)+CX : GOSUB 460 369 RO=VR(9) : MO=MR(CY+9)+CX : GOSUB 460 370 RO=VR(10) : MO=MR(CY+10)+CX : GOSUB 460 410 LX=CX : LY=CY 420 RETURN 430 REM 460 REM ---- POKE 一个视口行 ---- 470 FOR I=0 TO 10 : POKE RO+I, M(MO+I) : NEXT I 480 RETURN 490 REM 500 REM ---- 绘制玩家 ---- 510 SX=OX + (PX-LX) 520 SY=OY + (PY-LY) 530 POKE SC+R(SY)+SX, 81 540 RETURN 550 REM 600 REM ---- 构建测试地图 ---- 620 FOR Y=0 TO MH-1 625 YB=MR(Y) : PRINT "."; 630 FOR X=0 TO MW-1 640 T=46 650 IF X=0 OR Y=0 OR X=MW-1 OR Y=MH-1 THEN T=160 660 IF (X=10 AND Y>4 AND Y<15) THEN T=160 670 IF (Y=8 AND X>14 AND X<25) THEN T=87 675 M(YB+X)=T 680 NEXT X 690 NEXT Y 700 RETURN `` ## 未来优化思路 接下来是一些未来优化方向的想法…… ### 1. PRINT 比 POKE 更快 最大的优化是消除缓慢的 POKE(笑)。我们之前已经看到,在 C64 BASIC 中没有汇编子程序的情况下,print 比 poke 更快 (https://retrogamecoders.com/printing-petscii-faster/)。`POKE` 逐个字符写入屏幕内存是缓慢的。而使用光标转义控制符定位光标再 `PRINT`,是纯 BASIC 中剩余的最大优化点。 通过字符串拼接构建字符串仍然慢,所以我认为更好的做法是将地图构建为字符串数组的行。我们可以使用 C64 BASIC 的字符串操作命令 (https://retrogamecoders.com/strings-text-adventure) 来提取我们需要的部分。 ### 2. 从 BASIC 调用的汇编子程序 或者,作为补充,我们可以编写一个汇编子程序来完成显示,并使用内存拷贝。这将绕过我们的显示循环和逐字符的摩擦,只需给定一个起始内存地址,就能快速将源数据复制到目标位置。 ### 3. 元图块 Bitmap Brothers 的像素艺术给了我很大的启发。例如《混沌引擎》中元图块的出色运用。 最后想到的是使用**元图块**。初始化之所以慢,部分原因是地图是一个字符一个字符组成的,但在《创世纪》或《塞尔达传说》这类游戏中,你可能使用 3×3 或 5×5 的图块来构成墙角、房屋的一部分、道路的拐弯等。这将使加载或生成世界地图快得多,因为显示时虽然是相同大小,但数据量可以压缩到原来的 1/3 或更小。 ### 其他改进: 我还会如何改进? - **颜色**:并行 POKE 到 `$D800`(55296),使墙壁为灰色、水域为蓝色、草地为绿色、玩家为黄色…… - **局部重绘**:当移动一个图块时,你只需要绘制新显露出的行或列,以及旧/新玩家位置,大约 12 个字符而不是 121 个,因此速度大约提升 10 倍。 - **硬件平滑滚动**:写入 `$D016` / `$D011` 实现亚字符级别的像素滚动。 - **自定义字符集**:用定制的瓦片图形替换默认字体,为游戏带来真正的精致感。 ## 经验教训 本讨论中介绍的技术适用于任何平台或语言。将世界坐标与可见屏幕坐标解耦,并将可玩的可见区域视为一个进入更大"世界"缓冲区的窗口。虽然我的"塞尔达风格 (https://ide.retrogamecoders.com/?platform=rgc-basic&file=rpg%2Frpg.bas)"演示使用了推动滚动,但核心概念是相同的。 我们很快陷入了一个支线任务:试图让 CBM BASIC v2 跑得更快。特别是乘法运算的成本非常明显。 查找表是复古系统上最强大的优化工具:用一点点 RAM 和预先计算的开销,换取消运行时巨大的时间节省。你在演示场景中会反复看到这种技术。 最后,**展开循环**(以及任何你需要做的事情),但在测量出瓶颈所在(你的热点路径)之前不要进行优化。正如 Robin 的视频 (https://www.youtube.com/watch?v=wo14rDnGUbY) 中所提到的,仅仅因为某件事看起来应该能加快速度,并不意味着它真的会!

相似文章

这个周末你打算做什么?

Lobsters Hottest

一位开发者描述了将《完美黑暗64》关卡移植到 noclip.website 的过程,强调了读取 N64 显示列表和重新实现渲染引擎的挑战。

Voxel Space

Hacker News Top

本文介绍了1992年游戏《Comanche》中使用的Voxel Space渲染技术,涵盖其高度图、颜色图和简单的光栅化算法。

模拟器调试:Area 5150 的 Lake Effect

Lobsters Hottest

本文详细介绍了在MartyPC模拟器上调试Area5150演示中“Lake”效应的过程,解释了需要特定标题hack的原因,以及通过总线嗅探和动态时钟实现周期精确CGA模拟的后续修复方法。