Space Invaders — различия между версиями
Материал из Department of Theoretical and Applied Mechanics
(Новая страница: «((#widget : Iframe | url = http://tm.spbstu.ru/htmlets/js2020/Kashapov/Space Invaders/index.html | width = 800| height = 800| border = 1 ))») |
(→Описание) |
||
(не показано 13 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
− | + | ==Описание== | |
+ | Классическая аркада Space Invaders прямиком из 1980-х на JavaScript. | ||
+ | |||
+ | Автор: [[Кашапов Тимур |Кашапов Тимур]] | ||
+ | Группа: 3630103/90003 | ||
+ | |||
+ | ==Игровое поле== | ||
+ | {{#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"> | ||
+ | '''HTML:''' <div class="mw-collapsible-content"> | ||
+ | <syntaxhighlight lang="javascript" line start="1" enclose="div"> | ||
+ | <!DOCTYPE html> | ||
+ | <html> | ||
+ | |||
+ | <head> | ||
+ | <title>Space Invaders</title> | ||
+ | <meta charset="utf-8"> | ||
+ | <link rel="stylesheet" type="text/css" href="style.css"> | ||
+ | </head> | ||
+ | <body> | ||
+ | <div class="content"> | ||
+ | <div id="game"></div> | ||
+ | </div> | ||
+ | <div class="assets"> | ||
+ | <img id="spritesheet" src="spritesheet.png" | ||
+ | style="display: none;"> | ||
+ | </div> | ||
+ | <script type="text/javascript" src="script.js"></script> | ||
+ | </body> | ||
+ | |||
+ | </html> | ||
+ | </syntaxhighlight> | ||
+ | </div> | ||
+ | <div class="mw-collapsible mw-collapsed"> | ||
+ | '''JavaScript:''' <div class="mw-collapsible-content"> | ||
+ | <syntaxhighlight lang="javascript" line start="1" enclose="div"> | ||
+ | |||
+ | // проверяем, доступно ли локальное хранилище | ||
+ | var ls_enabled = false; | ||
+ | try { | ||
+ | localStorage.setItem("ls-test", true); | ||
+ | localStorage.getItem("ls-test"); | ||
+ | localStorage.removeItem("ls-test"); | ||
+ | ls_enabled = true; | ||
+ | } catch(error) { | ||
+ | console.info("LS", error); | ||
+ | } | ||
+ | // создаем холст | ||
+ | var canvas = document.createElement("canvas"); | ||
+ | canvas.width = 500; | ||
+ | canvas.height = 500; | ||
+ | |||
+ | // добавляем холст на страницу | ||
+ | document.getElementById("game").appendChild(canvas); | ||
+ | |||
+ | // создаем графический контекст | ||
+ | var ctx = canvas.getContext("2d"); | ||
+ | |||
+ | // настройка шрифта | ||
+ | var fontSize = 18; | ||
+ | ctx.font = fontSize + "px Courier New"; | ||
+ | |||
+ | // пресеты лейблов | ||
+ | var labelPauseText = "Game Paused (press Enter to continue)"; | ||
+ | var labelPause = ctx.measureText(labelPauseText); | ||
+ | var labelWinText = "Invaders defeated (press F5 to play again)"; | ||
+ | var labelWin = ctx.measureText(labelWinText); | ||
+ | var labelEndText = "You lose (press F5 to play again)"; | ||
+ | var labelEnd = ctx.measureText(labelEndText); | ||
+ | |||
+ | // переменные для игрового цикла | ||
+ | var lastTime = Date.now(); | ||
+ | var totalTime = 0; | ||
+ | var elapsed = 0; | ||
+ | |||
+ | // отступ (используется в отрисовке интерфейса и тд) | ||
+ | var padding = 16; | ||
+ | |||
+ | // текстура (тайлсет) для игры | ||
+ | var img = new Image(); | ||
+ | |||
+ | // класс Пуля | ||
+ | // hostile(флаг) -- является ли вражеской пулей (влияет на цвет и направление полета) | ||
+ | //dir -- скаляр для разделение на свои и вражеские пули, если | ||
+ | //hostile - true, то - враг, его пули летят вниз, | ||
+ | //если false - это игрок и его пули летят вверх | ||
+ | function Bullet(hostile, x, y) { | ||
+ | |||
+ | this.w = 5; | ||
+ | this.h = 8; | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | |||
+ | this.speed = 5; | ||
+ | if(hostile){ | ||
+ | this.dir = 1; | ||
+ | this.color = 'white'; | ||
+ | } else { | ||
+ | this.dir = -1; | ||
+ | this.color = '#00fc00'; | ||
+ | } | ||
+ | this.hostile = hostile; | ||
+ | } | ||
+ | |||
+ | Bullet.prototype = { | ||
+ | update: function(dt){ | ||
+ | this.y += this.speed * this.dir; | ||
+ | }, | ||
+ | render: function(ctx){ | ||
+ | ctx.fillStyle = this.color; | ||
+ | ctx.fillRect(this.x, this.y, this.w, this.h); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // класс Блок | ||
+ | function Block(x, y) { | ||
+ | |||
+ | this.w = 44; | ||
+ | this.h = 32; | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | |||
+ | //Спрайты | ||
+ | this.tw = 44; | ||
+ | this.th = 32; | ||
+ | this.tx = 0; | ||
+ | this.ty = 48; | ||
+ | |||
+ | this.health = 4; | ||
+ | } | ||
+ | |||
+ | Block.prototype = { | ||
+ | render: function(ctx){ | ||
+ | ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h); | ||
+ | }, | ||
+ | handleDamage: function(){ | ||
+ | this.health--; | ||
+ | this.ty += this.th; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // класс Космического захватчика (противника) | ||
+ | // tier -- уровень (тип) врага | ||
+ | // x, y -- координаты | ||
+ | // row, col -- индексы в построении | ||
+ | function Invader(tier, x, y, row, col) { | ||
+ | |||
+ | this.tier = tier; | ||
+ | |||
+ | this.w = 26; | ||
+ | this.h = 16; | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | |||
+ | //Спрайты | ||
+ | this.tw = 26; | ||
+ | this.th = 16; | ||
+ | this.tx = this.tw * tier; | ||
+ | this.ty = 0; | ||
+ | |||
+ | this.row = row; | ||
+ | this.col = col; | ||
+ | this.leading = false; | ||
+ | |||
+ | this.move = 0; | ||
+ | this.speed = 1; | ||
+ | } | ||
+ | |||
+ | Invader.prototype = { | ||
+ | render: function(ctx){ | ||
+ | ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // класс Игрок (комический корабль игрока) | ||
+ | function Player() { | ||
+ | |||
+ | this.w = 26; | ||
+ | this.h = 16; | ||
+ | this.x = (canvas.width - this.w) / 2; | ||
+ | this.y = canvas.height - padding * 3 - this.h; | ||
+ | |||
+ | //Спрайты | ||
+ | this.tw = 26; | ||
+ | this.th = 16; | ||
+ | this.tx = 277; | ||
+ | this.ty = 228; | ||
+ | |||
+ | // управление передвижением | ||
+ | this.moveLeft = false; | ||
+ | this.moveRight = false; | ||
+ | this.speed = 5; | ||
+ | |||
+ | // стрельба | ||
+ | this.shoot = false; | ||
+ | this.shootFired = 0; | ||
+ | this.shootDelay = 1.0; | ||
+ | |||
+ | // для анимации ранения | ||
+ | this.respawned = true; | ||
+ | this.visible = true; | ||
+ | this.flickElapsed = 0.0; | ||
+ | this.flickTime = 0.0; | ||
+ | } | ||
+ | |||
+ | Player.prototype = { | ||
+ | update: function(dt){ | ||
+ | // управление передвижением | ||
+ | if (this.moveLeft) { | ||
+ | this.x = Math.max(this.x - this.speed, padding); | ||
+ | } | ||
+ | |||
+ | if (this.moveRight) { | ||
+ | this.x = Math.min(this.x + this.speed, canvas.width - this.w - padding); | ||
+ | } | ||
+ | |||
+ | // для анимации ранения | ||
+ | if (this.respawned) { | ||
+ | |||
+ | this.flickTime += dt; | ||
+ | this.flickElapsed += dt; | ||
+ | |||
+ | if (this.flickTime > 2.0) { | ||
+ | this.respawned = false; | ||
+ | this.visible = true; | ||
+ | } | ||
+ | |||
+ | if (this.flickElapsed > 0.1) { | ||
+ | this.visible = !this.visible; | ||
+ | this.flickElapsed = 0.0; | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | render: function(ctx){ | ||
+ | if (this.visible) { | ||
+ | ctx.drawImage(img, this.tx, this.ty, this.tw, this.th, this.x, this.y, this.w, this.h); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | var player = new Player(); | ||
+ | var bullets = []; | ||
+ | var blocks = []; | ||
+ | var invaders = []; | ||
+ | var invader_index = 0; | ||
+ | var invader_dir = 1; | ||
+ | var invader_speed = 5; | ||
+ | var invader_it = 0.02; | ||
+ | var gameStatePause = 2; | ||
+ | var gameStateWin = 3; | ||
+ | var gameStateEnd = 4; | ||
+ | var gameStatePlaying = 5; | ||
+ | var gameState = gameStatePause; | ||
+ | var started = false; | ||
+ | var score = 0; | ||
+ | if(ls_enabled && parseInt(localStorage.getItem("ls-highscore")) || false){ | ||
+ | highscore = parseInt(localStorage.getItem("ls-highscore")); | ||
+ | } else{ | ||
+ | highscore = 0; | ||
+ | } | ||
+ | |||
+ | var life = 3; | ||
+ | |||
+ | // вызывается когда игра закончена | ||
+ | function game_finished() { | ||
+ | bullets = []; | ||
+ | player.visible = true; | ||
+ | update_highscore(); | ||
+ | } | ||
+ | |||
+ | // пересчет топа | ||
+ | function update_highscore() { | ||
+ | if (highscore < score) { | ||
+ | highscore = score; | ||
+ | if (ls_enabled) { | ||
+ | localStorage.setItem("ls-highscore", highscore); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // проверяем, какой противник является стрелком | ||
+ | //leading - стреляющий | ||
+ | // вызывается при запуске игры и при смерти противника | ||
+ | function update_leading_invaders() { | ||
+ | let dict = {}; | ||
+ | for (let i = 0; i < invaders.length; ++i) { | ||
+ | let invader = invaders[i]; | ||
+ | if (dict.hasOwnProperty(invader.col)) {//Если имеет данное свойство | ||
+ | let e = dict[invader.col]; | ||
+ | if (e.row < invader.row) { | ||
+ | e.row = invader.row; | ||
+ | e.i = i; | ||
+ | } | ||
+ | } else { | ||
+ | dict[invader.col] = {}; | ||
+ | dict[invader.col].row = invader.row; | ||
+ | dict[invader.col].i = i;// Получили линию и номер | ||
+ | } | ||
+ | } | ||
+ | console.log(dict);//Dict - словарь объектов | ||
+ | for (let key in dict) { | ||
+ | let e = dict[key]; | ||
+ | invaders[e.i].leading = true;//Результат работы всей функции | ||
+ | } | ||
+ | } | ||
+ | |||
+ | document.onkeydown = function(e) { | ||
+ | |||
+ | if (e.key == "ArrowLeft") { | ||
+ | player.moveLeft = true; | ||
+ | } else if (e.key == "ArrowRight") { | ||
+ | player.moveRight = true; | ||
+ | } else if (e.key == " ") { | ||
+ | |||
+ | if (!started) { | ||
+ | gameState = gameStatePlaying; | ||
+ | started = true; | ||
+ | } | ||
+ | |||
+ | player.shoot = true; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | document.onkeyup = function(e) { | ||
+ | |||
+ | if (e.key == "ArrowLeft") { | ||
+ | player.moveLeft = false; | ||
+ | } else if (e.key == "ArrowRight") { | ||
+ | player.moveRight = false; | ||
+ | } else if (e.key == " ") { | ||
+ | player.shoot = false; | ||
+ | } else if (e.key == "Enter") { | ||
+ | |||
+ | started = true; | ||
+ | |||
+ | if (gameState == gameStatePause) { | ||
+ | gameState = gameStatePlaying; | ||
+ | } else if (gameState == gameStatePlaying) { | ||
+ | gameState = gameStatePause; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // функция проверки пересечения двух прямоугольников (для столкновения с пулей) | ||
+ | function intersects(x1, y1, w1, h1, x2, y2, w2, h2) { | ||
+ | //Для 1 объекта | ||
+ | let r1MinX = Math.min(x1, x1 + w1); | ||
+ | let r1MaxX = Math.max(x1, x1 + w1); | ||
+ | let r1MinY = Math.min(y1, y1 + h1); | ||
+ | let r1MaxY = Math.max(y1, y1 + h1); | ||
+ | //Для 2 объекта | ||
+ | let r2MinX = Math.min(x2, x2 + w2); | ||
+ | let r2MaxX = Math.max(x2, x2 + w2); | ||
+ | let r2MinY = Math.min(y2, y2 + h2); | ||
+ | let r2MaxY = Math.max(y2, y2 + h2); | ||
+ | |||
+ | let interLeft = Math.max(r1MinX, r2MinX); | ||
+ | let interTop = Math.max(r1MinY, r2MinY); | ||
+ | let interRight = Math.min(r1MaxX, r2MaxX); | ||
+ | let interBottom = Math.min(r1MaxY, r2MaxY); | ||
+ | |||
+ | return (interLeft < interRight) && (interTop < interBottom);//true - столкновение | ||
+ | } | ||
+ | |||
+ | // вызов сл. кадра | ||
+ | var requestAnimFrame = (function() { | ||
+ | return window.requestAnimationFrame || | ||
+ | window.webkitRequestAnimationFrame || | ||
+ | window.mozRequestAnimationFrame || | ||
+ | window.oRequestAnimationFrame || | ||
+ | window.msRequestAnimationFrame || | ||
+ | function(callback) { | ||
+ | window.setTimeout(callback, 1000 / 60); | ||
+ | }; | ||
+ | })(); | ||
+ | |||
+ | // главный цикл игры | ||
+ | function update(dt) { | ||
+ | |||
+ | // если игра не начата | ||
+ | if (!started || gameState != gameStatePlaying) return; | ||
+ | |||
+ | // обновляем игрока и контролируем управление | ||
+ | player.update(dt); | ||
+ | if (player.shoot && player.shootDelay < totalTime - player.shootFired) { | ||
+ | bullets.push(new Bullet(false, player.x + player.w / 2, player.y)); | ||
+ | player.shootFired = totalTime; | ||
+ | let audio = new Audio("shoot.wav"); | ||
+ | audio.volume = 0.02; | ||
+ | audio.play(); | ||
+ | } | ||
+ | |||
+ | // проверяем пули на столкновение и обновляем движение | ||
+ | for (let i = 0; i < bullets.length; ++i) { | ||
+ | |||
+ | let bullet = bullets[i]; | ||
+ | bullet.update(dt); | ||
+ | |||
+ | // если пуля вражеская | ||
+ | if (bullet.hostile) { | ||
+ | |||
+ | // если игрок уязвим (не был только что ранен) и ранен | ||
+ | // наносим урон и удаляем пулю | ||
+ | if (!player.respawned && intersects(player.x, player.y, player.w, player.h, bullet.x, bullet.y, bullet.w, bullet.h)) { | ||
+ | |||
+ | player.respawned = true; | ||
+ | player.flickTime = 0.0; | ||
+ | player.flickElapsed = 0.0; | ||
+ | player.visible = false; | ||
+ | life -= 1; | ||
+ | // кончились жизни | ||
+ | if (life == 0) { | ||
+ | gameState = gameStateEnd; | ||
+ | game_finished(); | ||
+ | } | ||
+ | |||
+ | bullets.splice(i, 1); | ||
+ | break; | ||
+ | } else if (bullet.y > player.y + player.h) { // пуля улетела за игрока | ||
+ | bullets.splice(i, 1); | ||
+ | break; | ||
+ | } else { // проверяем попадание в укрепление | ||
+ | |||
+ | // ищем попадание | ||
+ | let inter = -1; | ||
+ | for (let j = 0; j < blocks.length; ++j) { | ||
+ | let block = blocks[j]; | ||
+ | if (intersects(block.x, block.y, block.w, block.h, bullet.x, bullet.y, bullet.w, bullet.h)) { | ||
+ | inter = j; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // если попала, наносим блоку ранение и удаляем пулю | ||
+ | if (inter >= 0) { | ||
+ | bullets.splice(i, 1); | ||
+ | blocks[inter].handleDamage(); | ||
+ | if (blocks[inter].health == 0) { | ||
+ | blocks.splice(inter, 1); | ||
+ | } | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | } else { // это пуля игрока, проверяем попала ли в цель | ||
+ | |||
+ | // ищем попадание | ||
+ | let inter = -1; | ||
+ | for (let j = 0; j < invaders.length; ++j) { | ||
+ | let invader = invaders[j]; | ||
+ | if (intersects(invader.x, invader.y, invader.w, invader.h, bullet.x, bullet.y, bullet.w, bullet.h)) { | ||
+ | inter = j; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // если попала, удаляем пулю/врага и повышаем счет | ||
+ | if (inter >= 0) { | ||
+ | |||
+ | bullets.splice(i, 1); | ||
+ | score += (3 - invaders[inter].tier) * 10; | ||
+ | invaders.splice(inter, 1); | ||
+ | |||
+ | update_leading_invaders(); | ||
+ | |||
+ | // все убиты | ||
+ | if (invaders.length == 0) { | ||
+ | gameState = gameStateWin; | ||
+ | game_finished(); | ||
+ | } | ||
+ | |||
+ | break; | ||
+ | } else if (bullet.y < padding) { // пуля улетела за экран | ||
+ | bullets.splice(i, 1); | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // пришло время обновить состояние врага (одного, по индексу) | ||
+ | if (elapsed > invader_it) { | ||
+ | |||
+ | elapsed = 0.0; | ||
+ | |||
+ | if (invader_index < invaders.length) { | ||
+ | |||
+ | // двинуть врага и переключить фрейм (анимацию) | ||
+ | let inv = invaders[invader_index]; | ||
+ | inv.ty = inv.th - inv.ty; | ||
+ | inv.x += invader_speed * invader_dir; | ||
+ | |||
+ | // противник стреляет | ||
+ | if (Math.random() > 0.9 && inv.leading) { | ||
+ | |||
+ | bullets.push(new Bullet(true, inv.x + inv.w / 2, inv.y + inv.h)); | ||
+ | |||
+ | let audio = new Audio("shoot.wav"); | ||
+ | audio.playbackRate = 4; | ||
+ | audio.volume = 0.1; | ||
+ | audio.play(); | ||
+ | } | ||
+ | |||
+ | // противники долетели до игрока | ||
+ | if (inv.y + inv.h > player.y) { | ||
+ | gameState = gameStateEnd; | ||
+ | game_finished(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // индекс сл. врага в построении | ||
+ | invader_index = (invader_index + 1) % invaders.length; | ||
+ | |||
+ | // проверяем, нужно ли опустить ряды ниже и сменить направление движения | ||
+ | for (let invader of invaders) { | ||
+ | if ((invader_dir > 0 && invader.x > canvas.width - padding) || (invader_dir < 0 && invader.x < padding)) { | ||
+ | |||
+ | if (invader_dir < 0) { | ||
+ | invader_index = (invader_index + invaders.length - 1) % invaders.length; | ||
+ | } | ||
+ | |||
+ | invader_dir = -invader_dir; | ||
+ | |||
+ | for (let i = 0; i < invaders.length; ++i) | ||
+ | invaders[i].y += 32; | ||
+ | |||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // главный цикл отрисовки | ||
+ | function render() { | ||
+ | |||
+ | // очищаем холст | ||
+ | ctx.fillStyle = "black"; | ||
+ | ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
+ | |||
+ | // рисуем игрока | ||
+ | player.render(ctx); | ||
+ | |||
+ | // если экран смерти, затеняем противников | ||
+ | // чтобы не сливались с текстом | ||
+ | if (gameState == gameStateEnd) | ||
+ | ctx.globalAlpha = 0.2; | ||
+ | |||
+ | // рисуем врагов | ||
+ | for (let invader of invaders) | ||
+ | invader.render(ctx); | ||
+ | |||
+ | ctx.globalAlpha = 1; | ||
+ | |||
+ | // рисуем укрепления | ||
+ | for (let block of blocks) | ||
+ | block.render(ctx); | ||
+ | |||
+ | // рисуем пули | ||
+ | for (let bullet of bullets) | ||
+ | bullet.render(ctx); | ||
+ | |||
+ | // рисуем кол-во жизней | ||
+ | if (life > 0) { | ||
+ | |||
+ | ctx.fillStyle = "white"; | ||
+ | ctx.fillText(life, padding, canvas.height - padding); | ||
+ | |||
+ | for (let i = 0; i < life; ++i) { | ||
+ | ctx.drawImage(img, 277, 228, 26, 16, padding * 2 + (34 * i), canvas.height - padding * 2, 26, 16); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // если пауза, рисуем рамку и лейбл | ||
+ | if (gameState == gameStatePause) { | ||
+ | ctx.strokeStyle = "#00fc00"; | ||
+ | ctx.lineDashOffset = totalTime * 50;//Величина смещения штрихов линии | ||
+ | ctx.setLineDash([15]); | ||
+ | ctx.strokeRect(0, 0, canvas.width, canvas.height); | ||
+ | ctx.fillStyle = "#00fc00"; | ||
+ | ctx.fillText(labelPauseText, (canvas.width - labelPause.width) / 2, fontSize); | ||
+ | } | ||
+ | |||
+ | // если проиграл, рисуем рамку, лейбл и счет (в центре) | ||
+ | if (gameState == gameStateEnd) { | ||
+ | ctx.strokeStyle = "#ff0000"; | ||
+ | ctx.lineDashOffset = totalTime * 50; | ||
+ | ctx.setLineDash([15]); | ||
+ | ctx.strokeRect(0, 0, canvas.width, canvas.height); | ||
+ | ctx.fillStyle = "#ff0000"; | ||
+ | let y = 100; | ||
+ | ctx.fillText(labelEndText, (canvas.width - labelEnd.width) / 2, y + (fontSize + padding) * 1); | ||
+ | ctx.fillStyle = "white"; | ||
+ | let text = ctx.measureText("Score: " + score); | ||
+ | ctx.fillText("Score: " + score, (canvas.width - text.width) / 2, y + (fontSize + padding) * 2); | ||
+ | text = ctx.measureText("High Score: " + highscore); | ||
+ | ctx.fillText("High Score: " + highscore, (canvas.width - text.width) / 2, y + (fontSize + padding) * 3); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // если победил, рисуем рамку, лейбл и счет (в центре) | ||
+ | if (gameState == gameStateWin) { | ||
+ | ctx.strokeStyle = "yellow"; | ||
+ | ctx.lineDashOffset = totalTime * 50; | ||
+ | ctx.setLineDash([15]); | ||
+ | ctx.strokeRect(0, 0, canvas.width, canvas.height); | ||
+ | ctx.fillStyle = "yellow"; | ||
+ | let y = 100; | ||
+ | ctx.fillText(labelWinText, (canvas.width - labelWin.width) / 2, y + (fontSize + padding) * 1); | ||
+ | ctx.fillStyle = "white"; | ||
+ | let text = ctx.measureText("Score: " + score); | ||
+ | ctx.fillText("Score: " + score, (canvas.width - text.width) / 2, y + (fontSize + padding) * 2); | ||
+ | text = ctx.measureText("High Score: " + highscore);//measureText - метод получения размеров текста | ||
+ | ctx.fillText("High Score: " + highscore, (canvas.width - text.width) / 2, y + (fontSize + padding) * 3); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // счет в нижней части экрана | ||
+ | ctx.fillStyle = "white"; | ||
+ | let text = ctx.measureText("Score: " + score); | ||
+ | ctx.fillText("Score: " + score, 150, canvas.height - padding); | ||
+ | ctx.fillText("High Score: " + highscore, 300, canvas.height - padding); | ||
+ | } | ||
+ | |||
+ | // загружаем текстуру | ||
+ | img.src = 'spritesheet.png'; | ||
+ | |||
+ | // по завершении загрузки инициализируем данные игры | ||
+ | img.onload = function() { | ||
+ | |||
+ | // построение противников | ||
+ | let invader_rows = 5; | ||
+ | let invader_cols = 11; | ||
+ | let tiers = [0, 1, 1, 2, 2]; | ||
+ | let invader_prototype = new Invader(); | ||
+ | let off_w = 8; | ||
+ | let off_h = 16; | ||
+ | let off_x = (canvas.width - (invader_prototype.w * invader_cols) - (off_w * (invader_cols - 1))) / 2; | ||
+ | let off_y = padding * 3; | ||
+ | let block_prototype = new Block(); | ||
+ | |||
+ | for (let r = 0; r < invader_rows; ++r) { | ||
+ | for (let c = 0; c < invader_cols; ++c) { | ||
+ | let tier = tiers[r]; | ||
+ | let invader = new Invader( | ||
+ | tier, | ||
+ | off_x + c * (invader_prototype.w + off_w), | ||
+ | off_y + r * (invader_prototype.h + off_h), | ||
+ | r, c | ||
+ | ); | ||
+ | invaders.push(invader); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // обновляем стрелков | ||
+ | update_leading_invaders(); | ||
+ | |||
+ | // построение укреплений | ||
+ | let block_off_x = (canvas.width - 7 * block_prototype.w) / 2; | ||
+ | for (let i = 0; i < 4; ++i) { | ||
+ | let block = new Block( | ||
+ | block_off_x + i * block_prototype.w * 2, | ||
+ | canvas.height - block_prototype.w * 2 - padding | ||
+ | ); | ||
+ | blocks.push(block); | ||
+ | } | ||
+ | |||
+ | // кадр | ||
+ | function main() { | ||
+ | |||
+ | let now = Date.now(); | ||
+ | let dt = (now - lastTime) / 1000.0; | ||
+ | |||
+ | totalTime += dt; | ||
+ | elapsed += dt; | ||
+ | |||
+ | update(dt); | ||
+ | render(); | ||
+ | |||
+ | lastTime = now; | ||
+ | requestAnimFrame(main); | ||
+ | } | ||
+ | |||
+ | main(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | </div> | ||
+ | <div class="mw-collapsible mw-collapsed"> | ||
+ | '''CSS:''' <div class="mw-collapsible-content"> | ||
+ | <syntaxhighlight lang="javascript" line start="1" enclose="div"> | ||
+ | |||
+ | html, body { | ||
+ | margin: 0; | ||
+ | padding: 0; | ||
+ | background-color: #001058; | ||
+ | height: 100%; | ||
+ | } | ||
+ | |||
+ | .content { | ||
+ | display: flex; | ||
+ | justify-content: center; | ||
+ | height: 100%; | ||
+ | background: url(background.png); | ||
+ | background-repeat: no-repeat; | ||
+ | background-size: 100% 750px; | ||
+ | } | ||
+ | |||
+ | #game { | ||
+ | margin: auto; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | </div> |
Текущая версия на 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 }