在 Scratch 里做一款 3D 恐怖游戏,听起来像是把 3D 建模、图形渲染和关卡设计全部塞进积木编程工具。实际做完后会发现,Scratch 本身虽然是 2D 舞台引擎,但只要理解一种叫“射线投射”的渲染思路,完全可以在不加载任何外部素材的情况下,渲染出第一人称 3D 迷宫。恐怖感的核心也不再是模型有多精细,而是视野受限制、墙壁突然出现、声音突然变化。下文会以“迷宫探索恐怖游戏”为目标,讲解从地图设计、摄像机移动、射线投射渲染到碰撞检测的完整实现,并提供可以在 Scratch 中逐步复现的变量、函数结构和伪代码。这个项目适合已经熟悉 Scratch 基础积木、想挑战更高难度作品的读者,也能作为理解 3D 图形原理的入门案例。
1. 先搞清楚:Scratch 的 2D 舞台如何呈现 3D 世界
Scratch 的舞台本质是平面坐标系,一个角色只有 x 坐标和 y 坐标,没有 z 坐标。所谓“3D 游戏”,并不是在 Scratch 内部真的创建了一个三维空间,而是通过数学计算,把三维世界里的位置投影到二维屏幕上。只要投影结果足够合理,玩家就会产生“我在走 3D 迷宫”的错觉。
1.1 Scratch 是 2D 引擎,3D 要靠“视觉欺骗”
做 3D 效果常见的方案有几种:
- 射线投射(Raycasting):从摄像机位置向屏幕的每一列发射一条射线,射线碰到墙后就停止,用墙的距离决定这一列墙画多高。经典游戏《德军总部 3D》就是这种思路。
- 多边形投影:把 3D 模型的顶点投影到 2D 平面上,再用画笔把顶点连起来。适合做低多边形风格的 3D 模型。
- 伪 3D 精灵:用大小变化、位置偏移和遮挡关系模拟远近,适合做赛车类或跑酷类小游戏。
恐怖迷宫更适合射线投射。因为射线投射非常适合表现走廊、墙角、房间和转角,而“转角遇到怪”正是恐怖游戏最常见的玩法。射线投射的实现不算复杂,计算量也小,在 Scratch 的积木循环里可以跑得动。
1.2 射线投射的核心公式:一列像素对应一条射线
在射线投射中,屏幕不是按“像素点”去画的,而是按“竖线”去画。假设舞台宽 480 像素,渲染分辨率设置成 120 列,那么每列宽度就是 4 像素。每一列对应一条射线,这条射线从摄像机位置出发,插入迷宫地图中。
射线投射的几个关键变量:
- 摄像机位置:posX、posY。
- 摄像机方向向量:dirX、dirY。
- 投影平面向量:planeX、planeY。
方向向量决定玩家朝哪个方向看,投影平面向量决定视野宽度。对屏幕第 x 列,可以算出这一列对应的射线方向:
cameraX = (2 * x) / W - 1 rayDirX = dirX + planeX * cameraX rayDirY = dirY + planeY * cameraX射线进入地图后,沿着网格一格一格前进,直到撞到墙。撞墙后得到一个垂直距离 perpDist,墙的高度就由这个距离决定:
lineHeight = H / perpDist这里的 H 是屏幕高度。距离越远,height 越小,墙看起来就越矮;距离越近,墙越接近整个屏幕。这种“距离越远,物体越小”的投影关系,就是 3D 感的来源。
特别要注意,计算墙高时用的是“垂直距离”,不是射线从摄像机到墙的直线距离。如果使用直线距离,左右两侧的墙会向外膨胀,画面出现明显的鱼眼变形。垂直距离可以消除这种变形。
1.3 恐怖感从哪里来:视野、明暗和声音才是重点
很多初学者以为恐怖游戏必须有精细的贴图和复杂的 3D 模型。在 Scratch 项目里,真正决定恐怖感的是三件事:视野限制、明暗层次、声音节奏。
| 设计元素 | 实现方式 | 效果 |
|---|---|---|
| 第一人称视角 | 射线投射画墙 | 玩家直接代入角色 |
| 视野限制 | 超过一定距离不画墙 | 玩家看不到远处,被迫前进 |
| 黑暗空间 | 渲染后叠一层半透明黑色遮罩 | 降低可见度,制造压迫感 |
| 惊吓点 | 走到指定区域后广播事件 | 突然出现怪物造型和声音 |
| 音效节奏 | 循环低频环境音 | 持续制造紧张情绪 |
在后续实现中,渲染部分只负责“三维空间感”,气氛部分靠黑幕、视野截断和广播事件完成。这样设计的好处是代码清晰,玩家体验也不会因为缺少贴图而崩塌。
2. 素材与地图准备:把迷宫变成程序读得懂的数据
做射线投射前,必须先有一张“逻辑地图”。地图不是图片,而是程序可以逐格读取的数据。用 Scratch 的列表保存地图,是最直接的做法。
2.1 环境准备:哪套 Scratch 环境最适合做渲染实验
建议使用 Scratch 3.0 官方在线编辑器或桌面版,并手动添加“画笔”扩展。射线投射会大量使用画笔的“落笔”“抬笔”“移动”和“全部擦除”积木。
| 项目 | 建议 |
|---|---|
| 推荐环境 | Scratch 3.0 网页版或桌面版 |
| 舞台分辨率 | 480 x 360 |
| 渲染列数 | 80 到 120 列 |
| 所需扩展 | 画笔扩展 |
| 前置技能 | 变量、列表、广播、自定义积木 |
如果网络不稳定,也可以使用 Scratch 桌面版。离线版和在线版在画笔积木上基本一致,不会影响项目迁移。
2.2 地图矩阵设计:0 是路,1 是墙,2 是门
地图可以用一个字符串列表表示。每一行是一个字符串,每个字符代表一个格子。比如:
1111111111 1000000001 1011110101 1010000101 1010111101 1000000001 1010000001 1111111111在这个迷宫地图里,“1”表示墙,“0”表示可以走的空地。最外面一圈全是“1”,保证玩家不会走出地图范围。
在 Scratch 中,可以把每一行作为列表mapRows的一项,读取某个坐标的字符时用:
第 (mapC) 个字符,第 (mapR) 项具体来说,如果玩家位置是 posX、posY,那么它对应的格子列号和行号可以这样计算:
mapX = 向下取整 (posX) + 1 mapY = 向下取整 (posY) + 1因为 Scratch 列表下标从 1 开始,而 posX 从 0 开始,所以计算时要加 1。下面示例地图中,如果玩家初始位置是 (1.5, 1.5),对应的地图格子是第 2 行第 2 列,字符是“0”,玩家就能正常移动。
地图设计要注意几点:
- 每行字符数必须一样,否则读取会错位。
- 边界必须是墙“1”。
- 玩家出生点不能是墙。
- 如果想做“门上锁”,可以再加字符“2”,在移动逻辑里单独判断。
2.3 精灵与舞台设计:只有一个观察者,其余靠画笔
这个项目里的“角色”不一定要是一个可见的小猫精灵。射线投射依靠画笔在舞台上画墙,所以可以专门用一个画笔精灵负责绘制。
项目结构可以设计成:
- 画笔精灵:隐藏状态,执行所有射线投射和画线操作。
- 黑幕精灵:一个全屏的黑色造型,用“虚像”效果控制透明度,用来压暗画面。
- 怪物精灵:平时隐藏,广播“惊吓事件”后突然显示。
- 舞台背景:纯黑色。
- 变量和列表:统一放在画笔精灵中,方便管理。
隐藏画笔精灵后仍然可以使用画笔积木绘制内容,这是 Scratch 画笔的常用技巧。为了避免玩家看到精灵自身,建议所有绘制精灵都设为隐藏。主循环里也不需要真的“移动角色”,只需要更新摄像机的变量值,然后重新绘制一帧。
3. 第一人称摄像机:移动、旋转和碰撞检测
“摄像机”在 Scratch 中不是一个精灵,而是一组变量。玩家的移动、转身、渲染,都在修改这些变量。理解了摄像机模型,后面的渲染函数才会顺畅。
3.1 摄像机变量:位置、方向、投影平面
一个完整的射线投射摄像机需要这些变量:
| 变量名 | 含义 | 初始值示例 |
|---|---|---|
| posX | 玩家在地图中的 x 坐标 | 1.5 |
| posY | 玩家在地图中的 y 坐标 | 1.5 |
| dirX | 摄像机方向向量的 x 分量 | 1 |
| dirY | 摄像机方向向量的 y 分量 | 0 |
| planeX | 投影平面向量的 x 分量 | 0 |
| planeY | 投影平面向量的 y 分量 | 0.66 |
| speed | 移动速度 | 0.08 |
| rotSpeed | 旋转速度 | 0.06 |
方向向量指向玩家“正前方”,投影平面向量定义了屏幕左右方向。当方向向量接近正 x 方向时,投影平面接近正 y 方向,两者保持垂直,视野才不会变形。
初始化伪代码:
定义 初始化摄像机 将 [posX v] 设为 (1.5) 将 [posY v] 设为 (1.5) 将 [dirX v] 设为 (1) 将 [dirY v] 设为 (0) 将 [planeX v] 设为 (0) 将 [planeY v] 设为 (0.66)这里 planeY 设置为 0.66,对应大约 60 度到 70 度的水平视野。值越大,画面越宽,但边缘变形越明显;值越小,视野越窄,越像“望远镜”效果。
3.2 前后左右移动和旋转的数学公式
移动很简单:向前走,就是把摄像机位置沿方向向量移动一小段距离。
posX = posX + dirX * speed posY = posY + dirY * speed向后走就是用减号,向左扫射可以用投影向量的反方向,向右扫射可以用投影向量的正方向:
posX = posX - planeX * speed posY = posY - planeY * speed旋转比较复杂。旋转时,方向向量 dir 和投影平面向量 plane 都要同时旋转。旋转公式如下:
oldDirX = dirX dirX = oldDirX * cos(角度) - dirY * sin(角度) dirY = oldDirX * sin(角度) + dirY * cos(角度) oldPlaneX = planeX planeX = oldPlaneX * cos(角度) - planeY * sin(角度) planeY = oldPlaneX * sin(角度) + planeY * cos(角度)Scratch 的三角函数积木使用角度制,不需要手动转换弧度。左转和右转就是把“角度”传成正数或负数,具体正负方向可以在测试时调整。
定义 旋转摄像机 (旋转角度) 将 [oldDirX v]