目錄

開發準備與環境搭建
第一步:繪製遊戲地圖
第二步:隨機生成食物
第三步:創建並移動蛇
第四步:鍵盤控制與遊戲邏輯
第五步:實現吃食物與計分
第六步:邊界碰撞與遊戲結束
第七步:遊戲重置功能
進階:面向對象封裝
總結與展望

  1. 開發準備與環境搭建

工具:安裝最新版的 DevEco Studio。
語言:本項目使用 ArkTS。
基礎知識:瞭解 HarmonyOS 應用開發的基本概念,如 @Entry、@Component、build() 函數以及 State 裝飾器等。

  1. 第一步:繪製遊戲地圖

遊戲的舞台是地圖。我們將創建一個 20x20 的網格來作為我們的遊戲區域。

核心思路:使用 ForEach 循環嵌套,動態生成一個由小方塊組成的二維網格。

// Index.ets

@Entry

@Component

struct SnakeGame {

// 創建一個 20x20 的二維數組,用於表示地圖網格

private map: number[][] = Array(20).fill(null).map(() => Array(20).fill(0));build() {

Column() {

Text('貪吃蛇').fontSize(30).fontWeight(FontWeight.Bold).margin({ bottom: 10 });// 使用 Stack 來疊加地圖、蛇和食物
  Stack() {
    // 繪製地圖背景
    Column() {
      ForEach(this.map, (row: number[]) => {
        Row() {
          ForEach(row, () => {
            // 每個小方塊代表一個單元格
            Column() {}.width(15).height(15)
              .backgroundColor('#f0f0f0') // 淺灰色背景
              .border({ width: 0.5, color: '#dddddd' }); // 加上邊框,看起來更像網格
          })
        }
      })
    }
  }
  .margin({ top: 20 })
  .backgroundColor('#ffffff') // 地圖容器背景
  .padding(5)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.alignItems(ItemAlign.Center)
.backgroundColor('#e8e8e8');}

}


代碼解析:

我們定義了一個 map 數組,它的唯一目的是提供一個數據源,讓 ForEach 能夠循環指定的次數(20 行,每行 20 列)。
Stack 組件是實現遊戲層疊效果的關鍵,後續的蛇和食物都將作為子組件放在這個 Stack 中。
每個單元格是一個 Column,通過設置固定的 width 和 height 來控制其大小。

  1. 第二步:隨機生成食物

食物是蛇的目標。我們需要在地圖上隨機位置生成一個紅色的方塊。

核心思路:在組件初始化時,隨機生成食物的 X 和 Y 座標,並使用 @State 裝飾器使其成為響應式數據,以便在位置改變時 UI 能夠自動更新。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

private map: number[][] = Array(20).fill(null).map(() => Array(20).fill(0));

// 使用 @State 裝飾器,讓食物位置變化時能刷新UI

@State food: number[] = []; // [y, x]// 封裝一個生成食物的函數

private generateFood() {

this.food = [

Math.floor(Math.random() * 20), // 隨機 Y 座標 (0-19)

Math.floor(Math.random() * 20)  // 隨機 X 座標 (0-19)

];

}// 組件即將出現時調用

aboutToAppear() {

this.generateFood();

}build() {

Column() {

// ... (省略標題)

Stack() {

// ... (省略地圖繪製)// 繪製食物
    if (this.food.length > 0) {
      Text()
        .width(15)
        .height(15)
        .backgroundColor(Color.Red)
        .position({
          top: this.food[0] * 15,  // Y座標 * 單元格高度
          left: this.food[1] * 15  // X座標 * 單元格寬度
        });
    }
  }
  // ... (省略其他樣式)
}
// ... (省略外層樣式)}

}


代碼解析:

@State food: number[]:food 數組的第一個元素是 Y 軸座標,第二個是 X 軸座標。@State 確保了一旦 food 的值改變,使用它的 UI 組件(這裏是食物的 position)會重新渲染。
aboutToAppear():這是一個生命週期回調,在組件即將在界面上顯示時執行。我們在這裏調用 generateFood(),確保遊戲一開始就有食物。
position({ top: ..., left: ... }):通過絕對定位將食物放置在計算好的位置上。

