HarmonyOS NEXT Development Case Study: Whac-A-Mole Game

This article demonstrates how to implement a classic "Whac-A-Mole" game using HarmonyOS NEXT's ArkUI framework. The implementation showcases various UI components, animation handling, and state management capabilities. Core Code Implementation 1. Hamster Component (Game Character) import { curves, window } from '@kit.ArkUI' @Component struct Hamster { @Prop cellWidth: number // Cell width for layout calculations build() { Stack() { // Stack layout container // Body Text() .width(`${this.cellWidth / 2}lpx`) .height(`${this.cellWidth / 3 * 2}lpx`) .backgroundColor("#b49579") .borderRadius({ topLeft: '50%', topRight: '50%' }) .borderColor("#2a272d") .borderWidth(1) // Mouth Ellipse() .width(`${this.cellWidth / 4}lpx`) .height(`${this.cellWidth / 5}lpx`) .fillOpacity(1) .fill("#e7bad7") .stroke("#563e3f") .strokeWidth(1) .margin({ top: `${this.cellWidth / 6}lpx`) // Left eye Ellipse() .width(`${this.cellWidth / 9}lpx`) .height(`${this.cellWidth / 6}lpx`) .fillOpacity(1) .fill("#313028") .stroke("#2e2018") .strokeWidth(1) .margin({ bottom: `${this.cellWidth / 3}lpx`, right: `${this.cellWidth / 6}lpx`) // Right eye Ellipse() .width(`${this.cellWidth / 9}lpx`) .height(`${this.cellWidth / 6}lpx`) .fillOpacity(1) .fill("#313028") .stroke("#2e2018") .strokeWidth(1) .margin({ bottom: `${this.cellWidth / 3}lpx`, left: `${this.cellWidth / 6}lpx`) // Eye highlights Ellipse() .width(`${this.cellWidth / 20}lpx`) .height(`${this.cellWidth / 15}lpx`) .fillOpacity(1) .fill("#fefbfa") .margin({ bottom: `${this.cellWidth / 2.5}lpx`, right: `${this.cellWidth / 6}lpx`) Ellipse() .width(`${this.cellWidth / 20}lpx`) .height(`${this.cellWidth / 15}lpx`) .fillOpacity(1) .fill("#fefbfa") .margin({ bottom: `${this.cellWidth / 2.5}lpx`, left: `${this.cellWidth / 6}lpx`) }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`) } } 2. Game Cell Management @ObservedV2 class Cell { @Trace scaleOptions: ScaleOptions = { x: 1, y: 1 }; @Trace isSelected: boolean = false cellWidth: number selectTime: number = 0 constructor(cellWidth: number) { this.cellWidth = cellWidth } setSelectedTrueTime() { this.selectTime = Date.now() this.isSelected = true } checkTime(stayDuration: number) { if (this.isSelected && Date.now() - this.selectTime >= stayDuration) { this.selectTime = 0 this.isSelected = false } } } 3. Game Timer Implementation class MyTextTimerModifier implements ContentModifier { applyContent(): WrappedBuilder { return wrapBuilder(buildTextTimer) } } @Builder function buildTextTimer(config: TextTimerConfiguration) { Column() { Stack({ alignContent: Alignment.Center }) { Circle({ width: 150, height: 150 }) .fill(config.started ? (config.isCountDown ? 0xFF232323 : 0xFF717171) : 0xFF929292) Column() { Text(config.isCountDown ? "Countdown" : "Elapsed Time").fontColor(Color.White) Text( (config.isCountDown ? "Remaining: " : "Elapsed: ") + (config.isCountDown ? Math.max(config.count/1000 - config.elapsedTime/100, 0).toFixed(0) : (config.elapsedTime/100).toFixed(0)) + "s" ).fontColor(Color.White) } } } } Key Implementation Points 1. Responsive Layout System Uses lpx units for dynamic sizing Flexible layout calculations based on cell dimensions Adaptive component positioning using margin properties 2. Animation System Spring animation curve implementation: curves.springCurve(10, 1, 228, 30) Scale transformations for hit effects Timed visibility control for game elements 3. Game State Management @ObservedV2 decorator for observable classes @trace for state tracking Fisher-Yates shuffle algorithm for random position selection: for (let i = 0; i

May 11, 2025 - 02:46
 0
HarmonyOS NEXT Development Case Study: Whac-A-Mole Game

Image description

This article demonstrates how to implement a classic "Whac-A-Mole" game using HarmonyOS NEXT's ArkUI framework. The implementation showcases various UI components, animation handling, and state management capabilities.

Core Code Implementation

1. Hamster Component (Game Character)

import { curves, window } from '@kit.ArkUI'

@Component
struct Hamster {
  @Prop cellWidth: number // Cell width for layout calculations

