鴻蒙 ArkUI 實現 2048 小游戲

2048 是一款經典的益智游戲,玩家通過滑動屏幕合并相同數字的方塊,最終目標是合成數字 2048。本文基于鴻蒙 ArkUI 框架,詳細解析其實現過程,幫助開發者理解如何利用聲明式 UI 和狀態管理構建此類游戲。

一、核心數據結構與狀態管理

1. 游戲網格與得分

游戲的核心是一個 4x4 的二維數組,用于存儲每個格子的數字。通過 @State 裝飾器管理網格狀態,確保數據變化時 UI 自動刷新:

@State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0));
@State score: number = 0; // 當前得分
@State bestScore: number = 0; // 歷史最高分
2. 初始化游戲

initGame 方法負責重置網格、添加初始方塊并重置得分。通過 addNewTile 在隨機空位生成新方塊(90% 概率生成 2,10% 概率生成 4):

initGame() {this.grid = this.grid.map(() => Array(4).fill(0));this.addNewTile();this.addNewTile();this.score = 0;
}

二、滑動邏輯與合并算法

1. 方向處理與矩陣旋轉

游戲支持 上下左右四個方向的滑動。為簡化代碼邏輯,通過矩陣旋轉將不同方向的移動統一轉換為 左移操作

  • 左移:直接處理每一行。
  • 右移:將行反轉后左移,再反轉回來。
  • 上移/下移:旋轉矩陣為行,處理后恢復為列。
// 矩陣旋轉輔助方法
const rotate = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse());
};
2. 單行合并邏輯

每行處理分為三步:

  1. 移除空格:過濾出非零數字。
  2. 合并相同數字:相鄰相同數字合并,并累加得分。
  3. 補齊長度:填充零至長度為 4。
