关于微服务:微信小游戏爆发式增长如何保证小游戏的版本迭代又快又稳

72次阅读

共计 20926 个字符,预计需要花费 53 分钟才能阅读完成。

导语 | 以《羊了个羊》为代表的微信小游戏在去年屡次刷屏,引爆全网。近期又有几款微信小游戏成为热门,一度让“微信小游戏”热度指数上涨 20% 以上。微信小游戏市场始终都充斥着心愿与竞争,开发者如何在爆品争霸中怀才不遇呢?在小游戏开发中有哪些传统开发教训能够借鉴与学习呢?咱们特邀腾讯云 TVP、计算机作家 / 讲师 李艺老师,在他新书《微信小游戏开发》的根底上带咱们看看在微信小游戏我的项目开发中,从架构师角度如何利用面向对象和软件设计思维和设计模式。

作者简介

李艺,腾讯云 TVP、日行一课联结创始人兼 CTO,极客工夫视频专栏《微信小程序全栈开发实战》讲师,一汽大众等知名企业内训培训讲师。具备近 20 年互联网软件研发教训,参加研发的音视频直播产品曾在腾讯 QQ 上线,为数千万人应用。是国内晚期闪客之一,曾自定义课件规范并实现全平台教育课件产品研发,官网评定为 Adobe 中国十五位社区管理员之一。同时,还是中国人工智能学会会员,在北京协同翻新研究院负责过人工智能我的项目的研发。业余喜爱写作,在微信公众号 / 视频号“艺述论”分享技术教训,著有《微信小游戏开发》、《小程序从 0 到 1:微信全栈工程师一本通》等计算机图书。

引言

去年 9 月,微信小游戏《羊了个羊》火爆全网,用户访问量骤增时甚至呈现过屡次宕机,其火爆水平远超预期。其实,微信小游戏开发整体而言简略、独立、易上手,即便单人也能够实现开发,不少程序员都是独立的微信小游戏开发者。《羊了个羊》微信小游戏的炽热,吸引了很多前端开发者向这个畛域转行。

为什么要在游戏开发中应用设计模式呢?

一般而言,游戏开发作为创意行业,不仅要有过硬的技术,更要有离奇的想法。尤其当任何一个创意火爆后,马上就会引发泛滥开发厂商疾速跟进。这在游戏行业的开发史上,曾经呈现过屡次后来者居上的案例了。

那么咱们该怎么应答这种状况呢?如果他人跑得快,就要想方法比他人跑得更快,跑得更久。游戏开发和其余所有软件产品的开发一样,并不是一锤子买卖,在第一个版本上线当前,后续依据玩家反馈和竞品性能的降级,须要一直研发和推出新版本。

在版本迭代的过程中,怎么样让新性能更快地开发进去,同时老性能还能更大范畴地保持稳定,这是最考验游戏架构师能力的。架构师在我的项目启动的时候,就要为后续可能的变动预留计划,让前面游戏版本的迭代进行得又快、又稳。这波及游戏架构师的一项外围能力:渐进式模块化重构与面向对象重构的能力。

软件开发是有成熟的套路的,前辈大牛通过实际总结的设计模式便是套路的结晶,无意识地在游戏开发中使用成熟的设计模式,不仅能够彰显程序员的内功程度,还能在肯定水平上保障版本迭代的疾速与稳固。

小游戏实战我的项目介绍

接下来分享的,是来自《微信小游戏开发》这本书中的一个小游戏实战案例,我的项目在基本功能开发完后,为了不便读者锻炼渐进式模块化重构与面向对象重构的能力,特意在这个阶段安顿了设计模式实战。

在目前的我的项目中,有两类碰撞检测:一类产生在球与挡板之间;另一类产生在球与屏幕边界之间。在游戏中,碰撞检测是十分常见一种性能,为了应答可能减少的碰撞检测需要,咱们应用设计模式将两类碰撞的耦合性升高,不便后续退出的碰撞与被碰撞对象。

具体从实现上来讲,咱们筹备利用桥接模式,将产生碰撞的单方,别离定义为两个能够独立变动的形象对象(HitObjectRectangle 与 HitedObjectRectangle),而后再让它们的具体实现局部独立变动,以此实现对桥接模式的利用。

目前球(Ball)与挡板(Panel)还没有基类,咱们能够让它们继承于新创建的形象基类,但这样并不是很正当,它们都属于可视化对象,如果要继承,更应该继承于 Component 基类。在 JS 中一个类的继承只能实现单继承,不能让一个类同时继承于多个基类,在这种状况下咱们怎么实现桥接模式中的形象局部呢?对象能力的扩大模式,除了继承,还有复合,咱们能够将定义好的桥接模式中的具体实现局部,以类属性的形式放在球和挡板对象中。

模式利用之桥接模式

在利用桥接模式之前,咱们首先须要把握它的概念,从定义动手。其实,桥接模式是一种结构型设计模式,可将一系列严密相干的类拆分为形象和实现两个独立的层次结构,从而能在开发时别离应用。

换言之,桥接模式将对象的形象局部与它的具体实现局部拆散,使它们都能够独立的变动。在桥接模式中,个别包含两个形象局部和两个具体实现的局部,一个形象局部和一个具体实现局部为一组,一共有两组,两组通过两头的形象局部进行桥接,从而让两组的具体实现局部能够绝对独立自在的变动。

为了更好地了解这个模式,咱们通过一张图看一个利用示例,如图 1 所示:

图 1,桥接模式示例示意图

