前言

继上一篇文章介绍贪吃蛇小游戏的实现方案后,本文继续讲解俄罗斯方块游戏的开发。相比贪吃蛇,俄罗斯方块的实现更加复杂,涉及方块旋转、多行消除、速度递增等机制。

通过这个案例,可以学习到如何用原生 JavaScript 实现经典的方块下落消除游戏。

项目结构

游戏放置在 source/ai-games/tetris/ 目录下,文件结构如下:

1
2
3
source/ai-games/tetris/
├── index.md # 游戏页面(HTML + 样式)
└── tetris-game.js # 游戏核心逻辑(约394行)

与贪吃蛇相同的独立目录结构,便于管理和扩展更多游戏。

游戏页面实现

Canvas 画布

1
<canvas id="game-canvas" width="240" height="400"></canvas>

俄罗斯方块采用 12×20 网格,每个格子 20px,宽度较窄以适应竖屏布局。

样式设计

1
2
3
4
5
6
7
canvas {
border: 2px solid #2196F3;
border-radius: 8px;
background: #111;
box-shadow: 0 0 20px rgba(33, 150, 243, 0.3);
touch-action: none;
}

使用蓝色主题(#2196F3)与贪吃蛇的绿色区分开。

核心逻辑实现

方块形状定义

游戏包含 7 种经典方块形状,用矩阵和颜色表示:

1
2
3
4
5
6
7
8
9
10
const SHAPES = {
I: { matrix: [[1,1,1,1]], color: '#00f5ff' }, // 青色长条
O: { matrix: [[1,1],[1,1]], color: '#ffeb3b' }, // 黄色方块
T: { matrix: [[0,1,0],[1,1,1]], color: '#bf5af2' }, // 紫色T形
S: { matrix: [[0,1,1],[1,1,0]], color: '#30d158' }, // 绿色S形
Z: { matrix: [[1,1,0],[0,1,1]], color: '#ff375f' }, // 红色Z形
J: { matrix: [[1,0,0],[1,1,1]], color: '#0a84ff' }, // 蓝色J形
L: { matrix: [[0,0,1],[1,1,1]], color: '#ff9f0a' } // 橙色L形
};
const SHAPE_KEYS = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];

每种方块用 0/1 矩阵表示形状,1 表示方块存在的位置。

游戏状态管理

1
2
3
4
5
6
let board = [];           // 12×20 游戏板,存储已落方块颜色
let currentPiece = null; // 当前下落的方块
let currentX = 0; // 当前方块X坐标
let currentY = 0; // 当前方块Y坐标
let score = 0; // 得分
let dropInterval = 800; // 下落间隔(毫秒)

board 是二维数组,存储已固定在底部的方块颜色,空位为 0。

随机生成方块

1
2
3
4
5
6
7
8
function randomPiece() {
const key = SHAPE_KEYS[Math.floor(Math.random() * SHAPE_KEYS.length)];
const shape = SHAPES[key];
return {
matrix: shape.matrix.map(row => [...row]), // 深拷贝矩阵
color: shape.color
};
}

使用 map(row => [...row]) 实现矩阵的深拷贝,避免修改原始形状定义。

碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function checkCollision(matrix, x, y) {
for (let r = 0; r < matrix.length; r++) {
for (let c = 0; c < matrix[r].length; c++) {
if (matrix[r][c]) {
const newX = x + c;
const newY = y + r;
// 边界检测
if (newX < 0 || newX >= COLS || newY >= ROWS) return true;
// 与已落方块碰撞检测
if (newY >= 0 && board[newY][newX]) return true;
}
}
}
return false;
}

遍历方块的每个单元格,检查是否越界或与已落方块重叠。

阅读全文 »

前言

在静态博客中嵌入 JavaScript 小游戏是一种提升用户体验的好方法。之前用 Lua 为 Unity 实现过多种小游戏,现在尝试将经典的贪吃蛇游戏移植到 Hexo 博客中。

本文将详细介绍如何在 Hexo 博客中实现一个可玩的贪吃蛇小游戏,包括游戏页面搭建、核心逻辑实现以及移动端适配。

项目结构

游戏放置在 source/ai-games/snake/ 目录下,文件结构如下:

1
2
3
source/ai-games/snake/
├── index.md # 游戏页面(HTML + 样式)
└── snake-game.js # 游戏核心逻辑

这种独立目录的方式便于管理,每个游戏都有自己完整的资源。

游戏页面实现

游戏页面使用 Hexo 的 标签来嵌入原始 HTML 代码,这样可以避免 Hexo 对 HTML 标签进行转义。

Canvas 画布

1
<canvas id="game-canvas" width="400" height="400"></canvas>

游戏采用 20×20 网格,每个格子 20px,整体画布大小为 400×400 像素。

样式设计

1
2
3
4
5
6
7
canvas {
border: 2px solid #4CAF50;
border-radius: 8px;
background: #111;
box-shadow: 0 0 20px rgba(76, 175, 80, 0.3);
touch-action: none;
}

暗色系背景配合绿色边框,营造游戏氛围。touch-action: none 用于禁止移动端默认触摸行为,避免游戏时页面滚动。