  1. 第三步:創建並移動蛇

蛇是遊戲的主角。它由多個方塊組成,需要根據規則移動。

核心思路:用一個二維數組 snake 來存儲蛇身體每個部分的座標。移動時,從蛇尾開始,每個身體部分都移動到前一個部分的位置,最後再根據當前方向移動蛇頭。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

// ... (省略 map, food)

@State snake: number[][] = [

[10, 6], // 蛇頭 [y, x]

[10, 5],

[10, 4],

[10, 3],

[10, 2]  // 蛇尾

];

private direction: 'top' | 'left' | 'bottom' | 'right' = 'right'; // 初始方向向右

private timer: number = 0; // 定時器ID// ... (省略 generateFood, aboutToAppear)
// 蛇移動的核心邏輯

private moveSnake() {

// 從蛇尾開始,依次向前移動

for (let i = this.snake.length - 1; i > 0; i--) {

this.snake[i] = [...this.snake[i - 1]]; // 複製前一個節點的座標

}// 根據方向移動蛇頭
switch (this.direction) {
  case 'top':
    this.snake[0][0]--;
    break;
  case 'bottom':
    this.snake[0][0]++;
    break;
  case 'left':
    this.snake[0][1]--;
    break;
  case 'right':
    this.snake[0][1]++;
    break;
}

// 在 HarmonyOS 中,直接修改數組元素可能無法觸發UI更新,
// 因此我們創建一個新數組來觸發重新渲染
this.snake = [...this.snake];}
build() {

Column() {

// ... (省略標題)

Stack() {

// ... (省略地圖繪製)

// ... (省略食物繪製)// 繪製蛇
    ForEach(this.snake, (segment: number[], index: number) => {
      Text()
        .width(15)
        .height(15)
        .backgroundColor(index === 0 ? Color.Pink : Color.Black) // 蛇頭用粉色,身體用黑色
        .position({
          top: segment[0] * 15,
          left: segment[1] * 15
        });
    })
  }
  // ... (省略其他樣式)
}
// ... (省略外層樣式)}

}


代碼解析:

@State snake: number[][]:snake 數組中的每個元素都是一個 [y, x] 座標對,代表蛇身體的一個部分。數組的第一個元素是蛇頭。
moveSnake():這是蛇移動的核心。通過循環,我們讓蛇的每一節身體都 “繼承” 前一節的位置,從而實現整體移動的效果。最後再單獨處理蛇頭的位置。
this.snake = [...this.snake];:這是一個在 ArkUI 中非常重要的技巧。由於 snake 是一個引用類型(數組),直接修改其內部元素(如 this.snake[0][0]--)並不會觸發 UI 的重新渲染。通過創建一個新的數組(使用擴展運算符 ...)並賦值給 this.snake,我們可以強制觸發 UI 更新。

