在这里插入图片描述

目录

  1. 概述
  2. 游戏规则
  3. 核心功能
  4. 实战案例
  5. 编译过程详解
  6. 游戏扩展
  7. 最佳实践
  8. 常见问题

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个小游戏 - 数字猜谜游戏。这个案例展示了如何使用 Kotlin 的函数式编程特性、集合操作和游戏逻辑来创建一个有趣的交互式应用。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。

游戏的特点

  • 简单易玩:规则简单,容易上手
  • 函数式设计:使用 Lambda 和高阶函数实现游戏逻辑
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台
  • 可扩展性:易于添加新的游戏模式和难度级别
  • 实时反馈:即时显示游戏进度和得分

游戏规则

基本规则

  1. 目标:猜出 1-100 之间的秘密数字
  2. 反馈:每次猜测后获得提示
    • 如果猜对了,显示成功信息
    • 如果猜小了,提示"太小了"
    • 如果猜大了,提示"太大了"
  3. 难度级别
    • 简单模式:100 次机会
    • 普通模式:10 次机会
    • 困难模式:5 次机会
  4. 得分计算:根据使用的尝试次数计算得分

游戏流程

开始游戏
  ↓
生成秘密数字
  ↓
玩家猜测
  ↓
检查猜测
  ├→ 猜对 → 游戏结束,显示得分
  ├→ 太小 → 提示并继续
  └→ 太大 → 提示并继续
  ↓
重复直到游戏结束

核心功能

1. 游戏检查函数

val checkGuess: (Int) -> String = { guess ->
    when {
        guess == secretNumber -> "🎉 恭喜!你猜对了!答案是 $secretNumber"
        guess < secretNumber -> "📍 太小了!答案更大"
        else -> "📍 太大了!答案更小"
    }
}

代码说明:

这是一个 Lambda 函数,用于检查玩家的猜测。函数接收一个整数参数(玩家的猜测),返回一个字符串表示提示信息。使用 when 表达式进行条件判断:如果猜测等于秘密数字,返回成功信息;如果猜测小于秘密数字,返回"太小了"的提示;否则返回"太大了"的提示。这个函数是游戏的核心逻辑。

2. 难度级别函数

val easyMode: (Int) -> Int = { 100 }    // 100次机会
val normalMode: (Int) -> Int = { 10 }   // 10次机会
val hardMode: (Int) -> Int = { 5 }      // 5次机会

代码说明:

这些是高阶函数,用于定义不同难度级别的机会数量。每个函数接收一个整数参数(虽然这里没有使用),返回该难度级别的最大尝试次数。简单模式提供 100 次机会,普通模式提供 10 次机会,困难模式提供 5 次机会。这个设计允许玩家根据自己的技能水平选择合适的难度。

3. 得分计算函数

val calculateScore: (Int, Int) -> Int = { attempts, maxAttempts ->
    ((maxAttempts - attempts) * 100 / maxAttempts).coerceAtLeast(0)
}

代码说明:

这是一个得分计算函数,接收两个参数:实际使用的尝试次数和最大允许的尝试次数。计算公式为:(最大次数 - 实际次数) × 100 / 最大次数。使用 coerceAtLeast(0) 确保得分不会为负数。使用的次数越少,得分越高,最多可得 100 分。这个函数鼓励玩家用更少的尝试次数猜对答案。


实战案例

案例:完整的数字猜谜游戏

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun numberGuessingGame(): String {
    // 秘密数字(固定为56)
    val secretNumber = 56
    
    // 定义游戏规则函数
    val checkGuess: (Int) -> String = { guess ->
        when {
            guess == secretNumber -> "🎉 恭喜!你猜对了!答案是 $secretNumber"
            guess < secretNumber -> "📍 太小了!答案更大"
            else -> "📍 太大了!答案更小"
        }
    }
    
    // 定义难度级别
    val easyMode: (Int) -> Int = { 100 }  // 100次机会
    val normalMode: (Int) -> Int = { 10 }  // 10次机会
    val hardMode: (Int) -> Int = { 5 }    // 5次机会
    
    // 游戏统计
    val guesses = listOf(25, 50, 75, 60, 55, 58, 57, 56)
    val results = guesses.map { guess ->
        "猜测: $guess${checkGuess(guess)}"
    }
    
    // 计算得分
    val calculateScore: (Int, Int) -> Int = { attempts, maxAttempts ->
        ((maxAttempts - attempts) * 100 / maxAttempts).coerceAtLeast(0)
    }
    
    val score = calculateScore(guesses.size, normalMode(0))
    
    return "🎮 数字猜谜游戏\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "秘密数字: 1-100\n" +
           "难度: 普通模式 (10次机会)\n\n" +
           "游戏过程:\n" +
           results.joinToString("\n") + "\n\n" +
           "━━━━━━━━━━━━━━━━━━━━━\n" +
           "总尝试次数: ${guesses.size}\n" +
           "最终得分: $score 分"
}

