DEV Community

Cover image for HarmonyOS NEXT Development Case Study: Whac-A-Mole Game
zhongcx
zhongcx

Posted on

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`)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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
  }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  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}` })
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)