基于HarmonyOS的悬疑探案推理小游戏开发实战
项目演示



一、项目背景与概述
1.1 项目背景
随着移动互联网的快速发展,互动式游戏应用越来越受到用户的喜爱。悬疑探案推理类游戏以其独特的沉浸感和智力挑战性,在各类游戏应用中占据了重要的一席之地。本文将详细介绍如何基于HarmonyOS平台开发一款悬疑探案推理小游戏,通过ArkTS语言和ArkUI框架实现完整的游戏功能。
本项目的核心玩法是:玩家通过向AI助手提问来收集案件线索,逐步推理出凶手的身份。游戏采用聊天式交互界面,玩家可以自由提问,系统会根据问题匹配相关线索,并以结构化的JSON格式返回结果,包括问题、提示、推理结果、正确性分析和通关文字五个核心字段。
1.2 游戏玩法介绍
游戏开始时,玩家会看到一个完整的案件背景介绍,包括死者信息、死亡时间、现场情况和嫌疑人名单。玩家需要通过输入问题来收集线索,例如询问"纽扣是什么"、“王管家的情况”、"密室是怎么形成的"等。
每当玩家提出一个问题,系统会返回结构化的线索信息,包括:
- 问题:当前询问的问题是什么
- 提示列表:关于这个问题的三个提示信息
- 推理结果列表:基于线索可以得出的三个推理方向
- 正确性分析:对推理结果的正确性判断和分析
- 通关文字:当玩家正确指出凶手时显示的结案陈词
当玩家收集到足够的线索后,可以直接指出凶手是谁。如果回答正确,游戏会进入胜利页面,展示完整的案件真相和推理过程。
1.3 技术选型
本项目选用HarmonyOS作为开发平台,主要基于以下考虑:
- 跨设备能力:HarmonyOS支持多种设备形态,游戏可以在手机、平板等多种设备上运行
- 声明式UI:ArkUI框架采用声明式编程范式,代码简洁易读
- 高性能:方舟编译器提供了优秀的运行时性能
- TypeScript基础:ArkTS基于TypeScript,学习曲线平缓
开发语言选用ArkTS,这是HarmonyOS的主要开发语言,在TypeScript的基础上增加了静态类型检查和UI描述能力。
二、项目架构与文件结构
2.1 整体架构设计
本项目采用经典的MVC(Model-View-Controller)架构变体,将数据模型、界面展示和业务逻辑清晰分离:
- 数据模型层(Model):定义案件数据结构,存储线索、嫌疑人、案件背景等信息
- 服务层(Service):处理用户输入匹配、生成响应数据等业务逻辑
- 视图层(View):负责UI渲染和用户交互,包括案件介绍页、游戏进行页和胜利页
2.2 文件结构
项目的核心文件结构如下:
entry/src/main/ets/
├── MysteryCase.ets # 案件数据模型定义和案件数据
├── AIChatService.ets # AI聊天服务,处理用户输入和返回响应
└── pages/
└── Index.ets # 主页面,包含游戏的三个状态界面
三个核心文件各司其职:
- MysteryCase.ets:定义了
ClueResponse、ClueNode、FinalAccusation、MysteryCase四个数据类,并初始化了一个完整的悬疑案件数据 - AIChatService.ets:实现了单例模式的AI聊天服务,提供关键字匹配、JSON响应生成等功能
- Index.ets:游戏主界面,包含三个游戏状态(介绍、进行、胜利)的完整UI和交互逻辑
2.3 数据流向
游戏的数据流向清晰明了:
- 用户在TextInput中输入问题
- 点击发送按钮触发sendQuestion方法
- 调用AIChatService的getResponse方法处理用户输入
- AIChatService通过关键字匹配找到对应的线索
- 将线索数据序列化为JSON字符串返回
- 页面解析JSON并更新currentResponse状态
- 界面根据状态变化重新渲染,展示新的线索信息
三、核心数据模型设计
3.1 数据模型概述
在悬疑探案游戏中,数据模型是整个游戏的骨架。我们需要精心设计数据结构,以支持线索查询、推理验证和最终破案等核心功能。
本项目定义了四个核心数据类,它们之间的关系如下:
MysteryCase:代表一个完整的案件,包含标题、背景、嫌疑人列表、线索列表和最终指控ClueNode:代表一个线索节点,包含匹配关键字和对应的响应数据FinalAccusation:代表最终指控,包含触发关键字和胜利响应ClueResponse:代表标准响应格式,包含问题、提示、推理结果、正确性分析和通关文字五个字段
3.2 ClueResponse类详解
ClueResponse是最核心的数据结构,它定义了系统返回给用户的标准响应格式。无论用户询问什么问题,系统都会返回统一格式的JSON数据,包含五个字段。
类的定义如下:
export class ClueResponse {
question: string = ''
hints: string[] = []
reasoningResults: string[] = []
correctnessAnalysis: string = ''
victoryText: string = ''
constructor(question: string, hints: string[], reasoningResults: string[], correctnessAnalysis: string, victoryText: string) {
this.question = question
this.hints = hints
this.reasoningResults = reasoningResults
this.correctnessAnalysis = correctnessAnalysis
this.victoryText = victoryText
}
}
五个字段的含义如下:
- question:当前回答的问题是什么。这个字段可以让玩家清楚地知道当前讨论的话题。
- hints:提示列表,通常包含3条提示信息。这些提示是引导玩家思考的阶梯,不会直接给出答案,但会指明思考方向。
- reasoningResults:推理结果列表,通常包含3条可能的推理方向。这些是基于当前线索可以得出的初步结论,但不一定全部正确。
- correctnessAnalysis:正确性分析,对推理结果的整体判断和详细分析。这个字段会告诉玩家哪些推理是正确的,为什么正确。
- victoryText:通关文字,只有当玩家正确指出凶手时才会有内容。这个字段包含完整的案件真相和结局。
3.3 ClueNode类详解
ClueNode类代表一个线索节点,它将用户可能的提问关键词与对应的响应数据关联起来。
类的定义如下:
export class ClueNode {
keywords: string[] = []
response: ClueResponse = new ClueResponse('', [], [], '', '')
constructor(keywords: string[], response: ClueResponse) {
this.keywords = keywords
this.response = response
}
}
- keywords:关键字数组,包含多个可能触发这条线索的关键词。当用户的问题中包含任何一个关键词时,就会返回对应的响应。
- response:对应的线索响应数据,是一个ClueResponse对象。
这种设计的好处是,同一个线索可以通过多个不同的提问方式触发,提高了游戏的容错性和用户体验。例如,关于"纽扣"的线索,可以通过"纽扣"、“银色纽扣”、"扣子"等不同的提问方式触发。
3.4 MysteryCase类详解
MysteryCase类代表一个完整的案件,是游戏的核心数据容器。
类的定义如下:
export class MysteryCase {
id: string = ''
title: string = ''
background: string = ''
suspects: string[] = []
clues: ClueNode[] = []
finalAccusation: FinalAccusation = new FinalAccusation([], new ClueResponse('', [], [], '', ''))
solved: boolean = false
constructor(id: string, title: string, background: string, suspects: string[], clues: ClueNode[], finalAccusation: FinalAccusation) {
this.id = id
this.title = title
this.background = background
this.suspects = suspects
this.clues = clues
this.finalAccusation = finalAccusation
this.solved = false
}
}
各字段的含义:
- id:案件的唯一标识符
- title:案件标题,会显示在游戏界面顶部
- background:案件背景介绍,包含死者信息、死亡时间、现场情况等
- suspects:嫌疑人名单数组
- clues:线索节点数组,包含所有可探索的线索
- finalAccusation:最终指控配置,用于判断玩家是否正确指出凶手
- solved:案件是否已破获的状态标志
3.5 案件数据设计
本项目预置了一个完整的悬疑案件——“午夜书房谋杀案”。案件讲述了富商李明远在自己的书房中被发现死亡的故事,涉及三名嫌疑人:
- 王管家:家中任职20年的老管家
- 李夫人:死者的第二任妻子,年轻貌美
- 张律师:死者的私人律师,负责处理遗产事宜
案件设计了9条可探索的线索:
- 纽扣线索:死者手中紧握着的银色纽扣
- 茶杯线索:桌上茶杯上只有死者的指纹
- 青铜摆件线索:带血的凶器上没有指纹
- 监控线索:走廊监控显示的进出记录
- 王管家线索:王管家的时间线和背景
- 李夫人线索:李夫人的时间线和背景
- 张律师线索:张律师的时间线和背景
- 遗产线索:遗产和遗嘱的相关情况
- 密室线索:书房密室是如何形成的
每条线索都设计了层层递进的提示和推理,引导玩家逐步接近真相。
四、AI聊天服务实现
4.1 服务架构设计
AIChatService是游戏的核心业务逻辑层,负责处理用户输入、匹配线索、生成响应等功能。服务采用单例模式设计,确保整个应用只有一个服务实例。
单例模式的实现:
export class AIChatService {
private static instance: AIChatService | null = null
constructor() {}
static getInstance(): AIChatService {
if (!AIChatService.instance) {
AIChatService.instance = new AIChatService()
}
return AIChatService.instance
}
}
export const aiChatService = AIChatService.getInstance()
单例模式的优点:
- 全局唯一实例:确保整个应用中只有一个AI服务实例,避免重复初始化
- 状态一致性:所有页面共享同一个服务实例,案件状态保持一致
- 资源节省:避免创建多个实例造成的资源浪费
4.2 关键字匹配算法
游戏的核心交互机制是用户提问,系统通过关键字匹配来返回对应的线索。匹配算法的设计直接影响游戏的体验。
匹配逻辑分为两个层次:
- 最终指控匹配:优先检查用户是否在指出凶手。如果匹配成功,直接返回胜利响应。
- 普通线索匹配:遍历所有线索节点,检查用户问题中是否包含某个线索的关键词。
匹配算法的实现:
private matchClue(userInput: string): ClueResponse | null {
const lowerInput = userInput.toLowerCase()
for (let i = 0; i < currentCase.clues.length; i++) {
const clue = currentCase.clues[i]
for (let j = 0; j < clue.keywords.length; j++) {
const keyword = clue.keywords[j]
if (lowerInput.includes(keyword.toLowerCase())) {
return clue.response
}
}
}
return null
}
算法的特点:
- 不区分大小写:将用户输入和关键词都转为小写进行比较,提高匹配率
- 子串匹配:使用includes方法进行子串匹配,用户不需要精确输入完整关键词
- 顺序匹配:按线索数组的顺序进行匹配,先定义的线索优先级更高
- 首个匹配:找到第一个匹配的线索就返回,不继续查找后续线索
4.3 响应生成机制
系统的响应生成遵循以下流程:
- 首先检查是否触发最终指控
- 如果是,设置案件已破获状态,返回胜利响应
- 否则,检查是否匹配普通线索
- 如果匹配,返回对应的线索响应
- 如果都不匹配,返回默认响应
响应生成的核心代码:
getResponse(userInput: string): string {
const accusationResponse = this.matchAccusation(userInput)
if (accusationResponse) {
currentCase.solved = true
return JSON.stringify(accusationResponse)
}
const clueResponse = this.matchClue(userInput)
if (clueResponse) {
return JSON.stringify(clueResponse)
}
const defaultResponse = new ClueResponse(
userInput,
['这个问题目前还没有相关指纹', '请尝试验收其他方面的问题', '可以问问各方面的问题'],
['当前信息不足以确定这个问题', '需要更多的指纹来支撑推理', '可能需要重新考虑现有的所有指纹'],
'您的问题当前还无法确定回答,请先探索其他指纹。可以问问指纹、茶杯、遗产等方面的问题。',
''
)
return JSON.stringify(defaultResponse)
}
所有响应都通过JSON.stringify()序列化为JSON字符串返回,确保数据格式的一致性。
4.4 JSON数据格式规范
系统返回的所有响应都遵循统一的JSON格式,包含五个字段。这种设计的优点:
- 前后端解耦:UI层只需要按照固定格式解析数据,不需要知道数据来源
- 易于扩展:如果未来接入真实的AI大模型,只需要保证返回格式一致
- 类型安全:通过ClueResponse类定义数据结构,确保数据完整性
- 调试方便:统一的格式使得日志记录和问题排查更加简单
标准响应示例:
{
"question": "死者手中的银色纽扣是什么?",
"hints": [
"这是一条银色的装饰怪按纹",
"纹路较大,适用于大裙或服装",
"可能来自进出时所穿的装饰"
],
"reasoningResults": [
"死者在最后一刻抓住了某个人的装饰",
"这可能是抢劫时产生的线索",
"按纹的来源可能指向某个有特定状态的人"
],
"correctnessAnalysis": "正确。银色装饰怪按纹通常用于特殊职业或高等服装上。",
"victoryText": ""
}
五、游戏界面开发
5.1 页面状态机设计
游戏界面采用状态机模式设计,定义了三种游戏状态:
- intro(介绍状态):游戏开始前的案件介绍页面
- playing(进行状态):游戏进行中的主交互页面
- victory(胜利状态):破案成功后的胜利页面
状态切换的逻辑:
- 初始状态为intro,玩家看到案件介绍
- 点击"开始探案"按钮,状态切换为playing
- 玩家正确指出凶手后,状态切换为victory
- 点击"重新开始"按钮,状态回到intro
状态管理通过@State装饰器实现:
@State gameState: string = 'intro'
在build方法中,根据gameState的值渲染不同的UI组件:
build() {
Column() {
if (this.gameState === 'intro') {
this.BuildIntro()
} else if (this.gameState === 'playing') {
this.BuildGame()
} else if (this.gameState === 'victory') {
this.BuildVictory()
}
}
}
5.2 案件介绍页实现
案件介绍页是玩家看到的第一个界面,其目标是让玩家快速了解案件背景,激发探案兴趣。
页面布局采用垂直Column布局,从上到下依次是:
- 案件标题
- 游戏副标题
- 案件背景介绍(可滚动)
- 游戏说明文字
- 开始探案按钮
关键代码片段:
@Builder
BuildIntro() {
Column() {
Text(this.aiService.getCaseTitle())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#e94560')
.margin({ bottom: 20 })
Text('悬疑探案推理小游戏')
.fontSize(18)
.fontColor('#a0a0a0')
.margin({ bottom: 30 })
Scroll() {
Text(this.aiService.getCaseBackground())
.fontSize(16)
.fontColor('#ffffff')
.lineHeight(28)
.textAlign(TextAlign.Start)
}
.width('90%')
.height(300)
.backgroundColor('#16213e')
.borderRadius(12)
.padding(20)
.margin({ bottom: 30 })
Button('开始探案')
.width(200)
.height(50)
.backgroundColor('#e94560')
.onClick(() => {
this.startGame()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
界面设计采用深色悬疑风格:
- 主背景色:#1a1a2e(深蓝紫色)
- 卡片背景色:#16213e(深蓝色)
- 强调色:#e94560(玫红色)
- 文字主色:#ffffff(白色)
- 文字辅助色:#a0a0a0(灰色)
5.3 游戏进行页实现
游戏进行页是游戏的核心交互界面,采用聊天式设计,玩家可以像与侦探助手对话一样进行探案。
页面结构分为三个区域:
- 顶部标题区:显示案件标题
- 聊天内容区:展示对话历史和当前线索详情(可滚动)
- 底部输入区:输入框和发送按钮
聊天内容区是最复杂的部分,包含:
- 用户消息气泡(右对齐,深蓝色背景)
- AI消息气泡(左对齐,稍浅色背景)
- 当前线索详情卡片,包含:
- 当前问题显示
- 查看提示按钮和提示列表
- 查看推理结果按钮和推理结果列表
- 查看分析按钮和分析内容
关键实现要点:
消息气泡的区分显示:
ForEach(this.messages, (msg: Message) => {
if (msg.type === 'user') {
Row() {
Text(msg.content)
.fontSize(15)
.fontColor('#ffffff')
.backgroundColor('#0f3460')
.padding(12)
.borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
}
.width('100%')
.justifyContent(FlexAlign.End)
} else {
Column() {
Text(msg.content)
.fontSize(15)
.fontColor('#ffffff')
.backgroundColor('#16213e')
.padding(12)
.borderRadius({ topLeft: 16, topRight: 4, bottomLeft: 16, bottomRight: 16 })
.width('100%')
}
.width('100%')
}
})
线索详情的折叠展开:
通过三个布尔状态变量控制提示、推理结果和分析的显示隐藏:
@State showHints: boolean = false
@State showResults: boolean = false
@State showAnalysis: boolean = false
点击按钮时切换状态:
Button('查看提示')
.onClick(() => {
this.showHints = !this.showHints
})
使用if条件渲染决定是否显示内容:
if (this.showHints) {
Column() {
Text('提示:')
ForEach(this.currentResponse.hints, (hint: string) => {
Text(hint)
})
}
}
底部输入框布局:
底部输入区域固定在页面底部,包含一个TextInput和一个Button:
Row() {
TextInput({ placeholder: '输入你的问题...', text: this.inputText })
.width('70%')
.height(45)
.backgroundColor('#16213e')
.onChange((value: string) => {
this.inputText = value
})
Button('发送')
.width(80)
.height(45)
.backgroundColor('#e94560')
.onClick(() => {
this.sendQuestion()
})
}
.width('100%')
.padding(15)
.backgroundColor('#0f3460')
使用layoutWeight(1)让聊天内容区自动填充剩余空间:
Scroll() {
// 聊天内容
}
.layoutWeight(1)
.width('100%')
5.4 胜利页面实现
胜利页面是玩家成功破案后看到的界面,展示完整的案件真相和推理过程,给玩家成就感和满足感。
页面布局:
- 恭喜破案标题
- 案件真相区域(可滚动)
- 推理过程列表
- 重新开始按钮
关键代码:
@Builder
BuildVictory() {
Column() {
Text('恭喜破案!')
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#4ade80')
.margin({ bottom: 20 })
Scroll() {
Column() {
Text('案件真相:')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#e94560')
if (this.currentResponse) {
Text(this.currentResponse.victoryText)
.fontSize(16)
.fontColor('#ffffff')
.lineHeight(30)
}
Text('推理过程:')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#60a5fa')
.margin({ top: 20, bottom: 15 })
if (this.currentResponse) {
ForEach(this.currentResponse.reasoningResults, (result: string) => {
Text(result)
.fontSize(15)
.fontColor('#a0a0a0')
.margin({ bottom: 8 })
})
}
}
}
.width('90%')
.height(350)
Button('重新开始')
.onClick(() => {
this.restartGame()
})
}
.justifyContent(FlexAlign.Center)
}
5.5 消息发送与处理
用户发送消息的处理流程是游戏交互的核心。sendQuestion方法负责处理完整的消息发送和响应流程:
sendQuestion() {
if (this.inputText.trim() === '') {
return
}
const userMsg: string = this.inputText.trim()
this.messages.push(new Message('user', userMsg))
this.inputText = ''
const response: string = this.aiService.getResponse(userMsg)
let parsedResponse: ClueResponse | null = null
try {
const parsed: ClueResponse = JSON.parse(response)
parsedResponse = parsed
} catch (e) {
parsedResponse = null
}
this.currentResponse = parsedResponse
if (parsedResponse && parsedResponse.victoryText !== '') {
this.messages.push(new Message('ai', '恭喜你找到了真相!'))
setTimeout(() => {
this.gameState = 'victory'
}, 500)
} else {
this.messages.push(new Message('ai', '我已经为你整理了相关线索,请查看下方信息。'))
this.showHints = false
this.showResults = false
this.showAnalysis = false
}
}
处理流程详解:
- 输入验证:检查用户输入是否为空,为空则直接返回
- 添加用户消息:将用户的问题添加到消息列表
- 清空输入框:清空TextInput的内容
- 调用AI服务:获取AI的响应JSON字符串
- 解析JSON:尝试解析响应,失败则设为null
- 更新当前响应:将解析后的数据赋值给currentResponse
- 判断是否胜利:检查victoryText是否非空
- 如果非空:添加胜利消息,延迟500ms后切换到胜利页面
- 如果为空:添加普通回复消息,重置所有折叠状态
六、HarmonyOS API详解(24个核心API)
在本项目的开发过程中,我们使用了大量HarmonyOS ArkUI框架的API。本节将详细介绍其中24个核心API的使用方法和应用场景。
6.1 组件装饰器API
API 1:@Entry装饰器
作用:标记自定义组件为页面的入口组件,即页面的根节点。一个页面有且仅有一个@Entry装饰的组件。
使用示例:
@Entry
@Component
struct Index {
build() {
Column() {
Text('Hello World')
}
}
}
在本项目中的应用:Index组件被@Entry装饰,是游戏页面的入口。系统加载页面时会首先渲染被@Entry标记的组件。
注意事项:
- 每个页面只能有一个@Entry装饰的组件
- @Entry必须与@Component配合使用
- @Entry装饰的组件的build方法返回的内容会被渲染到页面根节点
API 2:@Component装饰器
作用:标记一个结构体为自定义组件,使其具有组件化能力,可以在build方法中描述UI结构。
使用示例:
@Component
struct MyComponent {
build() {
Text('我是自定义组件')
}
}
在本项目中的应用:Index结构体被@Component装饰,成为一个可复用的UI组件。@Component装饰器赋予了结构体描述UI的能力。
注意事项:
- 被@Component装饰的结构体必须实现build方法
- 组件名必须以大写字母开头
- 组件内部可以包含状态变量和自定义方法
API 3:@State装饰器
作用:定义组件的内部状态变量。当@State装饰的变量发生变化时,系统会重新执行build方法更新UI。
使用示例:
@Component
struct Counter {
@State count: number = 0
build() {
Column() {
Text('计数:' + this.count)
Button('增加')
.onClick(() => {
this.count++
})
}
}
}
在本项目中的应用:游戏中大量使用@State管理状态:
- gameState:控制当前显示哪个游戏页面
- inputText:绑定输入框的文本内容
- messages:存储聊天消息列表
- currentResponse:当前显示的线索响应
- showHints、showResults、showAnalysis:控制线索详情的展开收起
工作原理:
- @State变量是组件的私有状态,只能在组件内部访问
- 当@State变量被修改时,ArkUI框架会检测到变化并触发UI更新
- 只有被@State变量变化影响的UI部分才会重新渲染,提高性能
API 4:@Builder装饰器
作用:将一个方法标记为UI构建函数,使其可以在build方法或其他@Builder方法中被调用,用于复用UI描述。
使用示例:
@Component
struct MyComponent {
@State title: string = '标题'
@Builder
MyHeader(title: string) {
Column() {
Text(title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Divider()
}
.width('100%')
}
build() {
Column() {
this.MyHeader(this.title)
Text('正文内容')
}
}
}
在本项目中的应用:游戏的三个页面状态都通过@Builder方法构建:
- BuildIntro():构建案件介绍页
- BuildGame():构建游戏进行页
- BuildVictory():构建胜利页面
这种方式的好处是:
- 代码结构清晰,每个页面的UI逻辑独立
- 便于维护和修改
- 可以传递参数实现动态UI
6.2 基础组件API
API 5:Text组件
作用:显示一段文本内容,是最基础的展示组件。
常用属性:
- fontSize:字体大小
- fontColor:字体颜色
- fontWeight:字体粗细
- textAlign:文本对齐方式
- lineHeight:行高
- maxLines:最大行数
- textOverflow:文本溢出时的处理方式
使用示例:
Text('这是一段文本')
.fontSize(16)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.lineHeight(24)
在本项目中的应用:Text组件贯穿整个游戏界面,用于显示案件标题、消息内容、提示文字、推理结果等所有文本信息。
高级用法:
- 支持字符串拼接:Text(‘当前问题:’ + this.currentResponse.question)
- 支持响应式数据:数据变化时文本自动更新
- 支持多种文本对齐方式:Start、Center、End
API 6:Button组件
作用:按钮组件,用户可以点击触发交互操作。
常用属性:
- width/height:按钮尺寸
- backgroundColor:背景颜色
- borderRadius:圆角
- fontSize/fontColor:文字样式
- onClick:点击事件回调
使用示例:
Button('点击我')
.width(100)
.height(40)
.backgroundColor('#007dff')
.borderRadius(20)
.onClick(() => {
console.log('按钮被点击了')
})
在本项目中的应用:游戏中有多个按钮:
- "开始探案"按钮:进入游戏
- "查看提示"按钮:展开/收起提示
- "查看推理结果"按钮:展开/收起推理结果
- "查看分析"按钮:展开/收起分析
- "发送"按钮:发送用户问题
- "重新开始"按钮:重新开始游戏
按钮是玩家与游戏交互的主要入口,每个按钮都通过onClick事件绑定了对应的处理方法。
API 7:TextInput组件
作用:文本输入框组件,允许用户输入文本内容。
常用属性:
- placeholder:占位提示文字
- placeholderColor:占位文字颜色
- fontColor:输入文字颜色
- fontSize:输入文字大小
- backgroundColor:背景颜色
- borderRadius:圆角
常用事件:
- onChange:输入内容变化时触发
- onSubmit:提交时触发(按回车键)
- onFocus:获得焦点时触发
- onBlur:失去焦点时触发
使用示例:
TextInput({ placeholder: '请输入用户名', text: this.username })
.width('80%')
.height(48)
.backgroundColor('#f5f5f5')
.placeholderColor('#999999')
.onChange((value: string) => {
this.username = value
})
在本项目中的应用:游戏底部的输入框使用TextInput组件,玩家在这里输入问题进行提问。输入框的text属性与this.inputText双向绑定,onChange事件实时更新状态变量。
API 8:Scroll组件
作用:可滚动容器组件,当内容超出容器范围时,用户可以滑动查看全部内容。
常用属性:
- scrollable:滚动方向(Vertical/Horizontal/Free/None)
- scrollBar:滚动条是否显示
- edgeEffect:边缘效果
使用示例:
Scroll() {
Column() {
ForEach(this.items, (item: string) => {
Text(item)
.height(50)
.width('100%')
})
}
.width('100%')
}
.height(300)
.scrollable(ScrollDirection.Vertical)
在本项目中的应用:游戏中有多处使用Scroll:
- 案件介绍页的背景介绍区域
- 游戏进行页的聊天内容区
- 胜利页面的真相展示区域
这些区域的内容可能超出屏幕高度,需要Scroll支持垂直滚动查看。
6.3 布局组件API
API 9:Column组件
作用:垂直线性布局容器,子组件沿垂直方向排列。
常用属性:
- width/height:容器尺寸
- justifyContent:主轴(垂直方向)对齐方式
- alignItems:交叉轴(水平方向)对齐方式
- padding:内边距
- margin:外边距
使用示例:
Column() {
Text('第一行')
Text('第二行')
Text('第三行')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
在本项目中的应用:Column是游戏中最常用的布局组件,几乎所有页面都以Column作为根布局容器。消息气泡、线索卡片、提示列表等也都使用Column进行垂直排列。
API 10:Row组件
作用:水平线性布局容器,子组件沿水平方向排列。
常用属性:
- width/height:容器尺寸
- justifyContent:主轴(水平方向)对齐方式
- alignItems:交叉轴(垂直方向)对齐方式
- padding:内边距
- margin:外边距
使用示例:
Row() {
Text('左')
Text('中')
Text('右')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.SpaceBetween)
在本项目中的应用:Row主要用于以下场景:
- 底部输入区域:输入框和发送按钮水平排列
- 用户消息气泡包装:使用Row的justifyContent(FlexAlign.End)实现右对齐
- 一些需要水平排列的按钮组
API 11:ForEach组件
作用:基于数组数据循环渲染子组件,实现列表式布局。
语法:
ForEach(array, (item, index?) => {
// 子组件描述
}, keyGenerator?)
使用示例:
@State fruitList: string[] = ['苹果', '香蕉', '橙子']
build() {
Column() {
ForEach(this.fruitList, (item: string) => {
Text(item)
.height(40)
.width('100%')
})
}
}
在本项目中的应用:ForEach在游戏中广泛用于列表渲染:
- 聊天消息列表:遍历messages数组渲染每条消息
- 提示列表:遍历hints数组渲染每条提示
- 推理结果列表:遍历reasoningResults数组渲染每条推理
- 嫌疑人列表(如果需要的话)
ForEach是实现动态列表的核心API,数据变化时列表会自动更新。
API 12:if条件渲染
作用:根据条件决定是否渲染某个组件,实现UI的动态显示和隐藏。
使用示例:
@State isVisible: boolean = true
build() {
Column() {
if (this.isVisible) {
Text('我是可见的')
}
Button('切换显示')
.onClick(() => {
this.isVisible = !this.isVisible
})
}
}
在本项目中的应用:if条件渲染在游戏中大量使用:
- 根据gameState渲染不同的游戏页面
- 根据showHints决定是否显示提示列表
- 根据showResults决定是否显示推理结果
- 根据showAnalysis决定是否显示分析内容
- 根据currentResponse是否存在决定是否显示线索卡片
条件渲染是实现动态UI的重要手段,配合@State状态变量可以实现丰富的交互效果。
6.4 属性样式API
API 13:width() / height()
作用:设置组件的宽度和高度。
支持的取值:
- 数值:直接指定像素值,如width(100)
- 百分比:相对于父组件的比例,如width(‘50%’)
- 特殊值:如width(‘100%’)表示铺满父容器宽度
使用示例:
Text('固定尺寸')
.width(100)
.height(50)
Text('百分比尺寸')
.width('80%')
.height(40)
在本项目中的应用:几乎所有组件都使用了width和height属性来控制尺寸。页面根容器使用width(‘100%’)和height(‘100%’)铺满整个屏幕,按钮、输入框等组件则设置具体的像素尺寸。
API 14:backgroundColor()
作用:设置组件的背景颜色。
支持的颜色格式:
- 十六进制颜色:如’#ffffff’、‘#1a1a2e’
- 颜色名:如Color.Red、Color.Blue(需要导入Color枚举)
- rgb/rgba格式:如’rgb(255, 0, 0)’
使用示例:
Column() {
Text('红色背景')
}
.width(100)
.height(100)
.backgroundColor('#ff0000')
在本项目中的应用:游戏的深色悬疑风格主要通过backgroundColor实现:
- 页面背景:#1a1a2e(深紫蓝色)
- 卡片背景:#16213e(深蓝色)
- 用户消息:#0f3460(亮蓝色)
- 强调按钮:#e94560(玫红色)
精心设计的配色方案营造出了浓厚的悬疑探案氛围。
API 15:borderRadius()
作用:设置组件的圆角大小。
支持的取值:
- 单个数值:四个角使用相同的圆角,如borderRadius(12)
- 对象:分别指定四个角的圆角,如borderRadius({ topLeft: 4, topRight: 16, bottomLeft: 16, bottomRight: 16 })
使用示例:
// 统一圆角
Text('圆角矩形')
.width(100)
.height(40)
.backgroundColor('#007dff')
.borderRadius(20)
// 分别设置每个角
Text('不规则圆角')
.width(100)
.height(40)
.backgroundColor('#007dff')
.borderRadius({ topLeft: 4, topRight: 20, bottomLeft: 20, bottomRight: 4 })
在本项目中的应用:游戏中大量使用圆角设计:
- 卡片容器:borderRadius(12),柔和的圆角
- 按钮:borderRadius(25),胶囊形按钮
- 消息气泡:不同的角有不同的圆角,模拟聊天气泡效果
用户消息气泡右上角是尖角(4px),其他角是圆角(16px);AI消息气泡左上角是尖角,其他角是圆角。这种设计让聊天界面更加生动自然。
API 16:padding()
作用:设置组件的内边距,即内容与组件边缘之间的距离。
支持的取值:
- 单个数值:四个方向使用相同的内边距,如padding(10)
- 对象:分别指定四个方向的内边距,如padding({ top: 10, right: 20, bottom: 10, left: 20 })
- 也可以单独设置:paddingTop、paddingRight、paddingBottom、paddingLeft
使用示例:
Text('有内边距的文本')
.backgroundColor('#f0f0f0')
.padding(20)
在本项目中的应用:padding用于控制组件内容与边缘的距离,例如:
- 文本气泡:padding(12),让文字不要贴边
- 卡片容器:padding(15)或padding(20),留出呼吸空间
- 输入框:padding({ left: 20 }),文字不要贴着左边框
合理的内边距可以提升界面的可读性和美观度。
API 17:margin()
作用:设置组件的外边距,即组件与其他组件之间的距离。
支持的取值:与padding类似
- 单个数值:margin(10)
- 对象:margin({ top: 10, bottom: 20 })
- 单独设置:marginTop、marginRight、marginBottom、marginLeft
使用示例:
Column() {
Text('第一个')
.margin({ bottom: 20 })
Text('第二个')
}
在本项目中的应用:margin用于控制组件之间的间距,构建合理的视觉层次:
- 标题与正文之间:margin({ bottom: 30 })
- 消息之间:margin({ bottom: 10 })
- 卡片与其他元素之间:margin({ top: 10, bottom: 10 })
通过精确控制外边距,可以营造出清晰的视觉节奏和空间感。
API 18:fontSize() / fontColor() / fontWeight()
作用:设置文本的字体样式,包括大小、颜色和粗细。
fontSize:设置字体大小,单位为vp(虚拟像素)
fontColor:设置字体颜色,支持与backgroundColor相同的格式
fontWeight:设置字体粗细,取值如FontWeight.Normal、FontWeight.Bold、FontWeight.Bolder等
使用示例:
Text('标题文字')
.fontSize(24)
.fontColor('#333333')
.fontWeight(FontWeight.Bold)
在本项目中的应用:游戏中通过不同的字体样式区分信息层级:
- 页面主标题:fontSize(32),fontWeight.Bold,强调色
- 卡片标题:fontSize(16),fontWeight.Bold,主题色
- 正文内容:fontSize(15)或fontSize(16),白色
- 辅助文字:fontSize(14),灰色
清晰的字体层级是良好UI设计的基础。
6.5 事件处理API
API 19:onClick()
作用:绑定组件的点击事件,当用户点击组件时触发回调函数。
使用示例:
Button('点击按钮')
.onClick(() => {
console.log('按钮被点击了')
})
在本项目中的应用:onClick是游戏中最主要的交互方式,所有按钮的点击都通过onClick处理:
- "开始探案"按钮:触发startGame方法
- "查看提示"按钮:切换showHints状态
- "查看推理结果"按钮:切换showResults状态
- "查看分析"按钮:切换showAnalysis状态
- "发送"按钮:触发sendQuestion方法
- "重新开始"按钮:触发restartGame方法
每次点击都会改变组件的状态,进而触发UI更新。
API 20:onChange()
作用:绑定输入组件的内容变化事件,当输入内容改变时触发回调。
使用示例:
TextInput({ placeholder: '请输入内容', text: this.inputText })
.onChange((value: string) => {
this.inputText = value
})
在本项目中的应用:游戏底部的TextInput使用onChange事件实时同步用户输入的内容到inputText状态变量。这种双向绑定确保了状态和UI的一致性。
工作原理:
- 用户在输入框中输入文字
- onChange事件触发,回调函数接收到新的输入值
- 将新值赋值给@State变量
- @State变量的变化触发UI更新
- TextInput的text属性反映最新的值
6.6 布局能力API
API 21:justifyContent()
作用:设置弹性布局容器中子组件在主轴方向上的对齐方式。
对于Column(主轴为垂直方向):
- FlexAlign.Start:顶部对齐
- FlexAlign.Center:垂直居中
- FlexAlign.End:底部对齐
- FlexAlign.SpaceBetween:两端对齐,中间均匀分布
- FlexAlign.SpaceAround:每个元素周围空间相等
对于Row(主轴为水平方向):
- FlexAlign.Start:左对齐
- FlexAlign.Center:水平居中
- FlexAlign.End:右对齐
- FlexAlign.SpaceBetween:两端对齐
- FlexAlign.SpaceAround:每个元素周围空间相等
使用示例:
Column() {
Text('垂直居中的文本')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
在本项目中的应用:
- 案件介绍页:justifyContent(FlexAlign.Center)让内容垂直居中
- 用户消息气泡:justifyContent(FlexAlign.End)让消息靠右显示
- 胜利页面:justifyContent(FlexAlign.Center)让内容垂直居中
API 22:alignItems()
作用:设置弹性布局容器中子组件在交叉轴方向上的对齐方式。
对于Column(交叉轴为水平方向):
- HorizontalAlign.Start:左对齐
- HorizontalAlign.Center:水平居中
- HorizontalAlign.End:右对齐
对于Row(交叉轴为垂直方向):
- VerticalAlign.Top:顶部对齐
- VerticalAlign.Center:垂直居中
- VerticalAlign.Bottom:底部对齐
使用示例:
Column() {
Text('水平居中')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
在本项目中的应用:大多数Column容器使用默认的Stretch对齐方式(子元素铺满宽度),对于一些需要居中对齐的场景使用alignItems精确控制。
API 23:layoutWeight()
作用:设置子组件在弹性布局容器中的权重,用于分配剩余空间。权重越大,分配到的空间越多。
使用示例:
Column() {
Text('顶部固定')
.height(50)
// 中间内容区自动填充剩余空间
Scroll() {
Column() {
// ... 大量内容
}
}
.layoutWeight(1)
Text('底部固定')
.height(50)
}
.height('100%')
在本项目中的应用:游戏进行页使用layoutWeight(1)让聊天内容区自动填充剩余空间:
- 顶部标题区域:固定高度
- 中间聊天内容区:layoutWeight(1),自动填充
- 底部输入区域:固定高度
这种布局方式确保了在不同屏幕尺寸下,输入框始终固定在底部,聊天内容区自适应高度。
API 24:textAlign()
作用:设置文本的水平对齐方式。
可选值:
- TextAlign.Start:左对齐(默认)
- TextAlign.Center:居中对齐
- TextAlign.End:右对齐
使用示例:
Text('居中显示的文字')
.width('100%')
.textAlign(TextAlign.Center)
在本项目中的应用:
- 案件背景文字:使用TextAlign.Start,实现正常阅读排版
- 一些标题文字:根据需要使用不同的对齐方式
- 胜利页面的正文:使用TextAlign.Start,保持段落整洁
合理的文本对齐可以提升内容的可读性和美观度。
七、开发中的问题与解决方案
7.1 ArkTS语法规范问题
问题描述:在开发过程中,最初使用了很多TypeScript的常用写法,比如any类型、对象字面量、接口定义等,但在编译时遇到了大量ArkTS语法错误。
错误示例:
- “Object literals cannot be used as type declarations”
- “Use explicit types instead of ‘any’, ‘unknown’”
- “Cannot find name ‘width’. Did you mean the instance member ‘this.width’?”
原因分析:ArkTS虽然基于TypeScript,但为了保证运行性能和静态分析能力,增加了更严格的语法约束:
- 不允许使用any类型,必须使用明确的类型声明
- 对象字面量必须对应明确的类或接口
- UI组件只能在build方法或@Builder方法中使用
- 不支持typeof等动态类型查询
解决方案:
- 使用class替代interface:将所有接口定义改为类定义,使用构造函数初始化对象
- 移除所有any类型:为每个变量和方法参数声明明确的类型
- 使用@Builder装饰器:将UI构建逻辑提取到@Builder方法中
- 使用具体类型联合:如ClueResponse | null替代any
经验总结:ArkTS的严格语法虽然增加了开发成本,但带来了更好的类型安全和运行性能。在项目开始时就应该遵循ArkTS的编码规范,避免后期大量返工。
7.2 UI组件使用位置问题
问题描述:最初将UI构建逻辑写在普通方法中,然后在build方法中调用,编译时报错"UI component ‘Column’ cannot be used in this place"。
原因分析:在ArkUI中,UI组件的声明式描述只能出现在特定的位置:
- build()方法内部
- @Builder装饰的方法内部
- @BuilderParam装饰的变量中
普通的方法中不能使用UI组件描述语法。
解决方案:
- 给所有包含UI描述的方法添加@Builder装饰器
- 确保build()方法是UI描述的入口
- 业务逻辑方法中不要包含UI组件描述
示例对比:
错误写法:
// 普通方法中不能使用UI组件
buildHeader() {
Column() {
Text('标题')
}
}
正确写法:
// @Builder方法中可以使用UI组件
@Builder
BuildHeader() {
Column() {
Text('标题')
}
}
7.3 动态列表渲染问题
问题描述:使用ForEach渲染动态列表时,如果数组元素是对象类型,可能会出现渲染异常或性能问题。
原因分析:ForEach需要为每个列表项生成唯一的key,用于高效的差分更新。如果没有显式指定key生成函数,系统会尝试自动生成,但可能不是最优的。
最佳实践:
- 为每个列表项提供唯一的标识符
- 显式指定keyGenerator函数
- 避免在循环中修改数组
本项目中的处理:
游戏中的消息列表使用简单的Message对象,由于项目规模较小,暂时没有显式指定key生成函数,但在实际生产项目中建议添加。
7.4 状态更新与UI同步问题
问题描述:修改状态变量后,UI没有如期更新,或者更新时机不符合预期。
原因分析:ArkUI的状态更新机制是异步的,状态变化后不会立即刷新UI,而是在下一个渲染周期统一更新。
解决方案:
- 确保状态变量被正确的装饰器修饰(@State、@Prop、@Link等)
- 状态变量的赋值操作要在同一个异步任务中完成
- 不要在for循环中反复修改同一个状态变量,尽量一次性修改
本项目中的应用:
sendQuestion方法中,先修改messages数组,再修改inputText,最后修改currentResponse。这些修改会被合并到同一个渲染周期中,不会出现中间状态的闪烁。
八、项目扩展与优化方向
8.1 接入真实大语言模型
当前版本的"AI"是基于关键字匹配的伪AI,可以考虑接入真实的大语言模型(如GPT、文心一言等),实现真正的智能对话:
- 网络请求:使用HarmonyOS的HTTP API发送请求给大模型API
- Prompt工程:设计精良的系统提示词,引导模型扮演侦探助手的角色
- 流式响应:实现打字机效果,提升对话体验
- 上下文管理:维护对话历史,让AI能记住之前的对话内容
8.2 增加更多案件
目前只有一个案件,可以开发更多不同类型的案件:
- 案件选择页面:玩家可以选择不同的案件进行游戏
- 难度分级:简单、中等、困难三个难度等级
- 案件下载:支持在线下载新案件,持续更新内容
8.3 游戏系统增强
可以增加更多游戏机制,提升可玩性:
- 积分系统:根据提问次数和用时计算得分
- 成就系统:设置各种探案成就
- 提示次数限制:增加挑战性,提示需要消耗点数
- 多人对战:支持多人同时推理,比拼破案速度
8.4 UI/UX优化
界面和交互还有很多可以优化的空间:
- 动画效果:增加页面切换动画、消息气泡出现动画
- 音效系统:添加背景音乐和点击音效
- 暗黑/亮色模式切换:支持不同的主题
- 字体大小调节:适应不同视力的用户
8.5 数据持久化
使用HarmonyOS的Preferences或关系型数据库保存游戏进度:
- 保存游戏状态:玩家退出后再次进入可以继续
- 历史记录:保存已通关的案件和得分
- 配置保存:保存用户的设置偏好
九、总结
本文详细介绍了基于HarmonyOS平台开发悬疑探案推理小游戏的全过程。从项目背景、架构设计、数据模型、服务层实现到UI开发,再到24个核心HarmonyOS API的详解,全面展示了一个完整应用的开发流程。
通过这个项目,我们可以看到HarmonyOS ArkUI框架的强大之处:
- 声明式UI:代码简洁直观,可读性强
- 状态驱动:@State等装饰器让状态管理变得简单
- 组件化:@Builder和@Component支持灵活的UI复用
- 高性能:方舟编译器和ArkTS的严格语法保证了运行效率
- 跨设备:一套代码可以在多种HarmonyOS设备上运行
悬疑探案推理小游戏虽然功能不复杂,但涵盖了HarmonyOS应用开发的多个核心方面:状态管理、列表渲染、条件渲染、用户交互、布局设计等。对于HarmonyOS初学者来说,这是一个很好的练手项目。
未来,随着HarmonyOS生态的不断完善和大模型技术的发展,互动式游戏应用将会有更广阔的发展空间。希望本文能为读者在HarmonyOS开发和互动游戏设计方面提供一些参考和启发。
更多推荐



所有评论(0)