代码说明:

这是数字猜谜游戏的完整 Kotlin 实现。函数使用 @JsExport 装饰器将其导出为 JavaScript 可调用的函数。首先定义秘密数字(56)。定义检查猜测的 Lambda 函数、难度级别函数和得分计算函数。模拟一系列玩家的猜测。使用 map() 将每个猜测转换为结果字符串。计算最终得分。最后返回格式化的游戏结果,包含游戏过程、尝试次数和最终得分。

编译后的 JavaScript 代码
function numberGuessingGame() {
  // 秘密数字
  var secretNumber = 56;
  
  // 定义游戏规则函数
  var checkGuess = function(guess) {
    if (guess === secretNumber) {
      return '🎉 恭喜!你猜对了!答案是 ' + secretNumber;
    } else if (guess < secretNumber) {
      return '📍 太小了!答案更大';
    } else {
      return '📍 太大了!答案更小';
    }
  };
  
  // 定义难度级别
  var easyMode = function() { return 100; };
  var normalMode = function() { return 10; };
  var hardMode = function() { return 5; };
  
  // 游戏统计
  var guesses = [25, 50, 75, 60, 55, 58, 57, 56];
  var results = [];
  for (var i = 0; i < guesses.length; i++) {
    var guess = guesses[i];
    results.push('猜测: ' + guess + ' → ' + checkGuess(guess));
  }
  
  // 计算得分
  var calculateScore = function(attempts, maxAttempts) {
    var score = (maxAttempts - attempts) * 100 / maxAttempts;
    return Math.max(score, 0);
  };
  
  var score = calculateScore(guesses.length, normalMode());
  
  return '🎮 数字猜谜游戏\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '秘密数字: 1-100\n' +
         '难度: 普通模式 (10次机会)\n\n' +
         '游戏过程:\n' +
         results.join('\n') + '\n\n' +
         '━━━━━━━━━━━━━━━━━━━━━\n' +
         '总尝试次数: ' + guesses.length + '\n' +
         '最终得分: ' + score + ' 分';
}

代码说明:

这是 Kotlin 代码编译到 JavaScript 后的结果。可以看到 Kotlin 的语言特性被转换为 JavaScript 等价物:Lambda 函数变成匿名函数,when 表达式变成 if-else 语句,map() 变成 for 循环,字符串插值变成字符串连接。虽然编译后的代码看起来不同,但它保留了原始 Kotlin 代码的逻辑。使用 ES Module 格式,可以被其他模块导入。包含完整的类型定义(.d.ts 文件),提供 TypeScript 支持。可以直接在浏览器或 Node.js 中运行。

ArkTS 调用代码
import { numberGuessingGame } from './hellokjs';

@Entry
@Component
struct Index {
  @State message: string = '加载中...';
  @State results: string[] = [];
  @State caseTitle: string = '小游戏 - 数字猜谜游戏';

  aboutToAppear(): void {
    this.loadResults();
  }

  loadResults(): void {
    try {
      // 调用 Kotlin 编译的 JavaScript 函数
      const gameResult = numberGuessingGame();
      this.results = [gameResult];
      this.message = '✓ 游戏已加载';
    } catch (error) {
      this.message = `✗ 错误: ${error}`;
    }
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('KMP 鸿蒙跨端')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Spacer()
        Text('Kotlin 案例')
          .fontSize(14)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(50)
      .backgroundColor('#3b82f6')
      .padding({ left: 20, right: 20 })
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.SpaceBetween)

