项目演示

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

一、项目背景与概述

1.1 项目背景

随着移动互联网的快速发展,互动式游戏应用越来越受到用户的喜爱。悬疑探案推理类游戏以其独特的沉浸感和智力挑战性,在各类游戏应用中占据了重要的一席之地。本文将详细介绍如何基于HarmonyOS平台开发一款悬疑探案推理小游戏,通过ArkTS语言和ArkUI框架实现完整的游戏功能。

本项目的核心玩法是:玩家通过向AI助手提问来收集案件线索,逐步推理出凶手的身份。游戏采用聊天式交互界面,玩家可以自由提问,系统会根据问题匹配相关线索,并以结构化的JSON格式返回结果,包括问题、提示、推理结果、正确性分析和通关文字五个核心字段。

1.2 游戏玩法介绍

游戏开始时,玩家会看到一个完整的案件背景介绍,包括死者信息、死亡时间、现场情况和嫌疑人名单。玩家需要通过输入问题来收集线索,例如询问"纽扣是什么"、“王管家的情况”、"密室是怎么形成的"等。

每当玩家提出一个问题,系统会返回结构化的线索信息,包括:

  • 问题:当前询问的问题是什么
  • 提示列表:关于这个问题的三个提示信息
  • 推理结果列表:基于线索可以得出的三个推理方向
  • 正确性分析:对推理结果的正确性判断和分析
  • 通关文字:当玩家正确指出凶手时显示的结案陈词

当玩家收集到足够的线索后,可以直接指出凶手是谁。如果回答正确,游戏会进入胜利页面,展示完整的案件真相和推理过程。

1.3 技术选型

本项目选用HarmonyOS作为开发平台,主要基于以下考虑:

  1. 跨设备能力:HarmonyOS支持多种设备形态,游戏可以在手机、平板等多种设备上运行
  2. 声明式UI:ArkUI框架采用声明式编程范式,代码简洁易读
  3. 高性能:方舟编译器提供了优秀的运行时性能
  4. 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:定义了ClueResponseClueNodeFinalAccusationMysteryCase四个数据类,并初始化了一个完整的悬疑案件数据
  • AIChatService.ets:实现了单例模式的AI聊天服务,提供关键字匹配、JSON响应生成等功能
  • Index.ets:游戏主界面,包含三个游戏状态(介绍、进行、胜利)的完整UI和交互逻辑

2.3 数据流向

游戏的数据流向清晰明了:

  1. 用户在TextInput中输入问题
  2. 点击发送按钮触发sendQuestion方法
  3. 调用AIChatService的getResponse方法处理用户输入
  4. AIChatService通过关键字匹配找到对应的线索
  5. 将线索数据序列化为JSON字符串返回
  6. 页面解析JSON并更新currentResponse状态
  7. 界面根据状态变化重新渲染,展示新的线索信息

三、核心数据模型设计

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
  }
}

五个字段的含义如下:

  1. question:当前回答的问题是什么。这个字段可以让玩家清楚地知道当前讨论的话题。
  2. hints:提示列表,通常包含3条提示信息。这些提示是引导玩家思考的阶梯,不会直接给出答案,但会指明思考方向。
  3. reasoningResults:推理结果列表,通常包含3条可能的推理方向。这些是基于当前线索可以得出的初步结论,但不一定全部正确。
  4. correctnessAnalysis:正确性分析,对推理结果的整体判断和详细分析。这个字段会告诉玩家哪些推理是正确的,为什么正确。
  5. 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 案件数据设计

本项目预置了一个完整的悬疑案件——“午夜书房谋杀案”。案件讲述了富商李明远在自己的书房中被发现死亡的故事,涉及三名嫌疑人:

  1. 王管家:家中任职20年的老管家
  2. 李夫人:死者的第二任妻子,年轻貌美
  3. 张律师:死者的私人律师,负责处理遗产事宜

案件设计了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()

单例模式的优点:

  1. 全局唯一实例:确保整个应用中只有一个AI服务实例,避免重复初始化
  2. 状态一致性:所有页面共享同一个服务实例,案件状态保持一致
  3. 资源节省:避免创建多个实例造成的资源浪费

4.2 关键字匹配算法

游戏的核心交互机制是用户提问,系统通过关键字匹配来返回对应的线索。匹配算法的设计直接影响游戏的体验。

匹配逻辑分为两个层次:

  1. 最终指控匹配:优先检查用户是否在指出凶手。如果匹配成功,直接返回胜利响应。
  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
}

