Статьи

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

За последние несколько лет JavaScript стал важной частью опыта веб-разработки и дизайна. Это позволяет нам приводить в порядок скучные, статичные страницы, избегать обновления страниц и совершать некоторые удивительные достижения в проектировании интерфейсов — вещи, которые были бы невозможны при использовании только HTML и CSS. Конечно, Ajax и DOM Scripting в настоящее время рассматриваются как мельница и являются частью набора инструментов каждого веб-разработчика при создании веб-сайтов. Но как далеко мы можем продвинуть это? Это мощный, объектно-ориентированный язык с богатым механизмом вывода, так что мы можем использовать его не только для запуска всплывающих окон?

Так что же делает любой уважающий себя гик, когда сталкивается с таким вопросом? Конечно, они пишут 2-D платформу с боковой прокруткой!

В этой серии из двух частей вы изучите достаточно HTML, CSS и JavaScript, чтобы вы могли создать свою собственную игру на платформе JavaScript. Я использовал библиотеку Prototype JavaScript в примерах просто потому, что это было то, что я знаю — многие из других доступных библиотек JavaScript вполне могут иметь эквивалентные возможности.

Прежде чем мы перейдем к забавным вещам, нам нужно познакомиться с некоторыми продвинутыми методами JavaScript, которые позволят нам заставить ваш браузер думать, что это 8-битная игровая консоль.

Строительство 101

JavaScript (JS) является прототипированным языком объектно-ориентированного программирования (ООП). Это означает, что мы можем представлять конструкции — например, персонажа видеоигры — как объект в нашем коде. Создание класса JS может показаться немного странным, если вы знакомы с некоторыми из более традиционных языков ООП. Для начала, вместо того, чтобы быть объектом, как в Ruby, все в JS является типом данных. Эти типы данных имеют внутренний тип данных — называемый прототипом — который сообщает типу данных, как себя вести. Итак, нам нужно определить класс таким образом, чтобы он:

  1. знает, что это класс
  2. может быть создан и инициализирован в определенное начальное состояние

Давайте посмотрим на некоторый код JS, который создает новый класс, а затем создает новый объект:

// Declare the class  function WalkingSprite(element, x, y) {  this.x = x;  this.y = y;  this.element = element;  }   WalkingSprite.prototype = {  x: 0,  y: 0,  element: null,   walk: function(direction) {  this.x += direction;  }  }   koopa = new WalkingSprite(null, 10, 10);  koopa.walk(20);  alert(koopa.x + "," + koopa.y); 

Беглый взгляд на этот код показывает, что мы создали новый класс WalkingSprite который имеет три свойства ( element , x и y ) и одну функцию, называемую walk . Если мы создадим новую версию объекта и назовем его функцией walk , наш объект koopa теперь будет находиться в координатной точке ( 20, 30 ). Объявление классов таким способом немного громоздко — мы должны создать класс, а затем обновить прототип. К счастью, Prototype (библиотека) инкапсулировал его в удобную функцию под названием Class.create . Код выше становится следующим:

 var WalkingSprite = Class.create({  x: 0,  y: 0,  element: null,   initialize: function(element, x, y) {    this.element = element;    this.x = x;    this.y = y;  },   walk: function(steps) {    this.x += steps;  }  });   koopa = new WalkingSprite(null, 10, 10);  koopa.walk(20);  alert(koopa.x + "," + koopa.y); 
Работа с наследованием классов

Еще одним фундаментальным компонентом ООП является концепция наследования. По сути, если у вас есть базовый класс, который имеет определенные переменные и функции, все классы, которые расширяют этот класс, наследуют эти переменные и функции. Затем вы можете добавить дополнительные функции и даже переопределить эти функции, чтобы сделать что-то еще. Это может быть очень полезно в нашей игре, потому что все наши персонажи, вероятно, будут иметь некоторые общие атрибуты — они все могут ходить по экрану — но, возможно, только один тип персонажа может прыгать. Похоже, идеальный кандидат на наследство.

К сожалению, JavaScript не поддерживает наследование изначально. Итак, почему я потратил впустую последний абзац, рассказывающий вам об этом? Ну, с небольшой хитростью, мы можем эмулировать наследование классов в JavaScript.