const moveRow = (row: number[]) => {let newRow = row.filter(cell => cell !== 0);for (let i = 0; i < newRow.length - 1; i++) {if (newRow[i] === newRow[i + 1]) {newRow[i] *= 2;this.score += newRow[i]; // 得分累加newRow.splice(i + 1, 1);}}return [...newRow, ...Array(4 - newRow.length).fill(0)];
};

三、游戲結束判斷

游戲結束的條件是 網格填滿且無相鄰可合并的方塊。通過以下步驟檢測:

  1. 檢查是否有空格:存在空格則游戲未結束。
  2. 橫向檢測:遍歷每一行,檢查是否有相鄰相同數字。
  3. 縱向檢測:遍歷每一列,檢查是否有相鄰相同數字。
isGameOver(): boolean {if (this.grid.some(row => row.includes(0))) return false;// 橫向和縱向檢測邏輯// ...return true;
}

四、UI 實現與交互設計

1. 網格渲染

使用 Grid 組件動態生成 4x4 網格,每個 GridItem 根據數字值顯示不同背景色和文字顏色:

Grid() {ForEach(this.grid, (row: number[], i) => {ForEach(row, (value: number, j) => {GridItem() {Text(value ? `${value}` : '').backgroundColor(this.getTileColor(value)).fontColor(this.getTextColor(value));}})})
}

2. 觸摸事件處理
通過 onTouch 監聽滑動事件,計算起始和結束坐標的差值,判斷滑動方向:

onTouch((event) => {if (event.type === TouchType.Down) {this.startX = event.touches[0].x;this.startY = event.touches[0].y;} else if (event.type === TouchType.Up) {const deltaX = event.touches[0].x - this.startX;const deltaY = event.touches[0].y - this.startY;// 判斷方向并調用 move 方法}
});

五、本地存儲與動畫效果

1. 最高分持久化

使用 PreferencesUtil 存儲和讀取最高分,確保數據在應用重啟后保留:

aboutToAppear() {this.bestScore = PreferencesUtil.getNumberSync("bestScore");
}// 更新最高分
if (this.score > this.bestScore) {PreferencesUtil.putSync('bestScore', this.score);
}
2. 動畫與視覺效果

每個方塊的文字變化添加了 150ms 的漸變動畫,提升用戶體驗:

Text(value ? `${value}` : '').animation({ duration: 150, curve: Curve.EaseOut });

六、完整代碼

import { HashMap } from '@kit.ArkTS'
import { AppUtil, PreferencesUtil, ToastUtil } from '@pura/harmony-utils'// index.ets
@Entry
@Component
struct Game2048 {@State grid: number[][] = Array(4).fill(0).map(() => Array(4).fill(0)) // 4x4游戲網格@State score: number = 0 // 當前得分@State bestScore: number = 0 // 歷史最高分private startX: number = 0 // 觸摸起始X坐標private startY: number = 0 // 觸摸起始Y坐標// 生命周期方法:頁面即將顯示時觸發aboutToAppear() {this.initGame()this.bestScore = PreferencesUtil.getNumberSync("bestScore") // 讀取本地存儲的最高分}// 初始化游戲initGame() {this.grid = this.grid.map(() => Array(4).fill(0)) // 重置網格this.addNewTile() // 添加兩個新方塊this.addNewTile() // 重置當前得分this.score = 0}addNewTile() {const emptyCells: [number, number][] = [] // 收集空單元格坐標this.grid.forEach((row, i) => {row.forEach((cell, j) => {if (cell === 0) {emptyCells.push([i, j])}})})if (emptyCells.length > 0) {let n = Math.floor(Math.random() * emptyCells.length) // 隨機選擇空單元格const i = emptyCells[n][0]const j = emptyCells[n][1]this.grid[i][j] = Math.random() < 0.9 ? 2 : 4 // 90%概率生成2,10%概率生成4}}// 處理移動邏輯move(direction: 'left' | 'right' | 'up' | 'down') {let newGrid = this.grid.map(row => [...row]) // 創建網格副本let moved = false // 移動標志位// 矩陣旋轉輔助方法const rotate = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[i]).reverse())}const rotateReverse = (matrix: number[][]) => {return matrix[0].map((_, i) => matrix.map(row => row[row.length - 1 - i]))}// 處理單行移動和合并const moveRow = (row: number[]) => {let newRow = row.filter(cell => cell !== 0) // 移除空格for (let i = 0; i < newRow.length - 1; i++) {if (newRow[i] === newRow[i + 1]) { // 合并相同數字newRow[i] *= 2this.score += newRow[i] // 更新得分newRow.splice(i + 1, 1) // 移除合并后的元素}}// 補齊長度while (newRow.length < 4) {newRow.push(0)}return newRow}// 根據方向處理移動switch (direction) {case 'left':newGrid.forEach((row, i) => newGrid[i] = moveRow(row))breakcase 'right':newGrid.forEach((row, i) => newGrid[i] = moveRow(row.reverse()).reverse())breakcase 'up':let rotatedDown = rotate(newGrid)rotatedDown.forEach((row, i) => rotatedDown[i] = moveRow(row.reverse()).reverse())newGrid = rotateReverse(rotatedDown)breakcase 'down':let rotatedUp = rotate(newGrid)rotatedUp.forEach((row, i) => rotatedUp[i] = moveRow(row))newGrid = rotateReverse(rotatedUp)break}moved = JSON.stringify(newGrid) !== JSON.stringify(this.grid) // 判斷是否發生移動this.grid = newGridif (moved) {this.addNewTile() // 移動后添加新方塊if (this.score > this.bestScore) { // 更新最高分this.bestScore = this.scorePreferencesUtil.putSync('bestScore', this.bestScore) //保存最高分}}if (this.isGameOver()) { // 游戲結束檢測ToastUtil.showToast('游戲結束!')}}// 游戲結束判斷isGameOver(): boolean {// 檢查空格子if (this.grid.some(row => row.includes(0))) {return false}// 檢查橫向可合并for (let i = 0; i < 4; i++) {for (let j = 0; j < 3; j++) {if (this.grid[i][j] === this.grid[i][j + 1]) {return false}}}// 檢查縱向可合并for (let j = 0; j < 4; j++) {for (let i = 0; i < 3; i++) {if (this.grid[i][j] === this.grid[i + 1][j]) {return false}}}return true}build() {Column() {// 分數顯示行Row() {Text(`得分: ${this.score}`).fontSize(20).margin(10)Text(`最高分: ${this.bestScore}`).fontSize(20).margin(10)Button('新游戲').onClick(() => this.initGame()).margin(10)}.margin({top:px2vp(AppUtil.getStatusBarHeight()) })// 游戲網格Grid() {ForEach(this.grid, (row: number[], i) => {ForEach(row, (value: number, j) => {GridItem() {Text(value ? `${value}` : '').textAlign(TextAlign.Center).fontSize(24).fontColor(this.getTextColor(value)).width('100%').height('100%').backgroundColor(this.getTileColor(value)).animation({duration: 150,curve: Curve.EaseOut})}.key(`${i}-${j}`)})})}.columnsTemplate('1fr 1fr 1fr 1fr')    // 4等分列.rowsTemplate('1fr 1fr 1fr 1fr')       // 4等分行.width('90%').aspectRatio(1)                        // 保持正方形.margin(10).onTouch((event) => {                  // 觸摸事件處理if (event.type === TouchType.Down) {this.startX = event.touches[0].xthis.startY = event.touches[0].y} else if (event.type === TouchType.Up) {const deltaX = event.touches[0].x - this.startXconst deltaY = event.touches[0].y - this.startY// 根據滑動方向判斷移動if (Math.abs(deltaX) > Math.abs(deltaY)) {deltaX > 0 ? this.move('right') : this.move('left')} else {deltaY > 0 ? this.move('down') : this.move('up')}}})}.width('100%')}// 獲取方塊背景色getTileColor(value: number): string {const colors = new HashMap<number, string>()colors.set(0, '#CDC1B4')colors.set(2, '#EEE4DA')colors.set(4, '#EDE0C8')colors.set(8, '#F2B179')colors.set(16, '#F59563')colors.set(32, '#F67C5F')colors.set(64, '#F65E3B')colors.set(128, '#EDCF72')colors.set(256, '#EDCF72')colors.set(512, '#EDCC61')colors.set(1024, '#EDC850')colors.set(2048, '#EDC22E')return colors.get(value) || '#CDC1B4'}// 獲取文字顏色getTextColor(value: number): Color {return value > 4 ? Color.White : Color.Black}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/70984.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/70984.shtml
英文地址,請注明出處:http://en.pswp.cn/web/70984.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Milvus高性能向量數據庫與大模型結合

Milvus | 高性能向量數據庫&#xff0c;為規模而構建Milvus 是一個為 GenAI 應用構建的開源向量數據庫。使用 pip 安裝&#xff0c;執行高速搜索&#xff0c;并擴展到數十億個向量。https://milvus.io/zh Milvus 是什么&#xff1f; Milvus 是一種高性能、高擴展性的向量數據…

kettle插件-自定義函數-數據脫敏

平常我們在使用kettle抽取數據的時候會涉及到敏感數據邀請脫敏或者進行掩碼的需求&#xff0c;今天我們使用自定義函數插件來實現這些需求。 1、將自定義函數插件&#xff08;kettle-func-plugin.zip&#xff09;解壓后放到kettle的plugins目錄下面&#xff0c;然后重啟服務。…

LeetCode 每日一題 2025/2/24-2025/3/2

記錄了初步解題思路 以及本地實現代碼&#xff1b;并不一定為最優 也希望大家能一起探討 一起進步 目錄 2/24 1656. 設計有序流2/25 2502. 設計內存分配器2/26 1472. 設計瀏覽器歷史記錄2/27 2296. 設計一個文本編輯器2/28 2353. 設計食物評分系統3/1 131. 分割回文串3/2 2/24 …

C++動態與靜態轉換區別詳解

文章目錄 前言一、 類型檢查的時機二、安全性三、適用場景四、代碼示例對比總結 前言 在 C 中&#xff0c;dynamic_cast 和 static_cast 是兩種不同的類型轉換操作符&#xff0c;主要區別體現在類型檢查的時機、安全性和適用場景上。以下是它們的核心區別&#xff1a; 一、 類…

探秘《矩陣之美》:解鎖矩陣的無限魅力

在這個數據驅動的時代&#xff0c;矩陣作為數學中的瑰寶&#xff0c;不僅在理論研究中占據核心地位&#xff0c;更在工程技術、計算機科學、物理學、經濟學等眾多領域發揮著不可替代的作用。今天&#xff0c;讓我們通過中科院大學耿修瑞老師&#xff08;中科院空天信息研究院研…

【MySQL】(2) 庫的操作

SQL 關鍵字&#xff0c;大小寫不敏感。 一、查詢數據庫 show databases; 注意加分號&#xff0c;才算一句結束。 二、創建數據庫 {} 表示必選項&#xff0c;[] 表示可選項&#xff0c;| 表示任選其一。 示例&#xff1a;建議加上 if not exists 選項。 三、字符集編碼和排序…

Vue3實現文件上傳、下載及預覽全流程詳解(含完整接口調用)

文章目錄 一、環境準備1.1 創建Vue3項目1.2 安裝依賴1.3 配置Element Plus 二、文件上傳實現2.1 基礎上傳組件2.2 自定義上傳邏輯&#xff08;Axios實現&#xff09; 三、文件下載實現3.1 直接下載&#xff08;已知文件URL&#xff09;3.2 后端接口下載&#xff08;二進制流&am…

分布式數據存儲:提升系統彈性與性能的技術之路

分布式數據存儲:提升系統彈性與性能的技術之路 在當今數據爆炸式增長的時代,傳統的單機存儲系統已無法滿足大規模、高并發、低延遲的需求。尤其是在大數據、云計算和物聯網的推動下,數據存儲面臨著前所未有的挑戰。分布式數據存儲應運而生,通過將數據分布在多個物理節點上…

在編譯Linux的內核鏡像和模塊時,必須先編譯內核鏡像,再編譯模塊,順序不可隨意調整的原因

問&#xff1a;在編譯Linux的內核鏡像和模塊時,必須先編譯內核鏡像,再編譯模塊,順序不可隨意調整 答&#xff1a;在編譯 Linux 內核和模塊時&#xff0c;必須先編譯內核鏡像&#xff0c;再編譯模塊&#xff0c;順序不可隨意調整。 原因&#xff1a; 模塊依賴內核的頭文件和符…

免費使用 DeepSeek API 教程及資源匯總

免費使用 DeepSeek API 教程及資源匯總 一、DeepSeek API 資源匯總1.1 火山引擎1.2 百度千帆1.3 阿里百煉1.4 騰訊云 二、其他平臺2.1 華為云2.2 硅基流動 三、總結 DeepSeek-R1 作為 2025 年初發布的推理大模型&#xff0c;憑借其卓越的邏輯推理能力和成本優勢&#xff0c;迅速…

千峰React:案例二

完成對html文檔還有css的引入&#xff0c;引入一下數據&#xff1a; import { func } from prop-types import ./購物車樣式.css import axios from axios import { useImmer } from use-immer import { useEffect } from reactfunction Item() {return (<li classNameacti…

用DeepSeek生成批量刪除處理 PDF第一頁工具

安裝依賴庫 在運行程序之前&#xff0c;請確保安裝所需的庫&#xff1a; pip install pymupdf python-docx Python 程序代碼 import os import fitz # PyMuPDF from docx import Documentdef delete_pdf_first_page(input_path, output_path):"""刪除 PDF…

redis的下載和安裝詳解

一、下載redis安裝包 進入redis官網查看當前穩定版本&#xff1a; https://redis.io/download/發現此時的穩定版本是6.2.4&#xff0c; 此時可以去這個網站下載6.2.4穩定版本的tar包。 暫時不考慮不在windows上使用redis&#xff0c;那樣將無法發揮redis的性能 二、上傳tar…

如何使用 Jenkins 實現 CI/CD 流水線:從零開始搭建自動化部署流程

如何使用 Jenkins 實現 CI/CD 流水線:從零開始搭建自動化部署流程 在軟件開發過程中,持續集成(CI)和持續交付(CD)已經成為現代開發和運維的標準實踐。隨著代碼的迭代越來越頻繁,傳統的手動部署方式不僅低效,而且容易出錯。為了提高開發效率和代碼質量,Jenkins作為一款…

Python基于Django的網絡課程在線學習平臺【附源碼】

博主介紹&#xff1a;?Java老徐、7年大廠程序員經歷。全網粉絲12w、csdn博客專家、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和畢業項目實戰? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精彩專欄推薦訂閱&#x1f447;&…

Pytorch為什么 nn.CrossEntropyLoss = LogSoftmax + nn.NLLLoss?

為什么 nn.CrossEntropyLoss LogSoftmax nn.NLLLoss&#xff1f; 在使用 PyTorch 時&#xff0c;我們經常聽說 nn.CrossEntropyLoss 是 LogSoftmax 和 nn.NLLLoss 的組合。這句話聽起來簡單&#xff0c;但背后到底是怎么回事&#xff1f;為什么這兩個分開的功能加起來就等于…

rabbitmq 延時隊列

要使用 RabbitMQ Delayed Message Plugin 實現延時隊列&#xff0c;首先需要確保插件已安裝并啟用。以下是實現延時隊列的步驟和代碼示例。 1. 安裝 RabbitMQ Delayed Message Plugin 首先&#xff0c;確保你的 RabbitMQ 安裝了 rabbitmq-delayed-message-exchange 插件。你可…

在 Vue 單文件組件(SFC)中,標簽的顯式關閉與隱式關閉有著重要的區別

一、顯式關閉標簽 1、定義&#xff1a; 所有的 HTML 標簽都必須有一個對應的結束標簽。 自閉合標簽也必須使用 / 來關閉。 <template> <div> <p>這是一個段落</p> <img src"image.png"…

第四屆大數據、區塊鏈與經濟管理國際學術會議

重要信息 官網&#xff1a;www.icbbem.com 時間&#xff1a;2025年3月14-16日 地點&#xff1a;中國-武漢 &#xff08;線上召開&#xff09; 簡介 第四屆大數據、區塊鏈與經濟管理國際學術會議(ICBBEM 2025)&#xff0c;將于2025年3月14-16日在中國湖北省武漢市召開。…

每日十個計算機專有名詞 (7)

Metasploit 詞源&#xff1a;Meta&#xff08;超越&#xff0c;超出&#xff09; exploit&#xff08;漏洞利用&#xff09; Metasploit 是一個安全測試框架&#xff0c;用來幫助安全專家&#xff08;也叫滲透測試人員&#xff09;發現和利用計算機系統中的漏洞。你可以把它想…