  build() {
    Stack() { // Stack layout container
      // Body
      Text()
        .width(`${this.cellWidth / 2}lpx`)
        .height(`${this.cellWidth / 3 * 2}lpx`)
        .backgroundColor("#b49579")
        .borderRadius({ topLeft: '50%', topRight: '50%' })
        .borderColor("#2a272d")
        .borderWidth(1)

      // Mouth
      Ellipse()
        .width(`${this.cellWidth / 4}lpx`)
        .height(`${this.cellWidth / 5}lpx`)
        .fillOpacity(1)
        .fill("#e7bad7")
        .stroke("#563e3f")
        .strokeWidth(1)
        .margin({ top: `${this.cellWidth / 6}lpx`)

      // Left eye
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)
        .height(`${this.cellWidth / 6}lpx`)
        .fillOpacity(1)
        .fill("#313028")
        .stroke("#2e2018")
        .strokeWidth(1)
        .margin({ bottom: `${this.cellWidth / 3}lpx`, right: `${this.cellWidth / 6}lpx`)

      // Right eye
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)
        .height(`${this.cellWidth / 6}lpx`)
        .fillOpacity(1)
        .fill("#313028")
        .stroke("#2e2018")
        .strokeWidth(1)
        .margin({ bottom: `${this.cellWidth / 3}lpx`, left: `${this.cellWidth / 6}lpx`)

      // Eye highlights
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)
        .height(`${this.cellWidth / 15}lpx`)
        .fillOpacity(1)
        .fill("#fefbfa")
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, right: `${this.cellWidth / 6}lpx`)

      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)
        .height(`${this.cellWidth / 15}lpx`)
        .fillOpacity(1)
        .fill("#fefbfa")
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, left: `${this.cellWidth / 6}lpx`)
    }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`)
  }
}

2. Game Cell Management

@ObservedV2
class Cell {
  @Trace scaleOptions: ScaleOptions = { x: 1, y: 1 };
  @Trace isSelected: boolean = false
  cellWidth: number
  selectTime: number = 0

  constructor(cellWidth: number) {
    this.cellWidth = cellWidth
  }

  setSelectedTrueTime() {
    this.selectTime = Date.now()
    this.isSelected = true
  }

  checkTime(stayDuration: number) {
    if (this.isSelected && Date.now() - this.selectTime >= stayDuration) {
      this.selectTime = 0
      this.isSelected = false
    }
  }
}

3. Game Timer Implementation

class MyTextTimerModifier implements ContentModifier<TextTimerConfiguration> {
  applyContent(): WrappedBuilder<[TextTimerConfiguration]> {
    return wrapBuilder(buildTextTimer)
  }
}

@Builder
function buildTextTimer(config: TextTimerConfiguration) {
  Column() {
    Stack({ alignContent: Alignment.Center }) {
      Circle({ width: 150, height: 150 })
        .fill(config.started ? (config.isCountDown ? 0xFF232323 : 0xFF717171) : 0xFF929292)
      Column() {
        Text(config.isCountDown ? "Countdown" : "Elapsed Time").fontColor(Color.White)
        Text(
          (config.isCountDown ? "Remaining: " : "Elapsed: ") + 
          (config.isCountDown ?
            Math.max(config.count/1000 - config.elapsedTime/100, 0).toFixed(0) :
            (config.elapsedTime/100).toFixed(0)) + "s"
        ).fontColor(Color.White)
      }
    }
  }
}

Key Implementation Points

1. Responsive Layout System

  • Uses lpx units for dynamic sizing
  • Flexible layout calculations based on cell dimensions
  • Adaptive component positioning using margin properties

2. Animation System

  • Spring animation curve implementation:
  curves.springCurve(10, 1, 228, 30)
  • Scale transformations for hit effects
  • Timed visibility control for game elements

3. Game State Management

  • @ObservedV2 decorator for observable classes
  • @trace for state tracking
  • Fisher-Yates shuffle algorithm for random position selection:
  for (let i = 0; i < availableIndexList.length; i++) {
    let index = Math.floor(Math.random() * (availableIndexList.length - i))
    // Swap positions
  }

4. Game Configuration

  • Customizable parameters:
    • gameDuration: Total game time
    • appearanceCount: Moles per wave
    • animationInterval: Spawn interval
    • hamsterStayDuration: Visible duration

Game Features

  1. Landscape orientation setup:
   windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE)
  1. Score tracking system
  2. Configurable game parameters
  3. Visual feedback mechanisms:
    • Scale animations
    • Color state indicators
  4. Game completion dialog:
   showAlertDialog({ title: 'Game Over', message: `Score: ${currentScore}` })

This implementation demonstrates HarmonyOS NEXT's capabilities in building interactive games with complex state management and smooth animations. The component-based architecture allows for maintainable code structure while leveraging ArkUI's declarative syntax for efficient UI updates.

Developers can extend this foundation by:

  • Adding sound effects
  • Implementing difficulty levels
  • Creating progressive animation systems
  • Adding multiplayer support
  • Integrating with device sensors for alternative input methods

The complete code demonstrates best practices in HarmonyOS application development, including responsive design, state management, and user interaction handling.