Поскольку все в JavaScript (включая функции в наших классах) являются переменными, мы можем присвоить их значения другим переменным. Итак, если мы думаем о том, что такое наследование на секунду, все, что нам нужно сделать, чтобы эмулировать его, — это скопировать свойства и функции из родительского класса в дочерний класс. Если мы хотим наследовать от класса, который мы создали выше, мы могли бы сделать это:

 // Declare the class  function WalkingSprite(element, x, y) {  this.x = x;  this.y = y;  this.element = element;  }   WalkingSprite.prototype = {  x: 0,  y: 0,  element: null,   walk: function(direction) {    this.x += direction;  }  }   // Create the child class  JumpingAndWalkingSprite = WalkingSprite;  JumpingAndWalkingSprite.prototype = {  x: 0,  y: 0,  walk: WalkingSprite.prototype.walk  jump: function() {    y += 20;  }  } 

Запустите код, и у вас будет новый класс, который имеет два свойства и одну функцию от его родителя, а также одну новую функцию: jump . Единственное, что такое кодирование, на самом деле не масштабируется; Что, если вы добавите функцию duck в родительский класс? Вам нужно будет пройти через каждый дочерний класс и добавить сигнатуру функции. Еще раз прототип на помощь! Функция Class.create мы узнали ранее, может принять другой класс в качестве первого аргумента. Этот предоставленный класс станет родительским, и он будет динамически находить все свойства и функции для нас, автоматически вставляя их в дочерний класс. Таким образом, выше будет:

 var JumpingAndWalkingSprite = Class.create(WalkingSprite);   mario = new JumpingAndWalkingSprite(null, 10, 10);  mario.walk(10):  alert(mario.x + "," + mario.y);  mario.jump();  alert(mario.x + "," + mario.y);  

Как и ожидалось, новый класс имеет все те же свойства родительского класса! Так что насчет добавления и переопределения свойств и функций? Выше мы продемонстрировали, как это сделать вручную, но Prototype позволяет нам определять новые функции, используя Class.create :

 var JumpingAndWalkingSprite = Class.create(WalkingSprite, {  walk: function($super, steps) {    $super(steps * 2);  },   jump: function() {    this.y += 20;  }  });  

Здесь мы переопределили функцию walk и добавили функцию jump . Подожди — верни грузовик — откуда взялась эта переменная $super ? Хороший вопрос! При использовании наследования иногда может быть полезно запустить версию функции родительского класса. В этом случае мы заставляем символ идти вдвое дальше, чем первоначально требовалось, удваивая входную переменную и передавая это новое значение родительскому классу. Prototype предоставит версию функции родительского класса в переменной $super , если вы объявите $super в качестве первого аргумента сигнатуры функции. Это позволяет вам легко вызывать родительскую версию функции из переопределенной версии. Вы заметите, что новая функция jump не имеет переменной $super ; мы не используем его, поэтому нам не нужно его поставлять. Если нам это нужно, мы можем просто добавить его в качестве первого аргумента сигнатуры функции.

Определение поведения по имени класса

Теперь у нас написан класс JavaScript, не было бы здорово, если бы мы могли указать элементу HTML стать объектом WalkingSprite просто присвоив ему определенное имя класса? В JavaScript 1.6 вы можете легко найти все элементы DOM с определенным именем класса, используя функцию document.getElementByClassName . Однако большинство браузеров пока не поддерживают версию 1.6. К счастью, Prototype предоставляет нам функцию $$ — передайте ей селектор CSS, и он вернет массив всех соответствующих элементов.

Посмотрите на следующий код:

 var WalkingSprite = Class.create({  x: 0,  y: 0,  element: null,   initialize: function(element) {    this.element = element,    this.x = element.offsetLeft,    this.y = element.offsetTop  },   walk: function(steps) {    this.x += steps;  }  });   var KoopaSprite = Class.create(WalkingSprite, {});   var koopas = new Array();  var koopaElements = $$('koopa');  for(el in koopaElements) {  koopas.push(new KoopaSpriteSprite(el));  } 

Сначала мы создаем класс WalkingSprite , а затем класс KoopaSprite который использует класс WalkingSprite качестве родителя. Затем мы создаем массив объектов KoopaSprite , выбирая все элементы в документе, которые имеют имя класса «koopa».

Теперь у нас есть массив объектов KoopaSprite со ссылками на соответствующие элементы DOM (это станет важным позже). То, что мы сделали здесь, является основой ненавязчивого JavaScript . Теперь, когда мы динамически нашли интересующие нас элементы HTML, мы можем связать события (такие как onclick и onfocus ), изменить их onfocus или сделать так, чтобы они исчезли!

