Espruino:Примеры/Игра «Змейка»

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Игра «Змейка»[1]

В этом руководстве мы создадим очень простую игру «Змейка» при помощи Espruino Pico и LCD-дисплея Nokia.

Нам понадобятся

Подсоединение

Устройства подключаются друг к другу очень просто, потому что LCD-дисплей потребляет так мало электроэнергии, что его можно питать даже от GPIO-контактов Espruino. Просто поместите Espruino Pico на макетную плату так, чтобы USB-коннектор смотрел влево, а затем поместите LCD-дисплей прямо над Pico, выровняв его контакты по правому крайнему контакту Pico. В результате самый левый контакт Pico (т.е. ближайший к USB-коннектору) должен оказаться свободным. Пример смотрите на фото выше.

Примечание

Если вы хотите использовать вместо Pico обычную плату Espruino, просто следуйте инструкциям из статьи об LCD-дисплее PCD8544 и соответствующим образом отредактируйте код для инициализации LCD-дисплея ниже.

Код

Для начала давайте возьмём код из статьи о LCD-дисплее Nokia 5110, но немного его изменим:

  • Вручную зададим контакты для питания LCD-дисплея.
  • Контакты Pico для аппаратной шины SPI не соответствуют аналогичным контактам на LCD-дисплее, поэтому мы воспользуемся программной шиной SPI (которая может работать на любых контактах).
A5.write(0); // контакт GND
A7.write(1); // контакт VCC

// Настраиваем SPI:
var spi = new SPI();
spi.setup({ sck:B1, mosi:B10 });
// Инициализируем LCD-дисплей:
var g = require("PCD8544").connect(spi,B13,B14,B15, function() {
  // После инициализации очищаем дисплей и пишем на нём текст:
  g.clear();
  g.drawString("Hello",0,0);
  // Отправляем графику на дисплей:
  g.flip();
});

Скопируйте этот код, вставьте его в правую часть Web IDE и кликните на кнопку загрузки кода. На дисплее должно появиться сообщение «Hello».

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

A5.write(0); // контакт GND
A7.write(1); // контакт VCC

// Настраиваем SPI:
var spi = new SPI();
// Графика:
var g;
// Позиция и направление змейки:
var pos, dir;

function start() {
  g.clear();
  g.drawRect(0,0,g.getWidth()-1, g.getHeight()-1);  
  // Настраиваем позицию змейки:
  pos = {x:g.getWidth()/2,y:g.getHeight()/2}; // центр экрана
  g.setPixel(pos.x, pos.y);
  dir = {x:1,y:0}; // направление змейки
  // Обновляем экран:
  g.flip();
  // Запускаем рендеринг кадров:
  setInterval(onFrame, 100);
  // При нажатии на кнопку вращаем змейку:
  setWatch(function() { rotate(1); }, BTN, { repeat:true, edge:"rising", debounce: 50});
}

// При запуске Espruino...
function onInit() {
  clearInterval();
  clearWatch();
  spi.setup({ sck:B1, mosi:B10 });
  // Инициализируем LCD-дисплей:
  g = require("PCD8544").connect(spi,B13,B14,B15, function() {
    // После инициализации очищаем дисплей и пишем на нём текст: 
    g.clear();
    g.drawString("Hello",0,0);
    // Отправляем графику на дисплей:
    g.flip();
    start();
  });
}

// Эта функция будет вызываться на каждом «кадре» игры:
function onFrame() {
  pos.x += dir.x;
  pos.y += dir.y;
  g.setPixel(pos.x, pos.y);
  g.flip();
}

function rotate(d) {
  if (dir.x) {
    dir = {x:0,y:d*dir.x};
  } else {
    dir = {x:-d*dir.y,y:0};
  }
}

// Запускаем всё:
onInit();

