# harmony_plane_game
**Repository Path**: lovesnsfi_admin/harmony_plane_game
## Basic Information
- **Project Name**: harmony_plane_game
- **Description**: 鸿蒙版的飞机大战
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 0
- **Created**: 2024-09-27
- **Last Updated**: 2025-06-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 面向对象游戏开发
* 项目名称:飞机大战
* 项目平台:鸿蒙OS
* 技术栈:Canvas技术,TypeScript技术,面向对象封装,ArkTS与ArkUI,事件与手势
* 目的:总结之前知识 ,将之前的东西做一个总结性的技术
### 一、界面预览
### 二、项目创建
略
### 三、项目素材

### 四、开发过程
#### 1.canvas准备
项目是通过canvas来完成的,它是一个画布,所以我要在界面上面创建一个canvas的组件
```typescript
@Entry
@Component
struct Index {
canvasCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D({ antialias: true } as RenderingContextSettings);
build() {
Column() {
Canvas(this.canvasCtx)
.width("100%")
.height("100%")
.onReady(() => {
})
}
.height('100%')
.width('100%')
}
}
```
#### 2.绘制游戏背景
在绘制游戏背景图片的时候要注意,它是一个上下扩展的2倍图片,为了实现动画的视觉差,所以我们在绘制的时候只需要绘制一半就行。
同时还要注意,图片的大小要与屏幕的大小实现等比缩放的关系,所以我们要创建缩放比例
```typescript
@Entry
@Component
struct Index {
canvasCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D({ antialias: true } as RenderingContextSettings);
build() {
Column() {
Canvas(this.canvasCtx)
.width("100%")
.height("100%")
.onReady(() => {
//准备和了一个背景图片
let bgImageBitmap = new ImageBitmap("/gameassets/background.png");
//得到比例
let ratio = this.canvasCtx.height / (bgImageBitmap.height / 2);
let bgX: number = 0;
let bgY: number = -bgImageBitmap.height * ratio / 2;
let bgWidth: number = bgImageBitmap.width * ratio;
let bgHeight: number = bgImageBitmap.height * ratio;
//画了背景
this.canvasCtx.drawImage(bgImageBitmap, bgX, bgY, bgWidth, bgHeight);
setInterval(() => {
bgY++;
this.canvasCtx.clearRect(0, 0, this.canvasCtx.width, this.canvasCtx.height);
this.canvasCtx.drawImage(bgImageBitmap, bgX, bgY, bgWidth, bgHeight);
if (bgY > 0) {
bgY = -bgImageBitmap.height * ratio / 2;
}
}, 17);
})
}
.height('100%')
.width('100%')
}
}
```
我们添加了一个定时器是为了让背景图片不停的移动,这个定时器的时间我们设置了17毫秒,因为刷新频率默认设置成了60Hz,后期在真机里面我们要根据屏幕的刷新率来完成
#### 3.绘制英雄人物
当背景图片完成以后,我们就可以绘制游戏英雄人物了,原理与上面相同
1. 准备图片
2. 绘制图片

#### 4.使用面向对象的思维考虑问题
当我们在绘制英雄飞机的时候,我们就发现了一个问题,大部分的代码都相同,我们可以把整个游戏上面所有的东西都当成对象,然后把这个对象画出来就可以了。
但是在画的时候我们需要这几个参数
1. 横坐标
2. 纵坐标
3. 图片
4. 宽度
5. 高度
但是如果要使用面向对象,需要考虑一些最基本的问题,如怎么创建游戏对象,以及怎么样保存游戏的一些配置信息,如比例,游戏的等级,尺寸大小
#### 5.游戏的配置信息
在ets的目录下面新建一个`config`的目录,然后在`config`的目录下面新建一个`GameConfig.ets`的文件,代码如下
```typescript
/**
* @name GameConfig
* @description 游戏配置对象 单例模式
* @author 杨标
*/
class GameConfig {
//1.私有的构造函数
private constructor() {
}
//2. 私有的静态变量
private static _instance: GameConfig | null = null;
//3. 公有的方法
public static getInstance():GameConfig{
if(GameConfig._instance===null){
GameConfig._instance = new GameConfig();
}
return GameConfig._instance as GameConfig;
}
ratio: number = 1;
}
export default GameConfig;
```
我们通过单例模式来配置了游戏的配置对象,里面有一个属性叫`ratio`,用于保存绘制游戏对象的比例

