Space Invaders
Материал из Department of Theoretical and Applied Mechanics
Описание
Классическая аркада Space Invaders прямиком из 1980-х.
Автор: Кашапов Тимур Группа: 3630103/90003
Игровое поле
Код программы
HTML:
1 <!DOCTYPE html>
2 <html>
3
4 <head>
5 <title>Space Invaders</title>
6 <meta charset="utf-8">
7 <link rel="stylesheet" type="text/css" href="style.css">
8 </head>
9 <body>
10 <div class="content">
11 <div id="game"></div>
12 </div>
13 <div class="assets">
14 <img id="spritesheet" src="spritesheet.png"
15 style="display: none;">
16 </div>
17 <script type="text/javascript" src="script.js"></script>
18 </body>
19
20 </html>
JavaScript:
1 // проверяем, доступно ли локальное хранилище
2 var ls_enabled = false;
3 try {
4 localStorage.setItem("ls-test", true);
5 localStorage.getItem("ls-test");
6 localStorage.removeItem("ls-test");
7 ls_enabled = true;
8 } catch(error) {
9 console.info("LS", error);
10 }
11 // создаем холст
12 var canvas = document.createElement("canvas");
13 canvas.width = 500;
14 canvas.height = 500;
15
16 // добавляем холст на страницу
17 document.getElementById("game").appendChild(canvas);
18
19 // создаем графический контекст
20 var ctx = canvas.getContext("2d");
21
22 // настройка шрифта
23 var fontSize = 18;
24 ctx.font = fontSize + "px Courier New";
25
26 // пресеты лейблов
27 var labelPauseText = "Game Paused (press Enter to continue)";
28 var labelPause = ctx.measureText(labelPauseText);
29 var labelWinText = "Invaders defeated (press F5 to play again)";
30 var labelWin = ctx.measureText(labelWinText);
31 var labelEndText = "You lose (press F5 to play again)";
32 var labelEnd = ctx.measureText(labelEndText);
33
34 // переменные для игрового цикла
35 var lastTime = Date.now();
36 var totalTime = 0;
37 var elapsed = 0;
38
39 // отступ (используется в отрисовке интерфейса и тд)
40 var padding = 16;
41
42 // текстура (тайлсет) для игры
43 var img = new Image();
44
45 // класс Пуля
46 // hostile(флаг) -- является ли вражеской пулей (влияет на цвет и направление полета)
47 //dir -- скаляр для разделение на свои и вражеские пули, если
48 //hostile - true, то - враг, его пули летят вниз,
49 //если false - это игрок и его пули летят вверх
50 function Bullet(hostile, x, y) {
51
52 this.w = 5;
53 this.h = 8;
54 this.x = x;
55 this.y = y;
56
57 this.speed = 5;
58 if(hostile){
59 this.dir = 1;
60 this.color = 'white';
61 } else {
62 this.dir = -1;
63 this.color = '#00fc00';
64 }
65 this.hostile = hostile;
66 }
67
68 Bullet.prototype = {
69 update: function(dt){
70 this.y += this.speed * this.dir;
71 },
72 render: function(ctx){
73 ctx.fillStyle = this.color;
74 ctx.fillRect(this.x, this.y, this.w, this.h);
75 }
76 }
77
78 // класс Блок
79 function Block(x, y) {
80
81 this.w = 44;
82 this.h = 32;
83 this.x = x;
84 this.y = y;
85
86 //Спрайты
87 this.tw = 44;
88 this.th = 32;
89 this.tx = 0;
90 this.ty = 48;
91
92 this.health = 4;
93 }
94
95 Block.prototype = {
96 render: function(ctx){
97 ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h);
98 },
99 handleDamage: function(){
100 this.health--;
101 this.ty += this.th;
102 }
103 }
104
105 // класс Космического захватчика (противника)
106 // tier -- уровень (тип) врага
107 // x, y -- координаты
108 // row, col -- индексы в построении
109 function Invader(tier, x, y, row, col) {
110
111 this.tier = tier;
112
113 this.w = 26;
114 this.h = 16;
115 this.x = x;
116 this.y = y;
117
118 //Спрайты
119 this.tw = 26;
120 this.th = 16;
121 this.tx = this.tw * tier;
122 this.ty = 0;
123
124 this.row = row;
125 this.col = col;
126 this.leading = false;
127
128 this.move = 0;
129 this.speed = 1;
130 }
131
132 Invader.prototype = {
133 render: function(ctx){
134 ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h);
135 }
136 }
137
138 // класс Игрок (комический корабль игрока)
139 function Player() {
140
141 this.w = 26;
142 this.h = 16;
143 this.x = (canvas.width - this.w) / 2;
144 this.y = canvas.height - padding * 3 - this.h;
145
146 //Спрайты
147 this.tw = 26;
148 this.th = 16;
149 this.tx = 277;
150 this.ty = 228;
151
152 // управление передвижением
153 this.moveLeft = false;
154 this.moveRight = false;
155 this.speed = 5;
156
157 // стрельба
158 this.shoot = false;
159 this.shootFired = 0;
160 this.shootDelay = 1.0;
161
162 // для анимации ранения
163 this.respawned = true;
164 this.visible = true;
165 this.flickElapsed = 0.0;
166 this.flickTime = 0.0;
167 }
168
169 Player.prototype = {
170 update: function(dt){
171 // управление передвижением
172 if (this.moveLeft) {
173 this.x = Math.max(this.x - this.speed, padding);
174 }
175
176 if (this.moveRight) {
177 this.x = Math.min(this.x + this.speed, canvas.width - this.w - padding);
178 }
179
180 // для анимации ранения
181 if (this.respawned) {
182
183 this.flickTime += dt;
184 this.flickElapsed += dt;
185
186 if (this.flickTime > 2.0) {
187 this.respawned = false;
188 this.visible = true;
189 }
190
191 if (this.flickElapsed > 0.1) {
192 this.visible = !this.visible;
193 this.flickElapsed = 0.0;
194 }
195 }
196 },
197 render: function(ctx){
198 if (this.visible) {
199 ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h);
200 }
201 }
202 }
203
204 var player = new Player();
205 var bullets = [];
206 var blocks = [];
207 var invaders = [];
208 var invader_index = 0;
209 var invader_dir = 1;
210 var invader_speed = 5;
211 var invader_it = 0.02;
212 var gameStatePause = 2;
213 var gameStateWin = 3;
214 var gameStateEnd = 4;
215 var gameStatePlaying = 5;
216 var gameState = gameStatePause;
217 var started = false;
218 var score = 0;
219 if(ls_enabled && parseInt(localStorage.getItem("ls-highscore")) || false){
220 highscore = parseInt(localStorage.getItem("ls-highscore"));
221 } else{
222 highscore = 0;
223 }
224
225 var life = 3;
226
227 // вызывается когда игра закончена
228 function game_finished() {
229 bullets = [];
230 player.visible = true;
231 update_highscore();
232 }
233
234 // пересчет топа
235 function update_highscore() {
236 if (highscore < score) {
237 highscore = score;
238 if (ls_enabled) {
239 localStorage.setItem("ls-highscore", highscore);
240 }
241 }
242 }
243
244 // проверяем, какой противник является стрелком
245 //leading - стреляющий
246 // вызывается при запуске игры и при смерти противника
247 function update_leading_invaders() {
248 let dict = {};
249 for (let i = 0; i < invaders.length; ++i) {
250 let invader = invaders[i];
251 if (dict.hasOwnProperty(invader.col)) {//Если имеет данное свойство
252 let e = dict[invader.col];
253 if (e.row < invader.row) {
254 e.row = invader.row;
255 e.i = i;
256 }
257 } else {
258 dict[invader.col] = {};
259 dict[invader.col].row = invader.row;
260 dict[invader.col].i = i;// Получили линию и номер
261 }
262 }
263 console.log(dict);//Dict - словарь объектов
264 for (let key in dict) {
265 let e = dict[key];
266 invaders[e.i].leading = true;//Результат работы всей функции
267 }
268 }
269
270 document.onkeydown = function(e) {
271
272 if (e.key == "ArrowLeft") {
273 player.moveLeft = true;
274 } else if (e.key == "ArrowRight") {
275 player.moveRight = true;
276 } else if (e.key == " ") {
277
278 if (!started) {
279 gameState = gameStatePlaying;
280 started = true;
281 }
282
283 player.shoot = true;
284 }
285 }
286
287 document.onkeyup = function(e) {
288
289 if (e.key == "ArrowLeft") {
290 player.moveLeft = false;
291 } else if (e.key == "ArrowRight") {
292 player.moveRight = false;
293 } else if (e.key == " ") {
294 player.shoot = false;
295 } else if (e.key == "Enter") {
296
297 started = true;
298
299 if (gameState == gameStatePause) {
300 gameState = gameStatePlaying;
301 } else if (gameState == gameStatePlaying) {
302 gameState = gameStatePause;
303 }
304 }
305 }
306
307 // функция проверки пересечения двух прямоугольников (для столкновения с пулей)
308 function intersects(x1, y1, w1, h1, x2, y2, w2, h2) {
309 //Для 1 объекта
310 let r1MinX = Math.min(x1, x1 + w1);
311 let r1MaxX = Math.max(x1, x1 + w1);
312 let r1MinY = Math.min(y1, y1 + h1);
313 let r1MaxY = Math.max(y1, y1 + h1);
314 //Для 2 объекта
315 let r2MinX = Math.min(x2, x2 + w2);
316 let r2MaxX = Math.max(x2, x2 + w2);
317 let r2MinY = Math.min(y2, y2 + h2);
318 let r2MaxY = Math.max(y2, y2 + h2);
319
320 let interLeft = Math.max(r1MinX, r2MinX);
321 let interTop = Math.max(r1MinY, r2MinY);
322 let interRight = Math.min(r1MaxX, r2MaxX);
323 let interBottom = Math.min(r1MaxY, r2MaxY);
324
325 return (interLeft < interRight) && (interTop < interBottom);//true - столкновение
326 }
327
328 // вызов сл. кадра
329 var requestAnimFrame = (function() {
330 return window.requestAnimationFrame ||
331 window.webkitRequestAnimationFrame ||
332 window.mozRequestAnimationFrame ||
333 window.oRequestAnimationFrame ||
334 window.msRequestAnimationFrame ||
335 function(callback) {
336 window.setTimeout(callback, 1000 / 60);
337 };
338 })();
339
340 // главный цикл игры
341 function update(dt) {
342
343 // если игра не начата
344 if (!started || gameState != gameStatePlaying) return;
345
346 // обновляем игрока и контролируем управление
347 player.update(dt);
348 if (player.shoot && player.shootDelay < totalTime - player.shootFired) {
349 bullets.push(new Bullet(false, player.x + player.w / 2, player.y));
350 player.shootFired = totalTime;
351 let audio = new Audio("shoot.wav");
352 audio.volume = 0.02;
353 audio.play();
354 }
355
356 // проверяем пули на столкновение и обновляем движение
357 for (let i = 0; i < bullets.length; ++i) {
358
359 let bullet = bullets[i];
360 bullet.update(dt);
361
362 // если пуля вражеская
363 if (bullet.hostile) {
364
365 // если игрок уязвим (не был только что ранен) и ранен
366 // наносим урон и удаляем пулю
367 if (!player.respawned && intersects(player.x, player.y, player.w, player.h, bullet.x, bullet.y, bullet.w, bullet.h)) {
368
369 player.respawned = true;
370 player.flickTime = 0.0;
371 player.flickElapsed = 0.0;
372 player.visible = false;
373 life -= 1;
374 // кончились жизни
375 if (life == 0) {
376 gameState = gameStateEnd;
377 game_finished();
378 }
379
380 bullets.splice(i, 1);
381 break;
382 } else if (bullet.y > player.y + player.h) { // пуля улетела за игрока
383 bullets.splice(i, 1);
384 break;
385 } else { // проверяем попадание в укрепление
386
387 // ищем попадание
388 let inter = -1;
389 for (let j = 0; j < blocks.length; ++j) {
390 let block = blocks[j];
391 if (intersects(block.x, block.y, block.w, block.h, bullet.x, bullet.y, bullet.w, bullet.h)) {
392 inter = j;
393 break;
394 }
395 }
396
397 // если попала, наносим блоку ранение и удаляем пулю
398 if (inter >= 0) {
399 bullets.splice(i, 1);
400 blocks[inter].handleDamage();
401 if (blocks[inter].health == 0) {
402 blocks.splice(inter, 1);
403 }
404 break;
405 }
406 }
407 } else { // это пуля игрока, проверяем попала ли в цель
408
409 // ищем попадание
410 let inter = -1;
411 for (let j = 0; j < invaders.length; ++j) {
412 let invader = invaders[j];
413 if (intersects(invader.x, invader.y, invader.w, invader.h, bullet.x, bullet.y, bullet.w, bullet.h)) {
414 inter = j;
415 break;
416 }
417 }
418
419 // если попала, удаляем пулю/врага и повышаем счет
420 if (inter >= 0) {
421
422 bullets.splice(i, 1);
423 score += (3 - invaders[inter].tier) * 10;
424 invaders.splice(inter, 1);
425
426 update_leading_invaders();
427
428 // все убиты
429 if (invaders.length == 0) {
430 gameState = gameStateWin;
431 game_finished();
432 }
433
434 break;
435 } else if (bullet.y < padding) { // пуля улетела за экран
436 bullets.splice(i, 1);
437 break;
438 }
439 }
440 }
441
442 // пришло время обновить состояние врага (одного, по индексу)
443 if (elapsed > invader_it) {
444
445 elapsed = 0.0;
446
447 if (invader_index < invaders.length) {
448
449 // двинуть врага и переключить фрейм (анимацию)
450 let inv = invaders[invader_index];
451 inv.ty = inv.th - inv.ty;
452 inv.x += invader_speed * invader_dir;
453
454 // противник стреляет
455 if (Math.random() > 0.9 && inv.leading) {
456
457 bullets.push(new Bullet(true, inv.x + inv.w / 2, inv.y + inv.h));
458
459 let audio = new Audio("shoot.wav");
460 audio.playbackRate = 4;
461 audio.volume = 0.1;
462 audio.play();
463 }
464
465 // противники долетели до игрока
466 if (inv.y + inv.h > player.y) {
467 gameState = gameStateEnd;
468 game_finished();
469 }
470 }
471
472 // индекс сл. врага в построении
473 invader_index = (invader_index + 1) % invaders.length;
474
475 // проверяем, нужно ли опустить ряды ниже и сменить направление движения
476 for (let invader of invaders) {
477 if ((invader_dir > 0 && invader.x > canvas.width - padding) || (invader_dir < 0 && invader.x < padding)) {
478
479 if (invader_dir < 0) {
480 invader_index = (invader_index + invaders.length - 1) % invaders.length;
481 }
482
483 invader_dir = -invader_dir;
484
485 for (let i = 0; i < invaders.length; ++i)
486 invaders[i].y += 32;
487
488 break;
489 }
490 }
491 }
492 }
493
494 // главный цикл отрисовки
495 function render() {
496
497 // очищаем холст
498 ctx.fillStyle = "black";
499 ctx.clearRect(0, 0, canvas.width, canvas.height);
500
501 // рисуем игрока
502 player.render(ctx);
503
504 // если экран смерти, затеняем противников
505 // чтобы не сливались с текстом
506 if (gameState == gameStateEnd)
507 ctx.globalAlpha = 0.2;
508
509 // рисуем врагов
510 for (let invader of invaders)
511 invader.render(ctx);
512
513 ctx.globalAlpha = 1;
514
515 // рисуем укрепления
516 for (let block of blocks)
517 block.render(ctx);
518
519 // рисуем пули
520 for (let bullet of bullets)
521 bullet.render(ctx);
522
523 // рисуем кол-во жизней
524 if (life > 0) {
525
526 ctx.fillStyle = "white";
527 ctx.fillText(life, padding, canvas.height - padding);
528
529 for (let i = 0; i < life; ++i) {
530 ctx.drawImage(img, 277, 228, 26, 16, padding * 2 + (34 * i), canvas.height - padding * 2, 26, 16);
531 }
532 }
533
534 // если пауза, рисуем рамку и лейбл
535 if (gameState == gameStatePause) {
536 ctx.strokeStyle = "#00fc00";
537 ctx.lineDashOffset = totalTime * 50;//Величина смещения штрихов линии
538 ctx.setLineDash([15]);
539 ctx.strokeRect(0, 0, canvas.width, canvas.height);
540 ctx.fillStyle = "#00fc00";
541 ctx.fillText(labelPauseText, (canvas.width - labelPause.width) / 2, fontSize);
542 }
543
544 // если проиграл, рисуем рамку, лейбл и счет (в центре)
545 if (gameState == gameStateEnd) {
546 ctx.strokeStyle = "#ff0000";
547 ctx.lineDashOffset = totalTime * 50;
548 ctx.setLineDash([15]);
549 ctx.strokeRect(0, 0, canvas.width, canvas.height);
550 ctx.fillStyle = "#ff0000";
551 let y = 100;
552 ctx.fillText(labelEndText, (canvas.width - labelEnd.width) / 2, y + (fontSize + padding) * 1);
553 ctx.fillStyle = "white";
554 let text = ctx.measureText("Score: " + score);
555 ctx.fillText("Score: " + score, (canvas.width - text.width) / 2, y + (fontSize + padding) * 2);
556 text = ctx.measureText("High Score: " + highscore);
557 ctx.fillText("High Score: " + highscore, (canvas.width - text.width) / 2, y + (fontSize + padding) * 3);
558 return;
559 }
560
561 // если победил, рисуем рамку, лейбл и счет (в центре)
562 if (gameState == gameStateWin) {
563 ctx.strokeStyle = "yellow";
564 ctx.lineDashOffset = totalTime * 50;
565 ctx.setLineDash([15]);
566 ctx.strokeRect(0, 0, canvas.width, canvas.height);
567 ctx.fillStyle = "yellow";
568 let y = 100;
569 ctx.fillText(labelWinText, (canvas.width - labelWin.width) / 2, y + (fontSize + padding) * 1);
570 ctx.fillStyle = "white";
571 let text = ctx.measureText("Score: " + score);
572 ctx.fillText("Score: " + score, (canvas.width - text.width) / 2, y + (fontSize + padding) * 2);
573 text = ctx.measureText("High Score: " + highscore);//measureText - метод получения размеров текста
574 ctx.fillText("High Score: " + highscore, (canvas.width - text.width) / 2, y + (fontSize + padding) * 3);
575 return;
576 }
577
578 // счет в нижней части экрана
579 ctx.fillStyle = "white";
580 let text = ctx.measureText("Score: " + score);
581 ctx.fillText("Score: " + score, 150, canvas.height - padding);
582 ctx.fillText("High Score: " + highscore, 300, canvas.height - padding);
583 }
584
585 // загружаем текстуру
586 img.src = 'spritesheet.png';
587
588 // по завершении загрузки инициализируем данные игры
589 img.onload = function() {
590
591 // построение противников
592 let invader_rows = 5;
593 let invader_cols = 11;
594 let tiers = [0, 1, 1, 2, 2];
595 let invader_prototype = new Invader();
596 let off_w = 8;
597 let off_h = 16;
598 let off_x = (canvas.width - (invader_prototype.w * invader_cols) - (off_w * (invader_cols - 1))) / 2;
599 let off_y = padding * 3;
600 let block_prototype = new Block();
601
602 for (let r = 0; r < invader_rows; ++r) {
603 for (let c = 0; c < invader_cols; ++c) {
604 let tier = tiers[r];
605 let invader = new Invader(
606 tier,
607 off_x + c * (invader_prototype.w + off_w),
608 off_y + r * (invader_prototype.h + off_h),
609 r, c
610 );
611 invaders.push(invader);
612 }
613 }
614
615 // обновляем стрелков
616 update_leading_invaders();
617
618 // построение укреплений
619 let block_off_x = (canvas.width - 7 * block_prototype.w) / 2;
620 for (let i = 0; i < 4; ++i) {
621 let block = new Block(
622 block_off_x + i * block_prototype.w * 2,
623 canvas.height - block_prototype.w * 2 - padding
624 );
625 blocks.push(block);
626 }
627
628 // кадр
629 function main() {
630
631 let now = Date.now();
632 let dt = (now - lastTime) / 1000.0;
633
634 totalTime += dt;
635 elapsed += dt;
636
637 update(dt);
638 render();
639
640 lastTime = now;
641 requestAnimFrame(main);
642 }
643
644 main();
645 }
CSS:
1 html, body {
2 margin: 0;
3 padding: 0;
4 background-color: #001058;
5 height: 100%;
6 }
7
8 .content {
9 display: flex;
10 justify-content: center;
11 height: 100%;
12 background: url(background.png);
13 background-repeat: no-repeat;
14 background-size: 100% 750px;
15 }
16
17 #game {
18 margin: auto;
19 }