      // 案例标题
      Column() {
        Text(this.caseTitle)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1f2937')
        Text(this.message)
          .fontSize(13)
          .fontColor('#6b7280')
          .margin({ top: 5 })
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 20, bottom: 15 })
      .alignItems(HorizontalAlign.Start)

      // 结果显示区域
      Scroll() {
        Column() {
          ForEach(this.results, (result: string) => {
            Column() {
              Text(result)
                .fontSize(13)
                .fontFamily('monospace')
                .fontColor('#374151')
                .width('100%')
                .margin({ top: 10 })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .border({ width: 1, color: '#e5e7eb' })
            .borderRadius(8)
            .margin({ bottom: 12 })
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
      }
      .layoutWeight(1)
      .width('100%')

      // 底部按钮区域
      Row() {
        Button('刷新')
          .width('48%')
          .height(44)
          .backgroundColor('#3b82f6')
          .fontColor(Color.White)
          .fontSize(14)
          .onClick(() => {
            this.loadResults();
          })

        Button('返回')
          .width('48%')
          .height(44)
          .backgroundColor('#6b7280')
          .fontColor(Color.White)
          .fontSize(14)
          .onClick(() => {
            // 返回操作
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f9fafb')
  }
}

代码说明:

这是 OpenHarmony ArkTS 页面的完整实现,展示了如何集成和调用 Kotlin 编译生成的游戏函数。首先通过 import 语句从 ./hellokjs 模块导入 numberGuessingGame 函数。页面使用 @Entry@Component 装饰器定义为可入口的组件。定义了三个响应式状态变量:message 显示操作状态,results 存储游戏结果,caseTitle 显示标题。aboutToAppear() 生命周期钩子在页面加载时调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 函数进行游戏,将结果存储在 results 数组中,并更新 message 显示状态。使用 try-catch 块捕获异常。build() 方法定义了完整的 UI 布局,包括顶部标题栏、游戏标题、结果显示区域和底部按钮区域。使用 monospace 字体显示游戏过程,保持格式对齐。


编译过程详解

Kotlin 到 JavaScript 的转换

Kotlin 特性 JavaScript 等价物
Lambda 函数 匿名函数
when 表达式 if-else 语句
List.map() 数组循环
字符串插值 字符串连接

关键转换点

  1. Lambda 表达式:转换为 JavaScript 函数对象
  2. 集合操作:转换为数组操作
  3. 字符串处理:保持功能一致
  4. 数学运算:直接转换为 JavaScript 运算

游戏扩展

扩展 1:添加计时功能

val startTime = System.currentTimeMillis()
// ... 游戏逻辑 ...
val endTime = System.currentTimeMillis()
val timeUsed = (endTime - startTime) / 1000

代码说明:

这段代码展示了如何添加计时功能。使用 System.currentTimeMillis() 获取当前时间戳(毫秒)。在游戏开始时记录开始时间,在游戏结束时记录结束时间。计算时间差并除以 1000 转换为秒。这允许统计玩家完成游戏所用的时间,用于排行榜或性能评估。

扩展 2:添加多轮游戏

val rounds = 3
val totalScore = (1..rounds).map { numberGuessingGame() }.sum()

代码说明:

这段代码展示了如何实现多轮游戏。定义轮数为 3。使用范围表达式 (1..rounds) 创建 1 到 3 的范围。使用 map() 对每一轮调用游戏函数。使用 sum() 计算所有轮次的总分。这允许玩家进行多轮对战,累计总分。

扩展 3:添加排行榜

data class GameRecord(val playerName: String, val score: Int, val attempts: Int)
val leaderboard = mutableListOf<GameRecord>()

代码说明:

这段代码展示了如何添加排行榜系统。定义一个数据类 GameRecord 存储玩家记录:玩家名字、得分和尝试次数。创建一个可变列表存储所有玩家的记录。这允许跟踪多个玩家的成绩,实现排行榜功能。

扩展 4:添加随机难度

val difficulties = listOf(easyMode, normalMode, hardMode)
val selectedDifficulty = difficulties.random()

代码说明:

这段代码展示了如何添加随机难度选择。将所有难度级别函数放在一个列表中。使用 random() 方法随机选择一个难度。这允许每次游戏都随机选择难度,增加游戏的变化性和挑战性。


最佳实践

1. 使用类型安全的 Lambda

// ✅ 好:明确的类型声明
val checkGuess: (Int) -> String = { guess -> ... }

// ❌ 不好:类型不明确
val checkGuess = { guess -> ... }

代码说明:

这个示例对比了两种定义 Lambda 函数的方法。第一种方法明确声明参数类型和返回类型,提高了代码的可读性和类型安全性。第二种方法省略类型声明,虽然 Kotlin 可以推断类型,但容易导致混淆。最佳实践是:明确声明 Lambda 的类型。

2. 避免副作用

// ✅ 好:纯函数,无副作用
val calculateScore: (Int, Int) -> Int = { attempts, max ->
    ((max - attempts) * 100 / max).coerceAtLeast(0)
}

// ❌ 不好:有副作用
var globalScore = 0
val calculateScore: (Int, Int) -> Unit = { attempts, max ->
    globalScore = ((max - attempts) * 100 / max).coerceAtLeast(0)
}

代码说明:

这个示例对比了两种函数设计方法。第一种方法是纯函数,只依赖于输入参数,返回计算结果,没有副作用。第二种方法修改全局变量,产生副作用,容易导致 bug 和难以测试。最佳实践是:设计纯函数,避免副作用。

3. 使用集合操作

// ✅ 好:使用 map 转换
val results = guesses.map { guess -> "猜测: $guess${checkGuess(guess)}" }

// ❌ 不好:使用 for 循环
val results = mutableListOf<String>()
for (guess in guesses) {
    results.add("猜测: $guess${checkGuess(guess)}")
}

代码说明:

这个示例对比了两种处理集合的方法。第一种方法使用 map() 函数式操作,简洁高效,一行代码完成转换。第二种方法使用 for 循环手动构建列表,代码冗长且容易出错。最佳实践是:使用集合操作函数进行数据转换。

4. 合理使用 emoji

// ✅ 好:使用 emoji 增加可读性
"🎮 数字猜谜游戏"
"🎉 恭喜!你猜对了!"
"📍 太小了!"

// ❌ 不好:过度使用 emoji
"🎮🎯🎲 数字猜谜游戏 🎮🎯🎲"

代码说明:

这个示例对比了两种使用 emoji 的方法。第一种方法适度使用 emoji,增加了文本的可读性和视觉吸引力。第二种方法过度使用 emoji,反而降低了可读性。最佳实践是:合理使用 emoji,增加用户体验,但不过度。


常见问题

Q1: 如何实现真正的随机数?

A: 在 Kotlin/JS 中,可以使用 JavaScript 的 Math.random()

external fun jsRandom(): Double = definedExternally
val secretNumber = (jsRandom() * 100).toInt() + 1

代码说明:

这段代码展示了如何实现真正的随机数。使用 external 关键字声明一个外部函数 jsRandom(),它调用 JavaScript 的 Math.random() 方法。生成 0 到 1 之间的随机数,乘以 100 得到 0 到 100 之间的数,转换为整数后加 1,得到 1 到 100 之间的随机数。

Q2: 如何保存游戏进度?

A: 使用本地存储(LocalStorage):

external object localStorage {
    fun setItem(key: String, value: String)
    fun getItem(key: String): String?
}

// 保存得分
localStorage.setItem("gameScore", score.toString())

// 读取得分
val savedScore = localStorage.getItem("gameScore")?.toIntOrNull() ?: 0

代码说明:

这段代码展示了如何保存游戏进度。定义一个 external object 来访问浏览器的 localStorage API。使用 setItem() 方法将得分转换为字符串并保存。使用 getItem() 方法读取保存的得分,使用 toIntOrNull() 安全地转换为整数,如果转换失败则使用默认值 0。这允许玩家在刷新页面后继续游戏。

Q3: 如何实现多人游戏?

A: 使用网络通信:

// 发送猜测到服务器
suspend fun submitGuess(guess: Int): String {
    return httpClient.post("/api/game/guess") {
        setBody(mapOf("guess" to guess))
    }.body()
}

代码说明:

这段代码展示了如何实现多人游戏。定义一个挂起函数 submitGuess() 用于发送猜测到服务器。使用 httpClient.post() 方法向 API 端点发送 POST 请求。使用 setBody() 方法设置请求体,包含玩家的猜测。使用 .body() 获取响应体。这允许实现网络多人游戏功能。

Q4: 如何优化游戏性能?

A:

  1. 使用 Sequence 代替 List 处理大数据
  2. 避免不必要的对象创建
  3. 使用内联函数减少函数调用开销

Q5: 如何添加声音效果?

A: 使用 Web Audio API:

external class AudioContext
external fun playSound(url: String)

// 游戏获胜时播放声音
if (guess == secretNumber) {
    playSound("victory.mp3")
}

代码说明:

这段代码展示了如何添加声音效果。定义 external class AudioContext 来访问浏览器的 Web Audio API。定义 external fun playSound() 来播放音频文件。当玩家猜对答案时,调用 playSound() 播放胜利音效。这增强了游戏的沉浸感和用户体验。


总结

关键要点

  • ✅ 使用 Lambda 和高阶函数实现游戏逻辑
  • ✅ 使用集合操作处理游戏数据
  • ✅ 使用 when 表达式处理游戏状态
  • ✅ KMP 能无缝编译到 JavaScript
  • ✅ 函数式编程使代码更简洁、更易维护
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