欢迎加入开源鸿蒙PC社区:
https://harmonypc.csdn.net/

源码仓库地址:
https://atomgit.com/feng8403000/electron_release_hongmengPClianliankan_code

效果演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 项目背景与目标

随着鸿蒙操作系统的不断发展,越来越多的开发者开始关注鸿蒙平台的应用开发。本文将详细介绍如何在鸿蒙平台上开发一款连连看小游戏,包括项目搭建、核心功能实现、界面设计等方面。

1.1 项目概述

本项目是基于 Electron 和鸿蒙原生能力开发的一款连连看小游戏,具有以下特点:

  • 基于 HTML5 + CSS3 + JavaScript 开发
  • 运行在 Electron (HarmonyOS 定制版) 上
  • 支持中文界面和中文菜单栏
  • 完整的游戏功能,包括计时、计分、游戏结束判断等

1.2 技术栈

技术版本用途
HTML5-页面结构
CSS3-样式设计
JavaScriptES6+游戏逻辑
ElectronHarmonyOS 定制版运行时环境
ArkTS-鸿蒙原生能力

2. 项目结构设计

2.1 整体架构

ohos_hap/
├── electron/                    # HAP 入口模块
├── web_engine/                  # 核心引擎模块
│   └── src/main/
│       ├── ets/                 # 鸿蒙原生代码
│       └── resources/resfile/resources/app/
│           ├── main.js          # Electron 主进程
│           ├── index.html       # 主页面
│           └── games/           # 游戏目录
│               └── link.html    # 连连看游戏
└── docs/                        # 文档资料

2.2 核心文件说明

文件功能
main.jsElectron 主进程,负责创建窗口、菜单栏等
index.html应用主页面
games/link.html连连看游戏页面

3. 核心功能实现

3.1 中文菜单栏实现

main.js 文件中,我们实现了中文菜单栏,包括文件、编辑、视图、游戏和帮助五个主要菜单项:

// 创建中文菜单栏
const template = [
    {
        label: '文件',
        submenu: [
            {
                label: '新建',
                accelerator: 'CmdOrCtrl+N',
                click: () => {
                    console.log('新建');
                }
            },
            {
                label: '打开',
                accelerator: 'CmdOrCtrl+O',
                click: () => {
                    console.log('打开');
                }
            },
            {
                label: '保存',
                accelerator: 'CmdOrCtrl+S',
                click: () => {
                    console.log('保存');
                }
            },
            {
                type: 'separator'
            },
            {
                label: '退出',
                accelerator: 'CmdOrCtrl+Q',
                click: () => {
                    app.quit();
                }
            }
        ]
    },
    // 其他菜单项...
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

3.2 游戏核心逻辑

3.2.1 游戏板生成

连连看游戏的核心是随机生成游戏板,我们使用以下代码实现:

// 生成游戏板
function generateBoard() {
    // 计算需要的符号数量
    const totalTiles = config.rows * config.cols;
    const symbolsNeeded = totalTiles / 2;
    
    // 随机选择符号
    const selectedSymbols = [];
    while (selectedSymbols.length < symbolsNeeded) {
        const randomSymbol = symbols[Math.floor(Math.random() * symbols.length)];
        if (!selectedSymbols.includes(randomSymbol)) {
            selectedSymbols.push(randomSymbol);
        }
    }
    
    // 生成配对的符号
    let tileSymbols = [];
    selectedSymbols.forEach(symbol => {
        tileSymbols.push(symbol, symbol);
    });
    
    // 打乱符号顺序
    tileSymbols = shuffleArray(tileSymbols);
    
    // 创建游戏板
    for (let i = 0; i < config.rows; i++) {
        gameState.tiles[i] = [];
        for (let j = 0; j < config.cols; j++) {
            const tile = document.createElement('div');
            tile.className = 'tile';
            tile.dataset.row = i;
            tile.dataset.col = j;
            tile.dataset.symbol = tileSymbols[i * config.cols + j];
            tile.textContent = tileSymbols[i * config.cols + j];
            tile.addEventListener('click', () => selectTile(i, j));
            gameState.gameBoard.appendChild(tile);
            gameState.tiles[i][j] = tile;
        }
    }
}
3.2.2 方块选择与匹配

当用户点击方块时,我们需要处理选择逻辑并检查是否匹配:

// 选择方块
function selectTile(row, col) {
    if (!gameState.isPlaying) return;
    
    const tile = gameState.tiles[row][col];
    
    // 检查方块是否已经被选中或匹配
    if (tile.classList.contains('selected') || tile.classList.contains('removed')) {
        return;
    }
    
    // 检查是否已经选择了两个方块
    if (gameState.selectedTiles.length >= 2) {
        // 取消之前的选择
        gameState.selectedTiles.forEach(([r, c]) => {
            gameState.tiles[r][c].classList.remove('selected');
        });
        gameState.selectedTiles = [];
    }
    
    // 选择当前方块
    tile.classList.add('selected');
    gameState.selectedTiles.push([row, col]);
    
    // 检查是否选择了两个方块
    if (gameState.selectedTiles.length === 2) {
        gameState.steps++;
        gameState.stepsElement.textContent = gameState.steps;
        checkMatch();
    }
}

// 检查匹配
function checkMatch() {
    const [row1, col1] = gameState.selectedTiles[0];
    const [row2, col2] = gameState.selectedTiles[1];
    
    const tile1 = gameState.tiles[row1][col1];
    const tile2 = gameState.tiles[row2][col2];
    
    // 检查符号是否匹配
    if (tile1.dataset.symbol === tile2.dataset.symbol) {
        // 检查是否可以连接
        if (canConnect(row1, col1, row2, col2)) {
            // 匹配成功
            setTimeout(() => {
                tile1.classList.add('removed');
                tile2.classList.add('removed');
                gameState.matchedTiles.push([row1, col1], [row2, col2]);
                gameState.selectedTiles = [];
                
                // 更新分数
                gameState.score += config.scorePerMatch;
                gameState.scoreElement.textContent = gameState.score;
                
                // 检查游戏是否结束
                if (gameState.matchedTiles.length === config.rows * config.cols) {
                    endGame(true);
                }
            }, 500);
        } else {
            // 不能连接,取消选择
            setTimeout(() => {
                tile1.classList.remove('selected');
                tile2.classList.remove('selected');
                gameState.selectedTiles = [];
            }, 500);
        }
    } else {
        // 符号不匹配,取消选择
        setTimeout(() => {
            tile1.classList.remove('selected');
            tile2.classList.remove('selected');
            gameState.selectedTiles = [];
        }, 500);
    }
}
3.2.3 连接判断算法

连连看游戏的核心算法是判断两个方块是否可以连接,我们实现了三种连接方式的判断:

// 检查两个方块是否可以连接
function canConnect(row1, col1, row2, col2) {
    // 直接连接
    if (isDirectConnection(row1, col1, row2, col2)) {
        return true;
    }
    
    // 一次转折
    if (isOneTurnConnection(row1, col1, row2, col2)) {
        return true;
    }
    
    // 两次转折
    if (isTwoTurnsConnection(row1, col1, row2, col2)) {
        return true;
    }
    
    return false;
}

// 检查直接连接
function isDirectConnection(row1, col1, row2, col2) {
    // 同一行
    if (row1 === row2) {
        const minCol = Math.min(col1, col2);
        const maxCol = Math.max(col1, col2);
        for (let col = minCol + 1; col < maxCol; col++) {
            if (!gameState.tiles[row1][col].classList.contains('removed')) {
                return false;
            }
        }
        return true;
    }
    
    // 同一列
    if (col1 === col2) {
        const minRow = Math.min(row1, row2);
        const maxRow = Math.max(row1, row2);
        for (let row = minRow + 1; row < maxRow; row++) {
            if (!gameState.tiles[row][col1].classList.contains('removed')) {
                return false;
            }
        }
        return true;
    }
    
    return false;
}

// 检查一次转折连接
function isOneTurnConnection(row1, col1, row2, col2) {
    // 检查右上角
    if (isPathClear(row1, col2, row1, col1) && isPathClear(row1, col2, row2, col2)) {
        return true;
    }
    
    // 检查左上角
    if (isPathClear(row2, col1, row1, col1) && isPathClear(row2, col1, row2, col2)) {
        return true;
    }
    
    return false;
}

// 检查两次转折连接
function isTwoTurnsConnection(row1, col1, row2, col2) {
    // 检查左侧
    for (let col = col1 - 1; col >= -1; col--) {
        if (col === -1 || gameState.tiles[row1][col].classList.contains('removed')) {
            if (isPathClear(row1, col, row1, col1) && 
                isPathClear(row1, col, row2, col) && 
                isPathClear(row2, col, row2, col2)) {
                return true;
            }
        } else {
            break;
        }
    }
    
    // 检查右侧
    for (let col = col1 + 1; col <= config.cols; col++) {
        if (col === config.cols || gameState.tiles[row1][col].classList.contains('removed')) {
            if (isPathClear(row1, col, row1, col1) && 
                isPathClear(row1, col, row2, col) && 
                isPathClear(row2, col, row2, col2)) {
                return true;
            }
        } else {
            break;
        }
    }
    
    // 检查上方
    for (let row = row1 - 1; row >= -1; row--) {
        if (row === -1 || gameState.tiles[row][col1].classList.contains('removed')) {
            if (isPathClear(row, col1, row1, col1) && 
                isPathClear(row, col1, row, col2) && 
                isPathClear(row, col2, row2, col2)) {
                return true;
            }
        } else {
            break;
        }
    }
    
    // 检查下方
    for (let row = row1 + 1; row <= config.rows; row++) {
        if (row === config.rows || gameState.tiles[row][col1].classList.contains('removed')) {
            if (isPathClear(row, col1, row1, col1) && 
                isPathClear(row, col1, row, col2) && 
                isPathClear(row, col2, row2, col2)) {
                return true;
            }
        } else {
            break;
        }
    }
    
    return false;
}
3.2.4 游戏状态管理

我们使用一个全局的 gameState 对象来管理游戏的状态:

// 游戏状态
let gameState = {
    tiles: [],
    selectedTiles: [],
    matchedTiles: [],
    steps: 0,
    score: 0,
    time: 0,
    timer: null,
    isPlaying: false,
    gameBoard: null,
    timeElement: null,
    stepsElement: null,
    scoreElement: null,
    messageElement: null
};

3.3 界面设计

连连看游戏的界面设计采用了现代化的 CSS 样式:

.game-board {
    display: grid;
    grid-template-columns: repeat(8, 80px);
    grid-template-rows: repeat(8, 80px);
    gap: 10px;
    background-color: #e0e0e0;
    padding: 15px;
    border-radius: 4px;
    margin: 0 auto;
    width: fit-content;
}

.tile {
    width: 80px;
    height: 80px;
    background-color: white;
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32px;
    cursor: pointer;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    transition: all 0.2s ease;
}

.tile:hover {
    transform: scale(1.05);
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.tile.removed {
    background-color: #e0e0e0;
    cursor: default;
    box-shadow: none;
}

.tile.selected {
    border: 3px solid #007AFF;
    transform: scale(1.1);
}

4. 技术实现细节

4.1 Electron 主进程配置

main.js 文件中,我们配置了 Electron 主进程,包括创建窗口、添加菜单栏等:

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 1280,
        height: 800,
        minWidth: 800,
        minHeight: 600,
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            nodeIntegration: false,
            contextIsolation: true
        }
    });

    mainWindow.setWindowButtonVisibility(true);

    // 创建中文菜单栏
    // ... 菜单栏代码 ...

    // 加载默认的HTML文件
    mainWindow.loadFile(path.join(__dirname, 'index.html'));

    mainWindow.on('closed', () => {
        mainWindow = null;
    });
}

