欢迎加入开源鸿蒙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 - 样式设计
JavaScript ES6+ 游戏逻辑
Electron HarmonyOS 定制版 运行时环境
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.js Electron 主进程,负责创建窗口、菜单栏等
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

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