在这张图中,两头是一个跨平台开发框架,它为开发者抽离出一套通用接口(形象局部 B),这些接口是通用的、零碎无关的,借此开发框架实现了跨平台个性。在开发框架中,具体到每个零碎(Mac、Windows 和 Linux),每个接口及 UI 有不同的实现(具体实现局部 B1、B2、B3)。右边,在应用程序中,开发者在软件中定义了一套形象局部 A,在每个零碎上有不同的具体实现(具体实现局部 A1、A2、A3)。应用程序面向形象局部 B 编程,不用关怀开发框架在每个零碎下的具体实现;应用程序的具体实现局部 A1、A2、A3 是基于形象局部 A 编程的,它们也不须要晓得形象局部 B。形象局部 A 与形象局部 B 之间好像有一个桥连贯了起来,这两套形象局部与其具体实现局部出现的模式便是桥接模式。

试想一下,如果咱们不应用桥接模式,没有两头这一层跨平台开发框架,没有形象局部 B 和形象局部 A,这时候咱们想实现具体实现局部 A1、A2、A3,须要怎么做呢?间接在各个系统的根底类库上实现呢?让 A1 与 B1 耦合、A2 与 B2 耦合、A3 与 B3 耦合吗?每次在应用程序中增加一个新性能,都要在三个中央别离实现。而有了桥接模式之后,B1、B2、B3 都不须要关怀了,只须要晓得形象局部 B 就能够了;增加新性能时,只须要在形象局部 A 中定义并基于形象局部 B 实现外围性能就能够了,在具体实现局部 A1、A2、A3 中只是 UI 和交互方式不同而已。这是应用桥接模式的价值。

桥接模式的具体实现

接下来便进入实际步骤,咱们先定义桥接模式当中的形象局部,一个是被动撞击对象的形象局部(HitObjectRectangle),一个是被动撞击对象的形象局部(HitedObjectRectangle)。因为两个局部的形象局部具备相似性,咱们能够先定义一个形象局部的基类 Rectangle:


1.  // JS:src\views\hitTest\rectangle.js
2.  /** 对象的矩形形容,默认将注册点放在左上角 */
3.  class Rectangle {4.    constructor(x, y, width, height) {
5.      this.x = x
6.      this.y = y
7.      this.width = width
8.      this.height = height
9.    }
10.  
11.    /** X 坐标 */
12.    x = 0
13.    /** Y 坐标 */
14.    y = 0
15.    /** X 轴方向上所占区域 */
16.    width = 0
17.    /** Y 轴方向上所占区域 */
18.    height = 0
19.  
20.    /** 顶部边界 */
21.    get top() {
22.      return this.y
23.    }
24.    /** 底部边界 */
25.    get bottom() {
26.      return this.y + this.height
27.    }
28.    /** 左边界 */
29.    get left() {
30.      return this.x
31.    }
32.    /** 右边界 */
33.    get right() {
34.      return this.x + this.width
35.    }
36.  }
37.  
38.  export default Rectangle

以上代码:

第 12 行至第 18 行,这是 4 个属性,x、y 决定注册点,width、height 决定尺寸。
第 21 行至第 35 行,这是 4 个 getter 拜访器,别离代表对象在 4 个方向上的边界值。
这 4 个属性不是理论存在的,而是通过注册点与尺寸计算出来的。依据注册点地位的不同,这 4 个 getter 的值也不同。默认注册点,即(0,0)坐标点在左上角,这时候 top 等于 y;如果注册点在左下角,这时候 top 则等于 y 减去 height。

Rectangle 形容了一个对象的距形范畴,对于 4 个边界属性 top、bottom、left、right 与注册点的关系,能够参见图 2:

图 2,注册点与边界值的关系

接下来咱们开始定义两个形象局部:一个是撞击对象的,另一个是受撞击对象的。先看受撞击对象的,它比较简单:

1.  // JS:src\views\hitTest\hited_object_rectangle.js
2.  import Rectangle from "rectangle.js"
3.  
4.  /** 被碰撞对象的形象局部,屏幕及左右挡板的注册点默认在左上角 */
5.  class HitedObjectRectangle extends Rectangle{6.    constructor(x, y, width, height){7.      super(x, y, width, height)
8.    }
9.  }
10.  
11.  export default HitedObjectRectangle

HitedObjectRectangle 类它没有新增属性或办法,所有特色都是从基类继承的。它的次要作用是被继承,稍后有 3 个子类继承它。

再看一下撞击对象的定义:

1.  // JS:src\views\hitTest\hit_object_rectangle.js
2.  import Rectangle from "rectangle.js"
3.  import LeftPanelRectangle from "left_panel_rectangle.js"
4.  import RightPanelRectangle from "right_panel_rectangle.js"
5.  import ScreenRectangle from "screen_rectangle.js"
6.  
7.  /** 碰撞对象的形象局部,球与方块的注册点在核心,不在左上角 */
8.  class HitObjectRectangle extends Rectangle {9.    constructor(width, height) {10.      super(GameGlobal.CANVAS_WIDTH / 2, GameGlobal.CANVAS_HEIGHT / 2, width, height)
11.    }
12.  
13.    get top() {
14.      return this.y - this.height / 2
15.    }
16.    get bottom() {
17.      return this.y + this.height / 2
18.    }
19.    get left() {
20.      return this.x - this.width / 2
21.    }
22.    get right() {
23.      return this.x + this.width / 2
24.    }
25.  
26.    /** 与被撞对象的碰撞检测 */
27.    hitTest(hitedObject) {
28.      let res = 0
29.      if (hitedObject instanceof LeftPanelRectangle) { // 碰撞到左挡板返回 1
30.        if (this.left < hitedObject.right && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
31.          res = 1 << 0
32.        }
33.      } else if (hitedObject instanceof RightPanelRectangle) { // 碰撞到右挡板返回 2
34.        if (this.right > hitedObject.left && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
35.          res = 1 << 1
36.        }
37.      } else if (hitedObject instanceof ScreenRectangle) {38.        if (this.right > hitedObject.right) { // 触达右边界返回 4
39.          res = 1 << 2
40.        } else if (this.left < hitedObject.left) { // 触达左边界返回 8
41.          res = 1 << 3
42.        }
43.        if (this.top < hitedObject.top) { // 触达上边界返回 16
44.          res = 1 << 4
45.        } else if (this.bottom > hitedObject.bottom) { // 触达下边界返回 32
46.          res = 1 << 5
47.        }
48.      }
49.      return res
50.    }
51.  }
52.  
53.  export default HitObjectRectangle