4.2 游戏配置

我们使用一个 config 对象来存储游戏的配置参数:

// 游戏配置
const config = {
    rows: 8,
    cols: 8,
    tileSize: 80,
    gap: 10,
    timeLimit: 60,
    scorePerMatch: 10
};

4.3 符号选择

为了使游戏更加有趣,我们使用了丰富的 emoji 符号作为游戏元素:

// 游戏符号
const symbols = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
                '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
                '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉',
                '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋'];

4.4 游戏结束判断

当所有方块都匹配完成或时间结束时,游戏结束:

// 结束游戏
function endGame(isWin) {
    gameState.isPlaying = false;
    clearInterval(gameState.timer);
    
    if (isWin) {
        gameState.messageElement.textContent = `恭喜你赢了!用时 ${gameState.time} 秒,步数 ${gameState.steps},得分 ${gameState.score}`;
    } else {
        gameState.messageElement.textContent = `游戏时间结束!得分 ${gameState.score}`;
    }
}

5. 开发过程中的挑战与解决方案

5.1 连接判断算法

挑战:实现一个高效、准确的连连看连接判断算法。

解决方案:我们实现了三种连接方式的判断:直接连接、一次转折和两次转折。通过分步骤检查,确保了算法的准确性和效率。

5.2 游戏状态管理

挑战:管理游戏的各种状态,包括选中的方块、匹配的方块、分数、时间等。

解决方案:使用一个全局的 gameState 对象来集中管理所有游戏状态,使代码更加清晰和易于维护。

5.3 界面响应式设计

挑战:确保游戏界面在不同屏幕尺寸下都能正常显示。

解决方案:使用 CSS Grid 布局和相对单位,使游戏板能够自适应不同的屏幕尺寸。

5.4 性能优化

挑战:在游戏运行过程中,确保界面流畅,没有卡顿。

解决方案:使用 setTimeout 来处理动画效果,避免阻塞主线程;使用高效的算法来判断连接,减少计算时间。

6. 功能测试

6.1 游戏功能测试

测试项预期结果实际结果
游戏开始随机生成游戏板,开始计时
方块选择点击方块后高亮显示
匹配判断相同符号且可连接的方块匹配成功
连接判断支持直接连接、一次转折、两次转折
游戏结束所有方块匹配完成后游戏胜利
时间结束60秒后游戏自动结束
分数计算每次匹配成功增加10分
步数计算每次选择两个方块增加1步

