鸿蒙跨端每日步数挑战游戏开发指南 原创
鸿蒙跨端每日步数挑战游戏开发指南
一、系统架构设计
基于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的分布式技术,我们构建了一个富有社交性和竞技性的步数挑战游戏,能够激励用户增加日常活动量,同时享受与朋友互动的乐趣。




















