Space Invaders — различия между версиями

Материал из Department of Theoretical and Applied Mechanics
Перейти к: навигация, поиск
(Код программы)
(Описание)
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
 
==Описание==
 
==Описание==
Классическая аркада Space Invaders прямиком из 1980-х.
+
Классическая аркада Space Invaders прямиком из 1980-х на JavaScript.
  
 
Автор: [[Кашапов Тимур |Кашапов Тимур]]
 
Автор: [[Кашапов Тимур |Кашапов Тимур]]
Строка 8: Строка 8:
 
{{#widget:Iframe | url =http://tm.spbstu.ru/htmlets/js2020/Kashapov/SpaceInvaders/index.html | width = 750| height = 750| border = 0 }}
 
{{#widget:Iframe | url =http://tm.spbstu.ru/htmlets/js2020/Kashapov/SpaceInvaders/index.html | width = 750| height = 750| border = 0 }}
  
==Код программы==
 
 
== Код программы ==
 
== Код программы ==
 
<div class="mw-collapsible mw-collapsed">
 
<div class="mw-collapsible mw-collapsed">
Строка 26: Строка 25:
 
</div>
 
</div>
 
<div class="assets">
 
<div class="assets">
<img id="spritesheet" src="spritesheet.png" style="display: none;">
+
<img id="spritesheet" src="spritesheet.png"  
 +
style="display: none;">
 
</div>
 
</div>
 
<script type="text/javascript" src="script.js"></script>
 
<script type="text/javascript" src="script.js"></script>

Текущая версия на 18:05, 31 мая 2020

Описание[править]

Классическая аркада Space Invaders прямиком из 1980-х на JavaScript.

Автор: Кашапов Тимур Группа: 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 }