Изобретение собственных HTML-элементов для создания DOM-игры
Это небольшой эксперимент, показывающий, как изобретать собственные HTML-элементы для создания игры в DOM.
Что это за пользовательские элементы?
Пользовательский элемент — это HTML-элемент, который позволяет добавлять собственные свойства и методы. Например, базовый HTMLЭлемент имеет style
имущество и click()
метод. Расширяя элемент HTML, мы получаем всю существующую функциональность и можем добавлять свои собственные.
В этом эксперименте у нас есть элемент Car
который имеет x
и y
имущество и update()
метод:
class Car extends HTMLElement {
public x: number;
public y: number;
constructor(){
super();
console.log("A car was created!");
}
public update() {
console.log("The car's update method was called!");
}
}
Прежде чем мы сможем добавить наш элемент Car в DOM, мы должны зарегистрировать его, связав наш класс с HTML-тегом. Обратите внимание, что тег html должен содержать дефис:
window.customElements.define("car-component", Car);
Теперь вы можете добавлять автомобили в дом, расставляя теги:
<body>
<car-component></car-component>
</body>
В нашей игре мы предпочитаем добавлять автомобили по коду. Мы можем создать новый экземпляр Car
и добавьте его в DOM одной строкой:
document.body.appendChild(new Car());
Это приведет к <car-component></car-component>
добавляется в вашу структуру HTML, а сообщение A car was created!
появится в консоли. Этот элемент HTML будет иметь x
и y
имущество и update()
метод.
Манипуляции с DOM
Вы можете запросить свой HTML-документ для автомобильных компонентов и использовать новый for of
петля для вызова их update()
метод.
let cars : NodeListOf<Car> = document.getElementsByTagName("car-component") as NodeListOf<Car>;
for(let c of cars){
c.update();
}
Жизненный цикл
Пользовательский элемент имеет хуки жизненного цикла: эти методы вызываются автоматически, когда Car добавляется или удаляется из DOM.
class Car extends HTMLElement {
public connectedCallback() {
console.log("A car was added to the DOM");
}
public disconnectedCallback():void{
console.log("hey! someone removed me from the DOM!");
}
}
Цикл игры
Наш игровой класс создаст элемент player и запустит игровой цикл. Игровой цикл обновляет наши игровые элементы 60 раз в секунду, используя requestAnimationFrame
.
- Игровой цикл добавит
new Car()
в DOM каждую секунду с помощью оператора по модулю. - Игровой цикл найдет все
<car-component>
теги и вызвать их метод обновления.
Игра создается с помощью new Game()
после загрузки окна. Обратите внимание, что самому классу Game не нужно расширять HTMLElement.
class Game {
private counter:number = 0;
constructor() {
document.body.appendChild(new Player());
requestAnimationFrame(() => this.gameLoop());
}
private gameLoop(){
this.counter++;
if(this.counter%60 == 0) {
document.body.appendChild(new Car());
}
let cars : NodeListOf<Car> = document.getElementsByTagName("car-component") as NodeListOf<Car>;
for(let c of cars){
c.update();
}
requestAnimationFrame(() => this.gameLoop());
}
}
window.addEventListener("load", () => new Game());
Удаление автомобилей
Когда автомобиль покидает экран или врезается в игрока, мы можем легко удалить его. Так как автомобиль наследуется от HTMLElement, мы можем использовать remove()
метод, который удаляет его из DOM. Игровой цикл больше не будет вызывать метод update, и если мы хотим, мы можем использовать метод disconnectedCallback()
чтобы выполнить какой-то окончательный код, прежде чем автомобиль будет полностью удален.
public disconnectedCallback():void{
console.log("the car is removed from the game!");
}
public update(): void { this.x += this.speed; if (this.x > window.innerWidth) { this.remove(); }
}
Стилизация и анимация
Обратите внимание, что стили наших пользовательских элементов полностью выполняются в CSS. Сначала мы объявляем, что ВСЕ элементы документа будут использовать position:absolute
а затем объявляем размер и фоновое изображение для каждого отдельного элемента:
body * {
position: absolute;
display: block;
margin:0px; padding:0px;
box-sizing: border-box;
background-repeat: no-repeat;
}
car-component {
width:168px;
height:108px;
background-image: url('../images/car.png');
}
Мы позиционируем наши элементы, используя css transform
чтобы мы могли использовать GPU для плавной анимации.
this.style.transform = `translate(${this.x}px, ${this.y}px)`;
Расширение других элементов
В этом примере используется только extends HTMLElement
но мы могли бы также расширить HTMLDivElement
, HTMLButtonElement
и т. д. В этом примере все наши элементы рассматриваются как <div>
просто установив display:block
в ССЦ.
Машинопись
Этот эксперимент построен с помощью Typescript, но вы можете легко перестроить его на чистом Javascript, удалив информацию о типе. Вы можете проверить main.js
файл, чтобы увидеть эквивалент Javascript.
Чтобы скомпилировать этот проект, вы можете установить Typescript с помощью npm install -g typescript
а затем введите tsc -p
в терминале в папке проекта.
Поддержка браузера
Приведенный выше эксперимент работает в Safari и Chrome. Расширение определенных элементов, таких как HTMLButtonElement, пока не работает ни в одном браузере. Использовать полифилл чтобы получить поддержку во всех браузерах.
Для цикла в NodeList
for of
цикл еще не поддерживается для NodeList
и HTMLCollection
в Safari и Firefox, потому что технически они не являются массивами. Включите его с помощью:
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
Скачать рабочий проект по адресу: