Пишем «Тир» на JavaScript для рекламы на сайте / Хабрахабр

Иногда так бывает, что на сайте хочется разместить рекламу, но сделать это хочется таким образом, дабы она не выглядела навязчиво, и воспринималась пользователем, как что-то интересное и необычное. Один мой приятель подсказал мне идею — сделать на сайте маленький виджет в виде игры, играя в которую пользователь мог бы наблюдать рекламу, либо же видел ее только при проигрыше (или выигрыше).
Мне захотелось попробовать написать такую игру, и об опыте создания поделиться с другими желающими.
image

Так получилось, что играми я занимаюсь уже достаточно давно, и для меня больше сложности составляет грамотно продумать именно вопрос рекламы внутри игры. Поэтому я решил идти по пунктам:

  1. Пользователь заходит на сайт
  2. Видит игру (тут надо его как-то заинтересовать)
  3. Играет (набирает очки)
  4. [если проиграл] Видит рекламу
  5. [если выиграл] Видит рекламу только в другом контексте

В этой части статьи рассмотрим пункт номер 3 и опишем саму игру, используя язык программирования JavaScript.

Мне, как программисту будет легко написать проект на чистом JavaScript, но этим я потрачу время на написание и отладку того, что уже написано и отлажено в специальных движках, которые именно для этого и предназначены. Поэтому быстрее будет выбрать движок.

Я посмотрел на три кандидата:

Phaser (навес на Pixi)

Pixi (отсылка к Phaser из-за рендера, но более низкоуровневый)

PointJS (от наших)

И первый движок, с которого я начал, стал PointJS. Зайдя на сайт и потыкав примеры, я уже примерно представил, как будет выглядеть моя будущая игра, прикинул размеры, расположение объектов, хотелось попробовать.

На официальном сайте так же нашел IDE, которая очень смахивает на Sublime Text, но с некоторыми плюшками типа живого редактирования и отладки прямо в редакторе, для работы даже не нужно ставить браузер.

Выглядит эта штука вот так:
image

При создании проекта базовый каркас приложения уже был выстроен, мне пришлось его менять по следующему принципу:

Настройки проекта

image

Немного о файлах:

init.js — файл с объявлениями всех глобальных переменных

game.js — игровой цикла Game

menu.js — игровой цикл Menu

init.js — Приготовления

В данном файле, как упоминал ранее — объявление переменных глобальных.

// объявляем движок
var pjs = new PointJS(640, 480, {
	backgroundColor : 'white' // optional
});

// растягиваем игру на всю страницу
pjs.system.initFullPage(); // for Full Page mode

// системные функции для сокращения
var log    = pjs.system.log;     // log = console.log;
var game   = pjs.game;           // Game Manager
var point  = pjs.vector.point;   // Constructor for Point
var camera = pjs.camera;         // Camera Manager
var brush  = pjs.brush;          // Brush, used for simple drawing
var OOP    = pjs.OOP;            // Objects manager
var math   = pjs.math;           // More Math-methods
var levels = pjs.levels;         // Levels manager

// включим мышь
var mouse = pjs.mouseControl.initControl();

// получим новые ширину и высоту экрана
var width  = game.getWH().w; // width of scene viewport
var height = game.getWH().h; // height of scene viewport

// заголовок
pjs.system.setTitle('Tir'); // Set Title for Tab or Window

// отключим показ кусора (заменим своим)
mouse.setVisible(false);

// переменная для счета
var score = 0;

// при изменении размера перезагрузим игру
window.onresize = function () {
  location.reload();
};

// этой функцией будем прогонять зависимые от высоты экрана значения
var i = function (num) {
  return num * (height/260);
};

menu.js — Работаем с меню

Данный файл отвечает за работу с меню, оно будет показываться перед игрой, во время выигрыша или проигрыша.
Сам файл (с комментариями)

