Привет, уважаемые участники Хабр! 

Сегодня мы поговорим о создании мини-игры, которую вы сможете использовать для украшения своего веб-сайта или просто оставить в качестве заставки. Мы разделим разработку проекта на две части: начнём с базового движения объектов и закончим созданием полноценного проекта. Данный курс подойдет как для новичков, которые уже немного освоили JavaScript, HTML и CSS, так и для уже опытных программистов.

Вторая часть урока.

Финальное демо первой части урока:

Давайте начинать!

Структура проекта

Для начала нам понадобится создать базовую структуру проекта. Это будут 3 пустых файла.

index.html
assets
  ├── style.css
  └── main.js

Подготовка проекта

В index.html создадим базовую структуру HTML5 с подключением файла стилей style.css и файла JavaScript main.js.

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>Spore</title>
    <link rel="stylesheet" href="assets/style.css">
</head>
<body>

<script src="assets/main.js"></script>
</body>
</html>

В style.css напишем стили для изменения отступов по умолчанию.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

HTML и CSS

Теперь можно приступать к самому интересному - разработке!

Добавим в тег <body> в index.html следующую структуру.

<div id="board">
    <div id="zone"></div>
</div>

Здесь мы используем два блока: “board” и “zone”, - которые нам понадобятся для того, чтобы в дальнейшем создать “капельный эффект” слияния объектов.

В файл style.css добавим следующие стили.

html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
}


#board {
    width: 100%;
    height: 100%;
    background: #fff;
    filter: contrast(10);
}


#zone {
    width: 100%;
    height: 100%;
    background: #fff;
    filter: blur(10px);
}


.spore {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: cyan;
    position: absolute;
    top: 50%;
    left: 50%;
}

Проверим результат, который у нас получился и добавим для тестирования два блока div с классом spore.

<div id="board">
    <div id="zone">
        <div class="spore"></div>
        <div class="spore"></div>
    </div>
</div>

Откроем нашу index.html страницу в браузере. Если всё было сделано правильно, то увидим результат в виде одного круга.

Затем в панели разработчика найдем второй круг, который в данный момент наложился на первый, и изменим его позиционирование. Добавим стили позиционирования слева (свойство left) для второго круга. Вы можете самостоятельно подобрать необходимое значение, чтобы увидеть этот эффект!

Капельный эффект при помощи CSS в действии!
Капельный эффект при помощи CSS в действии!

Данный эффект будет красиво смотреться, когда мы будем иметь десятки таких элементов, да ещё и разных цветов!

После тестирования удалим блоки div с классом spore, а также отредактируем ещё и файл style.css.

.spore {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: cyan;
    position: absolute;
    transform: translate(-50%, -50%);
}

Данные стили являются финальными, и больше мы не будем их править, как и файл HTML.

JavaScript

Наконец, мы начнём наполнять наш файл main.js кодом.

Для упрощения дальнейшей разработки в выборке элементов по их селектору добавим вспомогательную функцию.

const $ = el => document.querySelector(el);

Данная функция предназначена для тех случаев, когда нам нужно получить только первый элемент по заданному селектору (чаще всего используется только для получения элемента по заданному id).

Так как мы несколько раз будем обращаться к элементу div с идентификатором zone, то вынесем ссылку на этот элемент в виде константы.

const ZONE = $('#zone');

Структура классов

В качестве основной структуры JavaScript будет использоваться 4 класса:

  • Base

  • Substance

  • Piece

  • Game

При этом классы Substance и Piece будут наследоваться от класса Base. А класс Game будет отвечать за всё взаимодействие между элементами.

class Base {}

class Substance extends Base {}

class Piece extends Base {}

class Game {}

А в конце мы добавим создание экземпляра класса Game для того, чтобы скрипт запускался сразу после загрузки JavaScript файла.

const game = new Game();

Если в данный момент открыть в браузере страницу index.html, то мы ничего не увидим и при клике по пустому пространству ничего не произойдёт.

Создание экземпляров элементов

Поработаем с классом Base и добавим в него конструктор, которое будет являться параметром и хранить ссылку на элемент в HTML-структуре.

class Base {
    constructor(parent) {
        this.parent = parent;
    }
}

Класс Piece будет отвечать непосредственно за каждый элемент, когда большой элемент Substance будет “взрываться”. Добавим в него дополнительную характеристику: его позиционирование в пространстве. И ещё добавим функцию createElement, которая будет создавать новый элемент, добавлять этому элементу класс, устанавливать ширину и высоту, а также позицию, и добавлять только что созданный элемент в контейнер #zone.

class Piece extends Base {
    constructor(parent) {
        super(parent);

        this.data = {
            position: {
                x: this.parent.data.position.x,
                y: this.parent.data.position.y
            }
        }

        this.createElement();
    }

    createElement() {
        this.el = document.createElement('div');
        this.el.className = 'spore';
        this.el.style.width = `200px`;
        this.el.style.height = `200px`;
        this.el.style.left = `${this.data.position.x}px`;
        this.el.style.top = `${this.data.position.y}px`;

        ZONE.appendChild(this.el);
    }
}

Теперь для класса Substance нам необходимо также добавить некоторые дополнительные поля. Это будут позиция в пространстве и массив экземпляров класса Piece.

class Substance extends Base {
    constructor(parent, params) {
        super(parent);

        this.data = {
            position: {
                x: params.position.x,
                y: params.position.y
            },
            pieces: []
        }
        
        this.data.pieces.push(new Piece(this));
    }
}

Взаимодействие с пользователем