После загрузки этого кода на Espruino вы увидите, что змейка начала двигаться, и при нажатии на кнопку она даже будет поворачиваться в одну сторону. Но как сделать так, чтобы она поворачивалась и в другую сторону? Для этого к Espruino можно подключить дополнительную кнопку, но давайте реализуем этот поворот при помощи долгого нажатия на кнопку на Espruino. Замените функцию setWatch() в нижней части функции start() на следующее:

setWatch(function(e) {
  var duration = e.time - e.lastTime;
  if (duration<0.15)
    rotate(1);
  else
    rotate(-1);
}, BTN, { repeat:true, edge:"falling", debounce: 50});

Теперь, если нажать и быстро отпустить кнопку на Espruino, змейка повернёт в одну сторону, а если долго зажать, то в другую.

Итак, следующий шаг – это определение того, когда змейка врезается в саму себя. Для этого мы будем искать, какие пиксели в данный момент нарисованы на экране. Замените функцию onFrame() на код ниже и снова отправьте код на Espruino:

function gameOver() {
  // Останавливаем игру:
  clearWatch();
  clearInterval();
  // Пишем на экране «Game over»:
  g.clear();
  var s = "Game Over!";
  g.drawString(s, (g.getWidth()-g.stringWidth(s))/2, g.getHeight()/2-4);
  g.flip();
  // При нажатии на кнопку перезапускаем игру:
  setWatch(function(e) {
    start();
  }, BTN, { edge:"rising", debounce: 50});
}

// Вызывается на каждом «кадре» игры:
function onFrame() {
  pos.x += dir.x;
  pos.y += dir.y;
  if (g.getPixel(pos.x, pos.y)) {
    gameOver();
  } else {
    g.setPixel(pos.x, pos.y);
    g.flip();
  }
}

Теперь нам надо, чтобы игра запоминала, где в прошлый раз был хвост змейки, чтобы мы могли его удалить. Давайте реализуем это при помощи массива history, содержащего все предыдущие позиции змейки. Это не самый эффективный способ для хранения этих позиций, поэтому если у вас очень большой дисплей (по которому может бегать очень длинная змейка), то советуем рассмотреть какой-то другой вариант (более подробно читайте в этой статье). Но для нашей ситуации этого метода будет достаточно плюс так код гораздо проще для понимания.

Замените функцию onFrame() на следующее:

// Предыдущие позиции змейки:
var history = [];
// Максимальная длина, до которой может вырасти змейка:
var snakeLength = 20;

// Вызывается на каждом «кадре» игры:
function onFrame() {
  pos.x += dir.x;
  pos.y += dir.y;
  // Удаляем хвост:
  while (history.length>=snakeLength) {
    var p = history.shift(); // удаляем первый элемент из списка
    g.setPixel(p.x, p.y, 0); // очищаем этот пиксель
  }
  // Добавляем текущую позицию в конец:
  history.push({x:pos.x, y:pos.y});

  if (g.getPixel(pos.x, pos.y)) {
    gameOver();
  } else {
    g.setPixel(pos.x, pos.y);
    g.flip();
  }
}

И, наконец, давайте добавим немного «яблок», которые сможет съесть змейка, а также подсчёт баллов, и готово.

Яблоки будут храниться в массиве apples, и теперь, если змейка будет врезаться во что-то, мы также будет проверять, не является ли это «что-то» яблоком. И если это было яблоко, мы увеличим количество баллов, а не закончим игру.

Итак, финальная версия кода будет выглядеть вот так:

A5.write(0); // контакт GND
A7.write(1); // контакт VCC

// Настраиваем SPI:
var spi = new SPI();
// Графика:
var g;
// Текущее количество баллов:
var score = 0;
// Позиция и направление змейки:
var pos, dir;
// Предыдущие позиции змейки:
var history = [];
// Максимальная длина, до которой сможет дорасти змейка:
var snakeLength = 20;
// Список мест, где расположены яблоки (которые может съесть змейка):
var apples = [];

function drawScore() {
  // Удаляем старые баллы...
  g.setColor(0);  
  g.fillRect(0,0,g.getWidth(),5);
  g.setColor(1);
  g.drawString(score);
}