Делать кинофильмы

Поскольку мы не пишем приключенческую игру, основанную на тексте, нам понадобится способ анимировать наших персонажей. Это выходит за рамки перемещения их по экрану, что будет рассмотрено позже. Также было бы хорошо, если бы мы могли заставить персонажей выглядеть так, как будто они ходят, прыгают или уклоняются. Чтобы сделать это, мы будем использовать старый трюк CSS: взлом фоновой позиции.

Идея проста: мы строим ленту изображений, которые формируют кадры нашей анимации, а затем перебираем их, сдвигая их влево и вправо на количество пикселей. Вот пример фонового изображения:

Фоновое изображение для этой демонстрации

Как вы можете видеть, у нас есть 12 кадров на одном изображении с интервалом 48 пикселей. Если бы у нас был div класса mario , CSS для некоторых различных фреймов может выглядеть так:

 div.mario {  width: 45px;  height: 45px;  background-image: url(mario.gif);  background-repeat: no-repeat;  background-position: 0 0;  }   div.mario.jump-left {  background-position: -90px 0;  }   div.mario.duck-right {  background-position: -180px 0;  } 

Возможно, вы видели эту технику, прежде чем создавать роллеров без мерцания. В прежние времена вы создавали эффекты прокрутки изображений, используя небольшой кусочек JavaScript, который изменял значение src тега изображения при onmouseover события onmouseover . Тем не менее, в первый раз, когда вы это сделали, браузеру все равно нужно было загрузить изображение с сервера, что часто вызывало мерцание. Можно было предварительно загрузить изображения, но все было немного неуклюже. Превосходная техника CSS позволила дизайнеру загрузить все состояния одновременного нажатия клавиш в одном изображении и использовать псевдокласс :hover для создания отдельного правила CSS для смещения фона, обеспечивая плавные переходы без JavaScript.

В нашем игровом движке мы будем изменять положение фонового изображения с помощью JavaScript. Чтобы установить положение фона в JS, вы манипулируете атрибутом style.backgroundPosition элемента. Следующий код создает новый класс с именем MarioSprite который добавляет функцию рендеринга к родительскому классу WalkingSprite . Эта новая функция вызывается повторно с задержкой по времени, и анимирует ходьбу Марио, используя два кадра:

 var MarioSprite = Class.create(WalkingSprite, {  renderState: 0;   render: function() {    if(this.renderState == 0) {      this.element.backgroundPosition = '0px 0px';      this.renderState = 1;    } else {      this.element.backgroundPosition = '-48px 0px';      this.renderState = 0;    }  }  }); 

Использование таймеров

