概述
在 AI 辅助编程(Vibe Coding)的实践中,编写高质量的提示词(Prompt)是决定产出质量的关键环节。
本文档不涉及抽象的理论探讨,而是通过一个具体的网页游戏开发案例,详细拆解如何利用"风格化提示词"与"角色扮演"技术,引导 AI 生成可直接运行的 HTML 游戏文件。
我们将对比普通自然语言描述与结构化风格提示词的差异,并提供完整的代码示例与运行说明。
纲要
- 提示词工程基础
Prompt Engineering的核心价值- 自然语言描述 vs. 风格化提示词
- 角色扮演与任务设定
- 定义专家角色(
Role Prompting) - 具体化需求与文件约束
- 定义专家角色(
- 风格化设计语言
- 视觉基调定义(绿色/浅绿色)
- 元素属性指定(红色小蛇)
- 代码生成与验证
- AI 自动生成
snake.html - 跨端适配与交互测试
- AI 自动生成
- 游戏核心机制拆解
- 游戏循环与帧率控制
- 碰撞检测与边界处理
- 食物生成与分数累积
- 本地存储与最高分持久化
提示词工程的两种实践路径
在 AI 辅助开发中,提示词的编写方式直接影响生成代码的结构完整性与视觉表现。通常而言,我们可将提示词划分为两种风格:普通自然语言描述 与优雅风格化提示词。
普通自然语言描述侧重于功能逻辑的传达,结构松散,适合快速原型验证。而风格化提示词则在功能描述之外,融合了角色设定、设计约束与交互细节,能够引导 AI 生成具备更高完成度与审美一致性的产出物。本文重点剖析后者在实际案例中的应用策略。
角色扮演与上下文注入
为了让 AI 更准确地理解任务复杂度,我们需要在提示词中为其赋予一个特定的"身份"。这种做法在业内被称为角色提示(Role Prompting)。通过在提示词开头声明"你是一位拥有 20 年经验的资深游戏开发工程师,参与过《王者荣耀》与《黑神话:悟空》等大型项目的开发",可以有效提升 AI 在代码结构设计、游戏循环逻辑以及事件处理方面的专业性。
核心角色定义示例:
text
你是一位资深的游戏开发工程师,拥有 20 年全栈开发经验,擅长 HTML5 Canvas 游戏引擎设计与交互优化。
需求拆解与文件约束
在定义角色之后,需要明确具体的开发任务与输出格式。为了确保生成的产物可以直接在浏览器环境中运行,我们必须在提示词中显式声明技术栈限制与文件命名规范。
- 技术栈限定 :必须使用纯
HTML、CSS与JavaScript,生成单一可执行文件。 - 文件命名 :保存为
snake.html,确保用户可直接双击运行。 - 功能逻辑:实现经典的贪吃蛇移动、食物生成、碰撞检测与分数累计机制。
结构化需求描述示例:
text
### 核心需求
1. 开发一款基于 HTML5 Canvas 的贪吃蛇游戏。
2. 所有代码必须内嵌在单个 `snake.html` 文件中。
3. 游戏需支持键盘方向键(↑/↓/←/→)控制蛇的移动方向。
4. 页面需包含游戏重新开始与暂停/继续的功能按钮。
风格化设计语言的注入
为了使生成的游戏界面具有较高的视觉亲和力,我们需要在提示词中细化设计风格。不同于仅描述功能,风格化提示词强调"视觉基调"与"色彩心理学"的结合。
在本案例中,我们定义了以下视觉规范:
- 主色调:整体页面基调为绿色与浅绿色,营造清新、自然的视觉氛围。
- 强调色:蛇身使用红色,与背景绿色形成互补对比,提升游戏主体的辨识度。
- 交互反馈 :食物被吞噬时触发粒子特效(
Particle Effects),增强操作爽感。
通过上述设计约束,AI 在生成 CSS 样式与 JavaScript 动画逻辑时,会主动依据色彩搭配原则进行代码生成,而非随机选取颜色值。
生成流程与文件管理
在 VS Code 环境中,我们将上述整合后的提示词输入 AI 辅助编程插件(如 GitHub Copilot 或 Cursor)。在此过程中,建议将执行模式设置为"手动确认",以便在 AI 生成代码前再次核对文件路径与覆盖策略。
当 AI 完成代码编写后,工作区将生成 snake.html 文件。此时,我们无需启动复杂的开发服务器,直接通过文件管理器双击该文件,即可在系统默认浏览器中运行游戏。
项目目录结构:
dir
.
├── snake.html # 核心游戏文件(包含 HTML/CSS/JS)
└── README.md # 项目说明文档(可选)
游戏核心机制拆解
生成的贪吃蛇游戏本质上是一个基于 Canvas 的实时状态机。为了深入理解其工作原理,我们需要从以下几个核心维度进行剖析。
游戏循环与帧率控制
游戏循环是驱动所有逻辑更新的引擎。本案例采用 setInterval 配合 requestAnimationFrame 的双重策略:setInterval 负责以固定的时间间隔(140ms)驱动蛇的移动逻辑,而 requestAnimationFrame 则负责 UI 的平滑渲染。这种分离方式确保了游戏逻辑的稳定性与画面表现的流畅性。
#mermaid-svg-7F21TTjN58iLnPlh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7F21TTjN58iLnPlh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7F21TTjN58iLnPlh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7F21TTjN58iLnPlh .error-icon{fill:#552222;}#mermaid-svg-7F21TTjN58iLnPlh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7F21TTjN58iLnPlh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7F21TTjN58iLnPlh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7F21TTjN58iLnPlh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .marker.cross{stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7F21TTjN58iLnPlh p{margin:0;}#mermaid-svg-7F21TTjN58iLnPlh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label text{fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label span{color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster-label span p{background-color:transparent;}#mermaid-svg-7F21TTjN58iLnPlh .label text,#mermaid-svg-7F21TTjN58iLnPlh span{fill:#333;color:#333;}#mermaid-svg-7F21TTjN58iLnPlh .node rect,#mermaid-svg-7F21TTjN58iLnPlh .node circle,#mermaid-svg-7F21TTjN58iLnPlh .node ellipse,#mermaid-svg-7F21TTjN58iLnPlh .node polygon,#mermaid-svg-7F21TTjN58iLnPlh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .rough-node .label text,#mermaid-svg-7F21TTjN58iLnPlh .node .label text,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label,#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label{text-anchor:middle;}#mermaid-svg-7F21TTjN58iLnPlh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .rough-node .label,#mermaid-svg-7F21TTjN58iLnPlh .node .label,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label,#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label{text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .node.clickable{cursor:pointer;}#mermaid-svg-7F21TTjN58iLnPlh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .arrowheadPath{fill:#333333;}#mermaid-svg-7F21TTjN58iLnPlh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7F21TTjN58iLnPlh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7F21TTjN58iLnPlh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7F21TTjN58iLnPlh .cluster text{fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh .cluster span{color:#333;}#mermaid-svg-7F21TTjN58iLnPlh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7F21TTjN58iLnPlh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7F21TTjN58iLnPlh rect.text{fill:none;stroke-width:0;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape,#mermaid-svg-7F21TTjN58iLnPlh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape p,#mermaid-svg-7F21TTjN58iLnPlh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7F21TTjN58iLnPlh .icon-shape .label rect,#mermaid-svg-7F21TTjN58iLnPlh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7F21TTjN58iLnPlh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7F21TTjN58iLnPlh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7F21TTjN58iLnPlh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 运行中
是
否
暂停/结束
点击重来
点击暂停
启动游戏
初始化游戏状态
启动 setInterval 循环
检查暂停/结束状态
执行 moveSnake
是否吃到食物?
增加分数, 生成新食物
蛇身移动
绘制 Canvas
等待用户操作
碰撞检测与边界处理
碰撞检测是游戏逻辑的核心安全机制。本案例实现了两种碰撞检测:
- 边界碰撞 :检测蛇头坐标是否超出
0至BOARD_SIZE - 1的范围。若超出,则立即触发gameOver状态。 - 自身碰撞:在蛇移动后,遍历蛇身数组(除头部外),检测是否存在与头部坐标重合的节点。若存在,则判定为游戏结束。
食物生成与分数累积
食物的生成采用"排除法"策略:首先计算整个棋盘所有可用坐标的集合,然后剔除当前蛇身占据的坐标,最后从剩余的空闲坐标中随机选取一个作为新食物的位置。这种算法保证了食物不会生成在蛇身上,避免了逻辑冲突。
本地存储与最高分持久化
为了提升用户体验,本游戏利用 localStorage API 实现了最高分记录功能。每当游戏得分超过当前保存的最高分时,系统会自动调用 localStorage.setItem 更新存储值。页面加载时,则通过 localStorage.getItem 读取历史最高分并展示在画布角落。
交互适配与响应式设计
生成的 snake.html 不仅适配桌面端键盘操作,还通过 viewport 设置与触摸事件监听,实现了对移动端浏览器的兼容。这意味着用户可以将该文件部署至任何静态 Web 服务器,并通过手机浏览器访问,获得完整的触屏操作体验。
API 速览
本案例基于浏览器原生 API 实现,无需引入外部依赖。核心涉及的 API 如下:
Canvas API (HTMLCanvasElement)
- 方法 :
getContext('2d') - 描述:获取 2D 绘图上下文,用于绘制游戏网格、蛇身与食物。
- 示例:
javascript
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
requestAnimationFrame
- 方法 :
window.requestAnimationFrame(callback) - 描述:浏览器原生动画循环 API,用于实现稳定的游戏帧率更新。
- 示例:
javascript
function gameLoop() {
updateGameState();
renderCanvas();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
LocalStorage API
- 方法 :
localStorage.setItem(key, value)与localStorage.getItem(key) - 描述:用于在浏览器本地持久化存储游戏最高分记录。
- 示例:
javascript
localStorage.setItem('snakeHighScore', JSON.stringify(highScore));
const savedScore = JSON.parse(localStorage.getItem('snakeHighScore') || '0');
触摸事件 API (TouchEvent)
- 事件 :
touchstart、touchend、touchcancel - 描述:用于捕获移动端手指滑动方向,转化为游戏方向指令。
- 示例:
javascript
canvas.addEventListener('touchstart', onTouchStart, { passive: true });
canvas.addEventListener('touchend', onTouchEnd, { passive: true });
Demo 示例
以下提供一个可直接运行的贪吃蛇游戏完整代码。该 Demo 演示了完整的游戏循环、碰撞检测、随机食物生成以及响应式触屏支持。
运行说明:
- 新建一个文本文件,命名为
snake.html。 - 将下方代码完整复制并粘贴至该文件中。
- 双击
snake.html文件,使用 Chrome、Edge 或 Firefox 浏览器打开即可开始游戏。
完整代码示例:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>可爱贪吃蛇 · 风格化版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
min-height: 100vh;
background: linear-gradient(145deg, #1b3b2b 0%, #2a5e3a 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 16px;
}
.game-wrapper {
background: #1f4a2f;
padding: 28px 24px 32px;
border-radius: 56px 56px 48px 48px;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
border: 1px solid #3c7a53;
}
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px 16px 8px;
}
.game-title {
color: #b3e0c0;
font-weight: 600;
font-size: 1.5rem;
letter-spacing: 2px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
.score-panel {
background: #0f2b1b;
padding: 8px 18px;
border-radius: 40px;
color: #d4f4dd;
font-weight: 600;
font-size: 1.1rem;
border: 1px solid #3f7a57;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.6);
}
canvas {
display: block;
margin: 0 auto;
width: 100%;
max-width: 500px;
aspect-ratio: 1 / 1;
background: #143121;
border-radius: 36px;
box-shadow: inset 0 -8px 0 #0c1f14, 0 10px 20px rgba(0, 0, 0, 0.5);
image-rendering: crisp-edges;
touch-action: none;
cursor: pointer;
}
.control-bar {
display: flex;
justify-content: center;
gap: 24px;
padding-top: 20px;
}
.ctrl-btn {
background: #2c5d3d;
border: none;
padding: 10px 28px;
border-radius: 60px;
font-weight: 700;
font-size: 1.1rem;
color: #d2f0da;
box-shadow: 0 6px 0 #0d2113, 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.06s ease;
letter-spacing: 1px;
border: 1px solid #479a62;
cursor: pointer;
flex: 1;
max-width: 160px;
}
.ctrl-btn:active {
transform: translateY(5px);
box-shadow: 0 1px 0 #0d2113;
}
.ctrl-btn:disabled {
opacity: 0.5;
transform: translateY(4px);
box-shadow: 0 2px 0 #0d2113;
pointer-events: none;
}
@media (max-width: 500px) {
.game-wrapper {
padding: 16px 12px 20px;
border-radius: 36px;
}
.game-title {
font-size: 1.2rem;
}
.ctrl-btn {
font-size: 0.9rem;
padding: 8px 12px;
max-width: 120px;
}
}
</style>
</head>
<body>
<div class="game-wrapper">
<div class="game-header">
<span class="game-title">🐍 小青蛇</span>
<span class="score-panel">🍎 <span id="scoreDisplay">0</span></span>
</div>
<canvas id="gameCanvas" width="500" height="500"></canvas>
<div class="control-bar">
<button class="ctrl-btn" id="restartBtn">🔄 重来</button>
<button class="ctrl-btn" id="pauseBtn">⏸️ 暂停</button>
</div>
</div>
<script>
(function () {
// --- 配置 ---
const BOARD_SIZE = 20;
const CELL_SIZE = 25; // 500 / 20
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const restartBtn = document.getElementById('restartBtn');
const pauseBtn = document.getElementById('pauseBtn');
// --- 游戏状态 ---
let snake = [];
let direction = { dx: 1, dy: 0 };
let nextDirection = { dx: 1, dy: 0 };
let food = { x: 8, y: 10 };
let score = 0;
let highScore = parseInt(localStorage.getItem('snakeHighScore')) || 0;
let gameOver = false;
let winFlag = false;
let paused = false;
let gameInterval = null;
const MOVE_INTERVAL_MS = 140;
// --- 初始化 ---
function initGame() {
// 蛇: 初始长度为 3,水平放置
snake = [
{ x: 7, y: 10 },
{ x: 8, y: 10 },
{ x: 9, y: 10 }
];
direction = { dx: 1, dy: 0 };
nextDirection = { dx: 1, dy: 0 };
score = 0;
gameOver = false;
winFlag = false;
paused = false;
pauseBtn.textContent = '⏸️ 暂停';
updateScoreDisplay();
generateFood();
clearGameLoop();
startGameLoop();
drawCanvas();
}
// --- 游戏循环控制 ---
function startGameLoop() {
if (gameInterval) clearInterval(gameInterval);
gameInterval = setInterval(() => {
if (!paused && !gameOver && !winFlag) {
moveSnake();
drawCanvas();
}
}, MOVE_INTERVAL_MS);
}
function clearGameLoop() {
if (gameInterval) {
clearInterval(gameInterval);
gameInterval = null;
}
}
// --- 食物生成 (避开蛇身) ---
function generateFood() {
const totalCells = BOARD_SIZE * BOARD_SIZE;
if (snake.length >= totalCells) {
winFlag = true;
return;
}
const snakeSet = new Set(snake.map(cell => `${cell.x},${cell.y}`));
const freeCells = [];
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (!snakeSet.has(`${i},${j}`)) freeCells.push({ x: i, y: j });
}
}
if (freeCells.length === 0) {
winFlag = true;
return;
}
const randIndex = Math.floor(Math.random() * freeCells.length);
food = freeCells[randIndex];
}
// --- 移动逻辑 ---
function moveSnake() {
if (gameOver || winFlag) return;
// 应用有效方向
const tryDx = nextDirection.dx;
const tryDy = nextDirection.dy;
if (!(direction.dx === -tryDx && direction.dy === -tryDy)) {
direction = { dx: tryDx, dy: tryDy };
}
const head = snake[snake.length - 1];
const newHead = {
x: head.x + direction.dx,
y: head.y + direction.dy
};
// 边界碰撞
if (newHead.x < 0 || newHead.x >= BOARD_SIZE || newHead.y < 0 || newHead.y >= BOARD_SIZE) {
gameOver = true;
clearGameLoop();
drawCanvas();
return;
}
// 检查是否吃到食物
const isEating = (newHead.x === food.x && newHead.y === food.y);
// 构造新蛇 (先拷贝)
let newSnake = [...snake];
if (isEating) {
// 吃到了: 不删尾部,直接加头
newSnake.push(newHead);
} else {
// 没吃到: 移除尾部,加头
newSnake.shift();
newSnake.push(newHead);
}
// 检查蛇头是否撞到自己 (新蛇中除了头部自身)
const headPos = newSnake[newSnake.length - 1];
for (let i = 0; i < newSnake.length - 1; i++) {
if (newSnake[i].x === headPos.x && newSnake[i].y === headPos.y) {
gameOver = true;
clearGameLoop();
snake = newSnake; // 显示撞到的状态
drawCanvas();
return;
}
}
// 更新蛇
snake = newSnake;
if (isEating) {
score++;
updateScoreDisplay();
if (score > highScore) {
highScore = score;
localStorage.setItem('snakeHighScore', JSON.stringify(highScore));
}
generateFood();
if (winFlag) {
clearGameLoop();
drawCanvas();
return;
}
}
// 绘制在下一帧由 interval 触发,但需确保食物生成后立即重绘
// 这里不重绘,由 interval 统一绘制
}
// --- 渲染 ---
function drawCanvas() {
ctx.clearRect(0, 0, 500, 500);
// 绘制网格 (浅绿虚线)
ctx.strokeStyle = '#2b5a3b';
ctx.lineWidth = 0.6;
for (let i = 0; i <= BOARD_SIZE; i++) {
ctx.beginPath();
ctx.moveTo(i * CELL_SIZE, 0);
ctx.lineTo(i * CELL_SIZE, 500);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * CELL_SIZE);
ctx.lineTo(500, i * CELL_SIZE);
ctx.stroke();
}
// 绘制食物 (红色发光苹果)
ctx.shadowColor = '#ff7b7b';
ctx.shadowBlur = 18;
ctx.fillStyle = '#e33b3b';
ctx.beginPath();
const fx = food.x * CELL_SIZE + CELL_SIZE / 2;
const fy = food.y * CELL_SIZE + CELL_SIZE / 2;
const rad = CELL_SIZE / 2 - 2;
ctx.arc(fx, fy, rad, 0, Math.PI * 2);
ctx.fill();
// 高光
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffa5a5';
ctx.beginPath();
ctx.arc(fx - 4, fy - 4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// 绘制蛇 (红色渐变)
snake.forEach((seg, index) => {
const x = seg.x * CELL_SIZE + 1;
const y = seg.y * CELL_SIZE + 1;
const w = CELL_SIZE - 2;
const gradient = ctx.createRadialGradient(
x + 4, y + 4, 2,
x + w / 2, y + w / 2, w / 2
);
if (index === snake.length - 1) {
// 蛇头:亮红
gradient.addColorStop(0, '#ff5e5e');
gradient.addColorStop(1, '#cc2222');
} else {
gradient.addColorStop(0, '#e64949');
gradient.addColorStop(1, '#a81c1c');
}
ctx.fillStyle = gradient;
ctx.shadowColor = '#cc4444';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.roundRect(x, y, w, w, 6);
ctx.fill();
// 蛇眼睛 (蛇头)
if (index === snake.length - 1) {
ctx.shadowBlur = 0;
ctx.fillStyle = '#f5f9ff';
let eyeOffsets = [];
if (direction.dx === 1) {
eyeOffsets = [
[12, 6],
[12, 14]
];
} else if (direction.dx === -1) {
eyeOffsets = [
[4, 6],
[4, 14]
];
} else if (direction.dy === -1) {
eyeOffsets = [
[6, 4],
[14, 4]
];
} else if (direction.dy === 1) {
eyeOffsets = [
[6, 12],
[14, 12]
];
} else {
eyeOffsets = [
[12, 6],
[12, 14]
];
}
eyeOffsets.forEach(([ox, oy]) => {
ctx.beginPath();
ctx.arc(x + ox, y + oy, 3.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1a1a2c';
ctx.beginPath();
ctx.arc(x + ox + (direction.dx * 1.5), y + oy + (direction.dy * 1.5), 1.8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f5f9ff';
});
ctx.shadowBlur = 0;
}
});
// --- 游戏结束/胜利蒙层 ---
if (gameOver || winFlag) {
ctx.fillStyle = 'rgba(10, 25, 15, 0.7)';
ctx.shadowBlur = 0;
ctx.fillRect(0, 0, 500, 500);
ctx.font = 'bold 36px "Segoe UI", system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (winFlag) {
ctx.fillStyle = '#c7f7d3';
ctx.fillText('🎉 你赢了!', 250, 220);
} else if (gameOver) {
ctx.fillStyle = '#fbc2c2';
ctx.fillText('💔 游戏结束', 250, 220);
}
ctx.font = '18px sans-serif';
ctx.fillStyle = '#d0e8d0';
ctx.fillText('点击 "重来" 继续挑战', 250, 290);
}
// 显示最高分
ctx.font = '14px sans-serif';
ctx.fillStyle = '#b5d6b5';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(`🏆 最高: ${highScore}`, 480, 490);
ctx.shadowBlur = 0;
}
// --- 辅助 roundRect ---
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
return this;
};
// --- UI 更新 ---
function updateScoreDisplay() {
scoreSpan.textContent = score;
}
// --- 方向控制 (防止反向) ---
function setDirection(dx, dy) {
if (gameOver || winFlag || paused) return;
// 不允许直接掉头
if (direction.dx === -dx && direction.dy === -dy) return;
nextDirection = { dx, dy };
}
// --- 键盘事件 ---
function onKeyDown(e) {
const key = e.key;
e.preventDefault();
if (key === 'ArrowUp') setDirection(0, -1);
else if (key === 'ArrowDown') setDirection(0, 1);
else if (key === 'ArrowLeft') setDirection(-1, 0);
else if (key === 'ArrowRight') setDirection(1, 0);
else if (key === ' ' || key === 'Space') {
e.preventDefault();
togglePause();
}
}
// --- 触屏滑动支持 (移动端) ---
let touchStartX = 0,
touchStartY = 0;
let isSwiping = false;
function onTouchStart(e) {
const touch = e.touches[0];
if (!touch) return;
const rect = canvas.getBoundingClientRect();
touchStartX = touch.clientX - rect.left;
touchStartY = touch.clientY - rect.top;
isSwiping = true;
}
function onTouchEnd(e) {
if (!isSwiping) return;
isSwiping = false;
const touch = e.changedTouches[0];
if (!touch) return;
const rect = canvas.getBoundingClientRect();
const endX = touch.clientX - rect.left;
const endY = touch.clientY - rect.top;
const dx = endX - touchStartX;
const dy = endY - touchStartY;
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return; // 点击视为暂停
if (Math.abs(dx) > Math.abs(dy)) {
setDirection(dx > 0 ? 1 : -1, 0);
} else {
setDirection(0, dy > 0 ? 1 : -1);
}
}
// --- 暂停切换 ---
function togglePause() {
if (gameOver || winFlag) return;
paused = !paused;
pauseBtn.textContent = paused ? '▶️ 运行' : '⏸️ 暂停';
drawCanvas();
}
// --- 重置 ---
function resetGame() {
clearGameLoop();
initGame();
drawCanvas();
}
// --- 绑定事件 ---
window.addEventListener('keydown', onKeyDown);
canvas.addEventListener('touchstart', onTouchStart, { passive: true });
canvas.addEventListener('touchend', onTouchEnd, { passive: true });
canvas.addEventListener('touchcancel', () => { isSwiping = false; }, { passive: true });
pauseBtn.addEventListener('click', togglePause);
restartBtn.addEventListener('click', resetGame);
// --- 启动游戏 ---
initGame();
drawCanvas();
// 窗口失焦时防止键盘卡键
window.addEventListener('blur', () => { /* ignore */ });
})();
</script>
</body>
</html>
技术点总结:
- 单文件架构 :所有样式与逻辑均集成于
snake.html,无需构建工具即可运行。 - Canvas 绘制 :利用
Canvas 2D Context实现网格、蛇身渐变与粒子效果模拟。 - 响应式适配 :通过
viewport与 CSSaspect-ratio实现移动端自适应。 - 触摸与键盘双模控制:同时支持桌面键盘与移动端滑动手势。
- 本地持久化 :使用
localStorage存储最高分,提升用户体验。
两种提示词策略的对比分析
为了更清晰地展示不同提示词策略对生成结果的影响,下表从多个维度进行了对比:
| 对比维度 | 普通自然语言描述 | 优雅风格化提示词 |
|---|---|---|
| 角色设定 | 缺失或模糊,AI 仅作为通用助手 | 明确专家角色,激活领域知识 |
| 视觉风格 | 默认值或随机,缺乏统一性 | 显式定义色彩搭配与交互细节 |
| 代码完整性 | 仅实现核心逻辑,边缘情况处理较少 | 包含边界检测、本地存储与粒子特效 |
| 响应式支持 | 通常忽略移动端适配 | 主动添加 viewport 与触摸事件 |
| 迭代效率 | 需要多次对话修正样式与逻辑 | 一次性生成高完成度原型 |
实践表明,采用风格化提示词虽然增加了前期的编写时间,但显著降低了后续的调试与返工成本,尤其适合追求产出质量的单人开发团队。
参考文档
-
官方文档
-
参考链接
总结
本文通过一个完整的贪吃蛇游戏开发案例,系统阐述了在 Vibe Coding 工作流中,如何从角色扮演、需求拆解、风格注入三个维度构造高质量的提示词。
相较于简单的自然语言指令,风格化提示词能够引导 AI 生成具备完整视觉体系、健壮交互逻辑与跨端适配能力的生产级代码。该方法论不仅适用于游戏开发,同样可扩展至数据可视化、后台管理面板等前端场景。
最终产出的 snake.html 文件展示了浏览器原生 API 在游戏开发中的全部潜力,为后续更复杂的 HTML5 应用开发提供了可复用的范式。