function newApple() {
  var p;
  // Добавляем случайные места для расположения яблок,
  // пока не кончатся места, куда можно будет поместить яблоко:
  do {
    p = { x : Math.round(Math.random()*g.getWidth()),
          y : 6 + Math.round(Math.random()*(g.getHeight()-6)) };
  } while (g.getPixel(p.x, p.y));
  // Рисуем яблоко и сохраняем его в массив:
  g.setPixel(p.x, p.y);
  apples.push(p);
}

function start() {
  // Очищаем экран:
  g.clear();
  g.drawRect(0,6,g.getWidth()-1, g.getHeight()-1);
  // Сбрасываем баллы:
  score = 0;
  drawScore();  
  // Настраиваем позицию змейки:
  pos = {x:g.getWidth()/2,y:g.getHeight()/2}; // центр экрана
  history = pos.x, pos.y; // очищаем список «history»
  g.setPixel(pos.x, pos.y);
  dir = {x:1,y:0}; // направление змейки
  // Теперь добавляем случайно расположенные яблоки:
  apples = [];
  for (var i=0;i<10;i++)
    newApple();
  // Обновляем экран:  
  g.flip();
  // Запускаем рендеринг кадров:
  setInterval(onFrame, 100);
  // При нажатии на кнопку поворачиваем змейку:
  setWatch(function(e) {
    var duration = e.time - e.lastTime;
    if (duration<0.15)
      rotate(1);
    else
      rotate(-1);
  }, BTN, { repeat:true, edge:"falling", debounce: 50});
}

// При запуске Espruino...
function onInit() {
  clearInterval();
  clearWatch();
  spi.setup({ sck:B1, mosi:B10 });
  // Инициализируем LCD-дисплей:
  g = require("PCD8544").connect(spi,B13,B14,B15, function() {
    // После инициализации очищаем дисплей и рисуем немного текста: 
    g.clear();
    g.drawString("Hello",0,0);
    // Отправляем графику на дисплей:
    g.flip();
    start();
  });
}

function gameOver() {
  // Останавливаем игру:
  clearWatch();
  clearInterval();
  // Рисуем «Game Over!» на экране:
  g.clear();
  var s = "Game Over!";
  g.drawString(s, (g.getWidth()-g.stringWidth(s))/2, g.getHeight()/2-4);
  g.flip();
  // При нажатии на кнопку перезапускаем игру:
  setWatch(function(e) {
    start();
  }, BTN, { edge:"rising", debounce: 50});
}

// Вызывается на каждом «кадре» игры:
function onFrame() {
  pos.x += dir.x;
  pos.y += dir.y;
  // Удаляем хвост:
  while (history.length>=snakeLength) {
    var p = history.shift(); // удаляем первый элемент из списка
    g.setPixel(p[0], p[1], 0); // очищаем этот пиксель
  }
  // Добавляем текущую позицию в конец:
  history.push([pos.x, pos.y]);

  if (g.getPixel(pos.x, pos.y)) {
    // Проверяем, не является ли яблоком пиксель, 
    // в который мы врезались:
    var wasApple = false;
    for (var i in apples)
      if (apples[i].x==pos.x && apples[i].y==pos.y) {
        wasApple = true;
        // Удаляем это яблоко:
        apples.splice(i,1);
        // Добавляем новое яблоко:
        newApple();
        // Меняем баллы и увеличиваем длину змейки:
        snakeLength += 5;
        score += 10;
        drawScore();
        // Прерываем цикл, 
        // чтобы больше не делать «яблочную» проверку:
        break;
      }
    if (!wasApple)
      gameOver();
  } else {
    g.setPixel(pos.x, pos.y);
    g.flip();
  }
}


function rotate(d) {
  if (dir.x) {
    dir = {x:0,y:d*dir.x};
  } else {
    dir = {x:-d*dir.y,y:0};
  }
}

onInit();

См.также

Внешние ссылки