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

Материал из Department of Theoretical and Applied Mechanics
Перейти к: навигация, поиск
(Описание)
 
(не показано 10 промежуточных версий этого же участника)
Строка 1: Строка 1:
{{#widget : Iframe | url = http://tm.spbstu.ru/htmlets/js2020/Kashapov/SpaceInvaders/index.html | width = 800| height = 800| border = 0 }}
+
==Описание==
 +
Классическая аркада 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 }