// Объявляем игровой цикл
game.newLoopFromConstructor('Menu', function () {

  // Для меню мне требуется создать красивый фон в стиле DNS 
  var bg = game.newImageObject({
    x : 0, y : 0, // позиция
    file : 'dns_logo.jpeg', // файл фона
    w : width, // ширину установим, высота подгонится сама
    alpha : 0.8 // немного блеклый
  });
  
  this.update = function () {
    
    // запомним на всякий случай позицию курсора
    var mp = mouse.getPosition();

    // при наведении мыши сделаем ярче
    if (mp.x > 10 && mp.x < width - 10 && mp.y > 10 && mp.y < height - 10) bg.setAlpha(1);
    else bg.setAlpha(0.5);
    
    
    // по клику перейдем в игру
    if (mouse.isPress('LEFT')) {
      game.setLoop('Game');
    }
    
    // отрисуем фон
    bg.draw();
    
    // отрисуем свой курсор
    brush.drawImage({
      x : mp.x - 10, y : mp.y - 10, // 10 - смещение картинки из-за обрамления
      file : 'menu_cursor.png'
    });
    
  };
  
});

// Запускаем игру на текущем игровом цикле
game.startLoop('Menu');

И результат:
image

game.js — Пишем логику игры

Тут у нас все достаточно просто: есть три полки, по ним будут «ехать» ноутбуки, на некоторых из них будет логотип DNS, на других синий экран смерти. Задача игрока — убрать с полок все «сломанные» ноутбуки.

Реализация в коде:

game.newLoopFromConstructor('Game', function () {

  // создадим полки, по которым будут плыть компьютеры
  var bg = game.newImageObject({
    x : 0, y : 0, // позиция
    file : 'bg_game.jpg', // файл фона
    w : width, // ширину установим, высота подгонится сама,
    h : height
  });
  
  // массив с количеством компьютеров на полочках (для инициализации)
  var cntShelf;
  
  var cntRIP = 0;
  
  // уровни полок по оси Y
  var shelf = {
    1 : i(15),  // верхняя полка
    2 : i(86),  // средняя
    3 : i(160)  // нижняя
  };

  // скорости движения на полках  
  var shelfSpeed = {
    1 : math.random(2, 8),
    2 : math.random(2, 8),
    3 : math.random(2, 8)
  };
  
  var createLaptop = function (s) { // агрументом передаем полку
    var rnd = s ? s : math.random(1, 3); // выбираем полку (случайно) или из агрумента
    var rip = math.random(1, 5) == 5; // выберем тип компьютера (случайно)
    
    // увеличим количество компьютеров, которые надо убрать (для инициализации)
    if (!s && rip) cntRIP++;
    
    return game.newImageObject({
      y : shelf[rnd], // установим на выбранную полку
      h : i(50), // выота компьютера
      file : !rip ? 'laptop.png' : 'laptop_rip.png', // выбираем нужный спрайт
  
      userData : { // дополнительные данные (нестандартные)
        shelf : rnd, // полка
        rip : rip // тип компьютера
      },
  
      onload : function () { // отпозиционируем после загрузки по оси X 
        var dist = 10; // дистанция между компьютерами
        this.x = -cntShelf[rnd] * (this.w + dist); // рссчитываем позицию
        cntShelf[rnd]++; // увеличиваем количество на выбранной полке
      }
    });    
  };
  
  // объекты для "отстрела"
  var cells = []; // это массив
  
  var fog = 0; // степень прозрачности после тумана после выстрела
  var scale = 1; // степень деформации прицела

  // при входе в игровой цикл установим первичные данные
  this.entry = function () {
    score = 0; // сбросим счет
    
    // сбросим количества компьютеров
    cntShelf = {
      1 : 0,
      2 : 0,
      3 : 0
    };
    
    // заполнение массива
    OOP.fillArr(cells, math.random(20, 90), function () {
      return createLaptop(); // вернем в массив только что созданный объект
    });
  };

	this.update = function () {
	  var dt = game.getDT(30); // для плавного движения возьмем дельту и поделим на 100
	  
    // запомним на всякий случай позицию курсора
    var mp = mouse.getPosition();
    
    // запомним так же скорость мыши
    var mps = Math.max(mouse.getSpeed().x, mouse.getSpeed().y);
    
    // запомним состояние нажатия (true/false) для текущего кадра
    var fire = mouse.isPress('LEFT');

    // имитируем выстрел
    if (fire) {
      fog = 1;
      scale = 1.5;
    }

    // рисуем полочки
    bg.draw();
    
    // рисуем объекты
    OOP.forArr(cells, function (c, i) {
      var rnd;
      c.draw();
      c.move(point(shelfSpeed[c.shelf] * dt, 0)); // двигаем по оси X
      
      // если объект ушел за пределы видимости, меняем его позицию по X
      if (c.x > width) {
        c.x = -c.w * cntShelf[c.shelf];
      }
      
      // если выстрел, проверим попадание
      if (fire) {
        // если курсор попадает в статический бокс объекта BBOX
        if (mouse.isInStatic(c.getStaticBox())) {
          // если компьютер с синим экраном - то увеличиваем счет
          if (c.rip) {
            score++;
            
            // если мы набрали нужное количество очков
            if (score >= cntRIP) {
              score = 1;
              game.setLoop('Menu');
            }
            
          } else { // а иначе выкидываем в меню из-за неправильного выбора
            score = -1;
            game.setLoop('Menu');
            return;
          }
          
          cells.splice(i, 1);
          cells.push(createLaptop()); // добавляем еще новый компьютер
          return;
        }
      }
      
    });
    
    
    // рисуем прицел
    brush.drawImage({
      x : mp.x - i(50) * scale, y : mp.y - i(50) * scale, // 10 - смещение картинки из-за обрамления
      w : i(100) * scale, h : i(100) * scale, // размеры установим фактические
      file : 'cell.png'
    });
    
    // рисуем счет
    brush.drawText({
      text : 'Убрано: ' + score + '/' + cntRIP,
      color : 'black',
      size : i(15),
      font : 'serif'
    });
    
    // если есть туман, рисуем его и меняем прозрачность
    if (fog > 0) {
      brush.drawRect({
        w : width, h : height, // закрываем всё
        fillColor : 'rgba(255, 255, 255, '+fog+')'
      });
      
      fog -= 0.1; // прозрачность
      scale -= 0.05; // степень деформации прицела
    }
    
    
	};

});

