# 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,事件与手势 * 目的:总结之前知识 ,将之前的东西做一个总结性的技术 ### 一、界面预览 image-20240926103045780 ### 二、项目创建 略 ### 三、项目素材 ![image-20240926103111643](assets/81面向对象游戏开发/image-20240926103111643.png) ### 四、开发过程 #### 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,后期在真机里面我们要根据屏幕的刷新率来完成 image-20240926103411570 #### 3.绘制英雄人物 当背景图片完成以后,我们就可以绘制游戏英雄人物了,原理与上面相同 1. 准备图片 2. 绘制图片 ![image-20240926103558864](assets/81面向对象游戏开发/image-20240926103558864.png) #### 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`,用于保存绘制游戏对象的比例 ![image-20240926110337306](assets/81面向对象游戏开发/image-20240926110337306.png) #### 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; ``` ![image-20240926112319414](assets/81面向对象游戏开发/image-20240926112319414.png) ![image-20240926112644861](assets/81面向对象游戏开发/image-20240926112644861.png) #### 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; ``` ![image-20240926114104230](assets/81面向对象游戏开发/image-20240926114104230.png) 当我们在完成`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; ``` ![image-20240926115436575](assets/81面向对象游戏开发/image-20240926115436575.png) #### 9.背景移动 在之前的时候我们看到,如果要让背景移动,需要不停的去改变它的纵坐标,这个时候需要定时器 ![image-20240926134636549](assets/81面向对象游戏开发/image-20240926134636549.png) #### 10.记录屏幕的大小 接下来我们要记录一下屏幕的大小,方便后期的一些其它操作 ![image-20240926135657704](assets/81面向对象游戏开发/image-20240926135657704.png) 当我们记录了游戏界面的大小以后,我们就可以在初始化玩家飞机的时候重新设置玩家飞机的坐标 ![image-20240926135912251](assets/81面向对象游戏开发/image-20240926135912251.png) #### 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; ``` ![image-20240926141852876](assets/81面向对象游戏开发/image-20240926141852876.png) #### 12.玩家飞机跟随手指移动 ![image-20240926143130615](assets/81面向对象游戏开发/image-20240926143130615.png) 在画布上面添加`onTouch`事件 ![image-20240926143146928](assets/81面向对象游戏开发/image-20240926143146928.png) #### 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; ``` **发射子弹,绘制子弹** ![image-20240926153631895](assets/81面向对象游戏开发/image-20240926153631895.png) 这个时候,屏幕上面有子弹,但是子弹不会移动 **重写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的删除,所以会产生沙漏效应,那么之前在遍历这个集合的时候就应该改成倒序 ![image-20240926154424696](assets/81面向对象游戏开发/image-20240926154424696.png) > 上面的bulletList可以转换成bulletSet,以Set的集合来进行,这样方便一些,具体可以看代码 #### 14.修正子弹的坐标 ![image-20240926154829416](assets/81面向对象游戏开发/image-20240926154829416.png) 现在的情况是子弹从飞机的左上角出去的,我们希望它从飞机的正中间出去 image-20240926154933466 子弹不应该在左上角产生,应该在正中间 ![image-20240926155209366](assets/81面向对象游戏开发/image-20240926155209366.png) **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); } } } ``` ![image-20240926165455380](assets/81面向对象游戏开发/image-20240926165455380.png) **当敌机出到屏幕的外边去以后,要从集合里面删除它** ![image-20240927083212895](assets/81面向对象游戏开发/image-20240927083212895.png) #### 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.玩家飞机的图片切换动画 ![image-20240927085932167](assets/81面向对象游戏开发/image-20240927085932167.png) 上面有两张玩家习飞机的图片,当我们去循环切换这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.子弹击中飞机 这个操作在游戏里面有一个专用词语叫碰撞检测 ![image-20240927094609509](assets/81面向对象游戏开发/image-20240927094609509.png) 如上图所示,如果要判断子弹是否有击中敌机,只需要检测两个矩形是否有相交就可以了,如果两个矩形相交了就发生了碰撞 ![image-20240927095003686](assets/81面向对象游戏开发/image-20240927095003686.png) 在项目的`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`的文件里面,导入上面的方法,再调用 ![image-20240927100730667](assets/81面向对象游戏开发/image-20240927100730667.png) #### 20.敌机生命值 不同类型的敌机应该是有不同的生命值 小飞机:2, 中飞机:4, 大飞机:10 ![image-20240927104134269](assets/81面向对象游戏开发/image-20240927104134269.png) ![image-20240927104159390](assets/81面向对象游戏开发/image-20240927104159390.png) #### 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; ``` 接下来,在飞机死亡的地方添加爆炸动画 ![image-20240927112127507](assets/81面向对象游戏开发/image-20240927112127507.png) 接下来,再屏幕上面绘制出爆炸动画 在`Index.ets`里面 ![image-20240927112359861](assets/81面向对象游戏开发/image-20240927112359861.png) ![image-20240927112408552](assets/81面向对象游戏开发/image-20240927112408552.png) 这个时候的屏幕只有爆炸的图片,没有爆炸的动画,这说明原来的`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.添加游戏道具 ![image-20240927152406799](assets/81面向对象游戏开发/image-20240927152406799.png) 目前的资源素材里面有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** ![image-20240927154021634](assets/81面向对象游戏开发/image-20240927154021634.png) ![image-20240927154031987](assets/81面向对象游戏开发/image-20240927154031987.png) #### 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; ``` ![image-20240927162855362](assets/81面向对象游戏开发/image-20240927162855362.png) #### 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); } } } ``` ![image-20240927190849611](assets/81面向对象游戏开发/image-20240927190849611.png)