KMP 实现鸿蒙跨端:Kotlin 小游戏 - 数字猜谜游戏
本文介绍了基于Kotlin Multiplatform(KMP)开发的数字猜谜游戏实现方案。该游戏包含核心逻辑函数、三种难度模式(简单100次、普通10次、困难5次机会)和得分计算功能。通过KMP技术,一份Kotlin代码可编译为JavaScript在OpenHarmony应用运行。文章详细展示了游戏规则、核心函数实现以及完整的代码示例,包括Kotlin源码和编译后的JavaScript代码。该案

目录
概述
本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个小游戏 - 数字猜谜游戏。这个案例展示了如何使用 Kotlin 的函数式编程特性、集合操作和游戏逻辑来创建一个有趣的交互式应用。通过 KMP,这个游戏可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行。
游戏的特点
- 简单易玩:规则简单,容易上手
- 函数式设计:使用 Lambda 和高阶函数实现游戏逻辑
- 跨端兼容:一份 Kotlin 代码可同时服务多个平台
- 可扩展性:易于添加新的游戏模式和难度级别
- 实时反馈:即时显示游戏进度和得分
游戏规则
基本规则
- 目标:猜出 1-100 之间的秘密数字
- 反馈:每次猜测后获得提示
- 如果猜对了,显示成功信息
- 如果猜小了,提示"太小了"
- 如果猜大了,提示"太大了"
- 难度级别:
- 简单模式:100 次机会
- 普通模式:10 次机会
- 困难模式:5 次机会
- 得分计算:根据使用的尝试次数计算得分
游戏流程
开始游戏
↓
生成秘密数字
↓
玩家猜测
↓
检查猜测
├→ 猜对 → 游戏结束,显示得分
├→ 太小 → 提示并继续
└→ 太大 → 提示并继续
↓
重复直到游戏结束
核心功能
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() | 数组循环 |
| 字符串插值 | 字符串连接 |
关键转换点
- Lambda 表达式:转换为 JavaScript 函数对象
- 集合操作:转换为数组操作
- 字符串处理:保持功能一致
- 数学运算:直接转换为 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:
- 使用 Sequence 代替 List 处理大数据
- 避免不必要的对象创建
- 使用内联函数减少函数调用开销
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
- ✅ 函数式编程使代码更简洁、更易维护
更多推荐




所有评论(0)