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%' })
}
}
}
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
}
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()
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
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
}
Modify Index.ets, call the init function.
Canvas(this.game.canvasContext)
.onReady(() => {
this.game.init() // add this line
})
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
}
ShipType
// ShipType.ets
export enum ShipType {
USER,
ENEMY
}
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
}
}
}
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)
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
}
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



Top comments (0)