1 /**
2 * @description: 引擎的设计与实现
3 * @user: xiugang
4 * @time: 2018/10/01
5 */
6
7 /*
8 * V1.0: 引擎实现的基本模块思路
9 *
1.创建一个游戏引擎对象及精灵对象
10 *
2.将精灵对象添加到引擎中去,并实现播放动画效果以及需要用到的回调方法
11 *
3.启动引擎
12 * */
13
14 /*
15 * V2.0: 实现游戏循环模块
16 *
1.如果游戏暂停了,就跳过以下各步骤,并在100毫秒后再次执行游戏循环
17 *
2.更新帧速率
18 *
3.设置游戏时间
19 *
4.清除屏幕内容
20 *
5.在播放动画前,调用startAnimate的方法(可以进行碰撞检测)
21 *
6.绘制精灵背后的内容(绘制背景)
22 *
7.更新精灵
23 *
8.绘制精灵
24 *
9.绘制精灵前方的内容
25 *
10.动画播放完毕之后,调用endAnimate方法
26 *
11.请求浏览器播放下一帧动画
27 *
28 * */
29
30
31 /**
32 * V3.0: 实现在暂停状态与运行状态之间的切换togglePaused
33 */
34
35 /**
36 * V4.0:实现基于时间的运动效果 :pixelPerFrame
37 * 计算公式:(pixels / second) * (second / frame) = pixeld / second【单位:每一秒移动的像素数】
38 */
39
40 /**
41 * V5.0: 实现加载图像的功能:
42 * queueImage(imageUrl): 将图像放入到加载队列中去
43 * loadImages(): 开发者需要持续调用该方法,知道返回100位置(方法的返回值表示图像加载完成的百分比)
44 * getImage(imageUrl):返回图像对象, 只有咋loadImages()返回100之后,才可以调用该方法
45 */
46
47 /**
48 * V6.0:实现同时播放多个声音的功能
49 * canPlay(): 用于查询浏览器是否能够播放某种特定格式的声音文件
50 * playSound():用于播放声音
51 */
52
53
54 /**
55 * V7.0: 键盘事件的处理
56 * addKeyListener(): 用于向游戏注册按键监听器
57 */
58
59
60 /**
61 * V8.0: 高分榜的维护:游戏的高分榜数组以json格式存档在本地
62 */
63
64 /**
65 * V9.0: 实现了一个比较完整的游戏引擎,开始使用这个简单的游戏引擎去制作一个小游戏
66 * 需求分析:
67 * 1.资源加载的画面
68 * 2.游戏资源的管理
69 * 3.声音的播放
70 * 4.具有视差动画的滚动背景
71 * 5.生命数量的显示
72 * 6.高分榜的维护
73 * 7.按键的监听与处理
74 * 8.暂停功能与自动暂停机制实现
75 * 9.游戏结束的流程处理
76 */
77
78
79 /**
80 * 游戏类
81 * @param gameName 游戏名称
82 * @param canvasId 画布ID
83 * @returns {Game} 游戏实例
84 * @constructor
85 */
86 var Game = function (gameName, canvasId) {
87
// 获取canvas画布
88
var canvas = document.getElementById(canvasId);
89 console.log(canvas);
90
var self = this;
91
92
93
//----------------------------------------基本属性
94
this.context = canvas.getContext('2d');
// 定义游戏中的基本需要的属性
95
this.sprites = [];
// 游戏中的精灵对象
96
this.gameName = gameName;
// 游戏的名字
97
98
99
//----------------------------------------时间管理
100
this.startTime = 0;
// 游戏开始时间
101
this.lastTime = 0;
// 游戏上一次的时间
102
this.gameTime = 0;
// 游戏总时间
103
this.fps = 0;
// 游戏帧速率(实时更新的)
104
this.STARTING_FPS = 60;
// 默认启动的时候的帧速率
105
106
this.paused = false;
// 游戏是否暂停
107
this.startedPauseAt = 0;
108
this.PAUSE_TIMEOUT = 100;
// 游戏暂停的持续时间
109
110
111
//---------------------------------------图像加载
112
this.imageLoadingProgressCallback;
// 图像加载过程的回调函数
113
this.images = {};
// 图像对象
114
this.imageUrls = [];
// 图像的Urls
115
this.imagesLoaded = 0;
// 已加载完成的图像个数
116
this.imagesFailedToLoad = 0;
// 加载失败的图像个数
117
this.imagesIndex = 0;
// 图像数组的下标, 从0开始的
118
119
120
121
//-----------------------------------------声音加载播放
122
this.soundOn = true;
123
this.soundChannels = [];
// 初始化一个播放信道数组
124
this.audio = new Audio();
// 这里的Audio实际上是JavaScript内置的DOM对象, 不需要自己手动去创建一个Audio对象
125
this.NUM_SOUND_CHANNELS = 10;
// 设置初始信道的数量
126
127
128
129
//----------------------------------------键盘事件的监听
130
this.keyListeners = [];
// 用于存放keyandListener的键值对
131
132
window.onkeypress = function (ev) {
// 这里的对象处理的是DOM Window这个窗体对象,添加了一个监听事件
133
self.keyPressed(ev);
134 }
135
window.onkeydown = function (ev) {
136
self.keyPressed(ev);
137 }
138
139
140
//-----------------------------------------高分榜的维护
141
this.HIGH_SCORES_SUFFIX = '_highscores';
// 后缀名字
142
this.highScores = [];
// 用于存储游戏分数的数组
143
144
145
146
// 构造10个Audio对象,将其加入到数组中去, 当调用playSound()方法,游戏引擎会找出第一个未被占用的声道,并用它来播放指定的声音文件
147
for (var i = 0; i < this.NUM_SOUND_CHANNELS; i++){
148
var audio = new Audio();
149
this.soundChannels.push(audio);
150 }
151
152
return this;
// 把当前的游戏对象返回
153 }
154
155
156
157
158 /**
159 * 游戏的成员方法
160 * @type {{start: Game.start, animate: Game.animate, tick: Game.tick, updateFrameRate: Game.updateFrameRate, clearScreen: Game.clearScreen, startAnimate: Game.startAnimate, paintUnderSprites: Game.paintUnderSprites, updateSprites: Game.updateSprites, paintSprites: Game.paintSprites, paintOverSprites: Game.paintOverSprites, endAnimate: Game.endAnimate}}
161 */
162 Game.prototype = {
163
// 游戏加载图像的模块-------------------------------------------------------
164
/**
165
* 通过图像的Url地址,获取这个图像(json格式对象取出值的方法)对象
166
* @param imageUrl
167
*/
168
getImage : function (imageUrl) {
169
return this.images[imageUrl];
170 },
171
172
173
174
/**
175
* 图像加载完成的回调函数
176
*/
177
imageLoadedCallback : function (e) {
178
// 每次加载完成一个图像,次数加一
179
this.imagesLoaded++;
180 },
181
182
183
184
/**
185
* 当一个图像加载失败的回调函数
186
*/
187
imageLoadErrorCallback : function (e) {
188
this.imagesFailedToLoad++;
189 },
190
191
192
/**
193
* 正式加载一张图像
194
* @param imageUrl
195
*/
196
loadImage : function (imageUrl) {
197
var self = this;
198
var image = new Image();
199
200
image.src = imageUrl;
201
202
// 图像加载完成的回调函数
203
204
image.addEventListener("load", function (e) {
205
self.imageLoadedCallback(e);
206
207
// 显示出来, 测试成功
208
//self.context.drawImage(image, 0, 0);
209
});
210
211
212
// 图像加载失败的回调函数
213
image.addEventListener("error", function (e) {
214
self.imageLoadErrorCallback(e);
215
});
216
217
218
// 把所有的加载的Images存起来
219
this.images[imageUrl] = image;
220 },
221
222
/**
223
* 加载图像的过程中反复调用这个函数, 这个函数返回已经处理完成的图像百分比
224
* 当图像返回100%的时候, 表示所有的图像已经全部加载完毕
225
* @returns {number}
226
*/
227
loadImages : function () {
228
// 如果还有图像没有加载【图像的url个数多余已经加载完成的图像下标】
229
if (this.imagesIndex < this.imageUrls.length){
230
// 再次把当前这个图像去加载(把没有加载的全部加载进来)
231
this.loadImage(this.imageUrls[this.imagesIndex]);
232
this.imagesIndex ++;
233
}
234
235
236
// 返回已经加载完成的图像百分比(加载成功的个数+加载失败的个数 占整个事先提供的所有URL个数的百分比)
237
var percentage = (this.imagesLoaded + this.imagesFailedToLoad) / this.imageUrls.length * 100;
238
console.log(percentage);
239
return (this.imagesLoaded + this.imagesFailedToLoad) / this.imageUrls.length * 100;
240 },
241
242
/**
243
* 用于把所有的图像URL放在一个队列里面【数组】
244
* @param imageUrl
245
*/
246
queueImage : function (imageUrl) {
247
this.imageUrls.push(imageUrl);
248 },
249
250
251
252
253
254
// 游戏循环模块---------------------------------------------------------------
255
start: function () {
256
var self = this;
257
258
this.startTime = +new Date();
// 获取游戏当前的时间
259
console.log("游戏启动成功, 当前时间:", this.startTime);
260
261
262
// 开始游戏循环(这是一个系统实现的帧速率方法)
263
window.requestNextAnimationFrame(
264
function (time) {
265
// self is the game, and this is the window
266
console.log(self, this);
267
// 每次把游戏实例对象的引用和当前的时间传过去
268
self.animate.call(self, time); // self is the game
269
}
270
);
271 },
272
273
animate: function (time) {
274
// 这里的this 指向的是Game对象
275
var self = this;
276
277
if (this.paused) {
278
// 如果用户暂停了游戏,然后每隔100ms的时间检查一次去看一下有没有开始循环
279
// (由于游戏暂停的情况不会频繁出现,因此使用setTimeout()方法就可以满足我们的需求, 每隔100ms去看一次)
280
281
setTimeout(function () {
282
self.animate.call(self, time);
283
}, this.PAUSE_TIMEOUT);
284
}
285
// 没有暂停的话
286
else {
287
this.tick(time);
// 1.更新帧速率, 设置游戏时间
288
this.clearScreen();
// 2.清空屏幕内容
289
290
// 碰撞检测代码
291
292
this.startAnimate(time);
// 3.开始游戏动画
293
this.paintUnderSprites();
// 4.绘制精灵后面的内容---背景
294
295
this.updateSprites(time);
// 5.更新精灵的位置
296
this.paintSprites(time);
// 6.绘制精灵
297
298
this.paintOverSprites();
// 7.绘制精灵前方的内容
299
this.endAnimate();
// 8.动画结束
300
301
302
// 回调这个方法, 开始进入到下一帧动画
303
window.requestNextAnimationFrame(
304
function (time) {
305
console.log(self, this);
306
// 注意这里不能直接传过去哈, 如果直接传过去的话,第一个参数就是就会把time 的指向修改为Game这个类
307
// self.animate(self, time);
308
// 第一个参数是用来校正animate函数内部的this的指向, 第二个参数是用来传递animate()函数执行需要的参数
309
self.animate.call(self, time);
310
}
311
);
312
}
313 },
314
togglePaused : function () {
315
// 这是一个游戏暂停的方法
316
var now = +new Date();
// 获取游戏暂停的那个时间点
317
318
this.paused = !this.paused;
// 每次在暂停与不暂停之间来回切换
319
320
if (this.paused){
321
// 如果游戏暂停了(暂停的那个时间点就是当前的时间)
322
this.startedPauseAt = now;
323
}else{
324
// 没有暂停的话:调整开始的时间, 使得游戏开始是从点击开始游戏之后就开始计时的
325
// this.startTime 记录的是:开始时间 + 当前时间 - 游戏上一次暂停的时间点
326
// now - this.startedPauseAt = 游戏暂停的时长, 然后再加上游戏开始的时候的时间,就能从原来的暂停位置处继续执行下去
327
this.startTime = this.startTime + now - this.startedPauseAt;
328
this.lastTime = now;
329
}
330 },
331
// 实现动画中需要实现的功能
332
/**
333
* // 1.更新帧速率(实现基于时间的运动效果)
334
* @param time
335
*/
336
tick: function (time) {
337
// 1. 更新帧帧速率
338
this.updateFrameRate(time);
339
340
// 2. 设置游戏时间(每一帧间隔的时间)
341
this.gameTime = (+new Date()) - this.startTime;
342
console.log("设置游戏的时间:" + this.gameTime);
343
this.lastTime = time;
344
345 },
346
updateFrameRate: function (time) {
347
// 启动时候的帧速率
348
if (this.lastTime === 0) {
349
this.fps = this.STARTING_FPS;
350
}
351
else {
352
// 计算当前的帧速率(每秒执行的帧数)
353
this.fps = 1000 / (time - this.lastTime);
354
console.log("实时更新游戏当前的帧速率", this.fps);
355
}
356
357 },
358
/**
359
* 实现基于时间的运动效果
360
* @param time
361
* @param velocity
362
*/
363
pixelsPerFrame : function (time, velocity) {
364
// 是动画平滑地运动起来
365
// 计算公式:(pixels / second) * (second / frame) = pixeld / second【单位:每一秒移动的像素数】
366
return velocity / this.fps;
367 },
368
369
/**
370
* // 2.清空屏幕内容
371
*/
372
clearScreen: function () {
373
// 注意this.context.canvas.width, this.context.canvas.height 用于获取画布的宽度和高度
374
//
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
375
this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
376
console.log("画布屏幕清空成功!");
377 },
378
379
380
/**
381
* // 3.开始游戏动画
382
* @param time
383
*/
384
startAnimate: function (time) {
385
console.log(time, "开始游戏动画………………");
386 },
387
/**
388
* // 4.绘制精灵后面的内容
389
*/
390
paintUnderSprites: function () {
391
console.log("绘制精灵后面的内容!");
392 },
393
/**
394
* // 5. 更新精灵的位置
395
* @param time
396
*/
397
updateSprites: function (time) {
398
console.log("更新所有精灵的位置!");
399
for (var i = 0; i < this.sprites.length; i++) {
400
var sprite = this.sprites[i];
401
// 重新绘制精灵(调用每一个精灵自己的方法去绘制显示)
402
sprite.update(this.context, time);
403
}
404 },
405
406
// 6.绘制所有可见的精灵对象
407
paintSprites: function (time) {
408
console.log("绘制所有可见的精灵对象");
409
for (var i = 0; i < this.sprites.length; i++) {
410
var sprite = this.sprites[i];
411
// 绘制之前需要先判断一下
412
if (sprite.visible) {
413
sprite.paint(this.context);
//绘制精灵的时候需要拿到绘制精灵的绘图句柄
414
}
415
}
416 },
417
418
// 7. 绘制精灵前方的内容
419
paintOverSprites: function () {
420
console.log("绘制精灵前面的内容!");
421 },
422
// 8. 绘制动画结束
423
endAnimate: function () {
424
console.log("绘制动画结束!");
425 },
426
427
428
429
430
// 声音文件加载播放的模块----------------------------------------------------------
431
/**
432
* 浏览器是否支持ogg格式的文件
433
* @returns {boolean}
434
*/
435
canPlayOggVorbis : function () {
436
// 只要返回的有内容,就说明浏览器支持这个文件格式
437
return "" != this.audio.canPlayType('audio/ogg; codecs="vorbis"');
438 },
439
440
/**
441
* 浏览器是否支持MP3格式的音乐播放
442
* @returns {boolean}
443
*/
444
canPlayMp3 : function () {
445
// 返回的内容不为空,说明支持
446
return "" != this.audio.canPlayType('audio/mpeg');
447 },
448
449
/**
450
* 用于返回当前系统中可以使用的信道
451
* @returns {*}
452
*/
453
getAvailableSoundChannel : function () {
454
var audio;
455
456
// 遍历初始化中的所有信道
457
for (var i = 0; i < this.NUM_SOUND_CHANNELS; i++){
458
audio = this.soundChannels[i];
459
// 如果当前的audio信道已经开始播放了(而且已经播放的信道数量不为空)
460
if (audio.played && audio.played.length > 0){
461
// 如果当前的信道已经播放完毕了音乐
462
if (audio.ended){
463
return audio;
464
}
465
} else{
466
// 如果当前的信道已经播放完毕音乐了, 就返回当前的这个audio对象
467
if (!audio.ended)
468
return audio;
469
}
470
}
471
472
// 如果所有的信道都在使用的话,就返回undifined
473
return undefined;
474 },
475
/**
476
* 用于播放指定ID的音乐
477
* @param id
478
*/
479
playSound : function (id) {
480
// 获取当前可以使用的一个信道
481
var track = this.getAvailableSoundChannel(),
482
element = document.getElementById(id);
483
484
485
// 如果不为空(undefined)
486
if (track && element){
487
// 获取当前选中的媒体资源的URL地址
488
track.src = element.src === '' ? element.currentSrc : element.src;
489
490
// 加载并播放音乐
491
track.load();
492
track.play();
493
494
}
495 },
496
497
498
499
// 键盘事件的监听与处理操作---------------------------------------------
500
/**
501
* 把一个键值对添加到监听数组中去
502
* @param keyAndListener
503
*/
504
addKeyListener : function (keyAndListener) {
505
this.keyListeners.push(keyAndListener);
506 },
507
508
/**
509
* 通过key来查找相应的listener对象
510
* @param key
511
* @returns {undefined}
512
*/
513
findKeyListener : function (key) {
514
var listener = undefined;
515
516
// 遍历所有的keyListeners数组
517
for (var i = 0; i < this.keyListeners.length; i++){
518
// 拿到当前的键值监听对象及按键的key值
519
var keyAndListener = this.keyListeners[i],
520
currentKey = keyAndListener.key;
521
522
// 如果按下的按键是在我今天按下的所有keyAndListener中,就得到了这个listener
523
if (currentKey === key){
524
listener = keyAndListener.listener;
525
}
526
}
527
528
return listener;
529 },
530
531
/**
532
* 键盘按下的回调事件
533
* @param e
534
*/
535
keyPressed : function (e) {
536
var listener = undefined,
537
key = undefined;
538
539
switch (e.keyCode){
540
// 添加一些常用的按键处理键值对
541
case 32:
542
key = 'space';
543
break;
544
case 65:
545
key = 'a';
546
break;
547
case 83:
548
key = 's';
549
break;
550
case 80:
551
key = 'p';
552
break;
553
case 87:
554
key = 'w';
555
break;
556
// 记忆:左上右下的顺序,依次为:37 38 39 40
557
case 37:
558
key = 'left arrow';
559
break;
560
case 39:
561
key = 'right arrow';
562
break;
563
case 38:
564
key = 'up arrow';
565
break;
566
case 40:
567
key = 'down arrow';
568
break;
569
}
570
571
// 获取当前按下的按键的监听事件
572
listener = this.findKeyListener(key);
573
if (listener){
574
listener();
// 这里的listener是一个监听函数,如果按下的按键有监听事件的处理,就去处理这个监听事件
575
}
576
577 },
578
579
580
581
// 高分榜的维护管理模块----------------------------------------------------
582
/**
583
* 从本地存储中获取存储的数据(返回的是一个本地存储的高分列表)
584
* @returns {any}
585
*/
586
getHighScores : function () {
587
// 把key的值存储起来
588
var key = this.gameName + this.HIGH_SCORES_SUFFIX,
589
highScoresString = localStorage[key];
590
591
592
// 如果为空的话,返回一个空的Json数据
593
if (highScoresString == undefined){
594
localStorage[key] = JSON.stringify([]);
595
}
596
597
// 使用JSON解析字符串内容(返回的是一个JSon与key相对应的数值内容)
598
return JSON.parse(localStorage[key]);
599 },
600
/**
601
* 存储内容到本地存储
602
* @param highScore
603
*/
604
setHighScore : function (highScore) {
605
// unshift() 方法不创建新的创建,而是直接修改原有的数组【会在数组的头部插入新的元素】
606
var key = this.gameName + this.HIGH_SCORES_SUFFIX,
607
highScoresString = localStorage[key];
608
609
610
611
// 主要目的是把每一次最高分放在数组的第一位置,方便查看和管理
612
// 这里的highScores数组,是一个用户初始化的数组(全局变量)【数组的第一个元素始终是最高分】
613
//this.highScores.unshift(highScore);(每次都在原理的基础上添加数据)
614
if (this.highScores.length === 0){
615
this.highScores = this.getHighScores();
616
}
617
this.highScores.unshift(highScore);
618
619
620
// 游戏的key始终是惟一的,每一次都将会修改为最新的状态
621
localStorage[key] = JSON.stringify(this.highScores);
622 },
623
624
/**
625
* 清空高分榜(清空浏览器的本地存储)
626
*/
627
clearHighScores : function () {
628
// 直接把相应的键对应的值设置为空即可
629
localStorage[this.name + this.HIGH_SCORES_SUFFIX] = JSON.stringify([]);
630 }
631
632 }
【原创】使用JS封装的一个小型游戏引擎及源码分享
内容版权声明:除非注明,否则皆为本站原创文章。