DEV Community

HarmonyOS
HarmonyOS

Posted on

Building Space Shooter with ArkTS-1

Read the original article:Building Space Shooter with ArkTS-1

Introduction

Hello, fellow developers. Let's give a little break and explore the endless beauty of the space. But be careful, there can be enemies.

We will use our imagination and the Canvas to build the retro space shooter game.

Basic rules:

  • Users gain score with every hit.
  • Users start with 3 lives.
  • Users lose life if enemies pass through them.

UI

Let's create the user interface. We will not complicate our lives; we will keep it simple but good.

@Entry
@Component
struct Index {
  canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D()
  score: number = 0
  lives: number = 3

  build() {
    Stack() {
      RelativeContainer() 
        Canvas(this.canvasContext)
          .backgroundColor(Color.Grey) // to see the canvas, remove later
          .size({ width: '70%', height: '70%' })
          .alignRules({
            center: { anchor: '__container__', align: VerticalAlign.Center },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onReady(() => {
          })

        Column() {
          Text(this.score.toString())
        }
        .height('15%')
        .alignRules({
          top: { anchor: '__container__', align: VerticalAlign.Top },
          middle: { anchor: '__container__', align: HorizontalAlign.Center },
        })
        .justifyContent(FlexAlign.Center)

        Stack() {
          SymbolGlyph($r('sys.symbol.heart_fill'))
            .fontColor([Color.Red]).fontSize(24)
          Text(`${this.lives}`)
        }
        .width('15%')
        .alignRules({
          left: { anchor: '__container__', align: HorizontalAlign.Start },
          center: { anchor: '__container__', align: VerticalAlign.Center },
        })
      }
      .size({ width: '100%', height: '100%' })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The result should look like this:

Game

Time to add some action. We will start by defining the game controller. Create Game.ets under the viewmodel directory. This will store our state and control the game.

Define the Game class with the state parameters.

export default class Game {
  canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D()
  score: number = 0
  lives: number = 3
}
Enter fullscreen mode Exit fullscreen mode

Modify Index.ets to use the Game.

  • Remove state parameters.
  • Create a Game object.
  • Use game.canvasContext, game.score, and game.lives.
@State game: Game = new Game()
Enter fullscreen mode Exit fullscreen mode

One of the most needed information while working with the canvas is its width and height, which can be known after the canvas is ready. Let's store them for later use.

Define parameters:

// Game.ets
private canvasWidth: number = 0
private canvasHeight: number = 0
Enter fullscreen mode Exit fullscreen mode

Define the init function. We will call this from the onready function of the canvas.

init() {
  this.canvasWidth = this.canvasContext.width
  this.canvasHeight = this.canvasContext.height
}
Enter fullscreen mode Exit fullscreen mode

Modify Index.ets, call the init function.

Canvas(this.game.canvasContext)
  .onReady(() => {
    this.game.init() // add this line
  })
Enter fullscreen mode Exit fullscreen mode

Do you hear the call of the void? Let's answer!

Ship

We will create pixel art ships for our users and enemies.

But first, let's define some helpers.

Point

// Point.ets
export default interface Point {
  x: number,
  y: number
}
Enter fullscreen mode Exit fullscreen mode

ShipType

// ShipType.ets
export enum ShipType {
  USER,
  ENEMY
}
Enter fullscreen mode Exit fullscreen mode

Back to our ship;

import Point from './Point';
import { ShipType } from './ShipType';

export default class Ship {
  private leftTop: Point;
  private type: ShipType;
  private pixelSize: number;
  private pixelData = '--r--\n' + '-www-\n' + 'wwbww\n' 
                      + 'wbwbw\n' + 'wwwww\n' + '-oro-';

  constructor(canvasW: number, canvasH: number, 
              pixelSize: number, type: ShipType) {
    this.pixelSize = pixelSize
    this.type = type

    this.leftTop =
      this.type === ShipType.USER ?
        { x: canvasW / 2 - pixelSize * 2.5, y: canvasH - pixelSize * 6 } :
        { 
          x: Math.min(Math.random() * canvasW, canvasW - pixelSize * 5),
          y: 0 - pixelSize * 6 
        }
  }

  private getColor(c: string) {
    if (c === 'r') {
      return '#FF0000'
    } else if (c === 'o') {
      return '#FFA500'
    } else if (c === 'b') {
      return this.type === ShipType.USER ? '#00FF00' : '#0000FF'
    }
    return '#FFFFFF'
  }

  draw(ctx: CanvasRenderingContext2D) {
    let x = this.leftTop.x
    let y = this.leftTop.y

    const rows = this.type === ShipType.USER ? 
                 this.pixelData.split('\n') : 
                 this.pixelData.split('\n').reverse()

    for (let row = 0; row < rows.length; row++) {
      const column = rows[row].split('');

      for (let col = 0; col < column.length; col++) {
        const c = column[col];
        if (c !== '-') {
          ctx.fillStyle = this.getColor(c)
          ctx.fillRect(x, y, this.pixelSize, this.pixelSize)
        }
        x += this.pixelSize
      }
      x = this.leftTop.x
      y += this.pixelSize
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Back to Game; initialize the user's ship.

// define parameters
private PIXELSIZE = 3 // size of each pixel
userShip: Ship | undefined = undefined

// initialize in init()
this.userShip =
  new Ship(this.canvasWidth, this.canvasHeight, this.PIXELSIZE, ShipType.USER)
Enter fullscreen mode Exit fullscreen mode

Iteration

We reached the one most important point: the iteration. This is the place where we will make movements, draw everything to the canvas, and more.

// Game.ets
// define number of frames per second
private FRAMECOUNT = 32
private intervalId: number | undefined;

// define iteration function
private iterateGame() {
  if (this.userShip === undefined) {
    console.error('You messed up!');
    return
  }
  // clear canvas
  this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

  // draw user
  this.userShip?.draw(this.canvasContext)
}

// create interval in init() to call iterateGame
init() {
  this.intervalId = setInterval(() => {
    this.iterateGame()
  }, 1000 / this.FRAMECOUNT) // 32 times per second
}
Enter fullscreen mode Exit fullscreen mode

Now we can see our ship in space.

Conclusion

We have created the main user interface and implemented core functions for our game. We will add enemies, bullets, and more in the next parts.

See you all in new adventures. :)

~ Fortuna Favet Fortibus

Written by Mehmet Karaaslan

Top comments (0)