  1. 第四步:鍵盤控制與遊戲邏輯

現在,我們需要讓蛇動起來,並能通過按鈕控制它的方向。

核心思路:

添加 “開始”、“暫停”、“重置” 按鈕。
使用 setInterval 定時器來週期性地調用 moveSnake 函數,讓蛇自動移動。
添加方向控制按鈕(上、下、左、右),並在點擊時改變 direction 變量的值。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

// ... (省略 map, food, snake, direction, timer)

@State score: number = 0;

@State gameOverStr: string = '';// ... (省略 generateFood, aboutToAppear, moveSnake)
// 開始遊戲

private startGame() {

this.pauseGame(); // 先停止現有定時器,防止重複啓動

this.timer = setInterval(() => {

this.moveSnake();

this.checkCollisions(); // 移動後檢查碰撞

}, 200); // 每200毫秒移動一次

}// 暫停遊戲

private pauseGame() {

if (this.timer) {

clearInterval(this.timer);

this.timer = 0;

}

}build() {

Column() {

Text(分數: ${this.score}).fontSize(20).margin({ top: 10 });// 遊戲控制按鈕
  Row() {
    Button('開始').onClick(() => this.startGame());
    Button('暫停').margin({ left: 10 }).onClick(() => this.pauseGame());
    Button('重置').margin({ left: 10 }).onClick(() => this.resetGame());
  }.margin({ bottom: 10 });

  // ... (省略 Stack 中的地圖、蛇、食物)

  // 方向控制按鈕
  Column() {
    Button('↑').onClick(() => this.direction = 'top');
    Row() {
      Button('←').onClick(() => this.direction = 'left');
      Button('→').margin({ left: 20 }).onClick(() => this.direction = 'right');
    }.margin({ top: 5, bottom: 5 });
    Button('↓').onClick(() => this.direction = 'bottom');
  }
  .enabled(this.gameOverStr === '') // 遊戲結束時禁用按鈕
  .margin({ top: 20 });
}
// ... (省略外層樣式)}

}


注意:checkCollisions 和 resetGame 函數我們將在後續步驟中實現。

  1. 第五步:實現吃食物與計分

當蛇頭移動到食物的位置時,蛇的身體應該變長,分數應該增加,並且食物應該在新的位置重新生成。

核心思路:在 moveSnake 之後,檢查蛇頭座標是否與食物座標重合。如果重合,則在蛇尾添加一個新的身體部分,並重新生成食物。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

// ... (省略所有變量和其他函數)// 檢查碰撞(包括邊界、自己、食物)

private checkCollisions() {

const head = this.snake[0];// 1. 檢查是否吃到食物
if (head[0] === this.food[0] && head[1] === this.food[1]) {
  this.score += 10;
  this.snake.push([...this.snake[this.snake.length - 1]]); // 在蛇尾添加一節
  this.generateFood();
  return; // 吃到食物後,不再檢查死亡
}

// 後續將添加邊界和自撞檢測...}
private startGame() {

this.pauseGame();

this.timer = setInterval(() => {

this.moveSnake();

this.checkCollisions(); // 調用檢查函數

}, 200);

}// ... (省略 build 方法)

}


代碼解析:

checkCollisions 函數被放在 setInterval 中,在每次蛇移動後調用。
通過比較蛇頭 head 和食物 food 的座標來判斷是否吃到食物。
this.snake.push([...this.snake[this.snake.length - 1]]):當吃到食物時,我們在蛇數組的末尾添加一個新的元素,其座標與當前蛇尾相同。由於下一次移動時,所有身體部分都會向前移動,這個新的部分就會自然地成為新的蛇尾,從而實現蛇身變長的效果。

  1. 第六步:邊界碰撞與遊戲結束

遊戲需要有結束條件。最常見的就是蛇頭撞到地圖邊界或者撞到自己的身體。

核心思路:在 checkCollisions 函數中,添加對邊界和自身身體的檢測。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

// ... (省略所有變量和其他函數)// 檢查碰撞(包括邊界、自己、食物)

private checkCollisions() {

const head = this.snake[0];// 1. 檢查是否吃到食物 (上面已實現)
if (head[0] === this.food[0] && head[1] === this.food[1]) {
  // ... (省略吃食物的邏輯)
  return;
}

// 2. 檢查邊界碰撞
if (head[0] < 0 || head[0] >= 20 || head[1] < 0 || head[1] >= 20) {
  this.gameOver('遊戲結束!撞到牆了!');
  return;
}

// 3. 檢查自撞
for (let i = 1; i < this.snake.length; i++) {
  if (head[0] === this.snake[i][0] && head[1] === this.snake[i][1]) {
    this.gameOver('遊戲結束!撞到自己了!');
    return;
  }
}}
// 遊戲結束

private gameOver(message: string) {

this.pauseGame();

this.gameOverStr = message;

}build() {

Column() {

// ... (省略其他UI)Stack() {
    // ... (省略地圖、蛇、食物繪製)

    // 遊戲結束提示
    if (this.gameOverStr) {
      Column() {
        Text(this.gameOverStr).fontSize(30).fontColor(Color.Red).fontWeight(FontWeight.Bold);
        Text('點擊重置按鈕重新開始').fontSize(16).fontColor(Color.Gray).margin({ top: 10 });
      }
      .justifyContent(FlexAlign.Center)
      .alignItems(ItemAlign.Center)
      .backgroundColor('rgba(255, 255, 255, 0.8)')
      .width('100%')
      .height('100%');
    }
  }
  // ... (省略其他UI)
}
// ... (省略外層樣式)}

}


