鸿蒙HarmonyOS 5小游戏实践:记忆翻牌(附:源代码)

发布于:2025-06-28 ⋅ 阅读:(16) ⋅ 点赞:(0)

记忆翻牌游戏是一款经典的益智游戏,它能有效锻炼玩家的记忆力和观察能力。本文将详细介绍如何使用鸿蒙(HarmonyOS)的ArkUI框架开发一款完整的记忆翻牌游戏,涵盖游戏设计、核心逻辑实现和界面构建的全过程。

游戏设计概述

记忆翻牌游戏的基本规则很简单:玩家需要翻开卡片并找出所有匹配的卡片对。在我们的实现中,游戏包含以下特点:

  • 4×4的棋盘布局(16张卡片,8对图案)
  • 使用可爱的动物表情符号作为卡片内容
  • 计时和计步功能
  • 新游戏和重新开始功能
  • 游戏胜利提示

游戏状态管理

在鸿蒙开发中,状态管理是关键。我们使用@State装饰器来管理游戏的各种状态:

@State cards: Card[] = [];          // 所有卡片数组
@State firstCard: number | null = null;  // 第一张翻开的卡片索引
@State secondCard: number | null = null; // 第二张翻开的卡片索引
@State moves: number = 0;          // 移动步数
@State gameOver: boolean = false;   // 游戏是否结束
@State timer: number = 0;          // 游戏用时

这种状态管理方式确保了当这些值发生变化时,UI能够自动更新。

核心游戏逻辑实现

1. 游戏初始化

游戏初始化包括创建卡片对、洗牌和设置初始状态:

startNewGame() {
  // 重置游戏状态
  this.moves = 0;
  this.timer = 0;
  this.gameOver = false;
  this.firstCard = null;
  this.secondCard = null;

  // 创建卡片对
  let cardValues: string[] = [];
  for (let i = 0; i < this.PAIRS_COUNT; i++) {
    cardValues.push(this.CARD_TYPES[i]);
    cardValues.push(this.CARD_TYPES[i]);
  }
  
  // 洗牌
  this.shuffleArray(cardValues);

  // 初始化卡片状态
  this.cards = cardValues.map(value => ({
    value,
    flipped: false,
    matched: false
  })).slice(0); // 使用slice(0)确保UI更新

  // 开始计时
  this.timerInterval = setInterval(() => {
    this.timer++;
  }, 1000);
}

2. 洗牌算法

我们使用经典的Fisher-Yates洗牌算法来随机排列卡片:

private shuffleArray(array: string[]) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const temp = array[i];
    array[i] = array[j];
    array[j] = temp;
  }
}

3. 卡片点击处理

卡片点击是游戏的核心交互,需要处理多种情况:

handleCardClick(index: number) {
  // 检查是否可点击
  if (this.gameOver || this.cards[index].matched || this.cards[index].flipped) {
    return;
  }
  
  if (this.firstCard !== null && this.secondCard !== null) {
    return;
  }

  // 创建新数组触发UI更新
  let newCards = this.cards.slice(0);
  newCards[index].flipped = true;
  this.cards = newCards;

  // 设置第一张或第二张卡片
  if (this.firstCard === null) {
    this.firstCard = index;
  } else {
    this.secondCard = index;
    this.moves++;
    this.checkMatch(); // 检查匹配
  }
}

4. 匹配检查

匹配检查逻辑决定了游戏的胜负:

private checkMatch() {
  if (this.firstCard === null || this.secondCard === null) return;

  if (this.cards[this.firstCard].value === this.cards[this.secondCard].value) {
    // 匹配成功
    let newCards = this.cards.slice(0);
    newCards[this.firstCard].matched = true;
    newCards[this.secondCard].matched = true;
    this.cards = newCards;

    this.firstCard = null;
    this.secondCard = null;
    this.checkGameOver(); // 检查游戏是否结束
  } else {
    // 不匹配,1秒后翻回
    setTimeout(() => {
      let newCards = this.cards.slice(0);
      if (this.firstCard !== null) newCards[this.firstCard].flipped = false;
      if (this.secondCard !== null) newCards[this.secondCard].flipped = false;
      this.cards = newCards;

      this.firstCard = null;
      this.secondCard = null;
    }, 1000);
  }
}

界面构建

鸿蒙的ArkUI框架提供了声明式的UI构建方式,我们使用Grid布局来构建4×4的游戏棋盘:

build() {
  Column() {
    // 游戏标题和信息显示
    Text('记忆翻牌游戏')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
    
    Row() {
      Text(`步数: ${this.moves}`)
      Text(`时间: ${this.formatTime(this.timer)}`)
    }
    
    // 游戏棋盘
    Grid() {
      ForEach(this.cards, (card: Card, index) => {
        GridItem() {
          this.CardView(card, index)
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr 1fr 1fr')
    
    // 新游戏按钮
    Button('新游戏')
      .onClick(() => this.startNewGame())
    
    // 游戏结束提示
    if (this.gameOver) {
      Text('恭喜通关!')
    }
  }
}

卡片视图使用Stack和Column组合实现:

@Builder
CardView(card: Card, index: number) {
  Stack() {
    Column() {
      if (!card.flipped) {
        Text('?') // 卡片背面
      } else {
        Text(card.value) // 卡片正面
      }
    }
    .backgroundColor(card.flipped ? 
      (card.matched ? '#4CAF50' : '#FFFFFF') : '#2196F3')
    .borderRadius(10)
  }
  .onClick(() => {
    this.handleCardClick(index);
  })
}

关键技术与注意事项

  1. 状态管理:在鸿蒙开发中,直接修改数组元素不会触发UI更新。我们需要使用slice(0)创建新数组,然后修改并重新赋值给状态变量。
  2. 定时器管理:游戏计时器需要在组件销毁或游戏重新开始时正确清理,避免内存泄漏。
  3. UI更新优化:通过将卡片视图提取为独立的@Builder方法,可以提高代码的可读性和维护性。
  4. 用户体验
    • 添加了1秒的延迟让玩家有机会记住不匹配的卡片
    • 匹配成功的卡片变为绿色,提供视觉反馈
    • 显示游戏时间和步数,增加挑战性

附:代码

// MemoryGame.ets
@Entry
@Component
struct MemoryGame {
  // 游戏配置
  private readonly CARD_TYPES = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];
  private readonly PAIRS_COUNT = 8;
  private readonly BOARD_SIZE = 4;

  // 游戏状态
  @State cards: Card[] = [];
  @State firstCard: number | null = null;
  @State secondCard: number | null = null;
  @State moves: number = 0;
  @State gameOver: boolean = false;
  @State timer: number = 0;
  private timerInterval: number | null = null;

  aboutToAppear() {
    this.startNewGame();
  }

  startNewGame() {
    if (this.timerInterval) {
      clearInterval(this.timerInterval);
    }

    this.moves = 0;
    this.timer = 0;
    this.gameOver = false;
    this.firstCard = null;
    this.secondCard = null;

    let cardValues: string[] = [];
    for (let i = 0; i < this.PAIRS_COUNT; i++) {
      cardValues.push(this.CARD_TYPES[i]);
      cardValues.push(this.CARD_TYPES[i]);
    }
    this.shuffleArray(cardValues);

    // 使用slice(0)创建新数组触发UI更新
    // 初始化卡片
    this.cards = cardValues.map(value => {
      let card: Card = {
        value: value,
        flipped: false,
        matched: false
      };
      return card
    }).slice(0);

    this.timerInterval = setInterval(() => {
      this.timer++;
    }, 1000);
  }

  private shuffleArray(array: string[]) {
    for (let i = array.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = array[i];
      array[i] = array[j];
      array[j] = temp;
    }
  }

  handleCardClick(index: number) {
    if (this.gameOver || this.cards[index].matched || this.cards[index].flipped) {
      return;
    }

    if (this.firstCard !== null && this.secondCard !== null) {
      return;
    }

    // 创建新数组触发UI更新
    let newCards = this.cards.slice(0);
    newCards[index].flipped = true;
    this.cards = newCards;

    if (this.firstCard === null) {
      this.firstCard = index;
    } else {
      this.secondCard = index;
      this.moves++;
      this.checkMatch();
    }
  }

  private checkMatch() {
    if (this.firstCard === null || this.secondCard === null) return;

    if (this.cards[this.firstCard].value === this.cards[this.secondCard].value) {
      // 匹配成功
      let newCards = this.cards.slice(0);
      newCards[this.firstCard].matched = true;
      newCards[this.secondCard].matched = true;
      this.cards = newCards;

      this.firstCard = null;
      this.secondCard = null;
      this.checkGameOver();
    } else {
      // 不匹配,1秒后翻回
      setTimeout(() => {
        let newCards = this.cards.slice(0);
        if (this.firstCard !== null) newCards[this.firstCard].flipped = false;
        if (this.secondCard !== null) newCards[this.secondCard].flipped = false;
        this.cards = newCards;

        this.firstCard = null;
        this.secondCard = null;
      }, 1000);
    }
  }

  private checkGameOver() {
    this.gameOver = this.cards.every(card => card.matched);
    if (this.gameOver && this.timerInterval) {
      clearInterval(this.timerInterval);
    }
  }

  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }

  build() {
    Column() {
      Text('记忆翻牌游戏')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Row() {
        Text(`步数: ${this.moves}`)
          .fontSize(16)
          .layoutWeight(1)

        Text(`时间: ${this.formatTime(this.timer)}`)
          .fontSize(16)
          .layoutWeight(1)
      }
      .width('100%')
      .margin({ bottom: 20 })

      Grid() {
        ForEach(this.cards, (card: Card, index) => {
          GridItem() {
            this.CardView(card, index)
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .width('100%')
      .height(400)
      .margin({ bottom: 20 })

      Button('新游戏')
        .width(200)
        .height(40)
        .backgroundColor('#4CAF50')
        .fontColor(Color.White)
        .onClick(() => this.startNewGame())

      if (this.gameOver) {
        Text('恭喜通关!')
          .fontSize(20)
          .fontColor(Color.Red)
          .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  CardView(card: Card, index: number) {
    Stack() {
      Column() {
        if (!card.flipped) {
          Text('?')
            .fontSize(30)
        } else {
          Text(card.value)
            .fontSize(30)
        }
      }
      .width('90%')
      .height('90%')
      .backgroundColor(card.flipped ? (card.matched ? '#4CAF50' : '#FFFFFF') : '#2196F3')
      .borderRadius(10)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .onClick(() => {
      this.handleCardClick(index);
    })
  }
}

interface Card {
  value: string;
  flipped: boolean;
  matched: boolean;
}

总结

通过这个记忆翻牌游戏的开发,我们学习了鸿蒙应用开发中的几个重要概念:

  1. 使用@State管理应用状态
  2. 声明式UI构建方式
  3. 数组状态更新的正确方法
  4. 定时器的使用和管理
  5. 用户交互处理的最佳实践

这款游戏虽然简单,但涵盖了鸿蒙应用开发的许多核心概念。开发者可以在此基础上进一步扩展,比如添加难度选择、音效、动画效果、高分记录等功能,打造更加丰富的游戏体验。

鸿蒙的ArkUI框架为开发者提供了强大的工具来构建响应式、高性能的应用。通过这个实战项目,希望能帮助开发者更好地理解鸿蒙应用开发的思路和方法。


网站公告

今日签到

点亮在社区的每一天
去签到