鸿蒙跨端每日步数挑战游戏开发指南 原创

进修的泡芙
发布于 2025-6-22 17:43
浏览
0收藏

鸿蒙跨端每日步数挑战游戏开发指南

一、系统架构设计

基于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的分布式技术,我们构建了一个富有社交性和竞技性的步数挑战游戏,能够激励用户增加日常活动量,同时享受与朋友互动的乐趣。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