核心逻辑实现

游戏逻辑全部封装在 snake-game.js 文件中,共约 309 行代码。

全局初始化函数

1
2
3
4
5
6
window.initSnakeGame = function() {
// 获取 DOM 元素
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
// ... 初始化逻辑
};

将游戏初始化函数挂载到 window 对象上,便于 PJAX 页面切换后重新调用。

游戏配置

1
2
3
4
5
6
7
8
9
const gridSize = 20;      // 20x20 网格
const cellSize = 20; // 每个格子 20px
let snake = [
{x: 10, y: 10},
{x: 9, y: 10},
{x: 8, y: 10}
];
let direction = 'RIGHT';
let nextDirection = 'RIGHT';

蛇身使用数组存储,每个元素表示蛇的一节坐标。directionnextDirection 分离的设计可以防止连续快速按键导致蛇身掉头自杀。

蛇的移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function move() {
// 应用下次方向(防止掉头)
const opposite = { 'UP': 'DOWN', 'DOWN': 'UP', 'LEFT': 'RIGHT', 'RIGHT': 'LEFT' };
if (nextDirection && opposite[nextDirection] !== direction) {
direction = nextDirection;
}

const head = snake[0];
let newHead = { ...head };
switch (direction) {
case 'RIGHT': newHead.x++; break;
case 'LEFT': newHead.x--; break;
case 'UP': newHead.y--; break;
case 'DOWN': newHead.y++; break;
}

// 判断是否吃到食物
const isEating = food && newHead.x === food.x && newHead.y === food.y;
let newSnake = [newHead, ...snake];
if (!isEating) newSnake.pop(); // 没吃到则移除尾部

snake = newSnake;
}

核心思路:

  • 每次移动在头部添加新方块
  • 吃到食物时不移除尾部,蛇身变长
  • 未吃到食物时移除尾部,保持蛇身长度不变

碰撞检测

1
2
3
4
5
6
7
8
9
10
11
12
// 边界碰撞
if (newHead.x < 0 || newHead.x >= gridSize ||
newHead.y < 0 || newHead.y >= gridSize) {
gameOver = true;
return;
}

// 自身碰撞
if (newSnake.slice(1).some(seg => seg.x === newHead.x && seg.y === newHead.y)) {
gameOver = true;
return;
}

食物生成

1
2
3
4
5
6
7
8
9
10
11
function randomFood() {
for (let i = 0; i < 1000; i++) {
const x = Math.floor(Math.random() * gridSize);
const y = Math.floor(Math.random() * gridSize);
// 避开蛇身
if (!snake.some(seg => seg.x === x && seg.y === y)) {
return {x, y};
}
}
return null;
}

使用循环尝试生成随机位置,直到找到不与蛇身重叠的位置。

绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制网格线
for (let i = 0; i <= gridSize; i++) {
// ... 绘制横竖线
}

// 绘制蛇身
snake.forEach((seg, idx) => {
ctx.fillStyle = idx === 0 ? '#8bc34a' : '#4CAF50';
ctx.fillRect(seg.x * cellSize + 1, seg.y * cellSize + 1,
cellSize - 2, cellSize - 2);
// 蛇眼睛
if (idx === 0) {
// ... 绘制眼睛
}
});

// 绘制食物
if (food) {
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(food.x * cellSize + cellSize / 2,
food.y * cellSize + cellSize / 2, 8, 0, 2 * Math.PI);
ctx.fill();
}
}

绘制顺序:清空画布 → 网格线 → 蛇身 → 食物 → 游戏结束遮罩。

阅读全文 »

在 AI 编程工具蓬勃发展的今天,开源免费的 OpenCode 凭借其强大的功能和灵活的扩展性,已经成为超过 500 万开发者的首选。作为一个拥有 12 万 + GitHub Stars 的开源项目,OpenCode 提供了终端、桌面应用和 IDE 多种使用方式。本文将详细介绍如何在 VSCode 中搭建和使用 OpenCode,帮助你快速上手这款强大的 AI 编程助手。

阅读全文 »

针对 Visual Studio 2022 用户,要打造一个高效率的“本地 AI 程序员”,关键在于选择一款能与 VS 2022 深度集成、且支持连接本地模型(如 Ollama)的插件。目前最高效的方案是使用 Visual Studio 的插件 Copilot (第三方开源版,非官方) 配合 Ollama。

以下是部署全流程:

方案架构

- 核心引擎: Ollama。负责在本地运行 DeepSeek 模型。

- 模型选择: DeepSeek-Coder-V2:16b。这是目前代码能力极强且适合本地部署的专用模型(如果显存不足,可退而求其次选择 deepseek-coder:6.7b)。

- IDE 插件: Visual Copilot (在 VS 扩展市场中搜索)。这是一个开源免费插件,能够将 Ollama 接入 VS 2022,提供代码补全和聊天功能。

详细部署步骤

- 第一步:配置本地模型引擎 (Ollama)

下载安装: 访问 Ollama 官网 下载并安装 Windows 版本。

拉取专用代码模型: 打开命令行(PowerShell 或 CMD),输入以下命令下载 DeepSeek 的代码模型:

ollama run deepseek-coder-v2:16b

注:如果您的显卡显存小于 12GB,建议使用 ollama run deepseek-coder:6.7b 以保证速度。

验证运行: 安装完成后,确保 Ollama 在托盘区运行,且在浏览器访问 http://localhost:11434 能看到 “Ollama is running” 字样。

- 第二步:安装并配置 Visual Studio 2022 插件

Visual Studio 2022 原生集成了 GitHub Copilot,但它无法连接本地 Ollama(除非使用代理转发,非常麻烦)。我们需要安装支持 OpenAI 兼容接口的第三方插件。

打开扩展管理器: 在 Visual Studio 2022 中,点击顶部菜单栏的 扩展 -> 管理扩展。

搜索插件: 在搜索框中输入 “Visual Copilot” (或者搜索 “CodeGPT” ,两者皆可,这里以 Visual Copilot 为例,因为它对 OpenAI 兼容性很好)。

安装: 点击下载,安装完成后 重启 VS 2022。

- 第三步:连接插件与 Ollama

重启后,我们需要告诉插件去哪里找本地模型:

打开设置: 在 VS 菜单栏找到新出现的 Visual Copilot 图标或菜单项,选择 Settings / Options。

配置 API 地址:

找到 Api Base (或 Endpoint) 一栏。

填入:http://localhost:11434/v1

注意:必须加上 /v1,这是为了兼容 OpenAI 的协议格式。

配置密钥 (API Key):

找到 Api Key 一栏。

填入:ollama (或者随便填几个字符,本地模式下 Ollama 不验证 Key,但这栏通常不能为空)。

配置模型名称:

阅读全文 »

行为树(Behavior Tree, BT)是一种用于AI行为设计的数据结构,广泛应用于游戏开发,包括Unity游戏引擎中。行为树通过节点的组合来表示复杂的决策逻辑,每个节点代表一个行为或条件。下面是对Unity行为树的概要及用法的解释:

行为树概要

节点类型

- 选择器(Selector):会从上到下依次尝试子节点,直到有一个子节点成功执行。

- 序列器(Sequence):会从上到下依次执行子节点,直到有一个子节点失败。

- 条件节点(Condition Node):用于检查某个条件是否为真或假。通常是行为树的叶子节点。

- 动作节点(Action Node):执行某个动作。同样通常是行为树的叶子节点。

- 装饰器节点(Decorator Node):用于修改子节点的行为。可以嵌套在其他节点中,如选择器、序列器或动作节点。

行为树的特点

- 可重用性:节点可以被多次复用,从而简化行为树的设计。

- 模块化:行为树可以被拆分为多个子树,便于管理和维护。

- 并行执行:支持并行执行多个子节点,提高效率。

- 状态记忆:行为树可以记住每个节点的状态,便于从上次中断的地方继续执行。

Unity中的行为树用法

Unity提供了多种工具和库来支持行为树的创建和使用,其中比较流行的包括Behavior Designer和Unity官方的Behavior Tree插件(从Unity 2021.2开始引入)。

阅读全文 »

基本概念

CancellationTokenSource 是一个类,用于创建一个取消令牌 (CancellationToken),并通过该令牌来通知一个或多个异步操作取消请求。CancellationToken 是一个结构体,用于传递取消通知。

创建 CancellationTokenSource

首先,你需要创建一个 CancellationTokenSource 对象。

1
2
3
4
5
6
7
8
9
10
11
using System.Threading;

public class CancellationTokenExample
{
private CancellationTokenSource tokenSource;

public void Initialize()
{
tokenSource = new CancellationTokenSource();
}
}

获取 CancellationToken

从 CancellationTokenSource 获取 CancellationToken 对象。

1
2
3
4
5
6
7
8
9
10
11
12
using System.Threading;

public class CancellationTokenExample
{
private CancellationTokenSource tokenSource;

public void Initialize()
{
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
}
}

在异步方法中使用 CancellationToken

在异步方法中使用 CancellationToken 来检查取消请求,并在需要时抛出 OperationCanceledException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks; // 如果使用 UniTask

public class CancellationTokenExample : MonoBehaviour
{
private CancellationTokenSource tokenSource;

public async void StartLongRunningTask()
{
Initialize();
CancellationToken token = tokenSource.Token;

try
{
await LongRunningTask(token);
}
catch (OperationCanceledException)
{
Debug.Log("任务已被取消");
}
}

private async UniTask LongRunningTask(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested(); // 检查取消请求并抛出异常

// 模拟长时间运行的任务
await UniTask.Delay(1000, cancellationToken: token);

Debug.Log($"任务进行中: 第 {i + 1} 次");
}
}

private void Initialize()
{
tokenSource = new CancellationTokenSource();
}

private void OnClickCancelBtn()
{
if (tokenSource != null)
{
tokenSource.Cancel();
tokenSource.Dispose();
tokenSource = null;
}
}

void Start()
{
StartLongRunningTask();
}

void OnDestroy()
{
if (tokenSource != null)
{
tokenSource.Cancel();
tokenSource.Dispose();
}
}
}
阅读全文 »