纯前端实现光路解谜:射线步进、镜片转向与关卡状态机

举报
yd_213843956 发表于 2026/10/21 01:47:13 2026/10/21
【摘要】 用一个 7×7 网格光路解谜小游戏,拆解纯前端射线模拟:方向映射、循环检测、镜片旋转、路径动画和关卡状态管理,并记录几个容易忽略的实现细节。

光路解谜看起来像是简单的画线游戏,真正落到网页里,核心其实是一个很克制的小型模拟器:光从固定方向出发,在离散网格中前进;遇到镜片时改变方向;撞墙后终止;抵达目标才算通关。这篇笔记记录我用原生 JavaScript 实现一个 7×7 光路解谜小游戏时,对射线步进、镜片交互和关卡状态的整理。

一、用网格而不是浮点坐标

如果让光线在 Canvas 上按像素移动,碰撞判断会很快变成一堆坐标比较。关卡本身就是格子棋盘,因此我把所有逻辑都放在整数网格里。每格记录墙、镜片、光源、目标或空地,光线状态只保留一个位置和一个方向:

const directions = {
  up:    { x: 0, y: -1 },
  right: { x: 1, y: 0 },
  down:  { x: 0, y: 1 },
  left:  { x: -1, y: 0 },
};

function nextCell(position, direction) {
  const delta = directions[direction];
  return { x: position.x + delta.x, y: position.y + delta.y };
}

渲染层可以把一格画成几十像素,但模拟层始终只认行列编号。这样既能支持桌面和手机的不同尺寸,也让预览与正式游戏共用同一套判定。

二、镜片只负责改变方向

关卡中有两种镜片:/ 与 \。它们在视觉上是两条短线,逻辑上则是一张方向映射表。光线进入镜片后立即转向,然后继续走下一格:

const mirrors = {
  '/': {
    right: 'up',
    left: 'down',
    up: 'right',
    down: 'left',
  },
  '\\': {
    right: 'down',
    left: 'up',
    up: 'left',
    down: 'right',
  },
};

function turn(mirror, direction) {
  return mirrors[mirror][direction];
}

把规则做成表有三个好处:新增镜片类型时不用改主循环;单元测试可以直接覆盖所有方向组合;动画也能先求出整条路线,再根据路线长度决定播放速度。

三、射线步进必须有明确终点

光线每一步都走一格,但循环不能只靠“碰到目标就停”。最常见的漏洞是两张镜片把光来回反射,形成无限循环。我的做法是同时检查边界、障碍和重复状态:

function trace(level) {
  const seen = new Set();
  let ray = { ...level.source };
  const path = [];

  for (let step = 0; step < 100; step++) {
    ray = nextCell(ray, ray.direction);
    if (!inside(level, ray)) return { path, hit: 'wall' };

    const key = `${ray.x},${ray.y},${ray.direction}`;
    if (seen.has(key)) return { path, hit: 'loop' };
    seen.add(key);
    path.push({ ...ray });

    const cell = level.at(ray.x, ray.y);
    if (cell.type === 'wall') return { path, hit: 'wall' };
    if (cell.type === 'target') return { path, hit: 'target' };
    if (cell.type === 'mirror') {
      ray.direction = turn(cell.value, ray.direction);
    }
  }

  return { path, hit: 'limit' };
}

位置和方向一起组成状态键,是因为同一格从不同方向射入可能得到完全不同的结果。记录它既能识别死循环,也方便以后做“路径预测”或“提示下一步”的功能。

四、点击镜片,而不是拖动画线

移动端拖拽很容易和页面滚动冲突,因此交互被我简化为点击:点一次镜片旋转 90 度,点击空白区域不产生任何状态变化。每次旋转后都重新计算光路,并用短动画逐格展示:

function rotateAt(x, y) {
  const cell = level.at(x, y);
  if (cell.type !== 'mirror') return false;

  cell.value = cell.value === '/' ? '\\' : '/';
  const result = trace(level);
  renderPath(result.path, result.hit === 'target');
  return true;
}

光线动画以 requestAnimationFrame 驱动,每帧点亮路线中的若干格。动画期间暂时忽略新的镜片点击,避免上一条射线还没画完就被新状态覆盖。等动画结束,再开放下一次操作。

五、关卡状态与完成判定

一个小游戏能持续玩下去,不能只有“赢了”两个状态。我的关卡对象保留当前地图、镜片旋转次数、光路结果和是否完成:

const levelState = {
  index: 0,
  moves: 0,
  completed: false,
  path: [],
};

function evaluate(result) {
  levelState.path = result.path;
  levelState.completed = result.hit === 'target';
  if (levelState.completed) showCompletion(levelState.moves);
}

切换关卡时使用深拷贝后的初始地图,而不是直接复用上一关的格子对象。否则玩家旋转过的镜片会被带进下一局,调试时看起来像是“关卡数据丢失”,实际只是引用被共享了。

六、小结

光路解谜的骨架并不复杂:整数网格负责空间,方向映射表负责规则,带状态去重的射线步进负责安全终止,关卡对象负责保存进度。把这几层拆开后,新增镜片、换主题或制作移动端布局都不会牵动核心判定。对于纯前端小游戏来说,先把规则写成可预测的小函数,再考虑动画和装饰,通常比反过来更省时间。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。