8. 第七步:遊戲重置功能

遊戲結束後,玩家需要一個可以重新開始的按鈕。

核心思路:創建一個 resetGame 函數,將所有遊戲狀態(蛇的位置、方向、食物、分數、遊戲結束標誌等)恢復到初始值。

// Index.ets

// ... (省略之前的代碼)

struct SnakeGame {

// ... (省略所有變量和其他函數)// 重置遊戲

private resetGame() {

this.pauseGame();

this.snake = [

[10, 6],

[10, 5],

[10, 4],

[10, 3],

[10, 2]

];

this.direction = 'right';

this.score = 0;

this.gameOverStr = '';

this.generateFood();

}build() {

Column() {

// ...

Button('重置').margin({ left: 10 }).onClick(() => this.resetGame());

// ...

}

}

}


至此,一個功能完整的貪吃蛇遊戲就已經完成了!你可以將以上代碼整合到一個 .ets 文件中直接運行。

  1. 進階:面向對象封裝 (OOP)

對於簡單的應用,將所有邏輯放在一個組件裏是可行的。但為了提高代碼的可讀性、可維護性和可擴展性,我們應該採用面向對象的思想對代碼進行重構。

核心思路:將遊戲中的核心元素(如蛇、食物、遊戲控制器)抽象成獨立的類。

9.1 創建領域模型 (Models)

Area.ts - 地圖模型
// models/Area.ts

export class Area {

public readonly row: number;

public readonly col: number;constructor(row: number = 20, col: number = 20) {
    this.row = row;
    this.col = col;
}

// 創建地圖網格數據
create(): number[][] {
    return Array(this.row).fill(null).map(() => Array(this.col).fill(0));
}}

Food.ts - 食物模型// models/Food.ts

export class Food {

public data: number[] = []; // [y, x]constructor(private area: Area) {
    this.generate();
}

// 隨機生成食物位置
generate(): void {
    this.data = [
        Math.floor(Math.random() * this.area.row),
        Math.floor(Math.random() * this.area.col)
    ];
}}

Snake.ts - 蛇模型// models/Snake.ts

export class Snake {

public data: number[][] = []; // [ [y, x], [y, x], ... ]

private direction: 'top' | 'left' | 'bottom' | 'right' = 'right';constructor() {
    this.init();
}

// 初始化蛇的位置
init(): void {
    this.data = [
        [10, 6],
        [10, 5],
        [10, 4],
        [10, 3],
        [10, 2]
    ];
    this.direction = 'right';
}

// 改變方向
changeDirection(newDirection: 'top' | 'left' | 'bottom' | 'right'): void {
    // 防止蛇向相反方向移動(例如正在向右,不能直接向左)
    const oppositeDirections = {
        'top': 'bottom',
        'bottom': 'top',
        'left': 'right',
        'right': 'left'
    };
    if (newDirection !== oppositeDirections[this.direction]) {
        this.direction = newDirection;
    }
}

// 移動一步
move(): void {
    // 身體跟隨
    for (let i = this.data.length - 1; i > 0; i--) {
        this.data[i] = [...this.data[i - 1]];
    }
    // 移動頭部
    switch (this.direction) {
        case 'top': this.data[0][0]--; break;
        case 'bottom': this.data[0][0]++; break;
        case 'left': this.data[0][1]--; break;
        case 'right': this.data[0][1]++; break;
    }
}

// 吃到食物後增長
grow(): void {
    this.data.push([...this.data[this.data.length - 1]]);
}

// 獲取蛇頭
getHead(): number[] {
    return this.data[0];
}}


