# 九艾斯 3D 俄罗斯方块 · 原生 9S + 4D 虚拟机 # 绵阳九艾斯科技有限公司 版权所有 (c) 2026 # 运行: 九S虚拟机(4D).exe 俄罗斯方块3D.9s # # 说明: 10 列 × 16 行 3D 立体棋盘,已落方块是真实立方体, # 当前方块带发光材质,右侧有 3D 下一块预览。 # 改下面配置区,重新运行就能看到不同 3D 效果。 常量 颜色_白 = 16777215 常量 颜色_红 = 255 常量 颜色_绿 = 65280 常量 颜色_蓝 = 16711680 常量 颜色_黄 = 65535 常量 颜色_青 = 16776960 常量 颜色_紫 = 16711935 常量 颜色_橙 = 42495 常量 颜色_暗蓝 = 4202528 常量 颜色_黑 = 0 # ===================================================================== # ★★★ 可改配置:只改下面这些行,重新运行就能看到不同 3D 效果 ★★★ # ===================================================================== 变量 方块一色 = 颜色_青 # 长条块颜色 变量 方块二色 = 颜色_黄 # 方形块颜色 变量 方块三色 = 颜色_紫 # T 形块颜色 变量 方块四色 = 颜色_绿 # S 形块颜色 变量 方块五色 = 颜色_红 # Z 形块颜色 变量 方块六色 = 颜色_蓝 # J 形块颜色 变量 方块七色 = 颜色_橙 # L 形块颜色 变量 网格颜色 = 颜色_暗蓝 # 棋盘网格线颜色 变量 基础下落帧 = 14 # 每多少帧下落一格(越小越快,一帧 33ms) 变量 每级加速 = 2 # 每升一级下落间隔减少多少帧 变量 最快下落帧 = 3 # 下落间隔最快到多少帧 变量 相机高度 = 60 # 相机高度(越大越俯视) 变量 相机距离 = 900 # 相机离棋盘的距离(越大越远) 变量 相机摆动 = 12 # 相机左右摆动幅度 变量 幽灵投影 = 真 # 真=显示落点投影;假=关闭 # ===================================================================== 常量 列数 = 10 常量 行数 = 16 常量 格子 = 30 常量 块尺寸 = 28 变量 窗口 = 空 变量 画布 = 空 变量 场景 = 0 变量 形状表 = [] 变量 颜色表 = [] 变量 占用码 = [] 变量 方块ID = [] 变量 方块行 = [] 变量 方块列 = [] 变量 当前块ID = [] 变量 预览块ID = [] 变量 幽灵块ID = [] 变量 当前形状 = [] 变量 当前色 = 颜色_白 变量 当前行 = 0 变量 当前列 = 3 变量 下一索引 = 0 变量 分数 = 0 变量 行数统计 = 0 变量 下落计数 = 0 变量 下落间隔 = 基础下落帧 变量 状态 = 1 变量 计时 = 0 变量 提示文本 = "" 变量 提示寿命 = 0 函数 世界X(列) { 返回 (列 - 4.5) * 格子 } 函数 世界Y(行) { 返回 225 - 行 * 格子 } # 把格子坐标转成占用码 函数 占用(行, 列) { 返回 行 * 列数 + 列 } # 检查某个格子是否已被已落方块占用 函数 已占用(行, 列) { 变量 码 = 占用(行, 列) 变量 序号 = 0 当 (序号 < 列表长度(占用码)) { 如果 (列表查询(占用码, 序号) == 码) { 返回 真 } 序号 = 序号 + 1 } 返回 假 } # 检查形状放在 (测试行, 测试列) 是否碰撞 函数 碰撞(测试行, 测试列, 形状) { 变量 行号 = 0 当 (行号 < 4) { 变量 列号 = 0 当 (列号 < 4) { 如果 (列表查询(列表查询(形状, 行号), 列号) == 1) { 变量 目标行 = 测试行 + 行号 变量 目标列 = 测试列 + 列号 如果 (目标列 < 0 或 目标列 >= 列数 或 目标行 >= 行数) { 返回 真 } 如果 (目标行 >= 0 且 已占用(目标行, 目标列) == 真) { 返回 真 } } 列号 = 列号 + 1 } 行号 = 行号 + 1 } 返回 假 } # 7 种方块形状(统一 4x4,方便旋转) 函数 初始化形状() { 形状表 = [] 列表追加(形状表, [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]]) 列表追加(形状表, [[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]]) 列表追加(形状表, [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]]) 列表追加(形状表, [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]]) 列表追加(形状表, [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]]) 列表追加(形状表, [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]]) 列表追加(形状表, [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]]) 颜色表 = [方块一色, 方块二色, 方块三色, 方块四色, 方块五色, 方块六色, 方块七色] } # 立方体着色(已落方块) 函数 上色(物体ID, 颜色) { 设置塑料材质(场景, 物体ID, 颜色) 设置3D物体材质(场景, 物体ID, 颜色, 0.55, 48, 1.0, 0.12, 0.65, 0.05) } # 棋盘网格线 / 外框着色(暗光,不抢方块) 函数 上色网格(物体ID, 颜色) { 设置塑料材质(场景, 物体ID, 颜色) 设置3D物体材质(场景, 物体ID, 颜色, 0.0, 8, 1.0, 0.0, 0.3, 0.25) } # 立方体着色(落点投影:半透明) 函数 上色幽灵(物体ID, 颜色) { 设置塑料材质(场景, 物体ID, 颜色) 设置3D物体材质(场景, 物体ID, 颜色, 0.0, 2, 0.3, 0.0, 0.2, 0.1) } # 立方体着色(发光:当前方块 / 预览) 函数 上色发光(物体ID, 颜色) { 设置气泡材质(场景, 物体ID, 颜色) 设置3D物体材质(场景, 物体ID, 颜色, 0.0, 8, 1.0, 0.1, 0.3, 0.9) } # 一次创建棋盘背景、网格、当前方块池、预览块 函数 创建场景() { 如果 (场景 != 0) { 返回 } 场景 = 创建3D场景(860) 设置3D相机(场景, 0, 相机高度, 0 - 相机距离) 设置相机注视(场景, 0, 0, 0) 设置3D光源(场景, 0.35, -0.85, -0.35, 0.75) 设置泛光(0) 设置体积光束(0) 设置轮廓发光(0) 设置环境光遮蔽(0) 设置色彩校正(1, 1.08, 1.02, 0.0) 设置画质档位(0) 设置渲染质量(2) # 棋盘不铺背板:保留星空背景,让发光方块更突出 # 外框 变量 框上ID = 添加立方体(场景, 0, 240, 30, 340, 6, 10, 网格颜色) 上色网格(框上ID, 网格颜色) 变量 框下ID = 添加立方体(场景, 0, -240, 30, 340, 6, 10, 网格颜色) 上色网格(框下ID, 网格颜色) 变量 框左ID = 添加立方体(场景, -170, 0, 30, 6, 486, 10, 网格颜色) 上色网格(框左ID, 网格颜色) 变量 框右ID = 添加立方体(场景, 170, 0, 30, 6, 486, 10, 网格颜色) 上色网格(框右ID, 网格颜色) # 网格线:每 2 列 / 每 2 行一条,兼顾 3D 纵深和性能 变量 竖 = -105 当 (竖 <= 105) { 变量 竖线ID = 添加立方体(场景, 竖, 0, 30, 2, 480, 8, 网格颜色) 上色网格(竖线ID, 网格颜色) 竖 = 竖 + 60 } 变量 横 = 165 当 (横 >= -165) { 变量 横线ID = 添加立方体(场景, 0, 横, 30, 330, 2, 8, 网格颜色) 上色网格(横线ID, 网格颜色) 横 = 横 - 60 } # 下一块预览区(只保留四角边框,不铺底板) 变量 预览上ID = 添加立方体(场景, 245, 175, 24, 120, 3, 8, 网格颜色) 上色网格(预览上ID, 网格颜色) 变量 预览下ID = 添加立方体(场景, 245, 55, 24, 120, 3, 8, 网格颜色) 上色网格(预览下ID, 网格颜色) 变量 预览左ID = 添加立方体(场景, 185, 115, 24, 3, 120, 8, 网格颜色) 上色网格(预览左ID, 网格颜色) 变量 预览右ID = 添加立方体(场景, 305, 115, 24, 3, 120, 8, 网格颜色) 上色网格(预览右ID, 网格颜色) # 当前方块 + 预览方块:各 4 个立方体池,之后只移动/换色,不反复创建 当前块ID = [] 预览块ID = [] 幽灵块ID = [] 变量 序号 = 0 当 (序号 < 4) { 变量 主体ID = 添加立方体(场景, 0, -3000, 0, 块尺寸, 块尺寸, 块尺寸, 当前色) 上色发光(主体ID, 当前色) 列表追加(当前块ID, 主体ID) 变量 预览ID = 添加立方体(场景, 245, -3000, 20, 18, 18, 18, 当前色) 上色发光(预览ID, 当前色) 列表追加(预览块ID, 预览ID) 变量 幽灵ID = 添加立方体(场景, 0, -3000, 0, 块尺寸 - 4, 块尺寸 - 4, 块尺寸 - 4, 当前色) 上色幽灵(幽灵ID, 当前色) 列表追加(幽灵块ID, 幽灵ID) 序号 = 序号 + 1 } } # 删除所有已落方块 函数 清空已落方块() { 变量 序号 = 列表长度(方块ID) - 1 当 (序号 >= 0) { 删除3D物体(场景, 列表查询(方块ID, 序号)) 序号 = 序号 - 1 } 方块ID = [] 方块行 = [] 方块列 = [] 占用码 = [] } # 初始化 / R 键重开 函数 初始化() { 创建场景() 清空已落方块() 分数 = 0 行数统计 = 0 下落计数 = 0 下落间隔 = 基础下落帧 状态 = 1 计时 = 0 提示文本 = "" 提示寿命 = 0 下一索引 = 随机整数(0, 6) 生成新块() 播放音效文件("资源/start.wav") } # 把当前方块移动到当前行/列 函数 刷新当前块() { 变量 序号 = 0 变量 行号 = 0 当 (行号 < 4) { 变量 列号 = 0 当 (列号 < 4) { 如果 (列表查询(列表查询(当前形状, 行号), 列号) == 1) { 如果 (序号 < 4) { 移动3D物体(场景, 列表查询(当前块ID, 序号), 世界X(当前列 + 列号), 世界Y(当前行 + 行号), 0) 序号 = 序号 + 1 } } 列号 = 列号 + 1 } 行号 = 行号 + 1 } 当 (序号 < 4) { 移动3D物体(场景, 列表查询(当前块ID, 序号), 0, -3000, 0) 序号 = 序号 + 1 } 刷新幽灵() } # 落点投影:半透明立方体显示当前方块会落到哪 函数 刷新幽灵() { 变量 隐藏序号 = 0 如果 (幽灵投影 == 假) { 当 (隐藏序号 < 4) { 移动3D物体(场景, 列表查询(幽灵块ID, 隐藏序号), 0, -3000, 0) 隐藏序号 = 隐藏序号 + 1 } 返回 } 变量 落行 = 当前行 当 (碰撞(落行 + 1, 当前列, 当前形状) == 假) { 落行 = 落行 + 1 } 变量 序号 = 0 变量 行号 = 0 当 (行号 < 4) { 变量 列号 = 0 当 (列号 < 4) { 如果 (列表查询(列表查询(当前形状, 行号), 列号) == 1) { 如果 (序号 < 4) { 移动3D物体(场景, 列表查询(幽灵块ID, 序号), 世界X(当前列 + 列号), 世界Y(落行 + 行号), 4) 序号 = 序号 + 1 } } 列号 = 列号 + 1 } 行号 = 行号 + 1 } 当 (序号 < 4) { 移动3D物体(场景, 列表查询(幽灵块ID, 序号), 0, -3000, 0) 序号 = 序号 + 1 } } # 右侧 3D 下一块预览 函数 刷新预览() { 变量 预览形状 = 列表查询(形状表, 下一索引) 变量 预览颜色 = 列表查询(颜色表, 下一索引) 变量 序号 = 0 变量 行号 = 0 当 (行号 < 4) { 变量 列号 = 0 当 (列号 < 4) { 如果 (列表查询(列表查询(预览形状, 行号), 列号) == 1) { 如果 (序号 < 4) { 变量 块 = 列表查询(预览块ID, 序号) 移动3D物体(场景, 块, 245 + (列号 - 1.5) * 22, 130 - (行号 - 1.5) * 22, 20) 上色发光(块, 预览颜色) 序号 = 序号 + 1 } } 列号 = 列号 + 1 } 行号 = 行号 + 1 } 当 (序号 < 4) { 移动3D物体(场景, 列表查询(预览块ID, 序号), 245, -3000, 20) 序号 = 序号 + 1 } } # 生成新方块(当前 = 预览的下一块,再随机出一个新的下一块) 函数 生成新块() { 当前形状 = 列表查询(形状表, 下一索引) 当前色 = 列表查询(颜色表, 下一索引) 当前行 = 0 当前列 = 3 变量 序号 = 0 当 (序号 < 4) { 上色发光(列表查询(当前块ID, 序号), 当前色) 上色幽灵(列表查询(幽灵块ID, 序号), 当前色) 序号 = 序号 + 1 } 刷新当前块() 下一索引 = 随机整数(0, 6) 刷新预览() 如果 (碰撞(0, 3, 当前形状) == 真) { 状态 = 2 播放音效文件("资源/gameover.wav") } } # 左右移动 函数 左右移动(方向) { 如果 (状态 != 1) { 返回 } 如果 (碰撞(当前行, 当前列 + 方向, 当前形状) == 假) { 当前列 = 当前列 + 方向 刷新当前块() 播放音效文件("资源/move.wav") } } # 旋转:顺时针 90°,带简单踢墙 函数 旋转方块() { 如果 (状态 != 1) { 返回 } 变量 旋转后 = [] 变量 列号 = 0 当 (列号 < 4) { 变量 新行 = [] 变量 行号 = 3 当 (行号 >= 0) { 列表追加(新行, 列表查询(列表查询(当前形状, 行号), 列号)) 行号 = 行号 - 1 } 列表追加(旋转后, 新行) 列号 = 列号 + 1 } 变量 偏X = [0, -1, 1, -2, 2] 变量 序号 = 0 当 (序号 < 5) { 变量 试列 = 当前列 + 列表查询(偏X, 序号) 如果 (碰撞(当前行, 试列, 旋转后) == 假) { 当前形状 = 旋转后 当前列 = 试列 刷新当前块() 播放音效文件("资源/rotate.wav") 返回 } 序号 = 序号 + 1 } } # 下落一格;到底就锁定 函数 下落一格() { 如果 (状态 != 1) { 返回 } 如果 (碰撞(当前行 + 1, 当前列, 当前形状) == 假) { 当前行 = 当前行 + 1 刷新当前块() } 否则 { 锁定方块() } } # 软降:向下并加分 函数 软降() { 如果 (状态 != 1) { 返回 } 如果 (碰撞(当前行 + 1, 当前列, 当前形状) == 假) { 当前行 = 当前行 + 1 分数 = 分数 + 1 下落计数 = 0 刷新当前块() } } # 硬降:直接落底并锁定 函数 硬降() { 如果 (状态 != 1) { 返回 } 变量 落下 = 0 当 (碰撞(当前行 + 1, 当前列, 当前形状) == 假) { 当前行 = 当前行 + 1 落下 = 落下 + 1 } 分数 = 分数 + 落下 * 2 播放音效文件("资源/harddrop.wav") 下落计数 = 0 刷新当前块() 锁定方块() } # 当前方块落地:写入占用、生成立方体,再消行 函数 锁定方块() { 播放音效文件("资源/drop.wav") 变量 行号 = 0 当 (行号 < 4) { 变量 列号 = 0 当 (列号 < 4) { 如果 (列表查询(列表查询(当前形状, 行号), 列号) == 1) { 变量 目标行 = 当前行 + 行号 变量 目标列 = 当前列 + 列号 如果 (目标行 >= 0 且 目标行 < 行数 且 目标列 >= 0 且 目标列 < 列数) { 变量 块ID = 添加立方体(场景, 世界X(目标列), 世界Y(目标行), 0, 块尺寸, 块尺寸, 块尺寸, 当前色) 上色(块ID, 当前色) 列表追加(方块ID, 块ID) 列表追加(方块行, 目标行) 列表追加(方块列, 目标列) 列表追加(占用码, 占用(目标行, 目标列)) } } 列号 = 列号 + 1 } 行号 = 行号 + 1 } 消行() 如果 (状态 == 1) { 生成新块() } } # 统计每一行是否填满 函数 行是否满(行) { 变量 计数 = 0 变量 序号 = 0 当 (序号 < 列表长度(方块行)) { 如果 (列表查询(方块行, 序号) == 行) { 计数 = 计数 + 1 } 序号 = 序号 + 1 } 返回 计数 >= 列数 } # 消行:删掉满行立方体,其余立方体整体下落 函数 消行() { 变量 满行 = [] 变量 行号 = 0 当 (行号 < 行数) { 如果 (行是否满(行号) == 真) { 列表追加(满行, 行号) } 行号 = 行号 + 1 } 如果 (列表长度(满行) == 0) { 返回 } 播放音效文件("资源/clear.wav") # 先删掉满行里的立方体,其余立方体计算下落格数 变量 新ID = [] 变量 新行 = [] 变量 新列 = [] 变量 序号 = 0 当 (序号 < 列表长度(方块ID)) { 变量 原行 = 列表查询(方块行, 序号) 变量 已满 = 假 变量 检查 = 0 当 (检查 < 列表长度(满行)) { 如果 (列表查询(满行, 检查) == 原行) { 已满 = 真 } 检查 = 检查 + 1 } 如果 (已满 == 真) { 删除3D物体(场景, 列表查询(方块ID, 序号)) } 否则 { 变量 下落格 = 0 检查 = 0 当 (检查 < 列表长度(满行)) { 如果 (列表查询(满行, 检查) > 原行) { 下落格 = 下落格 + 1 } 检查 = 检查 + 1 } 变量 目标行 = 原行 + 下落格 移动3D物体(场景, 列表查询(方块ID, 序号), 世界X(列表查询(方块列, 序号)), 世界Y(目标行), 0) 列表追加(新ID, 列表查询(方块ID, 序号)) 列表追加(新行, 目标行) 列表追加(新列, 列表查询(方块列, 序号)) } 序号 = 序号 + 1 } 方块ID = 新ID 方块行 = 新行 方块列 = 新列 # 重建占用码 占用码 = [] 序号 = 0 当 (序号 < 列表长度(新行)) { 列表追加(占用码, 占用(列表查询(新行, 序号), 列表查询(新列, 序号))) 序号 = 序号 + 1 } # 计分 变量 块数 = 列表长度(满行) 行数统计 = 行数统计 + 块数 变量 分值表 = [0, 100, 300, 500, 800] 变量 升级前 = 取整(行数统计 / 10) 变量 加分 = 列表查询(分值表, 块数) * (升级前 + 1) 分数 = 分数 + 加分 提示文本 = "消 " + 文本(块数) + " 行 +" + 文本(加分) 提示寿命 = 60 # 等级加速 下落间隔 = 基础下落帧 - 升级前 * 每级加速 如果 (下落间隔 < 最快下落帧) { 下落间隔 = 最快下落帧 } } # 相机轻微摆动,保持 3D 空间感 函数 更新相机() { 变量 摆动 = 正弦(计时 * 0.02) * 相机摆动 相机运镜(场景, 摆动, 相机高度, 0 - 相机距离, 0, 0, 0) } # 绘制 函数 绘制界面() { 画布_清屏(九S画布()) 渲染3D场景(场景) 画布_画文本(九S画布(), 20, 18, "分数: " + 文本(分数), 颜色_白, 22) 画布_画文本(九S画布(), 20, 50, "等级: " + 文本(取整(行数统计 / 10) + 1), 颜色_黄, 20) 画布_画文本(九S画布(), 20, 78, "消行: " + 文本(行数统计), 颜色_青, 18) 画布_画文本(九S画布(), 20, 108, "3D 原生 9S · 4D 虚拟机", 颜色_青, 16) 画布_画文本(九S画布(), 20, 136, "帧率: " + 文本(取整(获取渲染帧率())) + " fps", 颜色_绿, 14) 画布_画文本(九S画布(), 530, 195, "下一块", 颜色_白, 18) 如果 (提示寿命 > 0) { 画布_画文本(九S画布(), 330, 520, 提示文本, 颜色_黄, 20) } 画布_画文本(九S画布(), 20, 566, "← → 移动 ↑ 旋转 ↓ 加速 空格 直落 R 重开 ESC 退出", 颜色_白, 14) 如果 (状态 == 2) { 画布_画文本(九S画布(), 270, 240, "游戏结束", 颜色_红, 40) 画布_画文本(九S画布(), 230, 300, "按 R 键重新开始", 颜色_白, 20) } } # 按键 函数 按键处理(键码) { 如果 (键码 == 键_R) { 初始化() 请求重绘() 返回 } 如果 (键码 == 键_退出) { 关闭窗口() 返回 } 如果 (状态 == 2) { 返回 } 如果 (键码 == 键_左) { 左右移动(-1) } 如果 (键码 == 键_右) { 左右移动(1) } 如果 (键码 == 键_上) { 旋转方块() } 如果 (键码 == 键_下) { 软降() } 如果 (键码 == 键_空格) { 硬降() } 请求重绘() } # 每帧推进 函数 帧刷新() { 如果 (状态 == 1) { 计时 = 计时 + 1 下落计数 = 下落计数 + 1 如果 (下落计数 >= 下落间隔) { 下落计数 = 0 下落一格() } 更新相机() } 如果 (提示寿命 > 0) { 提示寿命 = 提示寿命 - 1 } 请求重绘() } # 主程序 窗口 = 创建窗口("九艾斯3D俄罗斯方块 · 可改配置", 800, 600) 画布 = 九S画布(窗口) 初始化形状() 创建场景() 初始化() 设置绘制函数(绘制界面) 设置按键函数(按键处理) 设置定时器(33, 帧刷新) 显示窗口() 请求重绘() 启动消息循环()