
鸿蒙跨端每日步数挑战游戏开发指南 原创
鸿蒙跨端每日步数挑战游戏开发指南
一、系统架构设计
基于HarmonyOS的分布式能力和健康数据服务,构建多设备步数PK游戏:
数据采集层:从健康服务获取步数数据
游戏逻辑层:管理挑战规则和比赛状态
社交互动层:支持玩家互动和挑战
跨端同步层:实时同步步数数据和比赛状态
!https://example.com/harmony-step-challenge-arch.png
二、核心代码实现
步数挑战服务
// StepChallengeService.ets
import health from ‘@ohos.health’;
import distributedData from ‘@ohos.distributedData’;
import { PlayerInfo, Challenge, StepData } from ‘./StepChallengeTypes’;
class StepChallengeService {
private static instance: StepChallengeService = null;
private healthManager: health.HealthManager;
private dataManager: distributedData.DataManager;
private challengeListeners: ChallengeListener[] = [];
private currentChallenge: Challenge | null = null;
private constructor() {
this.initHealthManager();
this.initDataManager();
public static getInstance(): StepChallengeService {
if (!StepChallengeService.instance) {
StepChallengeService.instance = new StepChallengeService();
return StepChallengeService.instance;
private initHealthManager(): void {
try {
this.healthManager = health.createHealthManager(getContext());
// 注册步数数据监听
this.healthManager.registerDataListener({
dataType: health.DataType.STEP_COUNT,
callback: (data) => {
this.handleStepData(data);
});
catch (err) {
console.error('初始化健康管理器失败:', JSON.stringify(err));
}
private initDataManager(): void {
this.dataManager = distributedData.createDataManager({
bundleName: ‘com.example.stepchallenge’,
area: distributedData.Area.GLOBAL,
isEncrypted: true
});
this.dataManager.registerDataListener('challenge_sync', (data) => {
this.handleSyncData(data);
});
public async requestPermissions(): Promise<boolean> {
try {
const permissions = [
'ohos.permission.READ_HEALTH_DATA',
'ohos.permission.DISTRIBUTED_DATASYNC'
];
const result = await abilityAccessCtrl.requestPermissionsFromUser(
getContext(),
permissions
);
return result.grantedPermissions.length === permissions.length;
catch (err) {
console.error('请求权限失败:', JSON.stringify(err));
return false;
}
public async createChallenge(players: PlayerInfo[], duration: number): Promise<Challenge> {
try {
const challenge: Challenge = {
id: Date.now().toString(),
players: players,
startTime: Date.now(),
endTime: Date.now() + duration 60 60 * 1000, // 转换为毫秒
status: ‘active’,
stepRecords: players.map(player => ({
playerId: player.deviceId,
steps: 0,
lastUpdate: Date.now()
}))
};
this.currentChallenge = challenge;
this.syncChallenge(challenge);
return challenge;
catch (err) {
console.error('创建挑战失败:', JSON.stringify(err));
throw err;
}
public async joinChallenge(challengeId: string, player: PlayerInfo): Promise<Challenge> {
if (!this.currentChallenge || this.currentChallenge.id !== challengeId) {
throw new Error(‘未找到指定挑战’);
const updatedChallenge: Challenge = {
...this.currentChallenge,
players: [...this.currentChallenge.players, player],
stepRecords: [
...this.currentChallenge.stepRecords,
playerId: player.deviceId,
steps: 0,
lastUpdate: Date.now()
]
};
this.currentChallenge = updatedChallenge;
this.syncChallenge(updatedChallenge);
return updatedChallenge;
public async leaveChallenge(playerId: string): Promise<Challenge> {
if (!this.currentChallenge) {
throw new Error('当前没有参与挑战');
const updatedChallenge: Challenge = {
...this.currentChallenge,
players: this.currentChallenge.players.filter(p => p.deviceId !== playerId),
stepRecords: this.currentChallenge.stepRecords.filter(r => r.playerId !== playerId)
};
this.currentChallenge = updatedChallenge;
this.syncChallenge(updatedChallenge);
return updatedChallenge;
public async endChallenge(): Promise<Challenge> {
if (!this.currentChallenge) {
throw new Error('当前没有参与挑战');
const updatedChallenge: Challenge = {
...this.currentChallenge,
status: 'completed',
endTime: Date.now()
};
this.currentChallenge = updatedChallenge;
this.syncChallenge(updatedChallenge);
return updatedChallenge;
private handleStepData(data: health.StepData): void {
if (!this.currentChallenge) return;
const myRecord = this.currentChallenge.stepRecords.find(
=> r.playerId === this.localPlayer.deviceId
);
if (!myRecord) return;
const updatedRecord = {
...myRecord,
steps: data.count,
lastUpdate: Date.now()
};
const updatedRecords = this.currentChallenge.stepRecords.map(r =>
r.playerId === this.localPlayer.deviceId ? updatedRecord : r
);
const updatedChallenge: Challenge = {
...this.currentChallenge,
stepRecords: updatedRecords
};
this.currentChallenge = updatedChallenge;
this.syncStepUpdate({
challengeId: this.currentChallenge.id,
playerId: this.localPlayer.deviceId,
steps: data.count,
timestamp: Date.now()
});
private syncChallenge(challenge: Challenge): void {
this.dataManager.syncData('challenge_sync', {
type: 'challenge_update',
data: challenge,
timestamp: Date.now()
});
private syncStepUpdate(update: StepUpdate): void {
this.dataManager.syncData('step_sync', {
type: 'step_update',
data: update,
timestamp: Date.now()
});
private handleSyncData(data: any): void {
if (!data) return;
switch (data.type) {
case 'challenge_update':
this.handleChallengeUpdate(data.data);
break;
case 'step_update':
this.handleStepUpdate(data.data);
break;
case 'chat_message':
this.handleChatMessage(data.data);
break;
}
private handleChallengeUpdate(challenge: Challenge): void {
// 只处理当前挑战的更新
if (this.currentChallenge && this.currentChallenge.id === challenge.id) {
this.currentChallenge = challenge;
this.notifyChallengeUpdated(challenge);
}
private handleStepUpdate(update: StepUpdate): void {
if (!this.currentChallenge || this.currentChallenge.id !== update.challengeId) return;
const updatedRecords = this.currentChallenge.stepRecords.map(r =>
r.playerId === update.playerId ? { ...r, steps: update.steps, lastUpdate: update.timestamp } : r
);
const updatedChallenge: Challenge = {
...this.currentChallenge,
stepRecords: updatedRecords
};
this.currentChallenge = updatedChallenge;
this.notifyStepUpdated(update.playerId, update.steps);
private handleChatMessage(message: ChatMessage): void {
this.notifyChatMessageReceived(message);
private notifyChallengeUpdated(challenge: Challenge): void {
this.challengeListeners.forEach(listener => {
listener.onChallengeUpdated?.(challenge);
});
private notifyStepUpdated(playerId: string, steps: number): void {
this.challengeListeners.forEach(listener => {
listener.onStepUpdated?.(playerId, steps);
});
private notifyChatMessageReceived(message: ChatMessage): void {
this.challengeListeners.forEach(listener => {
listener.onChatMessage?.(message);
});
public async sendChatMessage(text: string): Promise<void> {
if (!this.currentChallenge) return;
const message: ChatMessage = {
challengeId: this.currentChallenge.id,
senderId: this.localPlayer.deviceId,
text: text,
timestamp: Date.now()
};
this.dataManager.syncData('chat_sync', {
type: 'chat_message',
data: message,
timestamp: Date.now()
});
public addListener(listener: ChallengeListener): void {
if (!this.challengeListeners.includes(listener)) {
this.challengeListeners.push(listener);
}
public removeListener(listener: ChallengeListener): void {
this.challengeListeners = this.challengeListeners.filter(l => l !== listener);
}
interface ChallengeListener {
onChallengeUpdated?(challenge: Challenge): void;
onStepUpdated?(playerId: string, steps: number): void;
onChatMessage?(message: ChatMessage): void;
export const stepChallengeService = StepChallengeService.getInstance();
主游戏界面
// GameScreen.ets
import { stepChallengeService } from ‘./StepChallengeService’;
import { PlayerInfo, Challenge, StepRecord } from ‘./StepChallengeTypes’;
@Component
export struct GameScreen {
@State hasPermission: boolean = false;
@State currentChallenge: Challenge | null = null;
@State mySteps: number = 0;
@State showCreateDialog: boolean = false;
@State showJoinDialog: boolean = false;
@State newChallengeDuration: number = 24; // 默认24小时
@State chatMessages: ChatMessage[] = [];
@State chatInput: string = ‘’;
// 模拟玩家信息
private localPlayer: PlayerInfo = {
deviceId: ‘device_001’,
nickname: ‘玩家1’,
avatar: ‘resources/rawfile/avatar1.png’
};
// 模拟其他玩家
private otherPlayers: PlayerInfo[] = [
deviceId: ‘device_002’,
nickname: '玩家2',
avatar: 'resources/rawfile/avatar2.png'
},
deviceId: ‘device_003’,
nickname: '玩家3',
avatar: 'resources/rawfile/avatar3.png'
];
build() {
Column() {
// 标题栏
Row() {
Text(‘每日步数挑战’)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Button(this.hasPermission ? '新挑战' : '授权')
.width(100)
.onClick(() => {
if (this.hasPermission) {
this.showCreateDialog = true;
else {
this.requestPermissions();
})
.padding(10)
.width('100%')
// 挑战状态显示
if (this.currentChallenge) {
Column() {
// 挑战信息
Row() {
Text('当前挑战')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
Text(this.formatRemainingTime())
.fontSize(16)
.fontColor('#FF5252')
.margin({ bottom: 15 })
// 步数排行榜
List({ space: 10 }) {
ForEach(this.getSortedRecords(), (record, index) => {
ListItem() {
Row() {
Text(${index + 1}.)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width(30)
Image(this.getPlayerAvatar(record.playerId))
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 15 })
Column() {
Text(this.getPlayerName(record.playerId))
.fontSize(16)
.fontWeight(FontWeight.Bold)
Row() {
Progress({
value: record.steps,
total: 10000,
style: ProgressStyle.Linear
})
.width('60%')
.height(8)
Text(${record.steps})
.fontSize(14)
.fontColor('#666666')
.margin({ left: 10 })
}
.layoutWeight(1)
.padding(10)
.width('100%')
})
.height(300)
.margin({ bottom: 20 })
// 我的步数
Column() {
Text('我的步数')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Row() {
Progress({
value: this.mySteps,
total: 10000,
style: ProgressStyle.Ring
})
.width(100)
.height(100)
Column() {
Text(${this.mySteps})
.fontSize(36)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(目标: 10000步)
.fontSize(16)
.fontColor('#666666')
.margin({ left: 20 })
}
.padding(20)
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ bottom: 20 })
// 聊天区域
Column() {
Text('聊天')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
if (this.chatMessages.length > 0) {
Column() {
ForEach(this.chatMessages.slice().reverse(), (message) => {
Row() {
if (message.senderId === this.localPlayer.deviceId) {
Blank()
.layoutWeight(1)
Column() {
Text(message.text)
.fontSize(14)
.padding(10)
.backgroundColor('#DCF8C6')
.borderRadius(8)
.maxWidth('70%')
Text(this.formatTime(message.timestamp))
.fontSize(12)
.fontColor('#666666')
.alignSelf(ItemAlign.End)
} else {
Column() {
Row() {
Image(this.getPlayerAvatar(message.senderId))
.width(24)
.height(24)
.borderRadius(12)
.margin({ right: 5 })
Text(this.getPlayerName(message.senderId))
.fontSize(12)
.fontColor('#666666')
Text(message.text)
.fontSize(14)
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.maxWidth('70%')
Text(this.formatTime(message.timestamp))
.fontSize(12)
.fontColor('#666666')
.alignSelf(ItemAlign.Start)
Blank()
.layoutWeight(1)
}
.margin({ bottom: 10 })
.width('100%')
})
.height(150)
.margin({ bottom: 10 })
else {
Column() {
Text('暂无消息')
.fontSize(16)
.fontColor('#666666')
.height(150)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Row() {
TextInput({ placeholder: '输入消息...', text: this.chatInput })
.layoutWeight(1)
.onChange((value: string) => {
this.chatInput = value;
})
Button('发送')
.width(80)
.margin({ left: 10 })
.onClick(() => {
this.sendChatMessage();
})
}
.padding(15)
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.width(‘100%’)
else {
Column() {
Text('没有进行中的挑战')
.fontSize(18)
.margin({ bottom: 10 })
Text('创建新挑战或加入已有挑战')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 30 })
Button('创建挑战')
.width(200)
.height(50)
.fontSize(18)
.onClick(() => {
this.showCreateDialog = true;
})
.margin({ bottom: 20 })
Button('加入挑战')
.width(200)
.height(50)
.fontSize(18)
.onClick(() => {
this.showJoinDialog = true;
})
.padding(20)
.width('90%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ top: 50 })
}
.width('100%')
.height('100%')
.padding(20)
// 创建挑战对话框
if (this.showCreateDialog) {
DialogComponent({
title: '创建新挑战',
content: this.buildCreateDialogContent(),
confirm: {
value: '创建',
action: () => this.createChallenge()
},
cancel: {
value: '取消',
action: () => {
this.showCreateDialog = false;
this.newChallengeDuration = 24;
}
})
// 加入挑战对话框
if (this.showJoinDialog) {
DialogComponent({
title: '加入挑战',
content: this.buildJoinDialogContent(),
confirm: {
value: '加入',
action: () => this.joinChallenge()
},
cancel: {
value: '取消',
action: () => {
this.showJoinDialog = false;
}
})
}
private buildCreateDialogContent(): void {
Column() {
Text(‘挑战时长 (小时)’)
.fontSize(16)
.margin({ bottom: 10 })
Slider({
value: this.newChallengeDuration,
min: 1,
max: 48,
step: 1,
style: SliderStyle.OutSet
})
.onChange((value: number) => {
this.newChallengeDuration = value;
})
Text(${this.newChallengeDuration}小时)
.fontSize(16)
.fontColor('#409EFF')
.margin({ top: 10 })
.padding(20)
private buildJoinDialogContent(): void {
Column() {
Text('输入挑战ID')
.fontSize(16)
.margin({ bottom: 10 })
TextInput({ placeholder: '挑战ID', text: this.challengeIdToJoin })
.margin({ bottom: 20 })
.padding(20)
private formatRemainingTime(): string {
if (!this.currentChallenge) return '';
const remaining = this.currentChallenge.endTime - Date.now();
if (remaining <= 0) return '已结束';
const hours = Math.floor(remaining / (1000 60 60));
const minutes = Math.floor((remaining % (1000 60 60)) / (1000 * 60));
return {hours}小时{minutes}分钟;
private getSortedRecords(): StepRecord[] {
if (!this.currentChallenge) return [];
return [...this.currentChallenge.stepRecords].sort((a, b) => b.steps - a.steps);
private getPlayerName(playerId: string): string {
if (playerId === this.localPlayer.deviceId) return this.localPlayer.nickname;
const player = this.otherPlayers.find(p => p.deviceId === playerId);
return player?.nickname || '未知玩家';
private getPlayerAvatar(playerId: string): Resource {
if (playerId === this.localPlayer.deviceId) return this.localPlayer.avatar;
const player = this.otherPlayers.find(p => p.deviceId === playerId);
return player?.avatar || $r('app.media.ic_default_avatar');
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
return {date.getHours()}:{date.getMinutes().toString().padStart(2, '0')};
aboutToAppear() {
this.checkPermissions();
stepChallengeService.addListener({
onChallengeUpdated: (challenge) => {
this.handleChallengeUpdated(challenge);
},
onStepUpdated: (playerId, steps) => {
this.handleStepUpdated(playerId, steps);
},
onChatMessage: (message) => {
this.handleChatMessage(message);
});
aboutToDisappear() {
stepChallengeService.removeListener({
onChallengeUpdated: (challenge) => {
this.handleChallengeUpdated(challenge);
},
onStepUpdated: (playerId, steps) => {
this.handleStepUpdated(playerId, steps);
},
onChatMessage: (message) => {
this.handleChatMessage(message);
});
private async checkPermissions(): Promise<void> {
try {
const permissions = [
'ohos.permission.READ_HEALTH_DATA',
'ohos.permission.DISTRIBUTED_DATASYNC'
];
const result = await abilityAccessCtrl.verifyPermissions(
getContext(),
permissions
);
this.hasPermission = result.every(perm => perm.granted);
catch (err) {
console.error('检查权限失败:', JSON.stringify(err));
this.hasPermission = false;
}
private async requestPermissions(): Promise<void> {
this.hasPermission = await stepChallengeService.requestPermissions();
if (!this.hasPermission) {
prompt.showToast({ message: '授权失败,无法使用步数挑战功能' });
}
private async createChallenge(): Promise<void> {
try {
const players = [this.localPlayer, …this.otherPlayers.slice(0, 1)]; // 默认带一个玩家
this.currentChallenge = await stepChallengeService.createChallenge(
players,
this.newChallengeDuration
);
this.showCreateDialog = false;
this.newChallengeDuration = 24;
catch (err) {
console.error('创建挑战失败:', JSON.stringify(err));
prompt.showToast({ message: '创建挑战失败,请重试' });
}
private async joinChallenge(): Promise<void> {
if (!this.challengeIdToJoin.trim()) {
prompt.showToast({ message: ‘请输入挑战ID’ });
return;
try {
this.currentChallenge = await stepChallengeService.joinChallenge(
this.challengeIdToJoin,
this.localPlayer
);
this.showJoinDialog = false;
this.challengeIdToJoin = '';
catch (err) {
console.error('加入挑战失败:', JSON.stringify(err));
prompt.showToast({ message: '加入挑战失败,请检查ID' });
}
private async leaveChallenge(): Promise<void> {
try {
this.currentChallenge = await stepChallengeService.leaveChallenge(
this.localPlayer.deviceId
);
catch (err) {
console.error('退出挑战失败:', JSON.stringify(err));
prompt.showToast({ message: '退出挑战失败,请重试' });
}
private async sendChatMessage(): Promise<void> {
if (!this.chatInput.trim()) return;
try {
await stepChallengeService.sendChatMessage(this.chatInput);
this.chatInput = '';
catch (err) {
console.error('发送消息失败:', JSON.stringify(err));
prompt.showToast({ message: '发送消息失败,请重试' });
}
private handleChallengeUpdated(challenge: Challenge): void {
this.currentChallenge = challenge;
// 更新我的步数
const myRecord = challenge.stepRecords.find(
=> r.playerId === this.localPlayer.deviceId
);
this.mySteps = myRecord?.steps || 0;
private handleStepUpdated(playerId: string, steps: number): void {
if (!this.currentChallenge) return;
this.currentChallenge = {
...this.currentChallenge,
stepRecords: this.currentChallenge.stepRecords.map(r =>
r.playerId === playerId ? { ...r, steps: steps } : r
)
};
if (playerId === this.localPlayer.deviceId) {
this.mySteps = steps;
}
private handleChatMessage(message: ChatMessage): void {
this.chatMessages = […this.chatMessages, message];
}
类型定义
// StepChallengeTypes.ets
export interface PlayerInfo {
deviceId: string;
nickname: string;
avatar: Resource | string;
export interface StepRecord {
playerId: string;
steps: number;
lastUpdate: number;
export interface Challenge {
id: string;
players: PlayerInfo[];
startTime: number;
endTime: number;
status: ‘active’ | ‘completed’;
stepRecords: StepRecord[];
export interface ChatMessage {
challengeId: string;
senderId: string;
text: string;
timestamp: number;
三、项目配置与权限
权限配置
// module.json5
“module”: {
"requestPermissions": [
“name”: “ohos.permission.READ_HEALTH_DATA”,
"reason": "读取步数数据"
},
“name”: “ohos.permission.DISTRIBUTED_DATASYNC”,
"reason": "同步挑战数据"
],
"abilities": [
“name”: “MainAbility”,
"type": "page",
"visible": true
]
}
四、总结与扩展
本步数挑战游戏实现了以下核心功能:
实时步数监测:准确获取并显示玩家步数
多玩家挑战:支持多名玩家参与同一挑战
实时排行榜:动态更新玩家步数排名
跨设备同步:多设备间实时同步步数数据
社交互动:内置聊天功能促进玩家交流
扩展方向:
成就系统:设置步数成就和奖励
团队挑战:支持分组团队对抗
健康分析:提供步数健康分析报告
历史记录:保存历次挑战数据
社交分享:分享挑战结果到社交平台
虚拟奖励:设置虚拟奖章和称号系统
通过HarmonyOS的分布式技术,我们构建了一个富有社交性和竞技性的步数挑战游戏,能够激励用户增加日常活动量,同时享受与朋友互动的乐趣。