6.2 界面测试

测试项预期结果实际结果
中文菜单栏显示中文菜单项
游戏界面美观、布局合理
响应式在不同屏幕尺寸下正常显示
动画效果方块选择和匹配时有动画效果

7. 未来扩展计划

7.1 功能扩展

  1. 难度级别:添加简单、中等、困难三个难度级别,对应不同的游戏板大小和时间限制。
  2. 排行榜:添加本地排行榜功能,记录玩家的最佳成绩。
  3. 音效:添加游戏音效,增强游戏体验。
  4. 主题:添加多种游戏主题,如动物、水果、数字等。
  5. 多人模式:添加本地多人对战模式。

7.2 技术优化

  1. 性能优化:进一步优化连接判断算法,提高游戏运行速度。
  2. 代码重构:使用模块化的方式重构代码,提高代码的可维护性。
  3. 测试覆盖:添加单元测试,确保代码的质量。
  4. 国际化:添加多语言支持,使游戏能够在不同语言环境下运行。

8. 总结

通过本项目的开发,我们成功在鸿蒙平台上实现了一款功能完整、界面美观的连连看小游戏。项目使用了 HTML5 + CSS3 + JavaScript 技术栈,运行在 Electron (HarmonyOS 定制版) 上,支持中文界面和中文菜单栏。

本项目的开发过程中,我们遇到了一些挑战,如连接判断算法的实现、游戏状态管理等,但通过合理的设计和实现,我们成功解决了这些问题。

未来,我们计划进一步扩展游戏功能,优化技术实现,使游戏更加完善和有趣。同时,我们也希望通过本项目的开发,为鸿蒙平台的应用开发提供一些参考和借鉴。

8.1 项目亮点

  1. 完整的游戏功能:实现了连连看游戏的所有核心功能,包括随机生成游戏板、方块选择与匹配、连接判断、计时计分等。
  2. 中文界面:所有界面元素都是中文的,符合中文用户的使用习惯。
  3. 现代化的界面设计:使用了现代化的 CSS 样式,界面美观、布局合理。
  4. 高效的连接判断算法:实现了三种连接方式的判断,确保了游戏的准确性和流畅性。
  5. 良好的代码结构:代码结构清晰,易于维护和扩展。

8.2 技术价值

  1. 鸿蒙平台应用开发:展示了如何在鸿蒙平台上开发桌面应用。
  2. Electron 应用开发:展示了如何使用 Electron 开发跨平台应用。
  3. 游戏开发:展示了如何使用 HTML5 + CSS3 + JavaScript 开发小游戏。
  4. 算法实现:展示了连连看游戏核心算法的实现。

9. 附录

9.1 完整代码

9.1.1 main.js
const { app, BrowserWindow, ipcMain, Menu } = require('electron');
const path = require('path');

let mainWindow;
app.disableHardwareAcceleration();

// 是否为开发模式
const isDev = process.env.NODE_ENV === 'development';

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 1280,
        height: 800,
        minWidth: 800,
        minHeight: 600,
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
            nodeIntegration: false,
            contextIsolation: true
        }
    });

    mainWindow.setWindowButtonVisibility(true);

    // 创建中文菜单栏
    const template = [
        {
            label: '文件',
            submenu: [
                {
                    label: '新建',
                    accelerator: 'CmdOrCtrl+N',
                    click: () => {
                        console.log('新建');
                    }
                },
                {
                    label: '打开',
                    accelerator: 'CmdOrCtrl+O',
                    click: () => {
                        console.log('打开');
                    }
                },
                {
                    label: '保存',
                    accelerator: 'CmdOrCtrl+S',
                    click: () => {
                        console.log('保存');
                    }
                },
                {
                    type: 'separator'
                },
                {
                    label: '退出',
                    accelerator: 'CmdOrCtrl+Q',
                    click: () => {
                        app.quit();
                    }
                }
            ]
        },
        {
            label: '编辑',
            submenu: [
                {
                    label: '撤销',
                    accelerator: 'CmdOrCtrl+Z',
                    click: () => {
                        console.log('撤销');
                    }
                },
                {
                    label: '重做',
                    accelerator: 'CmdOrCtrl+Y',
                    click: () => {
                        console.log('重做');
                    }
                },
                {
                    type: 'separator'
                },
                {
                    label: '剪切',
                    accelerator: 'CmdOrCtrl+X',
                    click: () => {
                        console.log('剪切');
                    }
                },
                {
                    label: '复制',
                    accelerator: 'CmdOrCtrl+C',
                    click: () => {
                        console.log('复制');
                    }
                },
                {
                    label: '粘贴',
                    accelerator: 'CmdOrCtrl+V',
                    click: () => {
                        console.log('粘贴');
                    }
                },
                {
                    label: '全选',
                    accelerator: 'CmdOrCtrl+A',
                    click: () => {
                        console.log('全选');
                    }
                }
            ]
        },
        {
            label: '视图',
            submenu: [
                {
                    label: '刷新',
                    accelerator: 'CmdOrCtrl+R',
                    click: () => {
                        mainWindow?.reload();
                    }
                },
                {
                    label: '切换全屏',
                    accelerator: 'F11',
                    click: () => {
                        mainWindow?.setFullScreen(!mainWindow?.isFullScreen());
                    }
                },
                {
                    type: 'separator'
                },
                {
                    label: '开发者工具',
                    accelerator: 'CmdOrCtrl+Shift+I',
                    click: () => {
                        mainWindow?.webContents.openDevTools();
                    }
                }
            ]
        },
        {
            label: '游戏',
            submenu: [
                {
                    label: '连连看',
                    click: () => {
                        mainWindow?.loadFile(path.join(__dirname, 'games/link.html'));
                    }
                }
            ]
        },
        {
            label: '帮助',
            submenu: [
                {
                    label: '关于',
                    click: () => {
                        console.log('关于');
                    }
                },
                {
                    label: '使用帮助',
                    click: () => {
                        console.log('使用帮助');
                    }
                }
            ]
        }
    ];

    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);

    // 加载默认的HTML文件
    mainWindow.loadFile(path.join(__dirname, 'index.html'));

    mainWindow.on('closed', () => {
        mainWindow = null;
    });
}

