ChatGPT 的 o3-mini 刚推出的第一天,就有用户展示了其对物理运动理解(小球滚动、弹跳等)相当出色,并且反映在其做出的程序中。
于是我试了试,让它用静态的 HTML ( 也就是不用到服务器 )来模拟《超级玛丽》的 1-1 关卡。
效果如下:
嗯,动是动起来了,弹跳以及踩到怪物的「触感」也有那么一些像。不过就是关卡的构造(砖头、水管位置等)不太对。
这么说起来,为什么一个大语言模型会知道《超级玛丽》游戏关卡画面的内容啊?
下面是代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>超级玛丽 1-1 模拟演示(扩展升级版)</title>
<style>
body {
margin: 0;
background: #87ceeb; /* 天空蓝背景 */
}
canvas {
display: block;
margin: auto;
background: #87ceeb;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="400"></canvas>
<script>
// 获取画布和上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 常量定义
const GRAVITY = 0.6;
const GROUND_HEIGHT = 50;
// 游戏状态:
// "playing":游戏中;"ending":进入结束动画;"finished":通关结束
let gameState = "playing";
// endingPhase 分为 "flag"(旗杆下降阶段)和 "castle"(走进城堡阶段)
let endingPhase = "";
// 用于控制旗帜下降的进度(0~1)
let flagDropProgress = 0;
// 定义玛丽(用红色矩形表示)
const mario = {
x: 50,
y: 0,
width: 20,
height: 30,
vx: 2, // 水平移动速度
vy: 0, // 垂直速度
onGround: false,
jumpSpeed: -12 // 跳跃初速度(增强起跳力度)
};
// 定义关卡(关卡宽度远大于画布宽度)
// obstacles 数组中包含管道、问号砖块、悬空平台以及旗杆
const level = {
width: 2000,
groundY: canvas.height - GROUND_HEIGHT,
obstacles: [
// --- 原作早期区域 ---
// 管道
{ x: 300, y: canvas.height - GROUND_HEIGHT - 40, width: 40, height: 40, type: 'pipe' },
{ x: 600, y: canvas.height - GROUND_HEIGHT - 40, width: 40, height: 40, type: 'pipe' },
// 问号砖块(用橙色矩形,并在砖块上绘制问号)
{ x: 200, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
{ x: 240, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
{ x: 280, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
// 悬空平台
{ x: 500, y: canvas.height - GROUND_HEIGHT - 80, width: 50, height: 10, type: 'platform' },
{ x: 800, y: canvas.height - GROUND_HEIGHT - 120, width: 50, height: 10, type: 'platform' },
// --- 中间区域增加更多原作元素 ---
// 一组连续的问号砖块
{ x: 1000, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
{ x: 1040, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
{ x: 1080, y: canvas.height - GROUND_HEIGHT - 100, width: 30, height: 30, type: 'block' },
// 头顶上方的砖块(模拟多层布局)
{ x: 1040, y: canvas.height - GROUND_HEIGHT - 140, width: 30, height: 30, type: 'block' },
// 额外的管道
{ x: 1200, y: canvas.height - GROUND_HEIGHT - 40, width: 40, height: 40, type: 'pipe' },
// 悬空平台
{ x: 1400, y: canvas.height - GROUND_HEIGHT - 80, width: 50, height: 10, type: 'platform' },
// --- 旗杆区域 ---
{ x: 1900, y: canvas.height - GROUND_HEIGHT - 150, width: 20, height: 150, type: 'flag' }
]
};
// 初始化旗杆:给旗杆增加一个属性 flagCurrentY,用来记录旗帜当前的绘制位置
level.obstacles.forEach(obs => {
if (obs.type === 'flag') {
obs.flagCurrentY = obs.y; // 初始时与旗杆顶部对齐
}
});
// 定义敌人(用棕色矩形表示,并在上面绘制简单眼睛)
const enemies = [
{ x: 400, y: level.groundY - 20, width: 20, height: 20, vx: -0.5 },
{ x: 900, y: level.groundY - 20, width: 20, height: 20, vx: -0.5 },
{ x: 1500, y: level.groundY - 20, width: 20, height: 20, vx: -0.5 }
];
// 定义城堡(位于关卡末端,用灰色矩形表示,并绘制城堡门)
const castle = { x: 1950, y: level.groundY - 150, width: 100, height: 150 };
// 相机水平偏移(实现关卡滚动效果)
let cameraX = 0;
// 自动判断玛丽是否需要跳跃:
// 若前方一定距离内存在障碍物或敌人,则自动起跳
function shouldJump() {
// 检查障碍物
for (let obs of level.obstacles) {
if ((obs.type === 'pipe' || obs.type === 'block' || obs.type === 'platform') &&
obs.x > mario.x && (obs.x - mario.x) < 70) {
if (mario.onGround) {
return true;
}
}
}
// 检查敌人
for (let enemy of enemies) {
if (enemy.x > mario.x && (enemy.x - mario.x) < 50) {
if (mario.onGround) {
return true;
}
}
}
return false;
}
// 轴对齐包围盒的碰撞检测
function isColliding(a, b) {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
// 更新游戏状态
function update() {
if (gameState === "playing") {
// 自动起跳判断
if (shouldJump() && mario.onGround) {
mario.vy = mario.jumpSpeed;
mario.onGround = false;
}
// 应用重力
mario.vy += GRAVITY;
// 更新玛丽的位置
mario.x += mario.vx;
mario.y += mario.vy;
// 与地面碰撞
if (mario.y + mario.height > level.groundY) {
mario.y = level.groundY - mario.height;
mario.vy = 0;
mario.onGround = true;
}
// 检测与障碍物的碰撞(不包括旗杆)
for (let obs of level.obstacles) {
if (obs.type !== 'flag') {
let obsRect = { x: obs.x, y: obs.y, width: obs.width, height: obs.height };
let marioRect = { x: mario.x, y: mario.y, width: mario.width, height: mario.height };
if (isColliding(marioRect, obsRect)) {
// 如果是从上方落下,则让玛丽站在障碍物上
if (mario.y + mario.height - mario.vy <= obs.y) {
mario.y = obs.y - mario.height;
mario.vy = 0;
mario.onGround = true;
} else {
// 否则简单将玛丽推到障碍物左侧
mario.x = obs.x - mario.width;
}
}
}
}
// 检测与敌人的碰撞
for (let i = enemies.length - 1; i >= 0; i--) {
let enemy = enemies[i];
let enemyRect = { x: enemy.x, y: enemy.y, width: enemy.width, height: enemy.height };
let marioRect = { x: mario.x, y: mario.y, width: mario.width, height: mario.height };
if (isColliding(marioRect, enemyRect)) {
// 如果玛丽正处于下落状态且从上方碰撞,则视为踩死敌人
if (mario.vy > 0 && mario.y + mario.height - mario.vy <= enemy.y) {
enemies.splice(i, 1);
mario.vy = mario.jumpSpeed / 2; // 踩死后轻微反弹
} else {
// 否则给予一个小反弹
mario.vy = mario.jumpSpeed / 2;
}
}
}
// 更新敌人位置
enemies.forEach(enemy => {
enemy.x += enemy.vx;
// 遇到关卡边界时反向
if (enemy.x <= 0 || enemy.x + enemy.width >= level.width) {
enemy.vx = -enemy.vx;
}
// 遇到障碍物时反向
for (let obs of level.obstacles) {
if (obs.type !== 'flag') {
let obsRect = { x: obs.x, y: obs.y, width: obs.width, height: obs.height };
let enemyRect = { x: enemy.x, y: enemy.y, width: enemy.width, height: enemy.height };
if (isColliding(enemyRect, obsRect)) {
enemy.vx = -enemy.vx;
}
}
}
});
// 检测与旗杆碰撞(扩展旗帜区域以便触发)
for (let obs of level.obstacles) {
if (obs.type === 'flag') {
let flagRect = { x: obs.x, y: obs.flagCurrentY, width: obs.width + 30, height: 30 };
let marioRect = { x: mario.x, y: mario.y, width: mario.width, height: mario.height };
if (isColliding(marioRect, flagRect)) {
gameState = "ending";
endingPhase = "flag";
// 停止玛丽的正常运动
mario.vx = 0;
mario.vy = 0;
}
}
}
// 相机跟随玛丽
cameraX = mario.x - 100;
if (cameraX < 0) cameraX = 0;
} else if (gameState === "ending") {
// 结束动画:分为“旗杆下降阶段”和“走进城堡阶段”
if (endingPhase === "flag") {
// 使用线性插值控制旗帜下降
// 取出旗杆对象(假设只有一个旗杆)
let flagObs = level.obstacles.find(obs => obs.type === 'flag');
// 最终目标:旗帜下降到旗杆底部上方 40 像素处
let finalFlagY = flagObs.y + flagObs.height - 40;
if (flagDropProgress < 1) {
flagDropProgress += 0.01; // 控制下降速度,可调整此数值
if (flagDropProgress > 1) flagDropProgress = 1;
flagObs.flagCurrentY = flagObs.y + flagDropProgress * (flagObs.height - 40);
} else {
// 旗帜下降完成后,玛丽沿旗杆滑下
let targetY = level.groundY - mario.height;
if (mario.y < targetY) {
mario.y += 2; // 滑下速度
if (mario.y > targetY) mario.y = targetY;
} else {
// 滑到底后切换到走进城堡阶段
endingPhase = "castle";
mario.vx = 1; // 玛丽开始向城堡走
}
}
// 相机跟随玛丽
cameraX = mario.x - 100;
if (cameraX < 0) cameraX = 0;
} else if (endingPhase === "castle") {
// 玛丽向城堡走动
mario.x += mario.vx;
if (mario.x > castle.x + castle.width / 2) {
gameState = "finished";
}
cameraX = mario.x - 100;
if (cameraX < 0) cameraX = 0;
}
}
// 当 gameState 为 "finished" 时,画面保持不动,并显示通关信息
}
// 绘制游戏画面
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景和地面
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#654321";
ctx.fillRect(0 - cameraX, level.groundY, level.width, GROUND_HEIGHT);
// 绘制障碍物
level.obstacles.forEach(obs => {
if (obs.type === 'pipe') {
ctx.fillStyle = "green";
ctx.fillRect(obs.x - cameraX, obs.y, obs.width, obs.height);
} else if (obs.type === 'block') {
ctx.fillStyle = "orange";
ctx.fillRect(obs.x - cameraX, obs.y, obs.width, obs.height);
ctx.fillStyle = "black";
ctx.font = "16px sans-serif";
ctx.fillText("?", obs.x - cameraX + 8, obs.y + 20);
} else if (obs.type === 'platform') {
ctx.fillStyle = "#A0522D";
ctx.fillRect(obs.x - cameraX, obs.y, obs.width, obs.height);
} else if (obs.type === 'flag') {
// 绘制旗杆
ctx.fillStyle = "black";
ctx.fillRect(obs.x - cameraX, obs.y, obs.width, obs.height);
// 绘制旗帜,使用 flagCurrentY 表示当前旗帜位置
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(obs.x - cameraX + obs.width, obs.flagCurrentY);
ctx.lineTo(obs.x - cameraX + obs.width + 30, obs.flagCurrentY + 15);
ctx.lineTo(obs.x - cameraX + obs.width, obs.flagCurrentY + 30);
ctx.closePath();
ctx.fill();
}
});
// 绘制敌人
enemies.forEach(enemy => {
ctx.fillStyle = "brown";
ctx.fillRect(enemy.x - cameraX, enemy.y, enemy.width, enemy.height);
// 绘制简单眼睛
ctx.fillStyle = "white";
ctx.fillRect(enemy.x - cameraX + 4, enemy.y + 4, 4, 4);
ctx.fillRect(enemy.x - cameraX + enemy.width - 8, enemy.y + 4, 4, 4);
});
// 绘制城堡
ctx.fillStyle = "gray";
ctx.fillRect(castle.x - cameraX, castle.y, castle.width, castle.height);
ctx.fillStyle = "black";
ctx.fillRect(castle.x - cameraX + castle.width / 2 - 10, castle.y + castle.height - 40, 20, 40);
// 绘制玛丽
ctx.fillStyle = "red";
ctx.fillRect(mario.x - cameraX, mario.y, mario.width, mario.height);
// 如果游戏通关,显示提示信息
if (gameState === "finished") {
ctx.fillStyle = "black";
ctx.font = "30px sans-serif";
ctx.fillText("关卡通关!", canvas.width / 2 - 70, canvas.height / 2);
}
}
// 主循环
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
// 延迟1秒后启动演示
setTimeout(() => {
gameLoop();
}, 1000);
</script>
</body>
</html>