Результат:
image

Рекламные вставки и переработка menu.js

Теперь нам надо переработать файл menu.js, и ввести в него дополнительные условия, что выводить, когда score == -1 (проиграл) и score == 1 (выиграл).

Переделанный файл menu.js выглядит:

// Объявляем игровой цикл
game.newLoopFromConstructor('Menu', function () {

  // Для меню мне требуется создать красивый фон в стиле DNS 
  var bg = game.newImageObject({
    x : 0, y : 0, // позиция
    file : 'dns_logo.jpeg', // файл фона
    w : width, // ширину установим
    h : height,
    alpha : 0.8 // немного блеклый
  });
  
  // объявим функция входа в игровой цикл
  this.entry = function () {
    
    var div, _s;
    
    if (score) {
      div = pjs.system.newDOM('div', true);
      _s = div.style; // ссылка на стили
      _s.height = _s.width = '100%'; // установили ширину и высоту HTML блока
      _s.backgroundColor = 'white'; // заливка
      _s.cursor = 'default'; // курсор

      if (score == 1) { // если выиграл
        div.innerHTML = `
          <table width="100%" height="100%"><tr><td valign="middle" align="center">
            <b>Вот так мы оставляем на прилавках только качественный продукт!</b> <br> <br>
            <a target="_parent" href="/">Перейти в магазин и попробовать лично</a> <br>
            <a href="#" onclick="location.reload();">Сыграть еще раз</a>
          </td></tr></table>`;
      } else if (score == -1) { // если проиграл
        div.innerHTML = `
          <table width="100%" height="100%"><tr><td valign="middle" align="center">
            <b>Задача - убрать только сломанные компьютеры!</b> <br> <br>
            <a href="#" onclick="location.reload();">Я понял, запускай!</a> <br>
            <a target="_parent" href="/">Перейти в магазин и попробовать лично</a>
          </td></tr></table>`;
      }
    }
  };
  
  this.update = function () {
    // запомним на всякий случай позицию курсора
    var mp = mouse.getPosition();

    // при наведении мыши сделаем ярче
    if (mp.x > 10 && mp.x < width - 10 && mp.y > 10 && mp.y < height - 10) bg.setAlpha(1);
    else bg.setAlpha(0.5);
    
    
    // по клику перейдем в игру
    if (mouse.isPress('LEFT')) {
      game.setLoop('Game');
    }
    
    // отрисуем фон
    bg.draw();
    
    // отрисуем свой курсор
    brush.drawImage({
      x : mp.x - 10, y : mp.y - 10, // 10 - смещение картинки из-за обрамления
      file : 'menu_cursor.png'
    });
    
  };
  
});

// Запускаем игру на текущем игровом цикле
game.startLoop('Menu');

Оформление самой рекламной информации следует переработать, это да, но сам прототип уже работает.

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

Посмотреть живой пример: Запустить в браузере

Источник