// ============ IPC 处理器 - 桥接鸿蒙原生能力 ============

// 系统信息
ipcMain.handle('ohos:getSystemInfo', async () => {
    // 这些会通过 JsBinding 调用鸿蒙原生 API
    return {
        platform: 'HarmonyOS',
        version: '5.0',
        deviceType: 'tablet'
    };
});

// 文件操作
ipcMain.handle('ohos:showOpenDialog', async (event, options) => {
    const { dialog } = require('electron');
    return await dialog.showOpenDialog(mainWindow, options);
});

ipcMain.handle('ohos:showSaveDialog', async (event, options) => {
    const { dialog } = require('electron');
    return await dialog.showSaveDialog(mainWindow, options);
});

// 通知
ipcMain.handle('ohos:showNotification', async (event, { title, body }) => {
    const { Notification } = require('electron');
    new Notification({ title, body }).show();
    return true;
});

// 剪贴板
ipcMain.handle('ohos:clipboard:read', async () => {
    const { clipboard } = require('electron');
    return clipboard.readText();
});

ipcMain.handle('ohos:clipboard:write', async (event, text) => {
    const { clipboard } = require('electron');
    clipboard.writeText(text);
    return true;
});

// 窗口控制
ipcMain.handle('ohos:window:minimize', async () => {
    mainWindow?.minimize();
});

ipcMain.handle('ohos:window:maximize', async () => {
    if (mainWindow?.isMaximized()) {
        mainWindow.unmaximize();
    } else {
        mainWindow?.maximize();
    }
});

ipcMain.handle('ohos:window:close', async () => {
    mainWindow?.close();
});

ipcMain.handle('ohos:window:setTitle', async (event, title) => {
    mainWindow?.setTitle(title);
});

ipcMain.handle('ohos:window:setSize', async (event, { width, height }) => {
    mainWindow?.setSize(width, height);
});