#### 6.创建游戏背景对象
在`ets`的目录下面创建一个目录,取名为`gameobject`,然后在这个目录下面创建一个`Background.ets`的文件
```typescript
/**
* @name Background
* @description 游戏的背景地图对象
* @author 杨标
*/
import GameConfig from "../config/GameConfig";
class Background {
x: number = 0;
y: number = 0;
img: ImageBitmap = new ImageBitmap("/gameassets/background.png");
//访问器属性
get width(): number {
return this.img.width * GameConfig.getInstance().ratio;
}
get height():number{
return this.img.height * GameConfig.getInstance().ratio;
}
draw(canvasCtx:CanvasRenderingContext2D):void{
canvasCtx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
export default Background;
```


#### 7.创建Hero玩家飞机对象
在`gameobject`目录下面创建`Hero.ets`文件,它的代码可以参考上面的`Background.ets`
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameConfig from "../config/GameConfig";
class Hero {
x: number = 0;
y: number = 0;
img: ImageBitmap = new ImageBitmap("/gameassets/hero1.png");
//访问器属性
get width(): number {
return this.img.width * GameConfig.getInstance().ratio;
}
get height():number{
return this.img.height * GameConfig.getInstance().ratio;
}
draw(canvasCtx:CanvasRenderingContext2D):void{
canvasCtx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
export default Hero;
```

当我们在完成`Hero.ets`的模块的时候,我们确实实现了效果,也实现模块与模块之间的解耦,同时也出现了另一个问题,代码的冗余量过大!
#### 8.重构对象封装的代码
在`gameobject`的目录下面创建一个`GameObject.ets`的文件,代码如下
```typescript
/**
* @name GameObject
* @description 游戏对象的根对象
* @author
*/
import GameConfig from "../config/GameConfig";
class GameObject {
x: number;
y: number;
img: ImageBitmap;
constructor(x: number, y: number, img: ImageBitmap) {
this.x = x;
this.y = y;
this.img = img;
}
get width():number{
return this.img.width * GameConfig.getInstance().ratio;
}
get height():number{
return this.img.height * GameConfig.getInstance().ratio;
}
draw(canvasCtx:CanvasRenderingContext2D):void{
canvasCtx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
export default GameObject;
```
接下来
**Background.ets代码如下**
```typescript
/**
* @name Background
* @description 游戏的背景地图对象
* @author 杨标
*/
import GameObject from "./GameObject";
class Background extends GameObject {
constructor() {
super(0, 0, new ImageBitmap("/gameassets/background.png"));
}
}
export default Background;
```
**Hero.ets代码如下**
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameObject from "./GameObject";
class Hero extends GameObject {
constructor() {
super(0,0,new ImageBitmap("/gameassets/hero1.png"))
}
}
export default Hero;
```

#### 9.背景移动
在之前的时候我们看到,如果要让背景移动,需要不停的去改变它的纵坐标,这个时候需要定时器

#### 10.记录屏幕的大小
接下来我们要记录一下屏幕的大小,方便后期的一些其它操作

当我们记录了游戏界面的大小以后,我们就可以在初始化玩家飞机的时候重新设置玩家飞机的坐标

#### 11.游戏对象的容器创建
在后期的时候,我们的游戏里面会有很多个对象,我们要统一管理游戏对象,这个时候可以在`gameobject`的目录下面创建一个`GameContainer.ets`的文件,代码如下
```typescript
/**
* @name GameContainer
* @description 游戏对象的容器,专门用来存放游戏对象
* @author 杨标
*/
import Background from './Background';
import Hero from './Hero';
class GameContainer {
private constructor() {
}
private static _instance: GameContainer | null = null;
public static getInstance(): GameContainer {
if (GameContainer._instance === null) {
GameContainer._instance = new GameContainer();
}
return GameContainer._instance as GameContainer;
}
p1: Hero | null = null;
bg: Background | null = null;
}
export default GameContainer;
```

#### 12.玩家飞机跟随手指移动

在画布上面添加`onTouch`事件

#### 13.玩家子弹的创建
玩家飞机是需要发射子弹的,它也是一个游戏对象,我们取名为`Bullet`
**创建Bullet对象**
```typescript
/**
* @name Bullet
* @description 玩家飞机的子弹
* @author 杨标
*/
import GameObject from './GameObject';
class Bullet extends GameObject {
constructor(x: number, y: number) {
super(x, y, new ImageBitmap('/gameassets/bullet1.png'));
}
speed: number = 10;
move(): void {
this.y -= this.speed;
//TODO:如果子弹移出到屏幕外边,子弹就应该消息
}
}
export default Bullet;
```
**创建子弹集合**
屏幕上面不可以只有一个子弹,应该有多个子弹,我们需要创建一个集合,用于放这个子弹,在`GameContainer.ets`里面,添加如下代码
```typescript
/**
* @name GameContainer
* @description 游戏对象的容器,专门用来存放游戏对象
* @author 杨标
*/
import Background from './Background';
import Hero from './Hero';
import Bullet from "./Bullet";
class GameContainer {
//省略部分代码........
/**
* 玩家子弹的集合
*/
bulletList: Array = [];
}
export default GameContainer;
```
**玩家发射子弹,所以我们要找玩家对象Hero**
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameObject from "./GameObject";
import GameConfig from "../config/GameConfig";
import Bullet from "./Bullet";
import GameContainer from './GameContainer';
class Hero extends GameObject {
//省略部分代码.......
/**
* 射击,发射子弹的方法
*/
shoot(): void {
//1. 飞机在哪里,子弹就在哪里
let b = new Bullet(this.x, this.y);
//2. 将子弹加入到容器里面
GameContainer.getInstance().bulletList.push(b);
}
}
export default Hero;
```
**发射子弹,绘制子弹**

这个时候,屏幕上面有子弹,但是子弹不会移动
**重写Bullet的draw的方法**
```typescript
/**
* @name Bullet
* @description 玩家飞机的子弹
* @author 杨标
*/
import GameObject from './GameObject';
class Bullet extends GameObject {
//省略部分代码......
/**
* 重写draw的方法
* @param canvasCtx
*/
draw(canvasCtx: CanvasRenderingContext2D): void {
this.move();
super.draw(canvasCtx);
}
}
export default Bullet;
```
**子弹在向上移动,当子弹移出到边界外边去了以后,我们要把子弹从集合里面,这个系统会自己销毁这个子弹**
> 鸿蒙系统,c#语言,java语言等都会有一个系统操作叫GC,全称叫垃圾回收机制,它会将系统不再需要的资源自动回收,以释放内存空间
```typescript
/**
* @name Bullet
* @description 玩家飞机的子弹
* @author 杨标
*/
import GameContainer from './GameContainer';
import GameObject from './GameObject';
class Bullet extends GameObject {
//省略部分代码........
move(): void {
this.y -= this.speed;
//TODO:如果子弹移出到屏幕外边,子弹就应
//1. 判断子弹是否移动到屏幕的外边了
if (this.y < -this.height) {
//2. 在子弹集合里面找到的索引 ,删除自己
let index = GameContainer.getInstance().bulletList.indexOf(this);
GameContainer.getInstance().bulletList.splice(index,1);
}
}
}
export default Bullet;
```
这里因为涉及到了bulletList的删除,所以会产生沙漏效应,那么之前在遍历这个集合的时候就应该改成倒序

> 上面的bulletList可以转换成bulletSet,以Set的集合来进行,这样方便一些,具体可以看代码
#### 14.修正子弹的坐标

现在的情况是子弹从飞机的左上角出去的,我们希望它从飞机的正中间出去
子弹不应该在左上角产生,应该在正中间

**Hero.ets文件**
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameObject from "./GameObject";
import GameConfig from "../config/GameConfig";
import Bullet from "./Bullet";
import GameContainer from './GameContainer';
class Hero extends GameObject {
//省略部分代码........
/**
* 射击,发射子弹的方法
*/
shoot(): void {
//1. 飞机在哪里,子弹就在哪里
let b = new Bullet(this.x, this.y);
//2. 修正子弹的坐标
b.x = this.x + this.width / 2 - b.width / 2;
b.y = this.y + this.height / 2 - b.height / 2;
//3. 将子弹加入到容器里面
GameContainer.getInstance().bulletSet.add(b);
}
}
export default Hero;
```
#### 15.敌机的创建
玩家有了,子弹有了,现在需要敌人
首先在`gameobject`的目录下面创建一个文件,取名为`Enemy`
```typescript
/**
* @name Enemy
* @description 敌人的飞机
* @author 杨标
*/
import GameConfig from '../config/GameConfig';
import GameObject from './GameObject';
class Enemy extends GameObject {
constructor() {
super(0, 0, new ImageBitmap("/gameassets/enemy0.png"));
//随机产生横坐标
this.x = Math.random() * (GameConfig.getInstance().gameWidth - this.width);
this.y = -this.height;
}
speed: number = Math.random() * 3 + 1;
move() {
this.y += this.speed;
//TODO:敌人飞机如果到移动到屏幕最下边以后,要移除自己
}
}
export default Enemy;
```
这个时候屏幕上面应该是多个敌人的,我们应该准备一个集合(与弹的过程得一样的)
```typescript
/**
* @name GameContainer
* @description 游戏对象的容器,专门用来存放游戏对象
* @author 杨标
*/
import Background from './Background';
import Hero from './Hero';
import Bullet from "./Bullet";
import Enemy from './Enemy';
class GameContainer {
//省略部分代码.......
/**
* 敌机的集合
*/
enemySet: Set = new Set();
}
export default GameContainer;
```
有了这个容器以后,就要添加敌机了,问题在于,屏幕上面最多只能添加多少敌机?这个数量应该是可变的,我们后期可以设置游戏等级,等级越高,敌机越多,游戏难度越大
```typescript
/**
* @name GameConfig
* @description 游戏配置对象 单例模式
* @author 杨标
*/
class GameConfig {
//省略部分代码......
/**
* 最大敌机数量
*/
maxEnemyCount:number = 6;
}
export default GameConfig;
```
设置最大敌人数量以后,要开始添加敌机了
**在Enemy.ets里面,封装添加敌机的方法**
```typescript
export function addEnemy() {
//1. 判断敌机数量是否够
if (GameContainer.getInstance().enemySet.size < GameConfig.getInstance().maxEnemyCount) {
// 2. 计算差多少敌机
let count = GameContainer.getInstance().enemySet.size - GameConfig.getInstance().maxEnemyCount;
//3. for循环添加敌机
for (let i = 0; i < count; i++) {
// 4. 创建了敌机对象
let e = new Enemy();
// 5. 将敌机加入到集合
GameContainer.getInstance().enemySet.add(e);
}
}
}
```

**当敌机出到屏幕的外边去以后,要从集合里面删除它**

#### 16.不同类型的敌机创建
现在是有小飞机,中飞机与大飞机,我们要随机的添加敌方的飞机
**第1步:先创建飞机类型的枚举**
我们在`ets`的目录下面创建一个`types`的文件夹,然后创建一个`EnemyType.ets`的文件,代码如下
```typescript
/**
* 定义飞机类型的枚举
*/
enum EnemyType {
BIG = 0,
MIDDLE = 1,
SMALL = 2
}
export default EnemyType;
```
**第2步:在Enemy.ets里面随机生成飞机的类型**
```typescript
/**
* @name Enemy
* @description 敌人的飞机
* @author 杨标
*/
import GameConfig from '../config/GameConfig';
import EnemyType from '../types/EnemyType';
import GameContainer from './GameContainer';
import GameObject from './GameObject';
class Enemy extends GameObject {
speed: number = 0;
/**
* 飞机制类型
*/
private _type: EnemyType = EnemyType.SMALL;
set type(v: EnemyType) {
this._type = v;
if (v === EnemyType.BIG) {
this.speed = Math.random() * 2 + 1;
this.img = new ImageBitmap("/gameassets/enemy2.png");
} else if (v === EnemyType.MIDDLE) {
this.speed = Math.random() * 3 + 2;
this.img = new ImageBitmap("/gameassets/enemy1.png");
} else if (v === EnemyType.SMALL) {
this.speed = Math.random() * 4 + 2;
this.img = new ImageBitmap("/gameassets/enemy0.png");
}
}
get type(){
return this._type;
}
constructor() {
super(0, 0, new ImageBitmap("/gameassets/enemy0.png"));
let temp = ~~(Math.random() * 100);
if (temp < 3) {
this.type = EnemyType.BIG;
} else if (temp < 13) {
this.type = EnemyType.MIDDLE;
} else {
this.type = EnemyType.SMALL;
}
//省略部分代码........
}
}
export default Enemy;
//省略部分代码........
```
> 在上面的代码里面,我们通过随机数控制了不同类型的飞机出现的概率。同时还使用了访问器属性,这样在操作type的时候会有一系列的联动操作
#### 17.玩家飞机的图片切换动画

上面有两张玩家习飞机的图片,当我们去循环切换这2张图片的时候,就会有一个动画的效果产生
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameObject from "./GameObject";
import GameConfig from "../config/GameConfig";
import Bullet from "./Bullet";
import GameContainer from './GameContainer';
class Hero extends GameObject {
/**
* 准备一张切换的备用图片
*/
switch_img: ImageBitmap = new ImageBitmap("/gameassets/hero2.png");
//省略部分代码......
//控制渲染图片的切换
private drawIndex: number = 0;
draw(canvasCtx: CanvasRenderingContext2D): void {
if (this.drawIndex < 2) {
canvasCtx.drawImage(this.switch_img, this.x, this.y, this.width, this.height);
} else if (this.drawIndex < 4) {
super.draw(canvasCtx)
}
this.drawIndex++;
if(this.drawIndex>=4){
this.drawIndex=0;
}
}
}
export default Hero;
```
#### 18.玩家飞机的双排子弹发射
```typescript
/**
* @name Hero
* @description 玩家的飞机
* @author 杨标
*/
import GameObject from "./GameObject";
import GameConfig from "../config/GameConfig";
import Bullet from "./Bullet";
import GameContainer from './GameContainer';
class Hero extends GameObject {
//省略部分代码.......
/**
* 是否具备双排子弹的buff
*/
isDoubleBuff: boolean = true;
/**
* 射击,发射子弹的方法
*/
shoot(): void {
if (this.isDoubleBuff) {
//说明这里是双排子弹,要创建两颗子弹
//1. 创建左右两颗子弹
let b_left = new Bullet(this.x, this.y);
let b_right = new Bullet(this.x, this.y);
//2. 修正子弹坐标
b_left.x = this.x + this.width / 4 - b_left.width / 2;
b_left.y = this.y + this.height / 2 - b_left.height / 2;
b_right.x = this.x + this.width / 4 * 3 - b_right.width / 2;
b_right.y = this.y + this.height / 2 - b_right.height / 2;
//3. 将子弹添加到容器里面
GameContainer.getInstance().bulletSet.add(b_left).add(b_right);
} else {
//1. 飞机在哪里,子弹就在哪里
let b = new Bullet(this.x, this.y);
//2. 修正子弹的坐标
b.x = this.x + this.width / 2 - b.width / 2;
b.y = this.y + this.height / 2 - b.height / 2;
//3. 将子弹加入到容器里面
GameContainer.getInstance().bulletSet.add(b);
}
}
}
export default Hero;
```
#### 19.子弹击中飞机
这个操作在游戏里面有一个专用词语叫碰撞检测

如上图所示,如果要判断子弹是否有击中敌机,只需要检测两个矩形是否有相交就可以了,如果两个矩形相交了就发生了碰撞

在项目的`ets`目录下面创建一个`gameUtils`的文件夹,代码如下
```typescript
/**
* @description 游戏相关的工具类
* @author 杨标
*/
import GameContainer from '../gameobject/GameContainer';
import GameObject from "../gameobject/GameObject";
/**
* 检测两个游戏对象是否有发生碰撞
* @param a 第1个游戏对象
* @param b 第2个游戏对象
* @returns true是发生碰撞,false是没有碰撞
*/
const checkCrash = (a: GameObject, b: GameObject): boolean => {
if (b.x + b.width < a.x ||
a.x + a.width < b.x ||
b.y + b.height < a.y ||
a.y + a.height < b.y) {
return false;
} else {
return true;
}
}
//检测玩家的子弹与敌方的飞机是否有发生碰撞
//玩家的子弹在在bulletSet,敌机在enemySet里面
/**
* 检测玩家子弹与敌方飞机是否发生了碰撞
*/
export const checkBulletAndEnemyCrash = (): void => {
//1.遍历所有的子弹
for (let b of GameContainer.getInstance().bulletSet) {
//2. 遍历所有的敌机
for (let e of GameContainer.getInstance().enemySet) {
//3. 检测两者之间是否有发生碰撞
let result = checkCrash(b, e);
if (result) {
//4. 说明子弹与敌机发生碰撞,也就是子弹击中了敌机
//4.1 移除子弹
GameContainer.getInstance().bulletSet.delete(b);
//4.2 TODO: 敌机血量减少
//4.3 移除敌机
GameContainer.getInstance().enemySet.delete(e);
//4.4 TODO: 添加爆炸动画
//4.5 TODO: 计分
}
}
}
}
```
在上面的代码里面,我们创建了一个玩家子弹与敌方飞机发生碰撞以后的方法,并导出了这个方法
在`Index.ets`的文件里面,导入上面的方法,再调用

#### 20.敌机生命值
不同类型的敌机应该是有不同的生命值
小飞机:2,
中飞机:4,
大飞机:10


#### 21.设置敌机的破损状态
飞机在血量不满的情况下是有破损的状态的,我们可以通过下面的代码去完成
```typescript
/**
* @name Enemy
* @description 敌人的飞机
* @author 杨标
*/
import GameConfig from '../config/GameConfig';
import EnemyType from '../types/EnemyType';
import GameContainer from './GameContainer';
import GameObject from './GameObject';
class Enemy extends GameObject {
//省略部分代码......
/**
* 当前血量
*/
life: number = 2;
/**
* 总血量
*/
totalLife: number = 2;
hit_img: ImageBitmap = new ImageBitmap("/gameassets/enemy1_hit.png");
/**
* 飞机制类型
*/
private _type: EnemyType = EnemyType.SMALL;
set type(v: EnemyType) {
this._type = v;
if (v === EnemyType.BIG) {
this.speed = Math.random() * 2 + 1;
this.img = new ImageBitmap("/gameassets/enemy2.png");
this.life = 10;
this.hit_img = new ImageBitmap("/gameassets/enemy2_hit.png")
} else if (v === EnemyType.MIDDLE) {
this.speed = Math.random() * 3 + 2;
this.img = new ImageBitmap("/gameassets/enemy1.png");
this.life = 4;
this.hit_img = new ImageBitmap("/gameassets/enemy1_hit.png");
} else if (v === EnemyType.SMALL) {
this.speed = Math.random() * 4 + 2;
this.img = new ImageBitmap("/gameassets/enemy0.png");
this.life = 2;
}
//飞柍 出生的时候,总血量与当前血量是相同的
this.totalLife = this.life;
}
get type() {
return this._type;
}
/**
* 重写draw方法
* @param canvasCtx
*/
draw(canvasCtx: CanvasRenderingContext2D): void {
if (this.life < this.totalLife) {
//说明是残血
if (this.type != EnemyType.SMALL) {
//将自己要画的图片换成破损的图片
this.img = this.hit_img;
}
}
this.move();
super.draw(canvasCtx);
}
}
export default Enemy;
//省略部分代码......
```
#### 22.游戏计分
小飞机:2
中飞机:10
大飞机:30
上面是不同类型飞机的分数
**在GameConfig.ets里面定义分值**
```typescript
/**
* @name GameConfig
* @description 游戏配置对象 单例模式
* @author 杨标
*/
class GameConfig {
//省略部分代码......
/**
* 游戏得分
*/
score:number = 0;
}
export default GameConfig;
```
**在gameUtils.ets里面,加入计分功能**
```typescript
/**
* 检测玩家子弹与敌方飞机是否发生了碰撞
*/
export const checkBulletAndEnemyCrash = (): void => {
//1.遍历所有的子弹
for (let b of GameContainer.getInstance().bulletSet) {
//2. 遍历所有的敌机
for (let e of GameContainer.getInstance().enemySet) {
//3. 检测两者之间是否有发生碰撞
let result = checkCrash(b, e);
if (result) {
//4. 说明子弹与敌机发生碰撞,也就是子弹击中了敌机
//4.1 移除子弹
GameContainer.getInstance().bulletSet.delete(b);
//4.2 TODO: 敌机血量减少
e.life--;
if (e.life <= 0) {
//4.3 移除敌机
GameContainer.getInstance().enemySet.delete(e);
//4.4 TODO: 添加爆炸动画
//4.5 TODO: 计分
if(e.type===EnemyType.SMALL){
GameConfig.getInstance().score += 2;
}
else if(e.type===EnemyType.MIDDLE){
GameConfig.getInstance().score += 10;
}
else if(e.type===EnemyType.BIG){
GameConfig.getInstance().score += 30;
}
}
}
}
}
}
```
**将分数绘制在背景上面,在Background.ets里面,代码如下**
```typescript
/**
* @name Background
* @description 游戏的背景地图对象
* @author 杨标
*/
import GameConfig from '../config/GameConfig';
import GameObject from "./GameObject";
class Background extends GameObject {
//省略部分代码.....
/**
* 在背景里面重写了draw方法
* @param canvasCtx
*/
draw(canvasCtx: CanvasRenderingContext2D): void {
// 每次画自己之前让自己移动
this.move();
super.draw(canvasCtx);
//再画得分
canvasCtx.save();
canvasCtx.fillStyle = "#FF0000";
canvasCtx.font = "24vp";
canvasCtx.textAlign = "left";
canvasCtx.textBaseline = "top";
canvasCtx.fillText(`得分:${GameConfig.getInstance().score}`,10,10);
canvasCtx.restore();
}
}
export default Background;
```
#### 23.爆炸动画
爆炸是由一系列的图片来构成的,相当于以前的帧动画
我们首先在gameobject的目录下面,新建一个对象叫`Boom.ets`
```typescript
/**
* @name Boom
* @description 爆炸动画
* @author 杨标
*/
import EnemyType from '../types/EnemyType';
import GameObject from './GameObject';
class Boom extends GameObject {
// 图片列表
imgList: Array = [];
constructor(x: number, y: number, type: EnemyType) {
// 要根据类型,决定图片
let imageList: Array = [];
if (type === EnemyType.SMALL) {
imageList = [
new ImageBitmap("/gameassets/enemy0_down1.png"),
new ImageBitmap("/gameassets/enemy0_down2.png"),
new ImageBitmap("/gameassets/enemy0_down3.png"),
new ImageBitmap("/gameassets/enemy0_down4.png"),
];
} else if (type === EnemyType.MIDDLE) {
imageList = [
new ImageBitmap("/gameassets/enemy1_down1.png"),
new ImageBitmap("/gameassets/enemy1_down2.png"),
new ImageBitmap("/gameassets/enemy1_down3.png"),
new ImageBitmap("/gameassets/enemy1_down4.png"),
];
} else if (type === EnemyType.BIG) {
imageList = [
new ImageBitmap("/gameassets/enemy2_down1.png"),
new ImageBitmap("/gameassets/enemy2_down2.png"),
new ImageBitmap("/gameassets/enemy2_down3.png"),
new ImageBitmap("/gameassets/enemy2_down4.png"),
new ImageBitmap("/gameassets/enemy2_down5.png"),
new ImageBitmap("/gameassets/enemy2_down6.png"),
];
}
super(x, y, imageList[0]);
this.imgList = imageList;
}
}
export default Boom;
```
在上面的代码里面,我们已经完成了最基本的代码
因为屏幕上面有多个敌机,也有多颗子弹,所以就可能会同时产生很多个爆炸动画,这样我们需要一个集合来装,所以我们要在`GameContainer`下面新建一个属性叫`boomSet`
```typescript
/**
* @name GameContainer
* @description 游戏对象的容器,专门用来存放游戏对象
* @author 杨标
*/
import Background from './Background';
import Hero from './Hero';
import Bullet from "./Bullet";
import Enemy from './Enemy';
import Boom from './Boom';
class GameContainer {
//省略部分代码......
/**
* 爆炸动画的集合
*/
boomSet:Set = new Set();
}
export default GameContainer;
```
接下来,在飞机死亡的地方添加爆炸动画

接下来,再屏幕上面绘制出爆炸动画
在`Index.ets`里面


这个时候的屏幕只有爆炸的图片,没有爆炸的动画,这说明原来的`draw()`方法是不满足现在的绘制要求的,我们要在`Boom.ets`里面重写`draw()`
```typescript
/**
* @name Boom
* @description 爆炸动画
* @author 杨标
*/
import EnemyType from '../types/EnemyType';
import GameContainer from './GameContainer';
import GameObject from './GameObject';
class Boom extends GameObject {
//省略部分代码......
private imgIndex: number = 0;
/**
* 重写绘制方法
* @param canvasCtx
*/
draw(canvasCtx: CanvasRenderingContext2D): void {
canvasCtx.drawImage(this.imgList[~~this.imgIndex], this.x, this.y, this.width, this.height);
this.imgIndex+=0.2;
if (this.imgIndex >= this.imgList.length) {
//说明图片画完成了,可以移除这个爆炸对象了
GameContainer.getInstance().boomSet.delete(this);
}
}
}
export default Boom;
```
> 上面,我们通过this.imgIndex+=0.2来控制每控制每张图片的渲染次数,这样动画就会更加明显一点
#### 24.添加游戏道具

目前的资源素材里面有2个道图片,这个时候至少是有2种道具类型
**GamePropType.ets**
```typescript
enum GamePropType {
/**
* 双子弹
*/
DOUBLE_BULLET = 0,
/**
* 导弹
*/
MISSILE = 1
}
export default GamePropType;
```
**GameProp.ets**
```typescript
/**
* @name GameProp
* @description 游戏道具
* @author 杨标
*/
import GameConfig from '../config/GameConfig';
import GamePropType from '../types/GamePropType'
import GameContainer from './GameContainer';
import GameObject from './GameObject'
class GameProp extends GameObject {
_type: GamePropType = GamePropType.DOUBLE_BULLET;
set type(v: GamePropType) {
this._type = v;
if (v === GamePropType.DOUBLE_BULLET) {
this.img = new ImageBitmap("/gameassets/prop_type_0.png");
} else if (v === GamePropType.MISSILE) {
this.img = new ImageBitmap("/gameassets/prop_type_1.png");
}
}
constructor() {
super(0, 0, new ImageBitmap('/gameassets/prop_type_0.png'))
let temp = ~~(Math.random() * 100);
if (temp < 50) {
this.type = GamePropType.DOUBLE_BULLET;
} else {
this.type = GamePropType.MISSILE;
}
this.x = Math.random() * (GameConfig.getInstance().gameWidth - this.width);
this.y = -this.height;
}
speed: number = Math.random() * 2 + 2;
move() {
this.y += this.speed;
//移动到屏幕外边的倒道我们应该移除容器
if (this.y > GameConfig.getInstance().gameHeight) {
GameContainer.getInstance().gamePropSet.delete(this);
}
}
draw(canvasCtx: CanvasRenderingContext2D): void {
this.move();
super.draw(canvasCtx);
}
}
export default GameProp
```
**gameUtils.ets**
```typescript
/**
* 根据概率添加道具
*/
export const addGameProp = (): void => {
if (GameContainer.getInstance().gamePropSet.size === 0) {
//千分之三的概率产生道具
let temp = ~~(Math.random() * 1000);
if (temp < 300) {
let _gameProp = new GameProp();
GameContainer.getInstance().gamePropSet.add(_gameProp);
}
}
}
```
**Index.ets**


#### 25.道具拾取
当玩家的飞机与道具碰撞以后,应该实现道具的功能,所以玩家飞机与倒具应该也有一个碰撞检测
**gameUtils.ets代码**
```typescript
/**
* 检测玩家飞机与道具的碰撞
*/
export const checkHeroAndGamePropCrash = (): void => {
if (GameContainer.getInstance().p1) {
let p1 = GameContainer.getInstance().p1 as Hero;
for (let g of GameContainer.getInstance().gamePropSet) {
let result = checkCrash(g, p1)
if (result) {
//玩家与道具发生碰撞
//1. 判断道具类型
if (g.type === GamePropType.DOUBLE_BULLET) {
//2. 双倍子弹
p1.isDoubleBuff = true;
//3. 设置一定时器,10秒以后恢复单排
setTimeout(() => {
p1.isDoubleBuff = false;
}, 10000);
} else if (g.type === GamePropType.MISSILE) {
//4.导弹数量变化
GameConfig.getInstance().addMissileCount();
}
//5. 移除道具
GameContainer.getInstance().gamePropSet.clear();
}
}
}
}
```
**GameConfig.ets代码**
```typescript
/**
* @name GameConfig
* @description 游戏配置对象 单例模式
* @author 杨标
*/
class GameConfig {
//省略部分代码
private missileCount: number = 0;
/**
* 增加一个导弹数量
*/
addMissileCount(): void {
this.missileCount++;
}
/**
* 减少一个导弹数量
*/
subMissileCount(): void {
this.missileCount--;
}
getMissileCount(): number {
return this.missileCount;
}
}
export default GameConfig;
```

#### 26.导弹道具功能的实现
**gameConfig.ets**
```typescript
/**
* @name GameConfig
* @description 游戏配置对象 单例模式
* @author 杨标
*/
class GameConfig {
//省略部分代码
/**
* 是否暂停添加敌机
*/
isPauseAddEnemy:boolean = false;
}
export default GameConfig;
```
**gameUtils.ets**
```typescript
/**
* 清除,销毁所有的敌人
*/
export const clearAllEnemy = (): void => {
// 1. 检查导弹的道具是否存在
if (GameConfig.getInstance().getMissileCount() > 0) {
//2.暂停添加敌机
GameConfig.getInstance().isPauseAddEnemy = true;
setTimeout(() => {
GameConfig.getInstance().isPauseAddEnemy = false;
}, 3000);
//3. 遍历所有的敌机
for (let e of GameContainer.getInstance().enemySet) {
e.life = 0;
e.die();
}
GameConfig.getInstance().subMissileCount();
}
}
export function addEnemy(): void {
//判断是否暂停添加敌机了
if (GameConfig.getInstance().isPauseAddEnemy) {
return;
}
//1. 判断敌机数量是否够
if (GameContainer.getInstance().enemySet.size < GameConfig.getInstance().maxEnemyCount) {
// 2. 计算差多少敌机
let count = GameConfig.getInstance().maxEnemyCount - GameContainer.getInstance().enemySet.size;
//3. for循环添加敌机
for (let i = 0; i < count; i++) {
// 4. 创建了敌机对象
let e = new Enemy();
// 5. 将敌机加入到集合
GameContainer.getInstance().enemySet.add(e);
}
}
}
```
