HarmonyOS 诗词填空游戏开发实战教程
效果视频
鸿蒙版古诗词填空游戏
前言
本教程将手把手教你开发一个古诗词填空小游戏,适合 HarmonyOS 初学者跟随学习。完成本教程后,你将学会:
- 如何组织 HarmonyOS 项目结构
- 如何管理游戏数据
- 如何实现游戏界面和交互逻辑
- 如何配置页面路由
开发环境要求:
- DevEco Studio(推荐最新版本)
- HarmonyOS SDK 6.0.0 或以上
- 基础的 TypeScript 知识
预计用时: 30-45 分钟
让我们开始吧!
第一步:创建项目和目录结构
1.1 打开你的 HarmonyOS 项目
假设你已经有一个名为 tiankong 的 HarmonyOS 项目。如果没有,请先用 DevEco Studio 创建一个新项目。
1.2 创建所需目录
我们需要创建两个新文件夹来组织代码:
方法一:使用 DevEco Studio
- 在项目视图中,找到
entry/src/main/ets/目录 - 右键点击
ets文件夹 →New→Directory - 输入
data,回车创建 - 再次右键点击
ets文件夹 →New→Directory - 输入
pages/game(会自动创建两级目录),回车创建
方法二:使用命令行
打开终端(Terminal),执行:
bash
cd 你的项目路径/tiankong
mkdir -p entry/src/main/ets/data
mkdir -p entry/src/main/ets/pages/game
验证结果:
你的目录结构应该如下:
tiankong/
└── entry/
└── src/
└── main/
└── ets/
├── data/ ← 新建(存放数据文件)
├── pages/
│ ├── game/ ← 新建(存放游戏页面)
│ └── Index.ets
├── entryability/
└── entrybackupability/
第二步:创建诗词数据库
现在我们来创建第一个文件,存放所有的诗词数据。
2.1 创建文件
- 在 DevEco Studio 中,右键点击
entry/src/main/ets/data文件夹 - 选择
New→ArkTS File - 输入文件名:
CultureData(不需要 .ets 后缀) - 点击确定
2.2 编写数据结构
打开刚创建的 CultureData.ets 文件,先定义数据类型:
typescript
/*
* 诗词园地数据
*/
// 定义文化类别:诗词、歇后语、故事
export type CultureCategory = 'poetry' | 'idiom' | 'story'
// 定义动物类型:鸡、鸭、鹅
export type AnimalType = 'chicken' | 'duck' | 'goose'
// 定义文化数据项的结构
export interface CultureItem {
id: string // 唯一标识
category: CultureCategory // 类别
animalType: AnimalType // 动物类型
title: string // 标题(诗词名)
content: string // 内容(诗词正文)
author?: string // 作者
dynasty?: string // 朝代
explanation?: string // 解释说明
tags: string[] // 标签
isFavorite: boolean // 是否收藏
}
💡 知识点:
export表示导出,其他文件可以导入使用type定义类型别名interface定义对象的结构?表示可选属性
2.3 创建数据库类
继续在同一个文件中添加数据库类:
typescript
export class CultureDatabase {
/**
* 获取所有诗词数据
*/
public static getPoetryData(): CultureItem[] {
return [
// 第一首诗:咏鹅
{
id: 'poetry_goose_001',
category: 'poetry',
animalType: 'goose',
title: '咏鹅',
author: '骆宾王',
dynasty: '唐',
content: '鹅,鹅,鹅,曲项向天歌。\n白毛浮绿水,红掌拨清波。',
explanation: '这首诗是骆宾王七岁时所作,通过对鹅的形态、颜色、动作的描写,生动形象地展现了白鹅的美丽和可爱。',
tags: ['骆宾王', '咏物诗', '儿童诗', '经典'],
isFavorite: false
},
// 第二首诗:画鸡
{
id: 'poetry_chicken_001',
category: 'poetry',
animalType: 'chicken',
title: '画鸡',
author: '唐寅',
dynasty: '明',
content: '头上红冠不用裁,满身雪白走将来。\n平生不敢轻言语,一叫千门万户开。',
explanation: '这首诗通过描绘大公鸡的形象,赞美了它的美丽和报晓的功劳。"一叫千门万户开"突出了公鸡报晓的重要作用。',
tags: ['唐寅', '咏物诗', '寓意深刻'],
isFavorite: false
},
// 第三首诗:惠崇春江晚景
{
id: 'poetry_duck_001',
category: 'poetry',
animalType: 'duck',
title: '惠崇春江晚景',
author: '苏轼',
dynasty: '宋',
content: '竹外桃花三两枝,春江水暖鸭先知。\n蒌蒿满地芦芽短,正是河豚欲上时。',
explanation: '"春江水暖鸭先知"成为千古名句,生动描绘了鸭子在春江中嬉戏的情景。',
tags: ['苏轼', '题画诗', '春景', '名句'],
isFavorite: false
}
// 💡 这里可以继续添加更多诗词
// 完整版包含17首诗词,为了简洁,这里只展示3首
// 你可以从文末的完整代码中复制其余诗词
]
}
/**
* 获取所有歇后语数据(暂时返回空数组)
*/
public static getIdiomData(): CultureItem[] {
return []
}
/**
* 获取所有成语故事数据(暂时返回空数组)
*/
public static getStoryData(): CultureItem[] {
return []
}
/**
* 获取所有文化数据
*/
public static getAllCultureData(): CultureItem[] {
let result: CultureItem[] = []
result = result.concat(CultureDatabase.getPoetryData())
result = result.concat(CultureDatabase.getIdiomData())
result = result.concat(CultureDatabase.getStoryData())
return result
}
/**
* 根据分类获取数据
*/
public static getDataByCategory(category: CultureCategory): CultureItem[] {
const allData = CultureDatabase.getAllCultureData()
return allData.filter(item => item.category === category)
}
/**
* 根据动物类型获取数据
*/
public static getDataByAnimalType(animalType: AnimalType): CultureItem[] {
const allData = CultureDatabase.getAllCultureData()
return allData.filter(item => item.animalType === animalType)
}
}
💡 知识点:
static表示静态方法,可以直接通过类名调用,无需创建实例filter()是数组方法,用于筛选符合条件的元素concat()用于合并数组
2.4 保存文件
按 Ctrl + S(Windows)或 Cmd + S(Mac)保存文件。
📝 小贴士: 完整的诗词数据(17首)请参考文末附录,现在继续下一步。
第三步:创建游戏数据管理器
这个文件负责把诗词转换成游戏题目。
3.1 创建文件
- 右键点击
entry/src/main/ets/data文件夹 - 选择
New→ArkTS File - 输入文件名:
GameDataManager - 点击确定
3.2 定义题目数据结构
打开 GameDataManager.ets,先导入数据库和定义题目结构:
typescript
/*
* 游戏数据管理器
*/
import { CultureDatabase, CultureItem } from './CultureData'
/**
* 游戏题目接口
*/
export interface GameQuestion {
id: string // 题目ID
type: 'idiom' | 'proverb' | 'poetry' // 题目类型
question: string // 题目内容(带填空的诗句)
answer: string // 正确答案
options?: string[] // 四个选项
hint: string // 提示信息
animalType: string // 动物类型
}
3.3 创建游戏数据管理器类
继续添加核心逻辑:
typescript
/**
* 游戏数据管理器
*/
export class GameDataManager {
/**
* 获取古诗词填空游戏题目
*/
static getPoetryQuestions(): GameQuestion[] {
// 1. 获取所有诗词数据
const poetry = CultureDatabase.getPoetryData()
const questions: GameQuestion[] = []
// 2. 遍历每首诗
poetry.forEach(poem => {
// 定义要查找的家禽名称(按长度从长到短,避免误匹配)
const animalNames = ['公鸡', '母鸡', '雏鸡', '小鸡', '鹅儿', '鸭儿', '鸡', '鸭', '鹅']
// 3. 查找诗句中是否包含这些名称
for (const animalName of animalNames) {
if (poem.content.includes(animalName)) {
// 4. 创建填空题:将家禽名称替换为 ____
const index = poem.content.indexOf(animalName)
const blankContent = poem.content.substring(0, index) +
'____' +
poem.content.substring(index + animalName.length)
// 5. 生成答案和选项
let correctOption = animalName
let options: string[] = []
// 将复合词简化为基础词作为答案
if (animalName.includes('鸡')) {
correctOption = '鸡'
options = ['鸡', '鸭', '鹅', '雀']
} else if (animalName.includes('鸭')) {
correctOption = '鸭'
options = ['鸭', '鸡', '鹅', '雁']
} else if (animalName.includes('鹅')) {
correctOption = '鹅'
options = ['鹅', '鸡', '鸭', '雁']
} else {
correctOption = animalName
options = [animalName, '鸡', '鸭', '鹅']
}
// 6. 打乱选项顺序
options = GameDataManager.shuffleArray(options)
// 7. 确保正确答案在选项中
if (!options.includes(correctOption)) {
options[options.length - 1] = correctOption
}
// 8. 创建题目对象
questions.push({
id: `${poem.id}_${animalName}`,
type: 'poetry',
question: `《${poem.title}》\n${poem.author ? `[${poem.dynasty}] ${poem.author}` : ''}\n\n${blankContent}`,
answer: correctOption,
options: options,
hint: poem.explanation ? poem.explanation.substring(0, 50) + '...' : '这是一首咏物诗',
animalType: poem.animalType
})
// 每首诗只生成一个题目
break
}
}
})
// 9. 打乱题目顺序,并限制为8道题
return GameDataManager.shuffleArray(questions).slice(0, 8)
}
/**
* 打乱数组(Fisher-Yates 洗牌算法)
*/
private static shuffleArray<T>(arr: T[]): T[] {
const result: T[] = []
// 复制数组
for (let i = 0; i < arr.length; i++) {
result.push(arr[i])
}
// 打乱顺序
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const temp = result[i]
result[i] = result[j]
result[j] = temp
}
return result
}
}
💡 知识点:
substring()截取字符串includes()检查字符串是否包含某个子串indexOf()查找子串位置slice()截取数组- Fisher-Yates 算法是经典的随机打乱算法
3.4 保存文件
按 Ctrl + S 保存。
第四步:创建游戏界面
这是最重要的一步,我们将创建游戏的用户界面。
4.1 创建文件
- 右键点击
entry/src/main/ets/pages/game文件夹 - 选择
New→ArkTS File - 输入文件名:
PoetryFillGame - 点击确定
4.2 导入必要的模块
打开 PoetryFillGame.ets,首先导入需要的模块:
typescript
/*
* 古诗词填空游戏
*/
import { router } from '@kit.ArkUI'
import { GameDataManager, GameQuestion } from '../../data/GameDataManager'
import { promptAction } from '@kit.ArkUI'
4.3 创建组件和状态
添加组件定义和状态管理:
typescript
@Entry
@Component
struct PoetryFillGame {
// 状态变量
@StorageProp('app_is_dark_mode') isDarkMode: boolean = false // 是否深色模式
@State questions: GameQuestion[] = [] // 题目列表
@State currentIndex: number = 0 // 当前题目索引
@State selectedAnswer: string = '' // 用户选择的答案
@State showHint: boolean = false // 是否显示提示
@State score: number = 0 // 得分
@State answered: boolean = false // 是否已答题
@State isCorrect: boolean = false // 答案是否正确
// 组件即将出现时调用
aboutToAppear() {
this.loadQuestions()
}
// 加载题目
private loadQuestions() {
this.questions = GameDataManager.getPoetryQuestions()
console.info('PoetryFillGame', `加载了 ${this.questions.length} 道题目`)
}
// 获取当前题目
private getCurrentQuestion(): GameQuestion | null {
if (this.currentIndex < this.questions.length) {
return this.questions[this.currentIndex]
}
return null
}
// 检查答案
private checkAnswer(option: string) {
if (this.answered) return // 已经答过题了,不能再答
this.selectedAnswer = option
this.answered = true
const question = this.getCurrentQuestion()
if (question) {
this.isCorrect = option === question.answer
if (this.isCorrect) {
this.score++ // 答对了,加分
}
}
}
// 下一题
private nextQuestion() {
if (this.currentIndex < this.questions.length - 1) {
// 还有下一题
this.currentIndex++
this.selectedAnswer = ''
this.answered = false
this.showHint = false
} else {
// 已经是最后一题,显示结果
this.showGameOver()
}
}
// 显示游戏结束对话框
private async showGameOver() {
const totalQuestions = this.questions.length
const percentage = Math.round((this.score / totalQuestions) * 100)
promptAction.showDialog({
title: '游戏结束',
message: `你答对了 ${this.score}/${totalQuestions} 题\n正确率:${percentage}%`,
buttons: [
{ text: '再玩一次', color: '#4CAF50' },
{ text: '返回', color: '#666666' }
]
}).then((data) => {
if (data.index === 0) {
// 再玩一次
this.currentIndex = 0
this.score = 0
this.selectedAnswer = ''
this.answered = false
this.showHint = false
this.loadQuestions()
} else {
// 返回首页
router.back()
}
})
}
// 构建界面(稍后填充)
build() {
Column() {
Text('游戏界面')
}
.width('100%')
.height('100%')
}
}
💡 知识点:
@Entry表示这是一个页面入口@Component表示这是一个组件@State表示状态变量,改变时会触发 UI 更新aboutToAppear()是生命周期函数,页面显示前调用
4.4 构建主界面
现在替换 build() 方法,构建完整界面:
typescript
build() {
Column() {
// 1. 导航栏
this.buildNavigationBar()
// 2. 游戏内容
if (this.questions.length > 0 && this.currentIndex < this.questions.length) {
Scroll() {
Column() {
this.buildProgressIndicator() // 进度指示器
this.buildQuestionContent() // 题目内容
this.buildOptions() // 选项按钮
this.buildHintButton() // 提示按钮
if (this.showHint) {
this.buildHintContent() // 提示内容
}
if (this.answered) {
this.buildNextButton() // 下一题按钮
}
Row().height(40) // 底部留白
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
} else {
// 加载中
Column() {
Text('加载题目中...')
.fontSize(16)
.fontColor(this.isDarkMode ? '#AAAAAA' : '#666666')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode ? '#121212' : '#F5F5F5')
}
4.5 构建导航栏
在 build() 方法之后添加导航栏构建器:
typescript
@Builder
buildNavigationBar() {
Row() {
// 返回按钮
Row() {
Text('←')
.fontSize(24)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
}
.width(40)
.height(40)
.justifyContent(FlexAlign.Center)
.onClick(() => {
router.back() // 返回上一页
})
// 标题
Text('古诗词填空')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 得分
Text(`${this.score}分`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#95E1D3')
.padding({ left: 12, right: 12 })
}
.width('100%')
.height(56)
.padding({ left: 8, right: 8 })
.backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
}
4.6 构建进度指示器
typescript
@Builder
buildProgressIndicator() {
Column() {
// 题目序号
Row() {
Text(`第 ${this.currentIndex + 1} / ${this.questions.length} 题`)
.fontSize(14)
.fontColor(this.isDarkMode ? '#AAAAAA' : '#666666')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 8 })
// 进度条
Stack() {
// 背景条
Row()
.width('100%')
.height(6)
.backgroundColor(this.isDarkMode ? '#2D2D2D' : '#E0E0E0')
.borderRadius(3)
// 进度条
Row()
.width(`${((this.currentIndex + 1) / this.questions.length) * 100}%`)
.height(6)
.backgroundColor('#95E1D3')
.borderRadius(3)
}
.width('100%')
.height(6)
}
.width('100%')
.margin({ bottom: 24 })
}
4.7 构建题目内容
typescript
@Builder
buildQuestionContent() {
if (this.currentIndex < this.questions.length) {
Column() {
// 提示文字
Text('请填入合适的家禽名称')
.fontSize(14)
.fontColor('#95E1D3')
.fontWeight(FontWeight.Medium)
.margin({ bottom: 16 })
// 题目内容
Text(this.questions[this.currentIndex].question)
.fontSize(18)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
.lineHeight(32)
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(24)
.backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
.borderRadius(16)
.margin({ bottom: 24 })
}
}
4.8 构建选项按钮(2x2 网格)
typescript
@Builder
buildOptions() {
if (this.currentIndex < this.questions.length &&
this.questions[this.currentIndex].options &&
this.questions[this.currentIndex].options!.length >= 4) {
// 2x2 网格布局
Column({ space: 12 }) {
// 第一行
Row({ space: 12 }) {
this.buildOptionButton(this.questions[this.currentIndex].options![0])
this.buildOptionButton(this.questions[this.currentIndex].options![1])
}
// 第二行
Row({ space: 12 }) {
this.buildOptionButton(this.questions[this.currentIndex].options![2])
this.buildOptionButton(this.questions[this.currentIndex].options![3])
}
}
.width('100%')
.margin({ bottom: 16 })
}
}
@Builder
buildOptionButton(option: string) {
Row() {
Text(option)
.fontSize(24)
.fontColor(this.getOptionTextColor(option))
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height(80)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.getOptionBackgroundColor(option))
.borderRadius(12)
.border({
width: 2,
color: this.getOptionBorderColor(option),
style: BorderStyle.Solid
})
.onClick(() => {
this.checkAnswer(option)
})
}
// 获取选项文字颜色
private getOptionTextColor(option: string): string {
const currentQuestion = this.getCurrentQuestion()
const correctAnswer = currentQuestion?.answer || ''
if (!this.answered) {
// 未答题:默认颜色
return this.isDarkMode ? '#FFFFFF' : '#212121'
}
if (option === correctAnswer) {
// 正确答案:白色
return '#FFFFFF'
}
if (option === this.selectedAnswer && !this.isCorrect) {
// 错误选择:白色
return '#FFFFFF'
}
// 其他选项:灰色
return this.isDarkMode ? '#AAAAAA' : '#666666'
}
// 获取选项背景颜色
private getOptionBackgroundColor(option: string): string {
const currentQuestion = this.getCurrentQuestion()
const correctAnswer = currentQuestion?.answer || ''
if (!this.answered) {
// 未答题:白色背景
return this.isDarkMode ? '#1E1E1E' : '#FFFFFF'
}
if (option === correctAnswer) {
// 正确答案:绿色
return '#4CAF50'
}
if (option === this.selectedAnswer && !this.isCorrect) {
// 错误选择:红色
return '#F44336'
}
return this.isDarkMode ? '#1E1E1E' : '#FFFFFF'
}
// 获取选项边框颜色
private getOptionBorderColor(option: string): string {
const currentQuestion = this.getCurrentQuestion()
const correctAnswer = currentQuestion?.answer || ''
if (!this.answered) {
return this.isDarkMode ? '#3D3D3D' : '#E0E0E0'
}
if (option === correctAnswer) {
return '#4CAF50'
}
if (option === this.selectedAnswer && !this.isCorrect) {
return '#F44336'
}
return this.isDarkMode ? '#3D3D3D' : '#E0E0E0'
}
4.9 构建提示按钮和内容
typescript
@Builder
buildHintButton() {
if (!this.answered) {
Button('💡 查看提示')
.width('100%')
.height(48)
.fontSize(16)
.fontColor('#FFA726')
.backgroundColor(this.isDarkMode ? '#2D2D2D' : '#FFF3E0')
.borderRadius(12)
.onClick(() => {
this.showHint = !this.showHint // 切换显示/隐藏
})
.margin({ bottom: 16 })
}
}
@Builder
buildHintContent() {
if (this.currentIndex < this.questions.length) {
Column() {
Row() {
Text('💡')
.fontSize(20)
.margin({ right: 8 })
Text('提示')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
}
.margin({ bottom: 12 })
Text(this.questions[this.currentIndex].hint)
.fontSize(14)
.fontColor(this.isDarkMode ? '#CCCCCC' : '#555555')
.lineHeight(22)
}
.width('100%')
.padding(16)
.backgroundColor(this.isDarkMode ? '#2D2D2D' : '#FFF3E0')
.borderRadius(12)
.margin({ bottom: 16 })
}
}
4.10 构建下一题按钮
typescript
@Builder
buildNextButton() {
Button(this.currentIndex < this.questions.length - 1 ? '下一题 →' : '查看结果')
.width('100%')
.height(56)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor('#95E1D3')
.borderRadius(12)
.onClick(() => {
this.nextQuestion()
})
}
4.11 保存文件
按 Ctrl + S 保存。至此,游戏页面已经完成!
第五步:配置页面路由
5.1 打开路由配置文件
在项目中找到并打开:
entry/src/main/resources/base/profile/main_pages.json
5.2 添加游戏页面路由
修改文件内容为:
json
{
"src": [
"pages/Index",
"pages/game/PoetryFillGame"
]
}
💡 注意:
- 第一个页面是应用首页
- 路径不包含
.ets后缀 - 路径相对于
entry/src/main/ets/
5.3 保存文件
按 Ctrl + S 保存。
第六步:修改首页,添加游戏入口
6.1 打开首页文件
找到并打开:
entry/src/main/ets/pages/Index.ets
6.2 替换全部内容
用以下代码替换文件的全部内容:
typescript
import { router } from '@kit.ArkUI'
@Entry
@Component
struct Index {
@State message: string = '古诗词填空游戏';
build() {
Column() {
// 标题
Text(this.message)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 40 })
// 开始游戏按钮
Button('开始游戏')
.fontSize(20)
.fontWeight(FontWeight.Medium)
.width(200)
.height(56)
.backgroundColor('#95E1D3')
.borderRadius(12)
.onClick(() => {
router.pushUrl({
url: 'pages/game/PoetryFillGame'
})
})
}
.height('100%')
.width('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#F5F5F5')
}
}
6.3 保存文件
按 Ctrl + S 保存。
第七步:编译和运行
7.1 检查文件结构
确保你创建了以下文件:
tiankong/
├── entry/src/main/ets/
│ ├── data/
│ │ ├── CultureData.ets ✅ 已创建
│ │ └── GameDataManager.ets ✅ 已创建
│ ├── pages/
│ │ ├── game/
│ │ │ └── PoetryFillGame.ets ✅ 已创建
│ │ └── Index.ets ✅ 已修改
│ └── ...
└── entry/src/main/resources/base/profile/
└── main_pages.json ✅ 已修改
7.2 编译项目
- 在 DevEco Studio 菜单栏,点击
Build→Build Hap(s) / APP(s)→Build Hap(s) - 等待编译完成,查看底部的 Build 窗口
编译成功标志:
BUILD SUCCESSFUL in 30s
常见错误:
| 错误 | 原因 | 解决方法 |
|---|---|---|
| Cannot find module | 导入路径错误 | 检查相对路径 ../../ |
| Property does not exist | 类型不匹配 | 检查接口定义 |
| Page not found | 路由未注册 | 检查 main_pages.json |
7.3 运行应用
- 连接 HarmonyOS 设备或启动模拟器
- 点击 DevEco Studio 顶部的绿色运行按钮 ▶️
- 等待应用安装并启动
第八步:测试游戏
测试清单
✅ 测试 1:启动应用
- 应显示首页,标题"古诗词填空游戏"
- 应显示"开始游戏"按钮(青绿色)
✅ 测试 2:进入游戏
- 点击"开始游戏"
- 应跳转到游戏页面
- 应显示第一道题目
✅ 测试 3:答题
- 应显示诗词标题、作者、朝代
- 应显示带填空的诗句(
____) - 应显示 4 个选项(2x2 网格)
- 点击选项后:
- 正确答案变绿色
- 错误选择变红色,正确答案也变绿色
- 显示"下一题"按钮
✅ 测试 4:提示功能
- 答题前点击"💡 查看提示"
- 应展开诗词解释
- 再次点击可收起
✅ 测试 5:进度显示
- 顶部应显示"第 X / 8 题"
- 进度条应随题目推进
- 右上角应显示当前得分
✅ 测试 6:游戏结束
- 答完 8 道题后应弹出对话框
- 显示得分和正确率
- 点击"再玩一次"应重新开始
- 点击"返回"应回到首页
第九步:添加更多诗词(可选)
目前我们只添加了 3 首诗词,你可以添加更多。
如何添加诗词
打开 CultureData.ets,在 getPoetryData() 方法的数组中添加:
typescript
{
id: 'poetry_goose_002',
category: 'poetry',
animalType: 'goose',
title: '题鹅',
author: '李商隐',
dynasty: '唐',
content: '眠沙卧水自成群,曲岸残阳极浦云。\n那解将心怜孔翠,羁雌长共故雄分。',
explanation: '诗人借鹅的成群而居、夫妻相守来表达人间的情感和世态。',
tags: ['李商隐', '咏物', '抒情'],
isFavorite: false
},
添加规则:
id必须唯一content中必须包含"鸡"、"鸭"或"鹅"- 用
\n表示换行 - 多首诗之间用逗号分隔
完整代码参考
CultureData.ets 完整版(17首诗词)
由于篇幅限制,完整的 CultureData.ets(包含17首诗词)可以从以下位置获取:
方法一: 从源项目复制
MyApplication8/entry/src/main/ets/data/CultureData.ets
方法二: 手动添加以下诗词到数组中
点击展开完整诗词列表(14首)
typescript
// 第4首:题鹅
{
id: 'poetry_goose_002',
category: 'poetry',
animalType: 'goose',
title: '题鹅',
author: '李商隐',
dynasty: '唐',
content: '眠沙卧水自成群,曲岸残阳极浦云。\n那解将心怜孔翠,羁雌长共故雄分。',
explanation: '诗人借鹅的成群而居、夫妻相守来表达人间的情感和世态。',
tags: ['李商隐', '咏物', '抒情'],
isFavorite: false
},
// 第5首:舟前小鹅儿
{
id: 'poetry_goose_003',
category: 'poetry',
animalType: 'goose',
title: '舟前小鹅儿',
author: '杜甫',
dynasty: '唐',
content: '鹅儿黄似酒,对酒爱新鹅。\n引颈嗔船逼,无行乱眼多。',
explanation: '杜甫描写小鹅的可爱形象,充满童趣。',
tags: ['杜甫', '咏物', '生动'],
isFavorite: false
},
// 第6首:鹅赠鹤
{
id: 'poetry_goose_004',
category: 'poetry',
animalType: 'goose',
title: '鹅赠鹤',
author: '白居易',
dynasty: '唐',
content: '君因风送入青云,我被人驱向鸭群。\n雪颈霜毛红网掌,请看何处不如君?',
explanation: '白居易借鹅鹤对比,讽刺了重名位轻实际的社会现象。',
tags: ['白居易', '寓言', '社会讽刺'],
isFavorite: false
},
// 第7首:鸡
{
id: 'poetry_chicken_002',
category: 'poetry',
animalType: 'chicken',
title: '鸡',
author: '崔道融',
dynasty: '唐',
content: '买得晨鸡共鸡语,常时不用等闲鸣。\n深山月黑风雨夜,欲近晓天啼一声。',
explanation: '诗人描写了公鸡司晨的职责,表现了公鸡的尽职尽责。',
tags: ['崔道融', '咏物', '职责'],
isFavorite: false
},
// 第8首:春晓
{
id: 'poetry_chicken_003',
category: 'poetry',
animalType: 'chicken',
title: '春晓',
author: '孟浩然',
dynasty: '唐',
content: '春眠不觉晓,处处闻啼鸟。\n夜来风雨声,花落知多少。',
explanation: '这首诗虽未直接写鸡,但"春眠不觉晓"的"晓"正是由鸡鸣报晓而来。',
tags: ['孟浩然', '春景', '经典名篇'],
isFavorite: false
},
// 第9首:鸡鸣
{
id: 'poetry_chicken_004',
category: 'poetry',
animalType: 'chicken',
title: '鸡鸣',
author: '诗经',
dynasty: '先秦',
content: '鸡既鸣矣,朝既盈矣。\n匪鸡则鸣,苍蝇之声。',
explanation: '这是《诗经》中描写鸡鸣的诗句,通过鸡鸣来表现时间的推移。',
tags: ['诗经', '古老', '朝政'],
isFavorite: false
},
// 第10首:风入松·寄柯敬仲
{
id: 'poetry_chicken_005',
category: 'poetry',
animalType: 'chicken',
title: '风入松·寄柯敬仲',
author: '虞集',
dynasty: '元',
content: '画堂红袖倚清酣,华发不胜簪。\n几回晚直金銮殿,东风软、花里停骖。\n书诏许传宫烛,香罗初试朝衫。\n御沟冰泮水挼蓝,飞燕语呢喃。\n重重帘幕卷轻寒,凭寄与、旧时相识。\n莫向春风笑我,花前须插红杉。',
explanation: '这首词虽未直接写鸡,但"晓"指鸡鸣时分,充满了对往昔的怀念。',
tags: ['虞集', '怀旧', '朝堂'],
isFavorite: false
},
// 第11首:春江晚景
{
id: 'poetry_duck_002',
category: 'poetry',
animalType: 'duck',
title: '春江晚景',
author: '张舜民',
dynasty: '宋',
content: '花映新林岸,云迎曲岛隈。\n江流添夜色,鸭宿鹭归来。',
explanation: '诗人描绘春江晚景,一幅宁静和谐的春江晚景图。',
tags: ['张舜民', '晚景', '和谐'],
isFavorite: false
},
// 第12首:江上
{
id: 'poetry_duck_003',
category: 'poetry',
animalType: 'duck',
title: '江上',
author: '王安石',
dynasty: '宋',
content: '江北秋阴一半开,晚云含雨却低徊。\n青山缭绕疑无路,忽见千帆隐映来。',
explanation: '虽然此诗未直接写鸭,但江上秋景常有群鸭嬉戏,富有诗意。',
tags: ['王安石', '江景', '秋色'],
isFavorite: false
},
// 第13首:春日
{
id: 'poetry_duck_004',
category: 'poetry',
animalType: 'duck',
title: '春日',
author: '朱熹',
dynasty: '宋',
content: '胜日寻芳泗水滨,无边光景一时新。\n等闲识得东风面,万紫千红总是春。',
explanation: '诗人在春天的美好日子里到泗水边寻找春天的气息,春江水暖,鸭子嬉戏。',
tags: ['朱熹', '春景', '哲理'],
isFavorite: false
},
// 第14首:游园不值
{
id: 'poetry_duck_005',
category: 'poetry',
animalType: 'duck',
title: '游园不值',
author: '叶绍翁',
dynasty: '宋',
content: '应怜屐齿印苍苔,小扣柴扉久不开。\n春色满园关不住,一枝红杏出墙来。',
explanation: '"春色满园关不住,一枝红杏出墙来"表达了春天的勃勃生机,园中池塘里必有鸭子嬉戏。',
tags: ['叶绍翁', '春游', '名句'],
isFavorite: false
}
常见问题解答
Q1:编译时提示"找不到模块"?
A: 检查导入路径:
typescript
// 正确:相对路径
import { CultureDatabase } from './CultureData'
import { GameDataManager } from '../../data/GameDataManager'
// 错误:绝对路径(不要用)
import { CultureDatabase } from 'entry/src/main/ets/data/CultureData'
Q2:运行时提示"页面未找到"?
A: 检查 main_pages.json 是否正确添加了路由:
json
{
"src": [
"pages/Index",
"pages/game/PoetryFillGame" // 确保这行存在
]
}
Q3:点击按钮没有反应?
A: 检查 onClick 事件是否正确绑定:
typescript
.onClick(() => {
this.checkAnswer(option) // 确保方法名正确
})
Q4:题目没有显示?
A:
- 检查
CultureData.ets中是否添加了诗词数据 - 打开 DevTools 查看控制台,看是否有错误信息
- 确认
aboutToAppear()方法被调用
Q5:如何调试?
A: 使用 console.info() 输出调试信息:
typescript
console.info('PoetryFillGame', `题目数量: ${this.questions.length}`)
console.info('PoetryFillGame', `当前答案: ${question.answer}`)
然后在 DevEco Studio 的 DevTools 中查看 Console 面板。
进阶扩展
扩展 1:添加音效
在答题正确/错误时播放音效:
typescript
import { media } from '@kit.MediaKit'
// 答题时
if (this.isCorrect) {
// 播放正确音效
media.createSoundPool().play(correctSoundId)
} else {
// 播放错误音效
media.createSoundPool().play(wrongSoundId)
}
扩展 2:添加动画效果
为按钮添加点击动画:
typescript
Button('开始游戏')
.animation({
duration: 300,
curve: Curve.EaseInOut
})
.scale(this.buttonPressed ? 0.95 : 1.0)
扩展 3:保存最高分
使用 Preferences 保存历史最高分:
typescript
import { preferences } from '@kit.ArkData'
// 保存最高分
const prefs = await preferences.getPreferences(getContext(), 'game_data')
await prefs.put('high_score', this.score)
// 读取最高分
const highScore = await prefs.get('high_score', 0)
扩展 4:添加难度选择
提供简单、中等、困难三种模式:
typescript
@State difficulty: 'easy' | 'medium' | 'hard' = 'medium'
// 根据难度调整题目数量
private getQuestionCount(): number {
switch (this.difficulty) {
case 'easy': return 5
case 'medium': return 8
case 'hard': return 12
}
}
总结
恭喜你!你已经完成了一个完整的诗词填空游戏。通过本教程,你学会了:
✅ 数据管理
- 定义数据结构(interface、type)
- 创建数据库类
- 使用静态方法提供数据
✅ 游戏逻辑
- 题目生成算法
- 答案验证
- 分数统计
- 随机打乱算法
✅ UI 开发
- ArkUI 组件使用
- 状态管理(@State)
- 条件渲染
- 事件处理
✅ 页面导航
- 路由配置
- 页面跳转
- 页面返回
下一步学习
- 学习更多 ArkUI 组件
- 探索动画和过渡效果
- 学习数据持久化(Preferences、数据库)
- 尝试网络请求获取诗词数据
项目文件总结
| 文件 | 行数 | 作用 |
|---|---|---|
| CultureData.ets | ~500 | 诗词数据库 |
| GameDataManager.ets | ~140 | 游戏数据管理 |
| PoetryFillGame.ets | ~380 | 游戏界面 |
| Index.ets | ~30 | 首页 |
| main_pages.json | ~6 | 路由配置 |
总计: 约 1050+ 行代码
附录
颜色参考
| 用途 | 颜色代码 | 示例 |
|---|---|---|
| 主题色 | #95E1D3 | 青绿色 |
| 正确 | #4CAF50 | 绿色 |
| 错误 | #F44336 | 红色 |
| 提示 | #FFA726 | 橙色 |
| 背景 | #F5F5F5 | 浅灰 |
快速命令
bash
# 创建目录
mkdir -p entry/src/main/ets/data
mkdir -p entry/src/main/ets/pages/game
# 查看文件
find entry/src/main/ets -name "*.ets"
# 查看行数
wc -l entry/src/main/ets/data/CultureData.ets
参考文档
本教程完成!
如有问题,欢迎查阅 HarmonyOS 官方文档或在社区提问。
祝你开发愉快!🎉