// 应用生命周期
app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    if (mainWindow === null) {
        createWindow();
    }
});
9.1.2 games/link.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>连连看小游戏</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f0f0f0;
            color: #333;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 20px;
        }
        h1 {
            text-align: center;
            color: #007AFF;
            margin-bottom: 30px;
        }
        .game-info {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
            padding: 10px;
            background-color: #f5f5f5;
            border-radius: 4px;
        }
        .info-item {
            font-size: 16px;
            font-weight: bold;
        }
        .game-board {
            display: grid;
            grid-template-columns: repeat(8, 80px);
            grid-template-rows: repeat(8, 80px);
            gap: 10px;
            background-color: #e0e0e0;
            padding: 15px;
            border-radius: 4px;
            margin: 0 auto;
            width: fit-content;
        }
        .tile {
            width: 80px;
            height: 80px;
            background-color: white;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 32px;
            cursor: pointer;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: all 0.2s ease;
        }
        .tile:hover {
            transform: scale(1.05);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }
        .tile.removed {
            background-color: #e0e0e0;
            cursor: default;
            box-shadow: none;
        }
        .tile.selected {
            border: 3px solid #007AFF;
            transform: scale(1.1);
        }
        .controls {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 20px;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            background-color: #007AFF;
            color: white;
            transition: background-color 0.2s ease;
        }
        button:hover {
            background-color: #0056b3;
        }
        button:active {
            transform: scale(0.98);
        }
        .message {
            text-align: center;
            margin-top: 20px;
            font-size: 18px;
            font-weight: bold;
            color: #007AFF;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>连连看小游戏</h1>
        
        <div class="game-info">
            <div class="info-item">时间: <span id="time">0</span></div>
            <div class="info-item">步数: <span id="steps">0</span></div>
            <div class="info-item">分数: <span id="score">0</span></div>
        </div>
        
        <div class="game-board" id="gameBoard"></div>
        
        <div class="controls">
            <button id="startBtn">开始游戏</button>
            <button id="restartBtn">重新开始</button>
            <button id="backBtn">返回主界面</button>
        </div>
        
        <div class="message" id="message"></div>
    </div>
    
    <script>
        // 游戏配置
        const config = {
            rows: 8,
            cols: 8,
            tileSize: 80,
            gap: 10,
            timeLimit: 60,
            scorePerMatch: 10
        };
        
        // 游戏状态
        let gameState = {
            tiles: [],
            selectedTiles: [],
            matchedTiles: [],
            steps: 0,
            score: 0,
            time: 0,
            timer: null,
            isPlaying: false,
            gameBoard: null,
            timeElement: null,
            stepsElement: null,
            scoreElement: null,
            messageElement: null
        };
        
        // 游戏符号
        const symbols = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
                        '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
                        '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉',
                        '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋'];
        
        // 初始化游戏
        function initGame() {
            // 获取DOM元素
            gameState.gameBoard = document.getElementById('gameBoard');
            gameState.timeElement = document.getElementById('time');
            gameState.stepsElement = document.getElementById('steps');
            gameState.scoreElement = document.getElementById('score');
            gameState.messageElement = document.getElementById('message');
            
            // 绑定事件
            document.getElementById('startBtn').addEventListener('click', startGame);
            document.getElementById('restartBtn').addEventListener('click', restartGame);
            document.getElementById('backBtn').addEventListener('click', goBack);
        }
        
        // 开始游戏
        function startGame() {
            if (gameState.isPlaying) return;
            
            // 重置游戏状态
            resetGame();
            
            // 生成游戏板
            generateBoard();
            
            // 开始计时
            gameState.isPlaying = true;
            gameState.timer = setInterval(() => {
                gameState.time++;
                gameState.timeElement.textContent = gameState.time;
                
                // 检查时间是否结束
                if (gameState.time >= config.timeLimit) {
                    endGame(false);
                }
            }, 1000);
            
            gameState.messageElement.textContent = '游戏开始!';
        }
        
        // 重新开始游戏
        function restartGame() {
            startGame();
        }
        
        // 返回主界面
        function goBack() {
            window.location.href = '../index.html';
        }
        
        // 重置游戏
        function resetGame() {
            // 清除定时器
            if (gameState.timer) {
                clearInterval(gameState.timer);
                gameState.timer = null;
            }
            
            // 重置游戏状态
            gameState.tiles = [];
            gameState.selectedTiles = [];
            gameState.matchedTiles = [];
            gameState.steps = 0;
            gameState.score = 0;
            gameState.time = 0;
            gameState.isPlaying = false;
            
            // 更新UI
            gameState.timeElement.textContent = '0';
            gameState.stepsElement.textContent = '0';
            gameState.scoreElement.textContent = '0';
            gameState.messageElement.textContent = '';
            
            // 清空游戏板
            gameState.gameBoard.innerHTML = '';
        }
        
        // 生成游戏板
        function generateBoard() {
            // 计算需要的符号数量
            const totalTiles = config.rows * config.cols;
            const symbolsNeeded = totalTiles / 2;
            
            // 随机选择符号
            const selectedSymbols = [];
            while (selectedSymbols.length < symbolsNeeded) {
                const randomSymbol = symbols[Math.floor(Math.random() * symbols.length)];
                if (!selectedSymbols.includes(randomSymbol)) {
                    selectedSymbols.push(randomSymbol);
                }
            }
            
            // 生成配对的符号
            let tileSymbols = [];
            selectedSymbols.forEach(symbol => {
                tileSymbols.push(symbol, symbol);
            });
            
            // 打乱符号顺序
            tileSymbols = shuffleArray(tileSymbols);
            
            // 创建游戏板
            for (let i = 0; i < config.rows; i++) {
                gameState.tiles[i] = [];
                for (let j = 0; j < config.cols; j++) {
                    const tile = document.createElement('div');
                    tile.className = 'tile';
                    tile.dataset.row = i;
                    tile.dataset.col = j;
                    tile.dataset.symbol = tileSymbols[i * config.cols + j];
                    tile.textContent = tileSymbols[i * config.cols + j];
                    tile.addEventListener('click', () => selectTile(i, j));
                    gameState.gameBoard.appendChild(tile);
                    gameState.tiles[i][j] = tile;
                }
            }
        }
        
        // 选择方块
        function selectTile(row, col) {
            if (!gameState.isPlaying) return;
            
            const tile = gameState.tiles[row][col];
            
            // 检查方块是否已经被选中或匹配
            if (tile.classList.contains('selected') || tile.classList.contains('removed')) {
                return;
            }
            
            // 检查是否已经选择了两个方块
            if (gameState.selectedTiles.length >= 2) {
                // 取消之前的选择
                gameState.selectedTiles.forEach(([r, c]) => {
                    gameState.tiles[r][c].classList.remove('selected');
                });
                gameState.selectedTiles = [];
            }
            
            // 选择当前方块
            tile.classList.add('selected');
            gameState.selectedTiles.push([row, col]);
            
            // 检查是否选择了两个方块
            if (gameState.selectedTiles.length === 2) {
                gameState.steps++;
                gameState.stepsElement.textContent = gameState.steps;
                checkMatch();
            }
        }
        
        // 检查匹配
        function checkMatch() {
            const [row1, col1] = gameState.selectedTiles[0];
            const [row2, col2] = gameState.selectedTiles[1];
            
            const tile1 = gameState.tiles[row1][col1];
            const tile2 = gameState.tiles[row2][col2];
            
            // 检查符号是否匹配
            if (tile1.dataset.symbol === tile2.dataset.symbol) {
                // 检查是否可以连接
                if (canConnect(row1, col1, row2, col2)) {
                    // 匹配成功
                    setTimeout(() => {
                        tile1.classList.add('removed');
                        tile2.classList.add('removed');
                        gameState.matchedTiles.push([row1, col1], [row2, col2]);
                        gameState.selectedTiles = [];
                        
                        // 更新分数
                        gameState.score += config.scorePerMatch;
                        gameState.scoreElement.textContent = gameState.score;
                        
                        // 检查游戏是否结束
                        if (gameState.matchedTiles.length === config.rows * config.cols) {
                            endGame(true);
                        }
                    }, 500);
                } else {
                    // 不能连接,取消选择
                    setTimeout(() => {
                        tile1.classList.remove('selected');
                        tile2.classList.remove('selected');
                        gameState.selectedTiles = [];
                    }, 500);
                }
            } else {
                // 符号不匹配,取消选择
                setTimeout(() => {
                    tile1.classList.remove('selected');
                    tile2.classList.remove('selected');
                    gameState.selectedTiles = [];
                }, 500);
            }
        }
        
        // 检查两个方块是否可以连接
        function canConnect(row1, col1, row2, col2) {
            // 直接连接
            if (isDirectConnection(row1, col1, row2, col2)) {
                return true;
            }
            
            // 一次转折
            if (isOneTurnConnection(row1, col1, row2, col2)) {
                return true;
            }
            
            // 两次转折
            if (isTwoTurnsConnection(row1, col1, row2, col2)) {
                return true;
            }
            
            return false;
        }
        
        // 检查直接连接
        function isDirectConnection(row1, col1, row2, col2) {
            // 同一行
            if (row1 === row2) {
                const minCol = Math.min(col1, col2);
                const maxCol = Math.max(col1, col2);
                for (let col = minCol + 1; col < maxCol; col++) {
                    if (!gameState.tiles[row1][col].classList.contains('removed')) {
                        return false;
                    }
                }
                return true;
            }
            
            // 同一列
            if (col1 === col2) {
                const minRow = Math.min(row1, row2);
                const maxRow = Math.max(row1, row2);
                for (let row = minRow + 1; row < maxRow; row++) {
                    if (!gameState.tiles[row][col1].classList.contains('removed')) {
                        return false;
                    }
                }
                return true;
            }
            
            return false;
        }
        
        // 检查一次转折连接
        function isOneTurnConnection(row1, col1, row2, col2) {
            // 检查右上角
            if (isPathClear(row1, col2, row1, col1) && isPathClear(row1, col2, row2, col2)) {
                return true;
            }
            
            // 检查左上角
            if (isPathClear(row2, col1, row1, col1) && isPathClear(row2, col1, row2, col2)) {
                return true;
            }
            
            return false;
        }
        
        // 检查两次转折连接
        function isTwoTurnsConnection(row1, col1, row2, col2) {
            // 检查左侧
            for (let col = col1 - 1; col >= -1; col--) {
                if (col === -1 || gameState.tiles[row1][col].classList.contains('removed')) {
                    if (isPathClear(row1, col, row1, col1) && 
                        isPathClear(row1, col, row2, col) && 
                        isPathClear(row2, col, row2, col2)) {
                        return true;
                    }
                } else {
                    break;
                }
            }
            
            // 检查右侧
            for (let col = col1 + 1; col <= config.cols; col++) {
                if (col === config.cols || gameState.tiles[row1][col].classList.contains('removed')) {
                    if (isPathClear(row1, col, row1, col1) && 
                        isPathClear(row1, col, row2, col) && 
                        isPathClear(row2, col, row2, col2)) {
                        return true;
                    }
                } else {
                    break;
                }
            }
            
            // 检查上方
            for (let row = row1 - 1; row >= -1; row--) {
                if (row === -1 || gameState.tiles[row][col1].classList.contains('removed')) {
                    if (isPathClear(row, col1, row1, col1) && 
                        isPathClear(row, col1, row, col2) && 
                        isPathClear(row, col2, row2, col2)) {
                        return true;
                    }
                } else {
                    break;
                }
            }
            
            // 检查下方
            for (let row = row1 + 1; row <= config.rows; row++) {
                if (row === config.rows || gameState.tiles[row][col1].classList.contains('removed')) {
                    if (isPathClear(row, col1, row1, col1) && 
                        isPathClear(row, col1, row, col2) && 
                        isPathClear(row, col2, row2, col2)) {
                        return true;
                    }
                } else {
                    break;
                }
            }
            
            return false;
        }
        
        // 检查路径是否清晰
        function isPathClear(row1, col1, row2, col2) {
            // 同一行
            if (row1 === row2) {
                const minCol = Math.min(col1, col2);
                const maxCol = Math.max(col1, col2);
                for (let col = minCol + 1; col < maxCol; col++) {
                    if (row1 >= 0 && row1 < config.rows && col >= 0 && col < config.cols) {
                        if (!gameState.tiles[row1][col].classList.contains('removed')) {
                            return false;
                        }
                    }
                }
                return true;
            }
            
            // 同一列
            if (col1 === col2) {
                const minRow = Math.min(row1, row2);
                const maxRow = Math.max(row1, row2);
                for (let row = minRow + 1; row < maxRow; row++) {
                    if (row >= 0 && row < config.rows && col1 >= 0 && col1 < config.cols) {
                        if (!gameState.tiles[row][col1].classList.contains('removed')) {
                            return false;
                        }
                    }
                }
                return true;
            }
            
            return false;
        }
        
        // 结束游戏
        function endGame(isWin) {
            gameState.isPlaying = false;
            clearInterval(gameState.timer);
            
            if (isWin) {
                gameState.messageElement.textContent = `恭喜你赢了!用时 ${gameState.time} 秒,步数 ${gameState.steps},得分 ${gameState.score}`;
            } else {
                gameState.messageElement.textContent = `游戏时间结束!得分 ${gameState.score}`;
            }
        }
        
        // 打乱数组
        function shuffleArray(array) {
            const newArray = [...array];
            for (let i = newArray.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
            }
            return newArray;
        }
        
        // 初始化游戏
        window.addEventListener('DOMContentLoaded', initGame);
    </script>