算法的特点:

  1. 不区分大小写:将用户输入和关键词都转为小写进行比较,提高匹配率
  2. 子串匹配:使用includes方法进行子串匹配,用户不需要精确输入完整关键词
  3. 顺序匹配:按线索数组的顺序进行匹配,先定义的线索优先级更高
  4. 首个匹配:找到第一个匹配的线索就返回,不继续查找后续线索

4.3 响应生成机制

系统的响应生成遵循以下流程:

  1. 首先检查是否触发最终指控
  2. 如果是,设置案件已破获状态,返回胜利响应
  3. 否则,检查是否匹配普通线索
  4. 如果匹配,返回对应的线索响应
  5. 如果都不匹配,返回默认响应

响应生成的核心代码:

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格式,包含五个字段。这种设计的优点:

  1. 前后端解耦:UI层只需要按照固定格式解析数据,不需要知道数据来源
  2. 易于扩展:如果未来接入真实的AI大模型,只需要保证返回格式一致
  3. 类型安全:通过ClueResponse类定义数据结构,确保数据完整性
  4. 调试方便:统一的格式使得日志记录和问题排查更加简单

标准响应示例:

{
  "question": "死者手中的银色纽扣是什么?",
  "hints": [
    "这是一条银色的装饰怪按纹",
    "纹路较大,适用于大裙或服装",
    "可能来自进出时所穿的装饰"
  ],
  "reasoningResults": [
    "死者在最后一刻抓住了某个人的装饰",
    "这可能是抢劫时产生的线索",
    "按纹的来源可能指向某个有特定状态的人"
  ],
  "correctnessAnalysis": "正确。银色装饰怪按纹通常用于特殊职业或高等服装上。",
  "victoryText": ""
}

五、游戏界面开发

5.1 页面状态机设计

游戏界面采用状态机模式设计,定义了三种游戏状态:

  1. intro(介绍状态):游戏开始前的案件介绍页面
  2. playing(进行状态):游戏进行中的主交互页面
  3. 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布局,从上到下依次是:

  1. 案件标题
  2. 游戏副标题
  3. 案件背景介绍(可滚动)
  4. 游戏说明文字
  5. 开始探案按钮

关键代码片段:

@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 游戏进行页实现

游戏进行页是游戏的核心交互界面,采用聊天式设计,玩家可以像与侦探助手对话一样进行探案。

页面结构分为三个区域:

  1. 顶部标题区:显示案件标题
  2. 聊天内容区:展示对话历史和当前线索详情(可滚动)
  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 胜利页面实现

胜利页面是玩家成功破案后看到的界面,展示完整的案件真相和推理过程,给玩家成就感和满足感。

页面布局:

  1. 恭喜破案标题
  2. 案件真相区域(可滚动)
  3. 推理过程列表
  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
  }
}

处理流程详解:

  1. 输入验证:检查用户输入是否为空,为空则直接返回
  2. 添加用户消息:将用户的问题添加到消息列表
  3. 清空输入框:清空TextInput的内容
  4. 调用AI服务:获取AI的响应JSON字符串
  5. 解析JSON:尝试解析响应,失败则设为null
  6. 更新当前响应:将解析后的数据赋值给currentResponse
  7. 判断是否胜利:检查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的一致性。

工作原理

  1. 用户在输入框中输入文字
  2. onChange事件触发,回调函数接收到新的输入值
  3. 将新值赋值给@State变量
  4. @State变量的变化触发UI更新
  5. 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,但为了保证运行性能和静态分析能力,增加了更严格的语法约束:

  1. 不允许使用any类型,必须使用明确的类型声明
  2. 对象字面量必须对应明确的类或接口
  3. UI组件只能在build方法或@Builder方法中使用
  4. 不支持typeof等动态类型查询

解决方案

  1. 使用class替代interface:将所有接口定义改为类定义,使用构造函数初始化对象
  2. 移除所有any类型:为每个变量和方法参数声明明确的类型
  3. 使用@Builder装饰器:将UI构建逻辑提取到@Builder方法中
  4. 使用具体类型联合:如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组件描述语法。

解决方案

  1. 给所有包含UI描述的方法添加@Builder装饰器
  2. 确保build()方法是UI描述的入口
  3. 业务逻辑方法中不要包含UI组件描述

示例对比

错误写法:

// 普通方法中不能使用UI组件
buildHeader() {
  Column() {
    Text('标题')
  }
}

