1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
| // Frank Poth 03/09/2018
/* Обработчик keyDownUp был перемещен в основной файл. */
const Controller = function() {
this.left = new Controller.ButtonInput();
this.right = new Controller.ButtonInput();
this.up = new Controller.ButtonInput();
this.downs = new Controller.ButtonInput();
this.keyDownUp = function(type, key_code) {
var down = (type == "keydown") ? true : false;
switch(key_code) {
case 37: this.left.getInput(down); break;
case 38: this.up.getInput(down); break;
case 39: this.right.getInput(down); break;
case 40: this.downs.getInput(down);
}
};
};
Controller.prototype = {
constructor : Controller
};
Controller.ButtonInput = function() {
this.active = this.down = false;
};
Controller.ButtonInput.prototype = {
constructor : Controller.ButtonInput,
getInput : function(down) {
if (this.down != down) this.active = down;
this.down = down;
}
};
//=>
// Frank Poth 03/23/2018
/* Я изменил несколько мелочей с третьей части. Во-первых, я избавился от своего значения плитки
смещение при рисовании плиток с карты игрового объекта. Каждое значение, используемое для смещения
на 1 из-за формата экспорта моего редактора карт плитки. Я также изменил округление
метода в функции drawPlayer от Math.floor до Math.round, чтобы лучше представлять
где игрок фактически стоит. */
const Display = function(canvas) {
this.buffer = document.createElement("canvas").getContext("2d"),
this.context = canvas.getContext("2d");
this.tile_sheet = new Display.TileSheet(16, 8);
/* Эта функция рисует карту в буфер. */
this.drawMap = function(map, columns) {
for (let index = map.length - 1; index > -1; -- index) {
let value = map[index]; // Больше не вычитайте 1. Значения на моей карте плитки сдвинуты на 1.
let source_x = (value % this.tile_sheet.columns) * this.tile_sheet.tile_size;
let source_y = Math.floor(value / this.tile_sheet.columns) * this.tile_sheet.tile_size;
let destination_x = (index % columns) * this.tile_sheet.tile_size;
let destination_y = Math.floor(index / columns) * this.tile_sheet.tile_size;
this.buffer.drawImage(this.tile_sheet.image, source_x, source_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size, destination_x, destination_y, this.tile_sheet.tile_size, this.tile_sheet.tile_size);
}
};
this.drawPlayer = function(rectangle, color1, color2) {
this.buffer.fillStyle = color1;
this.buffer.fillRect(Math.round(rectangle.x), Math.round(rectangle.y), rectangle.width, rectangle.height);
this.buffer.fillStyle = color2;
this.buffer.fillRect(Math.round(rectangle.x + 2), Math.round(rectangle.y + 2), rectangle.width - 4, rectangle.height - 4);
};
this.resize = function(width, height, height_width_ratio) {
if (height / width > height_width_ratio) {
this.context.canvas.height = width * height_width_ratio;
this.context.canvas.width = width;
} else {
this.context.canvas.height = height;
this.context.canvas.width = height / height_width_ratio;
}
this.context.imageSmoothingEnabled = false;
};
};
Display.prototype = {
constructor : Display,
render:function() { this.context.drawImage(this.buffer.canvas, 0, 0, this.buffer.canvas.width, this.buffer.canvas.height, 0, 0, this.context.canvas.width, this.context.canvas.height); },
};
Display.TileSheet = function(tile_size, columns) {
this.image = new Image();
this.tile_size = tile_size;
this.columns = columns;
};
Display.TileSheet.prototype = {};
//->
// Frank Poth 02/28/2018
/* Это фиксированный игровой цикл времени. Он может использоваться для любой игры и будет обеспечивать
что состояние игры обновляется с той же скоростью на разных устройствах, что важно
для равномерного игрового процесса. Представьте, что вы играете в свою любимую игру на новом телефоне и вдруг
он работает с другой скоростью. Это будет плохой пользовательский опыт, поэтому мы исправим
это с фиксированным шагом игрового цикла. Кроме того, вы можете делать такие вещи, как сброс кадров
и интерполяция с фиксированным шагом цикла, которые позволяют вашей игре играть и смотреть
плавный на более медленных устройствах, а не замерзание или отставание до точки неиграбельности. */
const Engine = function(time_step, update, render) {
this.accumulated_time = 0;// Время, накопленное с момента последнего обновления.
this.animation_frame_request = undefined,// ссылка на AFR
this.time = undefined,// Самая последняя временная метка выполнения цикла.
this.time_step = time_step,// 1000/30 = 30 кадров в секунду
this.updated = false;// Независимо от того, была вызвана функция обновления с момента последнего цикла.
this.update = update;// Функция обновления
this.render = render;// Функция рендеринга
this.run = function(time_stamp) {// Это один цикл игрового цикла
this.accumulated_time += time_stamp - this.time;
this.time = time_stamp;
/* Если устройство работает слишком медленно, обновления могут занять больше времени, чем наш временной шаг. Если
это так, это может заморозить игру и перегрузить процессор. Чтобы предотвратить это,
мы рано улавливаем спираль памяти и никогда не позволяем пройти три полных кадра без
обновление. Это не идеально, но по крайней мере пользователь не будет терпеть крах своего процессора. */
if (this.accumulated_time >= this.time_step * 3) {
this.accumulated_time = this.time_step;
}
/* Поскольку мы можем обновлять только после того, как экран готов к рисованию и requestAnimationFrame
вызывает функцию запуска, нам нужно отслеживать, сколько времени прошло. Мы
хранить накопленное время и тест, чтобы проверить, прошло ли достаточно, чтобы оправдать
обновление. Помните, что мы хотим обновлять каждый раз, когда мы накопили один шаг
ценность времени, и если накоплено несколько временных шагов, мы должны обновить один
время для каждого из них оставаться на скорости. */
while(this.accumulated_time >= this.time_step) {
this.accumulated_time -= this.time_step;
this.update(time_stamp);
this.updated = true;// Если игра обновлена, нам нужно ее снова нарисовать.
}
/* Это позволяет нам рисовать только когда игра обновилась. */
if (this.updated) {
this.updated = false;
this.render(time_stamp);
}
this.animation_frame_request = window.requestAnimationFrame(this.handleRun);
};
this.handleRun = (time_step) => { this.run(time_step); };
};
Engine.prototype = {
constructor:Engine,
start:function() {
this.accumulated_time = this.time_step;
this.time = window.performance.now();
this.animation_frame_request = window.requestAnimationFrame(this.handleRun);
},
stop:function() { window.cancelAnimationFrame(this.animation_frame_request); }
};
//!&&&->
// Frank Poth 03/28/2018
/* В части 4 я добавил обнаружение столкновения и ответ для карты плитки. Я также
исправлено смещение карты плитки из части 3, где каждое графическое значение было смещено на
1 из-за формата экспорта редактора карты плитки, который я использовал. Я добавил collision_map
и объект коллайдера для обработки столкновения. Я также добавил суперкласс класса Object
что все остальные игровые объекты будут расширяться. У этого есть куча методов для работы с
позиция объекта. */
const Game = function() {
this.world = new Game.World();// Все изменения находятся в мировом классе.
this.update = function() {
this.world.update();
};
};
Game.prototype = { constructor : Game };
Game.World = function(friction = 0.9) {
this.collider = new Game.World.Collider();// Вот новый класс коллайдера.
this.friction = friction;
this.player = new Game.World.Player();
this.columns = 12;
this.rows = 9;
this.tile_size = 16;
/* Эта карта остается прежней. Это графическая карта. Он только размещает графику и
не имеет ничего общего с столкновением. */
this.map = [48,17,17,17,49,48,18,19,16,17,35,36,
10,39,39,39,16,18,39,31,31,31,39,07,
10,31,39,31,31,31,39,12,05,05,28,01,
35,06,39,39,31,39,39,19,39,39,08,09,
02,31,31,47,39,47,39,31,31,04,36,25,
10,39,39,31,39,39,39,31,31,31,39,37,
10,39,31,04,14,06,39,39,03,39,00,42,
49,02,31,31,11,39,39,31,11,00,42,09,
08,40,27,13,37,27,13,03,22,34,09,24];
/* These collision values correspond to collision functions in the Collider class.
00 is nothing. everything else is run through a switch statement and routed to the
appropriate collision functions. These particular values aren't arbitrary. Their binary
representation can be used to describe which sides of the tile have boundaries.
0000 = 0 tile 0: 0 tile 1: 1 tile 2: 0 tile 15: 1
0001 = 1 0 0 0 0 0 1 1 1
0010 = 2 0 0 0 1
1111 = 15 No walls Wall on top Wall on Right four walls
This binary representation can be used to describe which sides of a tile are boundaries.
Each bit represents a side: 0 0 0 0 = l b r t (left bottom right top). Keep in mind
that this is just one way to look at it. You could assign your collision values
any way you want. This is just the way I chose to keep track of which values represent
which tiles. I haven't tested this representation approach with more advanced shapes.
Эти значения столкновений соответствуют функциям столкновения в классе Collider.
00 ничего. все остальное выполняется через оператор switch и направляется на
соответствующие функции столкновения. Эти конкретные значения не являются произвольными. Их двоичный
представление может быть использовано для описания границ сторон плитки.
0000 = 0 плитка 0: 0 плитка 1: 1 плитка 2: 0 плитка 15: 1
0001 = 1 0 0 0 0 0 1 1 1
0010 = 2 0 0 0 1
1111 = 15 Нет стен Стена сверху Стена справа Правая стена
Это двоичное представление может использоваться для описания границ сторон плитки.
Каждый бит представляет собой сторону: 0 0 0 0 = l b r t (левая нижняя правая верхняя часть). Иметь ввиду
что это всего лишь один из способов взглянуть на него. Вы можете назначить свои значения столкновений
любым способом, который вы хотите. Это именно то, как я решил отслеживать, какие значения представляют
которые плитки. Я не тестировал этот подход представления с более продвинутыми формами
*/
this.collision_map = [00,04,04,04,00,00,04,04,04,04,04,00,
02,00,00,00,12,06,00,00,00,00,00,08,
02,00,00,00,00,00,00,09,05,05,01,00,
00,07,00,00,00,00,00,14,00,00,08,00,
02,00,00,01,00,01,00,00,00,13,04,00,
02,00,00,00,00,00,00,00,00,00,00,08,
02,00,00,13,01,07,00,00,11,00,09,00,
00,03,00,00,10,00,00,00,08,01,00,00,
00,00,01,01,00,01,01,01,00,00,00,00];
this.height = this.tile_size * this.rows;
this.width = this.tile_size * this.columns;
};
Game.World.prototype = {
constructor: Game.World,
/* Эта функция была сильно изменена. */
collideObject:function(object) {
/* Давайте убедимся, что мы не можем покинуть границы мира. */
if (object.getLeft() < 0 ) { object.setLeft(0); object.velocity_x = 0; }
else if (object.getRight() > this.width ) { object.setRight(this.width); object.velocity_x = 0; }
if (object.getTop() < 0 ) { object.setTop(0); object.velocity_y = 0; }
else if (object.getBottom() > this.height) { object.setBottom(this.height); object.velocity_y = 0; }
/* Теперь давайте столкнемся с некоторыми плитами! Боковые значения относятся к сетке плитки
строки и столбцы, которые объект занимает на каждой из своих сторон. За
bottom внизу относится к строке на карте столкновения, в нижней части
объект занимает. Правило относится к столбцу на карте столкновения, занятой
правая сторона объекта. Значение относится к значению коллизионной плитки в
карта под указанной строкой и столбцом, занимаемая объектом. */
var bottom, left, right, top, value;
/* Сначала мы проверяем верхний левый угол объекта. Мы получаем строку и столбец
он занимает в карте столкновения, то мы получаем значение из карты столкновения
в этой строке и столбце. В этом случае строка верха и столбец остается. затем
мы передаем информацию коллидарной функции коллайдера. */
top = Math.floor(object.getTop() / this.tile_size);
left = Math.floor(object.getLeft() / this.tile_size);
value = this.collision_map[top * this.columns + left];
this.collider.collide(value, object, left * this.tile_size, top * this.tile_size, this.tile_size);
/* Мы должны переделать верхнюю часть с момента последней проверки на столкновение, потому что объект может
с момента последней проверки столкновения. Кроме того, причина, по которой я проверяю верхние углы
во-первых, потому что, если объект перемещается вниз при проверке верха, он будет
когда вы проверяете нижнюю часть, и лучше выглядеть, как будто он стоит
на земле, чем толкаться вниз по земле посредством. */
top = Math.floor(object.getTop() / this.tile_size);
right = Math.floor(object.getRight() / this.tile_size);
value = this.collision_map[top * this.columns + right];
this.collider.collide(value, object, right * this.tile_size, top * this.tile_size, this.tile_size);
bottom = Math.floor(object.getBottom() / this.tile_size);
left = Math.floor(object.getLeft() / this.tile_size);
value = this.collision_map[bottom * this.columns + left];
this.collider.collide(value, object, left * this.tile_size, bottom * this.tile_size, this.tile_size);
bottom = Math.floor(object.getBottom() / this.tile_size);
right = Math.floor(object.getRight() / this.tile_size);
value = this.collision_map[bottom * this.columns + right];
this.collider.collide(value, object, right * this.tile_size, bottom * this.tile_size, this.tile_size);
},
update:function() {
this.player.velocity_x *= this.friction;
this.player.velocity_y *= this.friction;
this.collideObject(this.player);
}
};
Game.World.Collider = function() {
/* Это метод маршрутизации функции. В принципе, вы знаете, как выглядит плитка
от его значения. Вы знаете, с каким объектом вы хотите столкнуться, и знаете
x и y позиции плитки, а также ее размеры. Эта функция просто решает
которые коллизия функционирует для использования на основе значения и позволяет настраивать
другие значения, соответствующие конкретной форме плитки. */
this.collide = function(value, object, tile_x, tile_y, tile_size) {
switch(value) { // какое значение имеет наша плитка?
/* Все 15 типов плиток могут быть описаны только с 4 методами столкновения. Эти
методы смешиваются и сопоставляются для каждой уникальной плитки. */
case 1: this.collidePlatformTop (object, tile_y ); break;
case 2: this.collidePlatformRight (object, tile_x + tile_size); break;
case 3: if (this.collidePlatformTop (object, tile_y )) return;// Если есть столкновение, нам не нужно проверять что-либо еще.
this.collidePlatformRight (object, tile_x + tile_size); break;
case 4: this.collidePlatformBottom (object, tile_y + tile_size); break;
case 5: if (this.collidePlatformTop (object, tile_y )) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 6: if (this.collidePlatformRight(object, tile_x + tile_size)) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 7: if (this.collidePlatformTop (object, tile_y )) return;
if (this.collidePlatformRight(object, tile_x + tile_size)) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 8: this.collidePlatformLeft (object, tile_x ); break;
case 9: if (this.collidePlatformTop (object, tile_y )) return;
this.collidePlatformLeft (object, tile_x ); break;
case 10: if (this.collidePlatformLeft (object, tile_x )) return;
this.collidePlatformRight (object, tile_x + tile_size); break;
case 11: if (this.collidePlatformTop (object, tile_y )) return;
if (this.collidePlatformLeft (object, tile_x )) return;
this.collidePlatformRight (object, tile_x + tile_size); break;
case 12: if (this.collidePlatformLeft (object, tile_x )) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 13: if (this.collidePlatformTop (object, tile_y )) return;
if (this.collidePlatformLeft (object, tile_x )) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 14: if (this.collidePlatformLeft (object, tile_x )) return;
if (this.collidePlatformRight(object, tile_x )) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
case 15: if (this.collidePlatformTop (object, tile_y )) return;
if (this.collidePlatformLeft (object, tile_x )) return;
if (this.collidePlatformRight(object, tile_x + tile_size)) return;
this.collidePlatformBottom (object, tile_y + tile_size); break;
}
}
};
/* Вот где живут все функции столкновения. */
Game.World.Collider.prototype = {
constructor: Game.World.Collider,
/* Это позволит разрешить конфликт (если есть) между объектом и местоположением y
некоторое дно плитки. Все эти функции практически одинаковы, просто скорректированы
для разных сторон плитки и разных траекторий объекта. */
collidePlatformBottom:function(object, tile_bottom) {
/* Если верхняя часть объекта находится над нижней частью плитки и предыдущей
рамка верхней части объекта была ниже нижней части плитки, мы вошли в
эта плитка. Довольно простые вещи. */
if (object.getTop() < tile_bottom && object.getOldTop() >= tile_bottom) {
object.setTop(tile_bottom);// Переместите верхнюю часть объекта к нижней части плитки.
object.velocity_y = 0; // Прекратите движение в этом направлении.
return true; // Вернуть true, потому что произошло столкновение.
} return false; // Вернуть false, если не было столкновений.
},
collidePlatformLeft:function(object, tile_left) {
if (object.getRight() > tile_left && object.getOldRight() <= tile_left) {
object.setRight(tile_left - 0.01);// -0.01 заключается в том, чтобы исправить небольшую проблему с округлением
object.velocity_x = 0;
return true;
} return false;
},
collidePlatformRight:function(object, tile_right) {
if (object.getLeft() < tile_right && object.getOldLeft() >= tile_right) {
object.setLeft(tile_right);
object.velocity_x = 0;
return true;
} return false;
},
collidePlatformTop:function(object, tile_top) {
if (object.getBottom() > tile_top && object.getOldBottom() <= tile_top) {
object.setBottom(tile_top - 0.01);
object.velocity_y = 0;
return true;
} return false;
}
};
/* Класс объекта - это просто базовый прямоугольник с набором функций прототипа
чтобы помочь нам работать с позиционированием этого прямоугольника. */
Game.World.Object = function(x, y, width, height) {
this.height = height;
this.width = width;
this.x = x;
this.x_old = x;
this.y = y;
this.y_old = y;
};
Game.World.Object.prototype = {
constructor:Game.World.Object,
/* Эти функции используются для получения и установки различных боковых позиций объекта. */
getBottom: function() { return this.y + this.height; },
getLeft: function() { return this.x; },
getRight: function() { return this.x + this.width; },
getTop: function() { return this.y; },
getOldBottom:function() { return this.y_old + this.height; },
getOldLeft: function() { return this.x_old; },
getOldRight: function() { return this.x_old + this.width; },
getOldTop: function() { return this.y_old },
setBottom: function(y) { this.y = y - this.height; },
setLeft: function(x) { this.x = x; },
setRight: function(x) { this.x = x - this.width; },
setTop: function(y) { this.y = y; },
setOldBottom:function(y) { this.y_old = y - this.height; },
setOldLeft: function(x) { this.x_old = x; },
setOldRight: function(x) { this.x_old = x - this.width; },
setOldTop: function(y) { this.y_old = y; }
};
Game.World.Player = function(x, y) {
Game.World.Object.call(this, 100, 100, 12, 12);
this.color1 = "#404040";
this.color2 = "#f0f0f0";
this.velocity_x = 0;
this.velocity_y = 0;
};
Game.World.Player.prototype = {
moveLeft:function() { this.velocity_x -= 0.5; },
moveRight:function() { this.velocity_x += 0.5; },
moveUp:function() { this.velocity_y -= 0.5; },
moveDowns:function() { this.velocity_y += 0.5; },
update:function() {
this.x_old = this.x;
this.y_old = this.y;
this.x += this.velocity_x;
this.y += this.velocity_y;
}
};
Object.assign(Game.World.Player.prototype, Game.World.Object.prototype);
Game.World.Player.prototype.constructor = Game.World.Player;
//&&&->
// Frank Poth 03/23/2017
window.addEventListener("load", function(event) {
"use strict";
///////////////////
//// FUNCTIONS ////
///////////////////
var keyDownUp = function(event) {
controller.keyDownUp(event.type, event.keyCode);
};
var resize = function(event) {
display.resize(document.documentElement.clientWidth - 32, document.documentElement.clientHeight - 32, game.world.height / game.world.width);
display.render();
};
var render = function() {
display.drawMap(game.world.map, game.world.columns);
display.drawPlayer(game.world.player, game.world.player.color1, game.world.player.color2);
display.render();
};
var update = function() {
if (controller.left.active) { game.world.player.moveLeft(); }
if (controller.right.active) { game.world.player.moveRight(); }
if (controller.up.active) { game.world.player.moveUp(); }
if (controller.downs.active) { game.world.player.moveDowns(); }
game.update();
};
/////////////////
//// OBJECTS ////
/////////////////
var controller = new Controller();
var display = new Display(document.querySelector("canvas"));
var game = new Game();
var engine = new Engine(1000/30, render, update);
////////////////////
//// INITIALIZE ////
////////////////////
display.buffer.canvas.height = game.world.height;
display.buffer.canvas.width = game.world.width;
display.tile_sheet.image.addEventListener("load", function(event) {
resize();
engine.start();
}, { once:true });
display.tile_sheet.image.src = "rabbit-trap.png";
window.addEventListener("keydown", keyDownUp);
window.addEventListener("keyup", keyDownUp);
window.addEventListener("resize", resize);
}); |