Чтобы заработала интерактивная часть со страницей браузера, мы добавим прослушивание события клика по странице в класс Game.

class Game {
    constructor() {
        this.substances = [];
        
        this.bindEvents();
    }

    bindEvents() {
        ZONE.addEventListener('click', ev => {
            this.substances.push(new Substance(this, {
                position: {
                    x: ev.clientX,
                    y: ev.clientY
                }
            }));
        });
    }
}

Теперь, при клике, в том же месте, где был совершён клик, будет появляться новый элемент.

Динамическое появление новых элементов при клике по странице
Динамическое появление новых элементов при клике по странице

Логика перемещения объектов

Нужно немного оживить элементы, которые появляются на нашей странице при клике. 

Перейдём к классу Game и добавим новую функцию random, которая позволит получать случайное число из заданного диапазона, так как изначально такой функции в JavaScript не существует.

class Game {
    constructor() {...}
    bindEvents() {...}

    static random(min, max) {
        return Math.round(Math.random() * (max - min) + min);
    }
}

А для класса Piece понадобится реализовать дополнительную логику, которая позволит генерировать случайным образом вектор направления движения, и будет перемещать элемент при каждом вызове функции update. 

Добавим две константы, которые будут хранить минимальную и максимальную скорости.

class Piece extends Base {
    MIN_SPEED = 3;
    MAX_SPEED = 8;

    constructor(parent) {
        super(parent);

        this.data = {
            position: {...},
            accelerations: {
                x: Game.random(-1, 0) === 0 ? Game.random(this.MIN_SPEED, this.MAX_SPEED) : Game.random(-this.MAX_SPEED, -this.MIN_SPEED),
                y: Game.random(-1, 0) === 0 ? Game.random(this.MIN_SPEED, this.MAX_SPEED) : Game.random(-this.MAX_SPEED, -this.MIN_SPEED),
            }
        }

        this.createElement();
    }
    
    createElement() {...}
    
    update() {
        this.data.position.x += this.data.accelerations.x;
        this.data.position.y += this.data.accelerations.y;

        this.draw();
    }
    
    draw() {
        this.el.style.top = `${this.data.position.y}px`;
        this.el.style.left = `${this.data.position.x}px`;
    }
}

Добавим цикл в класс Substance, который будет перебирать хранимые экземпляры Piece и вызывать функцию update для каждого из них.

class Substance extends Base {
    constructor(parent, params) {...}

    update() {
        this.data.pieces.forEach(piece => {
            piece.update();
        });
    }
}

Однако, если проверить текущий результат, наши элементы просто улетают за пределы экрана. Поэтому нам необходимо добавить обработку случаев, чтобы они отскакивали от края экрана.

Ограничение перемещения объектов

В начало файла добавим две новые константы: ширина и высота экрана.

const ZONE = …;

const SCREEN_WIDTH = window.innerWidth;
const SCREEN_HEIGHT = window.innerHeight;

В классе Substance понадобится добавить новое поле maxSize в объект data.

class Substance extends Base {
    constructor(parent, params) {
        
        this.data = {
            maxSize: 200,
            ...
        }
    }
    
    update() {...}
}

И теперь приступим непосредственно к реализации самой логики в классе Piece: добавим новое поле, как мы сделали это с классом Substance, но назовём просто size.

class Piece extends Base {
    constructor(parent) {
        this.data = {
            size: this.parent.data.maxSize,
            ...
        }
    }
}

Добавим новую функцию, которая каждый кадр будет сравнивать текущее положение и вектор движения объекта с крайними координатами окна браузера, чтобы при достижении этих координат объект изменял свой вектор движения на противоположный. При высчитывании координат, необходимо вычитать радиус окружности из текущей позиции, чтобы узнать его крайнюю точку. Ранее диаметр окружности определили в поле data.size.

class Piece extends Base {
    ...
    
    constructor(parent) {...}
    
    createElement() {...}

    update() {
        this.checkEdge();

        ...
    }

    draw() {...}

    checkEdge() {
        const halfSize = this.data.size / 2;


        if (this.data.position.x - halfSize <= 0 && this.data.accelerations.x < 0) this.data.accelerations.x *= -1;
        if (this.data.position.x + halfSize >= SCREEN_WIDTH && this.data.accelerations.x > 0) this.data.accelerations.x *= -1;


        if (this.data.position.y - halfSize <= 0 && this.data.accelerations.y < 0) this.data.accelerations.y *= -1;
        if (this.data.position.y + halfSize >= SCREEN_HEIGHT && this.data.accelerations.y > 0) this.data.accelerations.y *= -1;
    }
}

А в класс Game добавим новую функцию loop, которая внутри будет вызывать сама себя через метод requestAnimationFrame и при каждом ходе функции вызывать метод update для каждого элемента.

class Game {
    constructor() {
        ...

        this.loop();
    }

    loop() {
        this.substances.forEach((substance => {
            substance.update();
        }))

        requestAnimationFrame(_ => this.loop());
    }

    bindEvents() {...}

    static random(min, max) {...}
}

Если открыть страницу index.html и несколько раз кликнуть в любое место на странице, то при каждом клике будет добавлен новый круг, который будет перемещаться по экрану.

Заключение

В данной части мы реализовали взаимодействие с холстом страницы, а также написали логику для движения и ограничения перемещения элементов. В следующей части мы закончим данный проект и реализуем слияния объектов при их пересечении, а затем и последующий “взрыв” большого объекта. И даже раскрасим их в разные цвета, чтобы перемещения смотрелись более красочно и зрелищно!

Комментарии (0)