9.2 創建遊戲控制器 (Controller)

GameController.ts - 遊戲核心控制器
// controller/GameController.ts

import { Area } from '../models/Area';

import { Food } from '../models/Food';

import { Snake } from '../models/Snake';export class GameController {

public area: Area;

public food: Food;

public snake: Snake;

public score: number = 0;

public isGameOver: boolean = false;

public gameOverMessage: string = '';private timer: number = 0;
private speed: number = 200; // 移動速度,毫秒

constructor(private row: number = 20, private col: number = 20) {
    this.area = new Area(row, col);
    this.snake = new Snake();
    this.food = new Food(this.area);
}

// 開始遊戲
start(): void {
    this.pause();
    this.isGameOver = false;
    this.gameOverMessage = '';
    this.timer = setInterval(() => {
        this.snake.move();
        if (!this.checkCollisions()) {
            this.gameLoop();
        }
    }, this.speed);
}

// 暫停遊戲
pause(): void {
    if (this.timer) {
        clearInterval(this.timer);
        this.timer = 0;
    }
}

// 重置遊戲
reset(): void {
    this.pause();
    this.score = 0;
    this.isGameOver = false;
    this.gameOverMessage = '';
    this.snake.init();
    this.food.generate();
}

// 遊戲主循環邏輯
private gameLoop(): void {
    // 遊戲邏輯更新後,可以在這裏通知UI刷新
}

// 檢查所有碰撞
private checkCollisions(): boolean {
    const head = this.snake.getHead();

    // 邊界碰撞
    if (head[0] < 0 || head[0] >= this.area.row || head[1] < 0 || head[1] >= this.area.col) {
        this.endGame('撞到牆了!');
        return true;
    }

    // 自撞
    for (let i = 1; i < this.snake.data.length; i++) {
        if (head[0] === this.snake.data[i][0] && head[1] === this.snake.data[i][1]) {
            this.endGame('撞到自己了!');
            return true;
        }
    }

    // 食物碰撞
    if (head[0] === this.food.data[0] && head[1] === this.food.data[1]) {
        this.score += 10;
        this.snake.grow();
        this.food.generate();
    }
    
    return false; // 沒有發生導致遊戲結束的碰撞
}

// 結束遊戲
private endGame(message: string): void {
    this.pause();
    this.isGameOver = true;
    this.gameOverMessage = message;
}}

9.3 重構 UI 組件Index.ets - 最終的 UI 展示
// Index.ets

import { GameController } from '../controller/GameController';@Entry

@Component