</body>
</html>

9.2 开发环境搭建

  1. 安装 Node.js:下载并安装 Node.js 18+ 版本
  2. 安装 DevEco Studio:下载并安装 DevEco Studio 5.0+ 版本
  3. 安装 HarmonyOS SDK:在 DevEco Studio 中安装 HarmonyOS SDK
  4. 克隆项目:使用 git 克隆项目到本地
  5. 运行项目:在 DevEco Studio 中打开项目并运行

9.3 构建与部署

  1. 构建 HAP:在 ohos_hap 目录下运行 hvigorw assembleHap
  2. 部署应用:将构建生成的 HAP 文件部署到 HarmonyOS 设备上

10. 结语

本项目成功实现了一款在鸿蒙平台上运行的连连看小游戏,展示了如何使用 HTML5 + CSS3 + JavaScript 开发鸿蒙桌面应用。通过本项目的开发,我们不仅学习了游戏开发的基本原理和方法,还了解了如何在鸿蒙平台上构建和部署应用。

连连看游戏作为一款经典的休闲游戏,具有广泛的用户基础和良好的游戏体验。通过添加中文界面和中文菜单栏,我们使游戏更加符合中文用户的使用习惯。同时,我们也实现了完整的游戏功能,包括随机生成游戏板、方块选择与匹配、连接判断、计时计分等。

未来,我们将继续优化游戏功能,添加更多的游戏模式和特性,使游戏更加完善和有趣。同时,我们也希望通过本项目的开发,为鸿蒙平台的应用开发提供一些参考和借鉴,促进鸿蒙生态的发展。

欢迎加入开源鸿蒙PC社区:

https://harmonypc.csdn.net/

一起探索鸿蒙PC应用开发的无限可能!

Logo

赋能鸿蒙PC开发者,共建全场景原生生态,共享一次开发多端部署创新价值。

更多推荐