在下面代码中:

HitObjectRectangle 也是作为基类存在的,稍后有一个子类继承它。在这个基类中,第 13 行至第 24 行,咱们通过重写 getter 拜访器属性,将注册点由左上角移到了核心。
第 10 行,在结构器函数中咱们看到,默认的起始 x、y 是屏幕核心的坐标。
第 27 行至第 50 行,hitTest 办法的实现是外围代码,碰撞到左挡板与碰撞到右挡板返回的数字与之前定义的一样,碰撞周围墙壁返回的数字是 4 个新增的数字。
第 35 行,这行呈现的 1<<0 代表数值的二进制向左移 0 个地位。移 0 个地位没有意义,这样书写是为了与上面的第 35 行、第 39 行、第 41 行等放弃格局统一。1<<0 等于 1,1<<1 等于 2,1<<2 等于 4,1<<3 等于 8,这些数值是按 2 的 N 次幂递增的。
接下来咱们定义 ScreenRectangle,它是被撞击局部的具体实现局部:


1.  // JS:src\views\hitTest\screen_rectangle.js
2.  import HitedObjectRectangle from "hited_object_rectangle.js"
3.  
4.  /** 被碰撞对象屏幕的大小数据 */
5.  class ScreenRectangle extends HitedObjectRectangle {6.    constructor() {7.      super(0, 0, GameGlobal.CANVAS_WIDTH, GameGlobal.CANVAS_HEIGHT)
8.    }
9.  }
10.  
11.  export default ScreenRectangle

ScreenRectangle 是屏幕的大小、地位数据对象,是一个继承于 HitedObjectRectangle 的具体实现。ScreenRectangle 类作为一个具体的实现类,却没有增加额定的属性或办法,定义它的起因和意义在于是由它自身作为一个对象成立的,参见 HitObjectRectangle 类中的 hitTest 办法。

接下来咱们再看左挡板的大小、地位数据对象:

1.  // JS:src\views\hitTest\left_panel_rectangle.js
2.  import HitedObjectRectangle from "hited_object_rectangle.js"
3.  
4.  /** 被碰撞对象左挡板的大小数据 */
5.  class LeftPanelRectangle extends HitedObjectRectangle {6.    constructor() {7.      super(0, (GameGlobal.CANVAS_HEIGHT - GameGlobal.PANEL_HEIGHT) / 2, GameGlobal.PANEL_WIDTH, GameGlobal.PANEL_HEIGHT)
8.    }
9.  }
10.  
11.  export default LeftPanelRectangle

LeftPanelRectangle 与 ScreenRectangle 一样,是继承于 HitedObjectRectangle 的一个具体实现,依然没有新增属性或办法,所有信息,包含大小和地位,都曾经通过结构器参数传递进去了。

再看一下右挡板的大小、地位数据对象:

1.  // JS:src\views\hitTest\right_panel_rectangle.js
2.  import HitedObjectRectangle from "hited_object_rectangle.js"
3.  
4.  /** 被碰撞对象右挡板的大小数据 */
5.  class RightPanelRectangle extends HitedObjectRectangle {6.    constructor() {7.      super(GameGlobal.CANVAS_WIDTH - GameGlobal.PANEL_WIDTH, (GameGlobal.CANVAS_HEIGHT - GameGlobal.PANEL_HEIGHT) / 2, GameGlobal.PANEL_WIDTH, GameGlobal.PANEL_HEIGHT)
8.    }
9.  }
10.  
11.  export default RightPanelRectangle

RightPanelRectangle 也是继承于 HitedObjectRectangle 的一个具体实现,与 LeftPanelRectangle 不同的只是坐标地位。

接下来咱们再看撞击对象这边的具体实现局部,只有一个 BallRectangle 类:


1.  // JS:src\views\hitTest\ball_rectangle.js
2.  import HitObjectRectangle from "hit_object_rectangle.js"
3.  
4.  /** 碰撞对象的具体实现局部,球的大小及静止数据对象 */
5.  class BallRectangle extends HitObjectRectangle {6.    constructor() {7.      super(GameGlobal.RADIUS * 2, GameGlobal.RADIUS * 2)
8.    }
9.  }
10.  
11.  export default BallRectangle

BallRectangle 是形容球的地位、大小的,所有信息在基类中都具备了,所以它不须要增加任何属性或办法了。

以上就是咱们为利用桥接模式定义的所有类了,为了进一步明确它们之间的关系,看一张示意图,如图 3 所示:

图 3,桥接模式示例类关系图