正确写法:

// @Builder方法中可以使用UI组件
@Builder
BuildHeader() {
  Column() {
    Text('标题')
  }
}

7.3 动态列表渲染问题

问题描述:使用ForEach渲染动态列表时,如果数组元素是对象类型,可能会出现渲染异常或性能问题。

原因分析:ForEach需要为每个列表项生成唯一的key,用于高效的差分更新。如果没有显式指定key生成函数,系统会尝试自动生成,但可能不是最优的。

最佳实践

  1. 为每个列表项提供唯一的标识符
  2. 显式指定keyGenerator函数
  3. 避免在循环中修改数组

本项目中的处理
游戏中的消息列表使用简单的Message对象,由于项目规模较小,暂时没有显式指定key生成函数,但在实际生产项目中建议添加。

7.4 状态更新与UI同步问题

问题描述:修改状态变量后,UI没有如期更新,或者更新时机不符合预期。

原因分析:ArkUI的状态更新机制是异步的,状态变化后不会立即刷新UI,而是在下一个渲染周期统一更新。

解决方案

  1. 确保状态变量被正确的装饰器修饰(@State、@Prop、@Link等)
  2. 状态变量的赋值操作要在同一个异步任务中完成
  3. 不要在for循环中反复修改同一个状态变量,尽量一次性修改

本项目中的应用
sendQuestion方法中,先修改messages数组,再修改inputText,最后修改currentResponse。这些修改会被合并到同一个渲染周期中,不会出现中间状态的闪烁。

八、项目扩展与优化方向

8.1 接入真实大语言模型

当前版本的"AI"是基于关键字匹配的伪AI,可以考虑接入真实的大语言模型(如GPT、文心一言等),实现真正的智能对话:

  1. 网络请求:使用HarmonyOS的HTTP API发送请求给大模型API
  2. Prompt工程:设计精良的系统提示词,引导模型扮演侦探助手的角色
  3. 流式响应:实现打字机效果,提升对话体验
  4. 上下文管理:维护对话历史,让AI能记住之前的对话内容

8.2 增加更多案件

目前只有一个案件,可以开发更多不同类型的案件:

  1. 案件选择页面:玩家可以选择不同的案件进行游戏
  2. 难度分级:简单、中等、困难三个难度等级
  3. 案件下载:支持在线下载新案件,持续更新内容

8.3 游戏系统增强

可以增加更多游戏机制,提升可玩性:

  1. 积分系统:根据提问次数和用时计算得分
  2. 成就系统:设置各种探案成就
  3. 提示次数限制:增加挑战性,提示需要消耗点数
  4. 多人对战:支持多人同时推理,比拼破案速度

8.4 UI/UX优化

界面和交互还有很多可以优化的空间:

  1. 动画效果:增加页面切换动画、消息气泡出现动画
  2. 音效系统:添加背景音乐和点击音效
  3. 暗黑/亮色模式切换:支持不同的主题
  4. 字体大小调节:适应不同视力的用户

8.5 数据持久化

使用HarmonyOS的Preferences或关系型数据库保存游戏进度:

  1. 保存游戏状态:玩家退出后再次进入可以继续
  2. 历史记录:保存已通关的案件和得分
  3. 配置保存:保存用户的设置偏好

九、总结

本文详细介绍了基于HarmonyOS平台开发悬疑探案推理小游戏的全过程。从项目背景、架构设计、数据模型、服务层实现到UI开发,再到24个核心HarmonyOS API的详解,全面展示了一个完整应用的开发流程。

通过这个项目,我们可以看到HarmonyOS ArkUI框架的强大之处:

  1. 声明式UI:代码简洁直观,可读性强
  2. 状态驱动:@State等装饰器让状态管理变得简单
  3. 组件化:@Builder和@Component支持灵活的UI复用
  4. 高性能:方舟编译器和ArkTS的严格语法保证了运行效率
  5. 跨设备:一套代码可以在多种HarmonyOS设备上运行

悬疑探案推理小游戏虽然功能不复杂,但涵盖了HarmonyOS应用开发的多个核心方面:状态管理、列表渲染、条件渲染、用户交互、布局设计等。对于HarmonyOS初学者来说,这是一个很好的练手项目。

未来,随着HarmonyOS生态的不断完善和大模型技术的发展,互动式游戏应用将会有更广阔的发展空间。希望本文能为读者在HarmonyOS开发和互动游戏设计方面提供一些参考和启发。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