struct SnakeGameUI {

// 實例化遊戲控制器

private gameController: GameController = new GameController();// UI狀態
@State score: number = 0;
@State gameOverStr: string = '';
@State snakeSegments: number[][] = [];
@State foodPos: number[] = [];

build() {
    Column() {
        Text('貪吃蛇 (OOP版)').fontSize(30).fontWeight(FontWeight.Bold);
        Text(`分數: ${this.score}`).fontSize(20).margin({ top: 5 });

        // 控制按鈕
        Row() {
            Button('開始').onClick(() => {
                this.gameController.start();
                this.updateUI(); // 開始後立即更新一次UI
            });
            Button('暫停').margin({ left: 10 }).onClick(() => this.gameController.pause());
            Button('重置').margin({ left: 10 }).onClick(() => {
                this.gameController.reset();
                this.updateUI(); // 重置後立即更新UI
            });
        }.margin({ bottom: 10 });

        // 遊戲區域
        Stack() {
            // 地圖
            Column() {
                ForEach(this.gameController.area.create(), (row: number[]) => {
                    Row() {
                        ForEach(row, () => {
                            Column() {}.width(15).height(15).backgroundColor('#f0f0f0').border({ width: 0.5, color: '#dddddd' });
                        })
                    }
                })
            }

            // 食物
            Text().width(15).height(15).backgroundColor(Color.Red)
                .position({ top: this.foodPos[0] * 15, left: this.foodPos[1] * 15 });

            // 蛇
            ForEach(this.snakeSegments, (segment: number[], index: number) => {
                Text().width(15).height(15)
                    .backgroundColor(index === 0 ? Color.Pink : Color.Black)
                    .position({ top: segment[0] * 15, left: segment[1] * 15 });
            })

            // 遊戲結束 overlay
            if (this.gameOverStr) {
                Column() {
                    Text('遊戲結束').fontSize(30).fontColor(Color.Red).fontWeight(FontWeight.Bold);
                    Text(this.gameOverStr).fontSize(20).margin({ top: 10 });
                }
                .justifyContent(FlexAlign.Center)
                .alignItems(ItemAlign.Center)
                .backgroundColor('rgba(255, 255, 255, 0.7)')
                .width('100%')
                .height('100%');
            }
        }
        .width(this.gameController.area.col * 15 + 10) // 加上padding
        .height(this.gameController.area.row * 15 + 10)
        .backgroundColor('#ffffff')
        .padding(5)

        // 方向鍵
        Column() {
            Button('↑').onClick(() => this.gameController.snake.changeDirection('top'));
            Row() {
                Button('←').onClick(() => this.gameController.snake.changeDirection('left'));
                Button('→').margin({ left: 20 }).onClick(() => this.gameController.snake.changeDirection('right'));
            }.margin({ top: 5, bottom: 5 });
            Button('↓').onClick(() => this.gameController.snake.changeDirection('bottom'));
        }
        .enabled(!this.gameController.isGameOver)
        .margin({ top: 20 })

    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(ItemAlign.Center)
    .backgroundColor('#e8e8e8')
    .onChange(() => {
        // 這是一個簡化的UI刷新機制。在更復雜的應用中,你可能需要使用事件總線或狀態管理庫。
        // 這裏假設遊戲狀態變化時會觸發UI的重新渲染,從而調用updateUI
        this.updateUI();
    })
}

// 同步遊戲狀態到UI
private updateUI() {
    this.score = this.gameController.score;
    this.gameOverStr = this.gameController.gameOverMessage;
    this.snakeSegments = [...this.gameController.snake.data];
    this.foodPos = [...this.gameController.food.data];
}}


注意:上面的 onChange 是一種簡化的模擬。在實際的 HarmonyOS 開發中,更優雅的方式是使用 EventBus 或者讓控制器持有一個 UI 的回調接口,當遊戲狀態改變時主動通知 UI 刷新。這裏的重點是展示如何將邏輯與 UI 分離。

  1. 總結與展望

恭喜你!你已經成功地使用 HarmonyOS ArkUI 框架從零開始構建了一個貪吃蛇遊戲,並且還學習瞭如何使用面向對象的思想來重構代碼。

本項目涉及的關鍵技術點:

ArkUI 佈局與組件(Column, Row, Stack, ForEach, Button, Text)。
響應式狀態管理(@State)。
定時器的使用(setInterval, clearInterval)。
事件處理(onClick)。
遊戲開發的基本模式(遊戲循環、狀態更新、碰撞檢測)。
面向對象編程(封裝、繼承、多態的初步實踐)。
未來可以擴展的功能:

遊戲難度選擇:通過調整 speed 變量來實現不同的難度。
記錄最高分:使用 Preferences 存儲用户的最高得分。
更豐富的視覺效果:為蛇和食物添加圖片或動畫。
使用物理按鍵或手勢控制:除了屏幕按鈕,可以監聽設備的物理方向鍵或滑動手勢。
完善的狀態管理:使用 EventBus 或 Redux 等模式,讓控制器和 UI 的通信更加解耦和高效。
希望這篇詳細的教程能幫助你更好地理解 HarmonyOS 應用開發,並激發你開發更多有趣應用的靈感!