Очевидно, что функция рендеринга довольно бесполезна, если ее не вызывать повторно в течение всей игры. Чтобы убедиться, что он запускается пару раз в секунду, нам нужно использовать таймеры JavaScript. Существует два типа таймеров: один срабатывает один раз после истечения таймера, а второй — несколько раз каждые t миллисекунды, пока мы не скажем, чтобы он остановился. Мы реализуем последнее, используя функцию setInterval :

 mario = new MarioSprite(document.getElementById('mario');  var timer = setInterval(function() { mario.render() }, 500); 

Это заставит Марио делать шаг два раза в секунду (500 миллисекунд равны половине секунды). Поскольку для setInterval в качестве первого параметра требуется функция, нам нужно создать анонимную функцию, которая вызывает функцию mario.render .

Возможно, стоило бы объяснить ограничение JS, которое вернется к нам позже: JavaScript не является многопоточным . Это означает, что невозможно одновременно запустить два блока кода. Вы можете прервать другой фрагмент кода, установив таймер одиночного выстрела с интервалом в одну миллисекунду, что заставит ваш браузер при следующей возможности запустить функцию обратного вызова, но прерванный фрагмент кода будет остановлен , и не продолжит выполнение до тех пор, пока не будет завершена функция прерывания. Поэтому установка таймера на срабатывание каждую миллисекунду не гарантирует, что ваша функция будет вызываться так быстро. Мы увидим последствия этого, когда я буду говорить о цикле.

Разрешение ввода пользователя

Очевидно, что игры требуют своего рода человеческого ввода, будь то с помощью клавиатуры, мыши или джойстика. Поэтому, чтобы наша игра стала чем-то большим, чем постоянные спрайты, работающие на месте, нам нужно будет реагировать на входные данные пользователя; в JavaScript это называется прослушиванием событий .
Существуют две разные модели событий в зависимости от того, какой у вас браузер (сюрприз, сюрприз), и хотя Prototype проделывает фантастическую работу по инкапсуляции нюансов этих двух, стоит знать, что происходит под капотом.

Пузырь-кисточка

Вы можете выбрать, будут ли события перемещаться вниз через DOM к элементу, который его вызвал (захват событий), вверх по элементу (всплывание событий) или к их комбинации (официальная модель W3C). Ниже приведено графическое представление того, что происходит. Те из вас, кто живет в Internet Explorer, застряли в пузырях событий, в то время как другие браузеры поддерживают оба.

Если вы некоторое время играли с Интернетом, вы можете быть знакомы со встроенной обработкой событий с использованием таких атрибутов, как onmouseover или onclick . Эта техника эквивалентна использованию атрибута style в CSS — это зло, не делайте этого. К счастью, есть несколько способов динамического связывания событий с элементами в JavaScript. Рассмотрим следующий код:

 function clicked() {  alert('You clicked me!');  }   function doubleclicked() {  alert('You double clicked me!');  }   var mario = document.getElementById('mario');  var luigi = document.getElementById('luigi');  var yoshi = document.getElementById('yoshi');   mario.addEventListener('click', clicked, true);  mario.addEventListener('doubleclick', doubleclicked, false);   luigi.attachEvent('onclick', clicked);   yoshi.onclick = clicked; 

Здесь у нас есть три разных метода для прикрепления событий к элементам в DOM. Первый — с использованием addEventListener — это стандартный способ работы W3C; первый параметр — это имя события, второй — имя функции обратного вызова, а третий — логическое значение, которое указывает, захватываете ли мы (ложь) или всплывают (истина). Второе — использование attachEvent — это способ Internet Explorer; это в основном та же подпись, что и в версии W3C, без третьего параметра, потому что IE поддерживает только всплывающие события. Последний вариант — использование свойства элемента onclick — это метод, который работает во всех браузерах.

Такие события, как mouseover и mouseout , довольно просты, но события клавиатуры немного сложнее, потому что нам нужно знать, какая клавиша была нажата. В этом случае мы должны получить информацию из объекта JavaScript Event ; либо объект Event передается в функцию обратного вызова, либо, если вы находитесь на земле IE, в объекте window создается глобальный объект Event : window.event , в котором есть необходимая нам информация.

Вот пример:

 function keypressHandler(e) {  e = window.event || e;  alert("Keycode: " + e.keyCode);  }   window.onkeypress = keypressHandler; 
  keypressHandler - это наша функция обратного вызова события, которая вызывается при срабатывании события keypress .  Первая строка представляет кросс-браузерный метод для получения объекта Event .  Получив объект Event мы можем запросить свойство keyCode и узнать, какая клавиша была нажата.

Как мы показали, Prototype делает такие виды работ действительно легкими. Prototype добавил несколько методов к объекту Event , которые позаботились обо всех кросс-браузерных проблемах для нас. Мы можем сократить наш код до следующего:

 function keypressHandler(e) {  alert("Keycode: " + e.keyCode);  }   Event.observe(window, 'keypress', keypressHandler); 

Настройка нашего обработчика событий с помощью Event.observe позволяет нам отказаться от условного теста, который проверяет, есть ли у нас объект Event помощью параметра функции или из события окна. Все это без проблем обрабатывается для нас Prototype.

Вывод

На этом этапе мы изучили объекты и классы JavaScript (включая такие концепции ООП, как наследование), как использовать классы JavaScript и CSS для определения поведения элементов, как использовать таймеры, позволяющие нам многократно выполнять задачу (например, анимацию), и основы прослушивания событий. Это дает нам достаточно JavaScript в нашем наборе инструментов, чтобы мы могли добраться до сути построения игровой платформы. В следующей статье я расскажу о создании базового механизма столкновений - цикла анимации - и покажу вам несколько приемов прокрутки окна браузера, чтобы получить эффект боковой прокрутки этого подлинного 80-х годов.

А пока посмотрите демонстрацию , в которой приведенная выше теория применяется на практике (подсказка: нажмите клавиши со стрелками и посмотрите, что произойдет). Вы можете скачать архив кода для этой статьи - посмотрите, сможете ли вы расширить его самостоятельно, так как вам нужно будет понять, что происходит со второй статьей этой серии. До скорого ...