第二层的 HitObjectRectangle 和 HitedObjectRectangle 是桥接模式中的形象局部,第三层是具体实现局部。事实上如果咱们需要的话,咱们在 HitObjectRectangle 和 HitedObjectRectangle 两条干线上,还能够定义更多的具体实现类。

在我的项目中生产桥接模式

接下来看如何应用,先革新原来的 Ball 类:


1.  // JS:src/views/ball.js
2.  import BallRectangle from "hitTest/ball_rectangle.js"
3.  
4.  /** 小球 */
5.  class Ball {
6.    ...
7.  
8.    constructor() {}
9.  
10.    get x() {
11.      // return this.#pos.x
12.      return this.rectangle.x
13.    }
14.    get y() {
15.      // return this.#pos.y
16.      return this.rectangle.y
17.    }
18.    /** 小于碰撞检测对象 */
19.    rectangle = new BallRectangle()
20.    // #pos // 球的起始地位
21.    #speedX = 4 // X 方向分速度
22.    #speedY = 2 // Y 方向分速度
23.  
24.    /** 初始化 */
25.    init(options) {26.      // this.#pos = options?.ballPos ?? { x: GameGlobal.CANVAS_WIDTH / 2, y: GameGlobal.CANVAS_HEIGHT / 2} 
27.      // const defaultPos = {x: this.#pos.x, y: this.#pos.y}
28.      // this.reset = () => {
29.      //   this.#pos.x = defaultPos.x
30.      //   this.#pos.y = defaultPos.y
31.      // }
32.      this.rectangle.x = options?.x ?? GameGlobal.CANVAS_WIDTH / 2
33.      this.rectangle.y = options?.y ?? GameGlobal.CANVAS_HEIGHT / 2
34.      this.#speedX = options?.speedX ?? 4
35.      this.#speedY = options?.speedY ?? 2
36.      const defaultArgs = Object.assign({}, this.rectangle)
37.      this.reset = () => {
38.        this.rectangle.x = defaultArgs.x
39.        this.rectangle.y = defaultArgs.y
40.        this.#speedX = 4
41.        this.#speedY = 2
42.      }
43.    }
44.  
45.    /** 重设 */
46.    reset() {}
47.  
48.    /** 渲染 */
49.    render(context) {
50.      ...
51.    }
52.  
53.    /** 运行 */
54.    run() {
55.      // 小球静止数据计算
56.      // this.#pos.x += this.#speedX
57.      // this.#pos.y += this.#speedY
58.      this.rectangle.x += this.#speedX
59.      this.rectangle.y += this.#speedY
60.    }
61.  
62.    /** 小球与墙壁的周围碰撞查看 */
63.    // testHitWall() {64.    //   if (this.#pos.x > GameGlobal.CANVAS_WIDTH - GameGlobal.RADIUS) { // 触达右边界
65.    //     this.#speedX = -this.#speedX
66.    //   } else if (this.#pos.x < GameGlobal.RADIUS) { // 触达左边界
67.    //     this.#speedX = -this.#speedX
68.    //   }
69.    //   if (this.#pos.y > GameGlobal.CANVAS_HEIGHT - GameGlobal.RADIUS) { // 触达右边界
70.    //     this.#speedY = -this.#speedY
71.    //   } else if (this.#pos.y < GameGlobal.RADIUS) { // 触达左边界
72.    //     this.#speedY = -this.#speedY
73.    //   }
74.    // }
75.    testHitWall(hitedObject) {76.      const res = this.rectangle.hitTest(hitedObject)
77.      if (res === 4 || res === 8) {
78.        this.#speedX = -this.#speedX
79.      } else if (res === 16 || res === 32) {
80.        this.#speedY = -this.#speedY
81.      }
82.    }
83.  
84.    ...
85.  }
86.  
87.  export default Ball.getInstance()

在 Ball 类中产生了如下变动:

第 19 行,咱们增加了新的类属性 rectangle,它是 BallRectangle 的实例。所有对于球的地位、大小等信息都移到了 rectangle 中,所以原来的类属性 #pos(第 20 行)不再须要了,同时原来调用它的代码(例如第 58 行、第 59 行)都须要应用 rectangle 改写。
第 32 行至第 42 行,这是初始化代码,原来 #pos 是一个坐标,包含 x、y 两个值,当初将这两个值别离以 rectangle 中的 x、y 代替。
办法 testHitWall 用于屏幕边缘碰撞检测的,第 63 行至第 74 行的是旧代码,第 75 行至第 82 行是新代码。hitedObject 是新增的参数,它是 HitedObjectRectangle 子类的实例。
小球属于撞击对象,它的 rectangle 是一个 HitObjectRectangle 的子类实例(BallRectangle)。

看一下对 Panel 类的革新,它是 LeftPanel 和 RightPanel 的基类:


1.  // JS:src/views/panel.js
2.  /** 挡板基类 */
3.  class Panel {4.    constructor() { }
5.  
6.    // x // 挡板的终点 X 坐标
7.    // y // 挡板的终点 Y 坐标
8.    get x() {
9.      return this.rectangle.x
10.    }
11.    set x(val) {
12.      this.rectangle.x = val
13.    }
14.    get y() {
15.      return this.rectangle.y
16.    }
17.    set y(val) {
18.      this.rectangle.y = val
19.    }
20.    /** 挡板碰撞检测对象 */
21.    rectangle
22.    ...
23.  }
24.  
25.  export default Panel

这个基类产生了如下变动:

第 21 行,rectangle 是新增的 HitedObjectRectangle 的子类实例,具体是哪个实现,要在子类中决定。
第 6 行、第 7 即将 x、y 去掉,代之以第 8 行至第 19 行的 getter 拜访器和 setter 设置器,对 x、y 属性的拜访和设置,将转变为对 rectangle 中 x、y 的拜访和设置。
为什么要在 Panel 基类中新增一个 rectangle 属性?因为要在它的子类 LeftPanel、RightPanel 中新增这个属性,挡板是被撞击对象,rectangle 是 HitedObjectRectangle 的子类实例。与其在子类中别离设置,不如在基类中一个中央对立设置;另外,基类中 render 办法渲染挡板时要应用 x、y 属性,x、y 属性须要重写,这也要求 rectangle 必须定义在基类中定义。

对 LeftPanel 类的革新:


1.  // JS:src/views/left_panel.js
2.  ...
3.  import LeftPanelRectangle from "hitTest/left_panel_rectangle.js"
4.  
5.  /** 左挡板 */
6.  class LeftPanel extends Panel {7.    constructor() {8.      super()
9.      this.rectangle = new LeftPanelRectangle()
10.    }
11.  
12.    ...
13.  
14.    /** 小球碰撞到左挡板返回 1 */
15.    testHitBall(ball) {16.      return ball.rectangle.hitTest(this.rectangle)
17.      // if (ball.x < GameGlobal.RADIUS + GameGlobal.PANEL_WIDTH) { // 触达左挡板
18.      //   if (ball.y > this.y && ball.y < (this.y + GameGlobal.PANEL_HEIGHT)) {
19.      //     return 1
20.      //   }
21.      // }
22.      // return 0
23.    }
24.  }
25.  
26.  export default new LeftPanel()

以上代码产生了两处改变:

第 9 行,这里决定了基类中的 rectangle 是 LeftPanelRectangle 实例。LeftPanelRectangle 是 HitedObjectRectangle 的子类。
第 16 行,碰撞检测代码批改为:由小球的 rectangle 与以后对象的 rectangle 做碰撞测试。
接下来是对 RightPanel 类的改写:


1.  // JS:src/views/right_panel.js
2.  ...
3.  import RightPanelRectangle from "hitTest/right_panel_rectangle.js"
4.  
5.  /** 右挡板 */
6.  class RightPanel extends Panel {7.    constructor() {8.      super()
9.      this.rectangle = new RightPanelRectangle()
10.    }
11.  
12.    ...
13.  
14.    /** 小球碰撞到左挡板返回 2 */
15.    testHitBall(ball) {16.      return ball.rectangle.hitTest(this.rectangle)
17.      // if (ball.x > (GameGlobal.CANVAS_WIDTH - GameGlobal.RADIUS - GameGlobal.PANEL_WIDTH)) { // 碰撞右挡板
18.      //   if (ball.y > this.y && ball.y < (this.y + GameGlobal.PANEL_HEIGHT)) {
19.      //     return 2
20.      //   }
21.      // }
22.      // return 0
23.    }
24.  }
25.  
26.  export default new RightPanel()

与 LeftPanel 相似,在这个 RightPanel 类中也只有两处批改,见第 9 行与第 16 行。

最初,咱们开始革新 GameIndexPage,它是咱们利用桥接模式的最初一站了:


1.  // JS:src\views\game_index_page.js
2.  ...
3.  import ScreenRectangle from "hitTest/screen_rectangle.js"
4.  
5.  /** 游戏主页页面 */
6.  class GameIndexPage extends Page {
7.    ...
8.    /** 墙壁碰撞检测对象 */
9.    #rectangle = new ScreenRectangle()
10.  
11.    ...
12.  
13.    /** 运行 */
14.    run() {
15.      ...
16.      // 小球碰撞检测
17.      // ball.testHitWall()
18.      ball.testHitWall(this.#rectangle)
19.      ...
20.    }
21.  
22.    ...
23.  }
24.  
25.  export default GameIndexPage

在 GameIndexPage 类中,只有以下两处批改:

第 9 行,增加了一个公有属性 #rectangle,它是一个碰撞检测数据对象,是 HitedObjectRectangle 的子类实例。
第 18 行,在调用小球的 testHitWall 办法,将 #rectangle 作为参数传递了进去。
当初代码批改完了,从新编译测试,运行成果与之前统一,如下所示:

图 4,运行效果图

应用桥接模式的意义

咱们思考一下,咱们在碰撞检测这一块利用桥接模式,创立了许多新类,除了把我的项目变简单了,到底有什么踊跃作用?咱们将碰撞测试元素拆分为两个形象对象(HitObjectRectangle 和 HitedObjectRectangle)的意义在哪里?

看一张结构图,如图 5 所示:

图 5,待扩大的桥接模式示意图

HitObjectRectangle 代表碰撞对象的碰撞检测数据对象,HitedObjectRectangle 代表被碰撞对象的碰撞检测数据对象,后者有三个具体实现的子类:ScreenRectangle、LeftPanelRectangle 和 RightPanelRectangle,这三个子类代表三类被撞击的类型。

如果游戏中呈现一个周围须要被碰撞检测的对象,它的检测数据对象能够继承于 ScreenRectangle;如果呈现一个右侧须要碰撞检测的对象,它的检测数据对象能够继承于 RightPanelRectangle,以此类推左侧呈现的,它的数据对象能够继承于 LeftPanelRectangle。而如果呈现一个撞击对象,它的检测数据对象能够继承于 BallRectangle。

目前咱们这个小游戏我的项目太过简略,不足够显示桥接模式的作用。接下来咱们做一个人为拓展,新增一个红色立方体代替小球:


1.  // JS:src\views\cube.js
2.  import {Ball} from "ball.js"
3.  import CubeRectangle from "hitTest/cube_rectangle.js"
4.  
5.  /** 红色立方块 */
6.  class Cube extends Ball {7.    constructor() {8.      super()
9.      this.rectangle = new CubeRectangle()
10.    }
11.  
12.    /** 渲染 */
13.    render(context) {
14.      context.fillStyle = "red"
15.      context.beginPath()
16.      context.rect(this.rectangle.left, this.rectangle.top, this.rectangle.width, this.rectangle.height)
17.      context.fill()
18.    }
19.  }
20.  
21.  export default new Cube()

Cube 类的代码与 Ball 是相似的,只有 render 代码略有不同,让它继承于 Ball 是最简略的实现办法。第 9 行,rectangle 设置为 CubeRectangle 的实例,这个类尚不存在,稍后咱们创立,它是 BallRectangle 的子类。

在 cube.js 文件中引入的 Ball(第 2 行)当初还没有导出,咱们须要批改一下 ball.js 文件,如下所示:


1.  // JS:src/views/ball.js
2.  ...
3.  
4.  /** 小球 */
5.  // class Ball {
6.  export class Ball {
7.    ...
8.  }
9.  ...

第 6 行,应用 export 关键字增加了惯例导出,其它不会批改。

当初看一下新增的 CubeRectangle 类,如下所示:


1.  // JS:src\views\hitTest\ball_rectangle.js
2.  import BallRectangle from "ball_rectangle.js"
3.  
4.  /** 碰撞对象的具体实现局部,立方体的大小及静止数据对象 */
5.  class CubeRectangle extends BallRectangle { }
6.  
7.  export default CubeRectangle

CubeRectangle 是立方块的检测数据对象。CubeRectangle 能够继承于 HitObjectRectangle 实现,但因为立方体与小球特色很像,所以让它继承于 BallRectangle 更容易实现。事实上它像一个“富二代”,只须要继承(第 5 行),什么也不必做。

接下来开始应用立方块。为了使测试代码简略,咱们将 game.js 文件中的页面创立代码批改一下,如下所示:


1.  // JS:disc\ 第 11 章 \11.1\11.1.2\game.js
2.  ...
3.  // import PageBuildDirector from "src/views/page_build_director.js" // 引入页面建造指挥者
4.  import PageFactory from "src/views/page_factory.js" // 引入页面工厂
5.  
6.  /** 游戏对象 */
7.  class Game extends EventDispatcher {
8.    ...
9.  
10.    /** 游戏换页 */
11.    turnToPage(pageName) {
12.      ...
13.      // this.#currentPage = PageBuildDirector.buildPage(pageName, { game: this, context: this.#context})
14.      this.#currentPage = PageFactory.createPage(pageName, this, this.#context)
15.      ...
16.    }
17.  
18.    ...
19.  }
20.  ...

只有两处改变,第 4 行和第 14 行,继承应用 PageBuildDirector 不利于代码测试,应用 PageFactory 代码会更简略。这一步改变与本大节的桥接模式没有间接关系。

最初批改 game_index_page.js 文件,应用立方块,代码如下:


1.  // JS:src\views\game_index_page.js
2.  ...
3.  // import ball from "ball.js" // 引入小球单例
4.  import ball from "cube.js" // 引入立方块实例
5.  ...

只有第 4 行引入地址变了,其余不会扭转。代码扩大完了,从新编译测试,游戏的运行成果如图 6 所示:

图 6,小球变成了红色方块

改变后,红色的小球变成了红色的方块。此处,我的项目的可扩展性十分好,在利用了桥接模式当前,当咱们把小球扩大为方块时,只须要大量的变动就能够做到了。当初,将 CubeRectangle 纳入结构图,如图 7 所示:

图 7,扩大后的桥接模式示意图

第四层增加了一个 CubeRectangle,咱们的 HitObjectRectangle 批改了吗?没有。尽管在 HitObjectRectangle 的 hitTest 办法中,咱们应用 instanceof 进行了类型判断,如下所示:


1.  /** 与被撞对象的碰撞检测 */
2.  hitTest(hitedObject) {
3.    let res = 0
4.    if (hitedObject instanceof LeftPanelRectangle) { 
5.      ...
6.    } else if (hitedObject instanceof RightPanelRectangle) { 
7.      ...
8.    } else if (hitedObject instanceof ScreenRectangle) {
9.      ...
10.    }
11.    return res
12.  }

但判断的是根本类型,在第四层增加子类型不会影响代码的执行。咱们增加的 CubeRectangle 继承于 BallRectangle,属于 HitObjectRectangle 一支,如果增加一个新类继承于 HitedObjectRectangle 的子类(即 ScreenRectangle、LeftPanelRectangle 和 RightPanelRectangle),后果是一样的,代码不必批改依然无效。HitObjectRectangle 和 HitedObjectRectangle 作为形象局部,是咱们实现的桥接模式中的重要组成部分,它们帮忙具体实现局部屏蔽了变动的复杂性。

留神:如果咱们增加了新的碰撞检测类型,不同于 ScreenRectangle、LeftPanelRectangle 和 RightPanelRectangle 中的任何一个,代码应该如何拓展?这时候就须要批改 HitObjectRectangle 类的 hitTest 办法啦,须要增加 else if 分支。

桥接模式用法总结

综上所述,在桥接模式中,是有两局部对象别离实现形象局部与具体局部,而后这两局部对象绝对独立自在的变动。在本大节示例中,咱们次要利用桥接模式实现了碰撞检测。小球和立方块是撞击对象,左右挡板及屏幕是被撞击对象,通过雷同的形式定义它们的大小、地位数据,而后以一种绝对优雅的形式实现了碰撞检测。

比照重构前后的代码,咱们不难发现,在利用桥接模式之前,咱们的碰撞检测代码是与 GameIndexPage、Ball、LeftPanel 和 RightPanel 耦合在一起的,并且不不便进行新的碰撞对象扩大;在重构当前,咱们碰撞检测的代码变成了只有 top、bottom、left 和 right 属性数值的比照,变得十分清晰。

所有面向对象重构中应用的设计模式,桥接模式是最简单的,在大型跨平台 GUI 软件中,桥接模式根本也是必呈现的。

模式利用之访问者模式

在利用了桥接模式当前,置信大家对设计模式的作用会有更深的理解,也无意识地使用设计模式,它能够帮忙咱们更大限度地应答需要变动的复杂性,从而保障版本迭代的稳固与快捷。

访问者模式则是微信小游戏开发中另一利用设计,以下内容属于《微信小游戏开发》前端篇内容,咱们尝试在源码根底之上,尝试利用访问者模式,目标依然是有针对性地锻炼学习者渐进性模块化重构和面向对象重构思维的能力。

利用模式之前的我的项目状态

目前咱们在实现碰撞检测性能的时候,在 HitObjectRectangle 类中有一个很重要的办法:


1.  // JS:src\views\hitTest\hit_object_rectangle.js
2.  ...
3.  
4.  /** 碰撞对象的形象局部,球与方块的注册点在核心,不在左上角 */
5.  class HitObjectRectangle extends Rectangle {
6.    ...
7.  
8.    /** 与被撞对象的碰撞检测 */
9.    hitTest(hitedObject) {
10.      let res = 0
11.      if (hitedObject instanceof LeftPanelRectangle) { // 碰撞到左挡板返回 1
12.        ...
13.      } else if (hitedObject instanceof RightPanelRectangle) { // 碰撞到右挡板返回 2
14.        ...
15.      } else if (hitedObject instanceof ScreenRectangle) {
16.        ...
17.      }
18.      return res
19.    }
20.  }
21.  
22.  export default HitObjectRectangle

正是 hitTest 这个办法实现了碰撞检测,它依据不同的被撞击的对象,别离做了不同的边界检测。

然而这个办法它存在缺点,其外部有 if else,并且这个 if else 是会随着被检测对象的类型增长而减少的。那么在实践中该怎么优化它呢?咱们能够应用访问者模式重构。在访问者模式中,能够依据不同的对象别离作不同的解决,这里多个被撞击的对象,恰好是定义中所说的不同的对象。

什么是访问者模式

访问者模式是一种行为设计模式,它能将算法与算法所作用的对象隔离开来。换言之,访问者模式依据访问者不同,展现不同的行为或做不同的解决。应用访问者模式,个别意味着调用反转,原本是 A 调用 B,后果该调用最终反赤来是通过 B 调用 A 实现的。

在这个模式中个别有两个方面,咱们能够拿软件外包市场中的甲方乙方类比一下,甲方是发包方,乙方是接包方,原本须要甲方到乙方公司零碎说明需要,由乙方依据不同需要安顿不同的我的项目进行开发;当初则是与之相同。

访问者模式的实现与利用

接下来开始访问者模式的实际,咱们先给 LeftPanelRectangle、RightPanelRectangle 和 ScreenRectangle 都增加一个雷同的办法 accept,第一个 LeftPanelRectangle 的改变是这样的:


1.  // JS:src\views\hitTest\left_panel_rectangle.js
2.  ...
3.  
4.  /** 被碰撞对象左挡板的大小数据 */
5.  class LeftPanelRectangle extends HitedObjectRectangle {
6.    ...
7.  
8.    visit(hitObject) {9.      if (hitObject.left < this.right && hitObject.top > this.top && hitObject.bottom < this.bottom) {
10.        return 1 << 0
11.      }
12.      return 0
13.    }
14.  }
15.  
16.  export default LeftPanelRectangle

第 8 行至第 13 行,在这个新增的 visit 办法中,代码是从原来 HitObjectRectangle 类中摘取一段并稍加批改实现的,这里碰撞检测只波及两个对象的边界,没有 if else,逻辑上便会更加简洁清晰。

第二个 RightPanelRectangle 类的改变是这样的:


1.  // JS:src\views\hitTest\right_panel_rectangle.js
2.  ...
3.  
4.  /** 被碰撞对象右挡板的大小数据 */
5.  class RightPanelRectangle extends HitedObjectRectangle {
6.    ...
7.  
8.    visit(hitObject) {9.      if (hitObject.right > this.left && hitObject.top > this.top && hitObject.bottom < this.bottom) {
10.        return 1 << 1
11.      }
12.      return 0
13.    }
14.  }
15.  
16.  export default RightPanelRectangle

第 8 行至第 13 行,这个 visit 办法的实现,与 LeftPanelRectangle 中 visit 办法的实现一模一样。

第 3 个是 ScreenRectangle 类的改变:


1.  // JS:src\views\hitTest\screen_rectangle.js
2.  ...
3.  
4.  /** 被碰撞对象屏幕的大小数据 */
5.  class ScreenRectangle extends HitedObjectRectangle {
6.    ...
7.  
8.    visit(hitObject) {
9.      let res = 0
10.      if (hitObject.right > this.right) { // 触达右边界返回 4
11.        res = 1 << 2
12.      } else if (hitObject.left < this.left) { // 触达左边界返回 8
13.        res = 1 << 3
14.      }
15.      if (hitObject.top < this.top) { // 触达上边界返回 16
16.        res = 1 << 4
17.      } else if (hitObject.bottom > this.bottom) { // 触达下边界返回 32
18.        res = 1 << 5
19.      }
20.      return res
21.    }
22.  }
23.  
24.  export default ScreenRectangle

第 8 行至第 21 行,是新增的 visit 办法。所有返回值,与原来均是一样的,代码的逻辑构造也是一样的,只是从哪个对象上取值上进行比拟做了变动。

下面这 3 个类都是 HitedObjectRectangle 的子类,为了让基类的定义更加残缺,咱们也批改一下 hited_object_rectangle.js 文件,如下所示:


1.  // JS:src\views\hitTest\hited_object_rectangle.js
2.  ...
3.  
4.  /** 被碰撞对象的形象局部,屏幕及左右挡板的注册点默认在左上角 */
5.  class HitedObjectRectangle extends Rectangle {
6.    ...
7.  
8.    visit(hitObject) { }
9.  }
10.  
11.  export default HitedObjectRectangle

仅是第 8 行增加了一个空办法 visite,这个改变能够让所有 HitedObjectRectangle 对象都有一个默认的 visite 办法,在某些状况下能够防止代码出错。

最初咱们再看一下 HitObjectRectangle 类的改变,这也是访问者模式中的外围局部:


1.  // JS:src\views\hitTest\hit_object_rectangle.js
2.  ...
3.  
4.  /** 碰撞对象的形象局部,球与方块的注册点在核心,不在左上角 */
5.  class HitObjectRectangle extends Rectangle {
6.    ...
7.  
8.    /** 与被撞对象的碰撞检测 */
9.    hitTest(hitedObject) {
10.      // let res = 0
11.      // if (hitedObject instanceof LeftPanelRectangle) { // 碰撞到左挡板返回 1
12.      //   if (this.left < hitedObject.right && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
13.      //     res = 1 << 0
14.      //   }
15.      // } else if (hitedObject instanceof RightPanelRectangle) { // 碰撞到右挡板返回 2
16.      //   if (this.right > hitedObject.left && this.top > hitedObject.top && this.bottom < hitedObject.bottom) {
17.      //     res = 1 << 1
18.      //   }
19.      // } else if (hitedObject instanceof ScreenRectangle) {20.      //   if (this.right > hitedObject.right) { // 触达右边界返回 4
21.      //     res = 1 << 2
22.      //   } else if (this.left < hitedObject.left) { // 触达左边界返回 8
23.      //     res = 1 << 3
24.      //   }
25.      //   if (this.top < hitedObject.top) { // 触达上边界返回 16
26.      //     res = 1 << 4
27.      //   } else if (this.bottom > hitedObject.bottom) { // 触达下边界返回 32
28.      //     res = 1 << 5
29.      //   }
30.      // }
31.      // return res
32.      return hitedObject.visit(this)
33.    }
34.  }
35.  
36.  export default HitObjectRectangle

第 10 行至第 31 行,是 hitTest 办法中被正文掉的旧代码,原来简单的 if else 逻辑没有了,只留下简短的一句话(第 32 行)。这就是设计模式的力量,不仅当初简略,后续如果咱们要增加其余碰撞对象与被碰撞对象,这里也不须要变动,足以证实代码的可扩展性。

这样咱们在减少新的碰撞检测对象时,只须要创立新类,没有 if else 逻辑须要增加,也不影响旧代码。第 9 行,这里的 hitTest 办法,相当于个别访问者模式中的 accept 办法。

当咱们将访问者模式和桥接模式实现联合利用时,代码便变得异样简洁清晰。小游戏的运行成果与之前是统一的,如下所示:

图 7,运行成果示意图

访问者模式用法总结

综上,访问者模式特地善于将领有多个 if else 逻辑或 switch 分支逻辑的代码,以一种反向调用的形式,转化为两类对象之间一对一的逻辑关系进行解决。这是一个利用非常广泛的设计模式,当遇到简单的 if else 代码时,能够思考应用该模式重构。

总结

桥接模式与访问者模式是通用的,不仅能够利用于小游戏开发中,而且能够用在其余前端我的项目中,甚至在其余编程语言中也能够发挥作用。设计模式实质上是一种组织软件性能、架构代码模块的面向对象思维,这种思维貌似让咱们在开始写代码的时候多干了一些活,但干这些活的精力是值得投入的,它让咱们能够把其余的活干得更快、更稳、更好。

只有走得稳,才能够走得更远、更快。设计模式在我的项目开发中的作用高深莫测,但也有一些反驳的声音认为,我的项目焦急上线时基本没有仔细分析需要与架构的工夫,如何利用设计模式?

其实,疾速上线是没有问题的,工夫就是产品的生命;但在第一版本上线之后,程序员能够进行渐进式重构,重构并不产生在我的项目之初,对设计模式的利用也是在基本功能尘埃落定之后进行的。

只有走得稳,才能够走得更远、更快,而设计模式与渐进式面向对象重构思维便能够帮忙咱们实现。

本篇内容摘自腾讯云 TVP 李艺著、机械工业出版社出版的《微信小游戏开发》,该书已在京东上架,想要进一步深刻理解微信小游戏开发的敌人们能够自行返回购买,文中波及的所有设计模式源码在随书源码中都能够找到。

正文完
 0