Случайны выбор дневника Раскрыть/свернуть полный список возможностей


Найдено 335 сообщений
Cообщения с меткой

snake - Самое интересное в блогах

Следующие 30  »
rss_rss_hh_new

[Перевод] Укрощение Змейки с помощью реактивных потоков

Четверг, 28 Сентября 2017 г. 14:24 (ссылка)






Poccomaxa_zt


сегодня в 14:24

Разработка





Укрощение Змейки с помощью реактивных потоков







  • Перевод
  • Tutorial








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



Хотя это не самая простая тема, но как только мы сумеем ее понять, мы спросим себя, как мы могли без нее жить?



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







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



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



Код доступен на Github, и здесь можно найти демо-версию. Я призываю Вас клонировать проект, немного повозиться с ним и реализовать интересные новые игровые функции. Если вы это сделаете, напишите мне в Twitter.



Содержание




  • Игра

  • Настройка сцены

  • Идентификация исходных потоков

  • Управление змейкой


    • Поток направления (direction$)


  • Отслеживание длины


    • BehaviorSubject как спасение

    • Реализация счета (score$)


  • Укрощение змейки (snake$)

  • Создание яблок


    • Вещание событий


  • Собирая всё вместе


    • Поддержание производительности

    • Отображение сцены


  • Будущая работа

  • Особая благодарность





Игра



Как упоминалось ранее, мы собираемся воссоздать Змейку, классическую видеоигру с конца 1970-х годов. Но вместо простого копирования игры мы добавим немного вариации к ней. Вот как работает игра.



Как игрок Вы контролируете линию, похожую на голодную змею. Цель состоит в том, чтобы съесть столько яблок, сколько сможете, чтобы вырасти как можно длиннее. Яблоки можно найти в случайных позициях на экране. Каждый раз, когда змея ест яблоко, её хвост становится длиннее. Стены не остановят вас! Но послушайте, Вы должны стараться избегать попадания в своё собственное тело любой ценой. Если Вы этого не сделаете, игра окончена. Как долго Вы сможете выжить?



Вот предварительный пример того, что мы собираемся сделать:







Для этой конкретной реализации змея представлена в виде линии синих квадратов, где ее голова окрашена в черный цвет. Можете ли Вы сказать, как выглядят фрукты? Точно, красные квадраты. Все сущности это квадрат, и это не потому, что так они выглядят красивее, а потому что это очень простые геометрические фигуры и они легко рисуются. Графика не очень блестящая, но эй, речь ведь идет о переходе от императивного программирования к реактивному программированию, а не об игровом искусстве.



Настройка сцены



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



Если это совершенно ново для Вас, ознакомьтесь с этим курсом на egghead от Keith Peters.



Файл index.html довольно прост, потому что большая часть магии происходит в JavaScript.
















Сценарий (script), который мы добавляем к телу (body), по сути является результатом процесса сборки и содержит весь наш код. Однако вам может быть интересно, почему в body нет такого элемента как canvas. Это потому, что мы создадим этот элемент с помощью JavaScript. Кроме того, мы добавляем несколько констант, которые определяют количество строк и столбцов, а также ширину и высоту холста.



export const COLS = 30;
export const ROWS = 30;
export const GAP_SIZE = 1;
export const CELL_SIZE = 10;
export const CANVAS_WIDTH = COLS * (CELL_SIZE + GAP_SIZE);
export const CANVAS_HEIGHT = ROWS * (CELL_SIZE + GAP_SIZE);

export function createCanvasElement() {
const canvas = document.createElement('canvas');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
return canvas;
}




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



let canvas = createCanvasElement();
let ctx = canvas.getContext('2d');
document.body.appendChild(canvas);




Обратите внимание, что мы также получаем ссылку на CanvasRenderingContext2D, вызывая getContext ('2d') на элементе canvas. Этот контекст 2D-рендеринга для холста позволяет нам рисовать, например, прямоугольники, текст, линии, пути и многое другое.



Мы готовы двигаться! Давайте начнем работу над основной механикой игры.



Идентификация исходных потоков



Исходя из примера и описания игры мы знаем, что нам нужны следующие функции:




  • Управление змеей с помощью клавиш со стрелками

  • Слежение за счетом игрока

  • Слежение за змеей (включая еду и перемещение)

  • Слежение за яблоками на поле (включая создание новых яблок)





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



Попробуем найти наши исходные потоки, посмотрев на вышеприведенные функции.



Прежде всего, вводимые пользователем данные определенно будут меняться со временем. Игрок перемещает голодную змею с помощью клавиш со стрелками. Это означает, что наш первый исходный поток — keydown$, который будет инициировать изменение значения всякий раз, когда нажата клавиша.



Затем нам нужно следить за счетом игрока. Счет в основном зависит от того, сколько яблок съела змея. Можно сказать, что счет зависит от длины змеи, потому что всякий раз, когда змея растет, мы хотим увеличить счет на 1. Поэтому наш следующий исходный поток — snakeLength$.



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



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



Давайте начнём со змеи. Основной механизм змеи прост: она движется со временем, и чем больше яблок она съедает, тем больше она растет. Но что именно является потоком змеи? На данный момент мы можем забыть о том, что она ест и растет, потому что в первую очередь важно то, что она зависит от фактора времени, когда она движется со временем, например, 5 пикселей каждые 200 ms. Таким образом, наш исходный поток — это интервал, который отправляет значение после каждого периода времени, и мы называем это ticks$. Этот поток также определяет скорость нашей змеи.



И последнее, но не менее важное: яблоки. Расположить яблоки на поле учитывая всё — довольно просто. Этот поток в основном зависит от змеи. Каждый раз, когда змея движется, мы проверяем, сталкивается ли голова змеи с яблоком или нет. Если это так, мы удаляем это яблоко и генерируем новое в произвольной позиции на поле. Как упоминалось, нам не нужно вводить новый поток данных для яблок.



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




  • keydown$: события нажатия клавиши (KeyboardEvent)

  • snakeLength$: представляет длину змеи (Number)

  • ticks$: интервал, который представляет движение змеи (Number)





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



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



Управление змейкой



Давайте погрузимся прямо в код и внедрим механизм управления для нашей змеи. Как упоминалось в предыдущем разделе, управление зависит от ввода с клавиатуры. Оказывается, это ужасно просто, и первым шагом является создание наблюдаемой (observable) последовательности из событий клавиатуры. Для этого мы можем использовать оператор fromEvent():



let keydown$ = Observable.fromEvent(document, 'keydown');




Это наш самый первый исходный поток, и он будет инициировать KeyboardEvent каждый раз, когда пользователь нажимает клавишу. Обратите внимание, что буквально каждый keydown инициирует событие. Поэтому мы также получаем события для клавиш, которые нас абсолютно не интересуют, и это в основном все остальные клавиши, кроме клавиш со стрелками. Но прежде чем решать эту конкретную проблему, мы определяем постоянную карту направлений:



export interface Point2D {
x: number;
y: number;
}

export interface Directions {
[key: number]: Point2D;
}

export const DIRECTIONS: Directions = {
37: { x: -1, y: 0 }, // Left Arrow
39: { x: 1, y: 0 }, // Right Arrow
38: { x: 0, y: -1 }, // Up Arrow
40: { x: 0, y: 1 } // Down Arrow
};




Посмотрев на объект KeyboardEvent, можно предположить, что каждый ключ имеет уникальный keyCode. Чтобы получить коды для клавиш со стрелками, мы можем использовать эту таблицу.



Каждое направление имеет тип Point2D, который является просто объектом с x и y свойствами. Значение для каждого свойства может быть равно 1, -1 или 0, указывая, где должна быть змея. Позже мы будем использовать направление для получения новой позиции сетки для головы и хвоста змеи.



Поток направления (direction$)



Итак, у нас уже есть поток для событий keydown, и каждый раз, когда игрок нажимает клавишу, нам нужно сопоставить значение, которое являет собой KeyboardEvent, одному из векторов направления выше. Для этого мы можем использовать оператор map() для проецирования каждого события клавиатуры на вектор направления.



let direction$ = keydown$
.map((event: KeyboardEvent) => DIRECTIONS[event.keyCode])




Как упоминалось ранее, мы будем получать каждое событие нажатия клавиши, потому что мы не отфильтровываем те, которые нас не интересуют, например, клавиши символов. Однако можно утверждать, что мы уже фильтруем события, просматривая их на карте направлений. Для каждого keyCode, который не определен на этой карте, он будет возвращен как undefined. Тем не менее, на самом деле это не фильтрует значения в потоке, поэтому мы можем использовать оператор filter(), чтобы обрабатывать только нужные значения.



let direction$ = keydown$
.map((event: KeyboardEvent) => DIRECTIONS[event.keyCode])
.filter(direction => !!direction)




Хорошо, это было легко. Код выше отлично работает и работает так, как ожидалось. Тем не менее, есть еще кое-что для улучшения. Вы догадались что именно?



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



Решение довольно просто. Мы кэшируем предыдущее направление и когда вызывается новое событие, мы проверяем, не отличается ли новое направление от противоположного. Вот функция, которая вычисляет next (следующее) направление:



export function nextDirection(previous, next) {
let isOpposite = (previous: Point2D, next: Point2D) => {
return next.x === previous.x * -1 || next.y === previous.y * -1;
};

if (isOpposite(previous, next)) {
return previous;
}

return next;
}




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



Чтобы избежать внешней переменной, нам нужен способ сортировки совокупных бесконечных Observables. RxJS имеет очень удобный оператор, который мы можем использовать для решения нашей проблемы — scan().



Оператор scan() очень похож на Array.reduce(), но вместо того, чтобы только возвращать последнее значение, он инициирует отправку каждого промежуточного результата. С помощью scan() мы можем в основном накапливать значения и бесконечно сокращать поток входящих событий до одного значения. Таким образом, мы можем отслеживать предыдущее направление, не полагаясь на внешнее состояние.



Давайте применим это и посмотрим на наш окончательный direction$ поток:



let direction$ = keydown$
.map((event: KeyboardEvent) => DIRECTIONS[event.keyCode])
.filter(direction => !!direction)
.scan(nextDirection)
.startWith(INITIAL_DIRECTION)
.distinctUntilChanged();




Обратите внимание, что мы используем startWith(), чтобы инициировать начальное значение, прежде чем начинать отправлять значения из источника Observable (keydown$). Без этого оператора наш Observable начнет отправлять значения только тогда, когда игрок нажимает клавишу.



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



Следующая схема визуализирует наш поток direction$ и то, как он работает. Значения, окрашенные в синий цвет, представляют начальные значения, желтый означает, что значение было изменено на потоке Observable, а значения, отправленные в потоке результатов, окрашены в оранжевый цвет.







Отслеживание длины



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



В реактивном мире решение немного отличается. Один из простых подходов может заключаться в использовании потока snake$, и каждый раз, когда он отправляет значение, мы знаем, что змея увеличилась в длину. Хотя это действительно зависит от реализации snake$, пока мы не будем реализовывать этот поток. С самого начала мы знаем, что змея зависит от ticks$, поскольку она перемещается на определенное расстояние с течением времени. Таким образом, snake$ будет накапливать массив сегментов тела, и поскольку она основана на ticks$, она будет генерировать значение каждые x миллисекунд. Тем не менее, даже если змея не сталкивается ни с чем, snake$ будет по-прежнему отправлять значения. Это потому, что змея постоянно движется по полю, и поэтому массив всегда будет другим.



Это может быть немного сложно понять, потому что между различными потоками есть определенные зависимости. Например, apples$ будут зависеть от snake$. Причина этого в том, что каждый раз, когда движется змея, нам нужен массив сегментов её тела, чтобы проверить, не сталкивается ли какая-либо из этих частей с яблоком. В то время как поток apples$ будет накапливать массив яблок, нам нужен механизм моделирования коллизий, который в то же время избегает круговых зависимостей.



BehaviorSubject как спасение



Решение этой задачи заключается в том, что мы будем внедрять механизм вещания, используя BehaviorSubject. RxJS предлагает различные типы Subjects (предметов) с различными функциональными возможностями. Таким образом, класс Subject предоставляет базу для создания более специализированных Subjects. В двух словах, Subject является типом, который реализует одновременно типы Observer (наблюдатель) и Observable (наблюдаемый). Observables определяют поток данных и создают данные, в то время как Observers могут подписаться на Observables и получать данные.



BehaviorSubject — это более специализированный Subject, предоставляющий значение, которое изменяется со временем. Теперь, когда Observer подписывается на BehaviorSubject, он получит последнее отправленное значение, а затем все последующие значения. Его уникальность заключается в том, что он включает в себя начальное значение, так что все Observers получат как минимум одно значение при подписке.



Давайте продолжим и создадим новый BehaviorSubject с начальным значением SNAKE_LENGTH:



// SNAKE_LENGTH specifies the initial length of our snake
let length$ = new BehaviorSubject(SNAKE_LENGTH);




С этого места остался лишь небольшой шаг для реализации snakeLength$:



let snakeLength$ = length$
.scan((step, snakeLength) => snakeLength + step)
.share();




В приведенном выше коде мы видим, что snakeLength$ основана на length$, которая является нашим BehaviorSubject. Это означает, что всякий раз, когда мы передаем новое значение для Subject, используя next(), он будет отправлять значение на snakeLength$. Кроме того, мы используем scan() для накопления длины с течением времени. Круто, но вам может быть интересно, что это за share(), не так ли?



Как уже упоминалось, snakeLength$ будет позже использоваться как входящие данные для snake$, но в то же время действует как исходный поток для счёта игрока. Как результат мы в конечном итоге воссоздаем этот исходный поток со второй подпиской на тот же Observable. Это происходит потому, что length$ является cold Observable (холодным Наблюдаемым).



Если вы совершенно не знакомы с hot and cold Observables (горячими и холодными наблюдателями), мы написали статью о Cold vs Hot Observables.



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



Потрясающие! Теперь, когда у нас есть механизм, который мы можем использовать для передачи значений нескольким подписчикам, мы можем пойти дальше и реализовать score$.



Реализация счета (score$)



Реализация счета игрока настолько проста, насколько это возможно. Вооружившись snakeLength$, мы теперь можем создать поток score$, который просто накапливает счет игрока с помощью scan():



let score$ = snakeLength$
.startWith(0)
.scan((score, _) => score + POINTS_PER_APPLE);




По сути мы используем snakeLength$ или, скорее, length$, чтобы уведомлять подписчиков о том, что произошло столкновение, и если это действительно было так, мы просто увеличиваем счет на POINTS_PER_APPLE, постоянное количество очков за яблоко. Обратите внимание, что startWith(0) необходимо добавить перед scan(), чтобы избежать указания начального значения.



Давайте посмотрим на более наглядное представление того, что мы только что реализовали:







Посмотрев на приведенную выше схему, вы можете задаться вопросом, почему начальное значение BehaviorSubject появляется только на snakeLength$ и отсутствует в score$. Это связано с тем, что первый подписчик заставит share() подписаться на базовый источник данных и, поскольку исходный источник данных сразу же отправляет значение, это значение уже будет отправлено к тому времени, когда произошли последующие подписки.



Чудно. Начиная с этого места, давайте реализуем поток для нашей змеи. Разве это не захватывающе?



Укрощение змейки (snake$)



К этому моменту мы уже выучили много операторов и теперь можем использовать их для реализации нашего потока snake$. Как обсуждалось в начале этой статьи, нам нужен какой-то тикер, который удерживает нашу голодную змею двигающейся. Оказывается, есть удобный оператор для этого interval(x), который отправляет значения каждые x миллисекунд. Давайте будем называть каждое значение тиком.



let ticks$ = Observable.interval(SPEED);




С этого момента до реализации финального потока snake$ совсем немного. Для каждого тика, в зависимости от того, съела ли змея яблоко или нет, мы хотим либо переместить её вперёд, либо добавить новый сегмент. Поэтому мы можем использовать уже знакомую нам функцию scan() для накопления массива сегментов тела. Но, как вы, возможно, догадались, перед нами возникает вопрос. Где вступают в игру потоки direction$ или snakeLength$?



Абсолютно законный вопрос. Направление, как и длина змеи легко доступны внутри нашего потока snake$, если мы сохраним эту информацию в переменной за пределами наблюдаемого потока. Но опять же, мы нарушаем наше правило не изменять внешнее состояние.



К счастью, RxJS предлагает еще один очень удобный оператор с именем withLatestFrom(). Это оператор, используемый для объединения потоков, и это именно то, что мы ищем. Этот оператор применяется к первичному потоку, который контролирует, когда данные будут отправляться в результирующий поток. Другими словами, вы можете думать о withLatestFrom() как о способе регулирования отправки данных вторичного потока.



Учитывая вышесказанное у нас есть инструменты, необходимые для окончательного внедрения голодной snake$:



let snake$ = ticks$
.withLatestFrom(direction$, snakeLength$, (_, direction, snakeLength) => [direction, snakeLength])
.scan(move, generateSnake())
.share();




Наш основной источник — это ticks$, и всякий раз, когда новое значение проходит вниз по своему пути, мы берем последние значения как с direction$, так и с snakeLength$. Обратите внимание, что даже если вторичные потоки часто отправляют значения, например, если игрок разбивает себе голову на клавиатуре, мы будем обрабатывать данные только для каждого тика.



Кроме того, мы передаем селекторную функцию (функцию для настройки) в withLatestFrom, которая вызывается, когда первичный поток инициирует отправку значения. Эта функция является необязательной, и если ее убрать, то появляется список со всеми элементами.



Мы не будем объяснять функцию move(), поскольку основной целью этого поста является содействие изменению фундаментального мышления. Тем не менее, вы можете найти её исходный код на GitHub.



Вот схема, которая наглядно демонстрирует код выше:







Видите как мы регулируем поток direction$? Дело в том, что использование withLatestFrom() очень практично, когда вы хотите объединить несколько потоков, и Вы не заинтересованы в создании значений в блоке Observable (наблюдаемом), когда любой из потоков отправляет данные.



Создание яблок



Вы можете заметить, что внедрение наших основных строительных блоков для игры становится всё проще, поскольку мы узнаем все больше и больше операторов. Если Вы дошли до этой точки, то оставшиеся шаги будут легкими.



На данный момент мы реализовали несколько потоков, таких как direction$, snakeLength$, score$ и snake$. Если мы объединим их вместе, мы сможем перемещаться по этому зверю змею. Но что это за игра, если нечего есть. Довольно скучно.



Давайте создадим несколько яблок, чтобы удовлетворить аппетит нашей змеи. Во-первых, давайте проясним, какое состояние нам нужно сохранить. Ну, это может быть либо один объект, либо массив объектов. Для нашей реализации мы выберем массив яблок. Вы слышите звон колоколов?



Правильно, мы снова можем использовать scan() для накопления массива яблок. Мы начинаем с начального значения, и каждый раз, когда змея движется, мы проверяем, произошло ли столкновение. Если это так, мы создаем новое яблоко и возвращаем новый массив. Таким образом мы можем использовать distinctUntilChanged() для фильтрации одинаковых значений.



let apples$ = snake$
.scan(eat, generateApples())
.distinctUntilChanged()
.share();




Круто! Это означает, что всякий раз, когда apples$ отправляет новое значение, мы можем предположить, что наша змея съела один из этих вкусных фруктов. Осталось увеличить счет, а также сообщить другим потокам об этом событии, таким как snake$, которая берет последнее значение от snakeLength$, чтобы определить, добавить ли новый сегмент тела.



Вещание событий



Немного ранее мы реализовали механизм вещания, помните? Давайте использовать его для запуска желаемых действий. Вот наш код для eat():



export function eat(apples: Array, snake) {
let head = snake[0];

for (let i = 0; i < apples.length; i++) {
if (checkCollision(apples[i], head)) {
apples.splice(i, 1);
// length$.next(POINTS_PER_APPLE);
return [...apples, getRandomPosition(snake)];
}
}

return apples;
}




Простым решением является вызов length$.next(POINTS_PER_APPLE) прямо внутри блока условия. Но тогда мы столкнемся с проблемой, потому что мы не сможешь извлечь этот метод в свой собственный модуль утилит (модуль ES2015). В ES2015 модули хранятся в файлах, и для каждого файла существует ровно один модуль. Наша цель состоит в том, чтобы организовать наш код таким образом, чтобы его было легко поддерживать и расширять.



Более утонченным решением является введение еще одного потока, называемого applesEaten$. Этот поток основан на apples$ и каждый раз, когда в потоке отправляется новое значение, мы хотели бы выполнить какое-то действие, вызывая length$.next(). Для этого мы можем использовать оператор do(), который будет выполнять часть кода для каждого события.



Звучит выполнимо. Но нам как-то нужно пропустить первое (начальное) значение, испускаемое apples$. В противном случае мы в конечном итоге увеличиваем счет сразу, что не имеет большого смысла, поскольку игра только началась. Оказывается, у RxJS есть оператор для этого, а именно skip().



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



let appleEaten$ = apples$
.skip(1)
.do(() => length$.next(POINTS_PER_APPLE))
.subscribe();




Собирая всё вместе



На этом этапе мы реализовали все основные строительные блоки нашей игры, и мы готовы, наконец, объединить все в один поток результата — scene$. Для этого мы будем использовать combineLatest. Этот оператор очень похож на withLatestFrom, но отличается в деталях. Во-первых, давайте посмотрим на код:



let scene$ = Observable.combineLatest(snake$, apples$, score$, (snake, apples, score) => ({ snake, apples, score }));




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







Поддержание производительности



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



Мы можем сделать это, представив другой поток, похожий на ticks$, но для рендеринга. В основном это лишь другой интервал:



// Interval expects the period to be in milliseconds which is why we devide FPS by 1000
Observable.interval(1000 / FPS)




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



К счастью, мы можем использовать requestAnimationFrame, позволяя браузеру работать и выполнять наш скрипт в самое подходящее время. Но как мы используем его для нашего блока Observable? Хорошей новостью является то, что многие операторы, включая interval(), принимают в качестве последнего аргумента Scheduler (планировщик). В двух словах, Scheduler — это механизм, позволяющий запланировать выполнение какой-либо задачи в будущем.



Хотя RxJS предлагает множество планировщиков, тот, который нас интересует, называется animationFrame. Этот планировщик выполняет задачу при запуске window.requestAnimationFrame.



Отлично! Давайте применим это к нашему интервалу, и назовем результирующий Observable game$:



// Note the last parameter
const game$ = Observable.interval(1000 / FPS, animationFrame)




Этот интервал теперь будет отправлять значения примерно каждые 16 мс, поддерживающие 60 FPS.



Отображение сцены



Нам осталось объединить нашу game$ с scene$. Можете ли вы догадаться, какой оператор мы используем для этого? Помните, что оба потока отправляются с разными интервалами, и теперь цель состоит в том, чтобы отобразить нашу сцену на холсте 60 раз в секунду. Мы будем использовать game$ в качестве основного потока, и каждый раз, когда он отправляет значение, мы объединяем его с последним значением из scene$. Звучит знакомо? Да, мы снова можем использовать withLatestFrom.



// Note the last parameter
const game$ = Observable.interval(1000 / FPS, animationFrame)
.withLatestFrom(scene$, (_, scene) => scene)
.takeWhile(scene => !isGameOver(scene))
.subscribe({
next: (scene) => renderScene(ctx, scene),
complete: () => renderGameOver(ctx)
});




Возможно, вы заметили takeWhile() в приведенном выше коде. Это еще один очень полезный оператор, который мы можем вызвать на существующем Observable. Он будет возвращать значения из game$ до тех пор, пока isGameOver() не вернет true.



Вот живое демо для вас с которым можно поиграться:







Будущая работа



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



Будьте на связи!



Особая благодарность



Особая благодарность James Henry и Brecht Billiet за их помощь с кодом.


Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/338910/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
odi

Snake. Printable contour drawing. Raskraska coloring book for kids.

Вторник, 19 Сентября 2017 г. 13:41 (ссылка)

Raskraska coloring book for children. Drawings of Animals.

SNAKE
Click on small image to view larger picture, which you can print and coloring.



Aosta - Milano

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
vendamostswag

Nokia 3310

Вторник, 19 Сентября 2017 г. 12:13 (ссылка)


A6bZ08jqSof7fsJshXdOQ2ztlMDQ8ZwIPizupOJq9bf

Комментарии (0)КомментироватьВ цитатник или сообщество
lenov_ru

Snake VS Block v 1.17 » Клуб пользователей планшетов на ANDROID / Lenovo IdeaTab A2109 8GB / Samsung Galaxy Tab 2 7.0 / Asus Transformer TF700T / NVIDIA Tegra 3

Среда, 16 Августа 2017 г. 08:01 (ссылка)
lenov.ru/games/30107-snake-...v-117.html


Snake VS Block - увлекательная и довольно сложная аркада на скорость, реакцию, а так же умение быстро вести подсчёт. Геймеры управляя змейкой из определённого количества звеньев должны проделать как

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Из песочницы] Пишем игру змейка с помощью JavaScript + Canvas

Воскресенье, 30 Июля 2017 г. 15:30 (ссылка)

Доброго времени суток, друзья. Сейчас я постараюсь вам показать как можно написать игру Змейка. Конечно, не самым быстрым способом и не самым маленьким в плане количества строк кода, но по-моему самым понятным для начинающих разработчиков, как я. Статья написана для людей, желающих чуть-чуть познакомиться с элементом canvas и его простыми методами для работы с 2D графикой.

image

Напишем змейку в «старом» виде, без особо красивой графики — в виде кубиков. Но это только упростит понимание разработки. Ну что же, поехали!



Подготовка



Пожалуй, стоит вообще начать с подготовки к созданию игры и написанию кода. Мы будем использовать простой редактор Sublime Text. Впрочем, это не важно. Все делаем в одном документе, чтобы было быстрее.



Первым делом, напишем сам код для встраивания canvas в документ. Напомню, что canvas поддерживается только в HTML5.




HTML5 не поддерживается


Подготовка завершена, теперь мы можем приступать к созданию самой игры.



Начинаем



Для начала, я хотел бы вам вообще объяснить как будет работать змейка, так будет гораздо понятнее. Наша змейка — это массив. Массив элементов, элементы — это ее части, на которые она делиться. Это всего лишь квадратики, которые имеют координаты X и Y. Как вы знаете, X — горизонталь, Y — вертикаль. В обычном виде мы представляем себе координатную плоскость вот так:



image



Она абсолютно правильная, в этом нет сомнения, но на мониторе компьютера (в частности, canvas) она выглядит по-другому, вот так:



image



Это нужно знать, если вы вдруг в первый раз столкнулись с canvas. Я, когда столкнулся с этим, сначала вообще не понял где точка (0,0), благо я быстро разобрался. Надеюсь и у вас проблем не возникло.



Вернемся к элементам змейки, ее частям. Представим, что каждый элемент имеет свои координаты, но одинаковую высоту и одинаковую ширину. Это квадратики, не более. А теперь представим, что мы вот нарисовали змейку, все квадратики, они идут друг за другом, одинаковые такие. И тут мы решили, что нам нужно подвинуть змейку вправо. Как бы вы поступили?



Некоторые люди ответили бы, что нам нужно первый элемент подвинуть вправо, затем второй, затем третий и так далее. Но для меня этот вариант не является правильным, так как в случае, если вдруг змейка огромная, а компьютер слабый, мы можем заметить, что змейка иногда разрывается, что вообще не должно быть. Да и вообще, данный способ требует слишком много команд, когда можно обойтись гораздо меньшим количеством, не потеряв качество. А теперь мой способ: Мы берем последний элемент змейки, и ставим его в начало, изменяя его координаты так, чтобы он был после головы. Теперь этот элемент — голова. Всего-то! И да, эффект движения будет присутствовать, а на компьютере вообще не будет заметно, как мы спрятали хвост, а потом его поставили в начало. именно так мы и будем поступать во время создания движения змейки.



Вот тут точно начинаем



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



//Возвращает случайное число.
function rand (min, max) {k = Math.floor(Math.random() * (max - min) + min); return (Math.round( k / s) * s);}
//Функция для создания нового яблока.
function newA () {a = [rand(0, innerWidth),rand(0, innerHeight)];}
//Функция для создания тела змейки из одного элемента.
function newB () {sBody = [{x: 0,y: 0}];}

var gP = document.getElementById('gP'), //Достаем canvas.
//Получаем "контекст" (методы для рисования в canvas).
g = gP.getContext('2d'),
sBody = null, //Тело змейки, мы потом его создадим.
d = 1, //Направление змейки 1 - dправо, 2 - вниз 3 - влево, 4 - вверх.
a = null, //Яблоко, массив, 0 элемент - x, 1 элемент - y.
s = 30; newB(); newA(); //Создаем змейку.


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



Вы могли бы возразить, сказав, что ты мог бы просто холсту сделать ширину и высоту, кратную 30. Но на самом деле, это не лучший вариант. Так как я лично привык использовать всю ширину экрана. И в случае, если ширина = 320, то мне пришлось бы аж целых 20 пикселей забирать у пользователя, что могло бы доставить дискомфорт. Именно поэтому в нашей змейки все координаты объектов делятся на 30, чтобы не было никаких неожиданных моментов. Было бы даже правильнее вынести это как отдельную функцию, так как она достаточно часто используется в коде. Но к этому выводу я пришел поздно. (Но возможно это даже не нужно).



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



gP.width = innerWidth; //Сохраняем четкость изображения, выставив полную ширину экрана.
gP.height = innerHeight; //То же самое, но только с высотой.


Если что, то переменные innerWidth и innerHeight хранятся в глобальном пространстве имен.

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




Ну что же, теперь начинаем писать код змейки.



Чтобы было движение, нам нужна анимация, мы будем использовать функцию setInterval, вторым параметром которой будет число 60. Можно чуть больше, 75 на пример, но мне нравится 60. Функция всего на всего каждые 60 мс. рисует змейку «заново». Дальнейшее написание кода — это только этот интервал.



Покажу вообще простую отрисовку нашей змейки, пока что без движения.



setInterval(function(){
g.clearRect(0,0,gP.width,gP.height); //Очищаем старое.
g.fillStyle = "red"; //Даем красный цвет для рисования яблока.
g.fillRect(...a, s, s); //Рисуем яблоко на холсте 30x30 с координатами a[0] и a[1].
g.fillStyle = "#000"; //А теперь черный цвет для змейки.
}, 60);


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




sBody.forEach(function(el, i){

//Проверка на то, что яблоко ушло за границы окна, мы его не можем увидеть.
if (a[0] + s >= gP.width || a[1] + s >= gP.height) newA();

//Проверка на столкновение.
var last = sBody.length - 1;
if ( el.x == sBody[last].x && el.y == sBody[last].y && i < last) {
sBody.splice(0,last); //Стираем тело змейки.
sBody = [{x:0,y:0}]; //Создаем его заново.
d = 1; //Меняем направление на правую сторону.
}

});

//+
// Сохраняем хвост и голову змейки.
var m = sBody[0], f = {x: m.x,y: m.y}, l = sBody[sBody.length - 1];

/*

Далее мы создаем тот самый эффект движения.
Напомню, что мы меняем координаты лишь хвоста, оставляя неподвижным
остальную часть тела.

Делается это путем проверки направления змейки (изначально - это 1, - право),
а затем уже изменяем координаты. Соответственно, комментарии все описывают.

*/


//Если направление вправо, то тогда сохраняем Y, но меняем X на + s.
if (d == 1) f.x = l.x + s, f.y = Math.round(l.y / s) * s;
// Если направление вниз, то сохраняем X, но меняем Y на + s.
if (d == 2) f.y = l.y + s, f.x = Math.round(l.x / s) * s;
//Если направление влево, то сохраняем Y, но меняем X на -s.
if (d == 3) f.x = l.x - s, f.y = Math.round(l.y / s) * s;
//Если направление вверх, то сохраняем X, Но меняем Y на -s.
if (d == 4) f.y = l.y - s, f.x = Math.round(l.x / s) * s;



А теперь, вы наверное заметили, что во время того, как мы изменяем координаты, мы вечно что-то «сохраняем», сначала поделив, а потом округлив и умножив на число s. Это все тот же самый способ выравнивания змейки относительно яблока. Движение в данном случае строгое, простое, поэтому и есть змейка яблоко может строго по определенным правилам, которые задан в самом начале интервала. И если бы координаты головы змейки хоть на 1px сместились бы, то яблоко нельзя было бы съесть. И да, это простой вариант, поэтому все так сильно ограничено.



Ну а нам же осталось что сделать? Правильно, удалить из массива хвост (первый элемент), добавить новый элемент в самый конец и отрисовать всю змейку. Сделаем это, добавив в конец интервала вот такие строчки кода.




sBody.push(f); //Добавляем хвост после головы с новыми координатами.
sBody.splice(0,1); //Удаляем хвост.

//Отрисовываем каждый элемент змейки.
sBody.forEach(function(pob, i){
//Если мы двигаемся вправо, то если позиция элемента по X больше, чем ширина экрана, то ее надо обнулить
if (d == 1) if (pob.x > Math.round(gP.width / s) * s) pob.x = 0;
//Если мы двигаемся вниз, то если позиция элемента по X больше, чем высота экрана, то ее надо обнулить.
if (d == 2) if (pob.y > Math.round(gP.height / s) * s) pob.y = 0;
//Если мы двигаемся влево, и позиция по X меньше нуля, то мы ставим элемент в самый конец экрана (его ширина).
if (d == 3) if (pob.x < 0) pob.x = Math.round(gP.width / s) * s;
//Если мы двигаемся вверх, и позиция по Y меньше нуля, то мы ставим элемент в самый низ экрана (его высоту).
if (d == 4) if (pob.y < 0) pob.y = Math.round(gP.height / s) * s;

//И тут же проверка на то, что змейка съела яблоко.
if (pob.x == a[0] && pob.y == a[1]) newA(), sBody.unshift({x: f.x - s, y:l.y})

//А теперь рисуем элемент змейки.
g.fillRect(pob.x, pob.y, s, s);
});


В добавок к отрисовке змейки, я добавил код, который делает ощущение, что конец экрана — это его начало. И если змейка выходит за границы, то она потом выходит из начала, на погибая.

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



//setInerval(...);

onkeydown = function (e) {
var k = e.keyCode;
if ([38,39,40,37].indexOf(k) >= 0) e.preventDefault();
if (k == 39 && d != 3) d = 1; //Вправо
if (k == 40 && d != 4) d = 2; //Вниз
if (k == 37 && d != 1) d = 3; //Влево
if (k == 38 && d != 2) d = 4; //Вверх
};


Тут мы сделали так, что если змейка двигается вправо, то она не может изменить направление налево. Это логично, на самом деле.



Вот и все, друзья. Моя первая статья, написанная новичком для новичков. Надеюсь, все было понятно и кому-то это пригодилось. Змейку можно усовершенствовать, добавив на пример, счетчик очков, рекорды, дополнительные фишки, но это все уже дополнения, которые вы можете сделать сами. На этом все, всем удачи!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334434/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
Timitim

ГИГАНТСКАЯ ЗМЕЯ против Вредных деток Змеи атакуют Crying Baby Giant Snake vs Bad Kids

Воскресенье, 02 Июля 2017 г. 12:52 (ссылка)


ГИГАНТСКАЯ ЗМЕЯ против Вредных деток Змеи атакуют Crying Baby Giant Snake vs Bad Kids





https://www.youtube.com/watch?v=29R1yJpk0SI&amp;list=PLa6CMB_HrTL1hE





Вредные детки с папой играли. Тим увидел маленькую змею. Это был змеиный детеныш. Гигантская змея атакует вредных деток и папу. Малышка спасает папу и брата. Страшное веселое видео для детей.



☆ ПОДПИШИСЬ на канал Timi Tim http://qoo.by/SS4

Предыдущее видео http://qoo.by/2ev5



Giant Snake Attack Kids Bad Baby vs Snakes Crying Babies



Children, toddlers and babies play with Dad. Kid see a little Snake on the Beach. It was a snake Baby. Giant snake attacks Crying babies. Crying baby saves Bad kids and Bad Daddy. Scary a d funny video for kids. 



Subscribe Timi Tim http://qoo.by/SS4


Гигантская змея на озере 9 (700x393, 121Kb)
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
dream_out

Маисовый полоз

Суббота, 13 Мая 2017 г. 10:07 (ссылка)

24 апреля моему змейсу по имени Рыж исполнился годик.
И хотя Рыж не способен это как-то осознать я все равно устроила ему праздничную фотосессию
И плевать что он так же не понимал что это в честь праздника...

Из всей длинны мне удалось замерить только 50см.
Сложно это - замерять длину змеи

24 апреля. Рыж 50+

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Hits Collection [Compiled by Zebyte] (2017)

Суббота, 11 Марта 2017 г. 12:36 (ссылка)
djmp-3.com/electronic/6372-...-2017.html

VA - Hits Collection [Compiled by Zebyte] (2017)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
kieplexacum

Шар лабиринт Magical Ball

Суббота, 04 Марта 2017 г. 16:41 (ссылка)

bigimg (197x700, 84Kb)
GQsOdmhcnXqOx3l4dL6bCr8pMHWOyt9AaoRO4Fw8PSFobg9ICAadk0FWk

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_boingboing

Will the new Nokia 3310 be the Nokia 3310-killer?

Понедельник, 27 Февраля 2017 г. 16:16 (ссылка)

As warned last week, Nokia has relaunched its classic 3310 model candybar phone. The good news: it's a pretty little burner that honors and updates the original's design. The bad news: that's the only connection, and it's otherwise a modern dumbphone with no clear picture yet on how well-designed the interface and hardware is. It's not even made by Nokia, but under license. [via Daneel]



The new device is very cute and looks like a sleeker, updated version of the original. HMD Global retained the keypad buttons and the general shape of the old device. On the back, we see a camera. The new phone also has a color display.


As for details about the phone’s specifications and what HMD has done to update a very rudimentary device for the modern world, we didn’t get much. The company spent less than five minutes on the new device, and only rattled off some battery life details: The new 3310 is going to have 22 hours of talk-time (LOL), and one month of standby battery life. But hey, it has Snake and the classic Nokia ringtone. Take my money!



One worriome portent: you can apparently go diagonally in the new version of Snake.

UPDATE: Reader Brian_McNett writes in to point out that the licencee, despite having the banktastic name HMD Global, is stocked to the gills with former Nokia executives and based in Finland. A good sign!

http://feeds.boingboing.net/~r/boingboing/iBag/~3/ZLbRsoKwCxo/will-the-new-nokia-3310-be-the.html

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Ready Styles Final Songs (2017)

Суббота, 18 Февраля 2017 г. 17:21 (ссылка)
djmp-3.com/electronic/6132-...-2017.html

VA - Ready Styles Final Songs (2017)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
lenov_ru

Snake Game - Puzzle Solving v 1.0.4 (Mod Money) » Клуб пользователей планшетов на ANDROID / Lenovo IdeaTab A2109 8GB / Samsung Galaxy Tab 2 7.0 / Asus Transformer TF700T / NVIDIA Tegra 3

Вторник, 24 Января 2017 г. 09:23 (ссылка)
lenov.ru/games/27892-snake-...money.html


Snake Game - Puzzle Solving - увлекательный казуальный проект представляющий собой классическую змейку только с красивой графикой, интересными геймплейными находками и миссиями. Главным героем будет

Комментарии (0)КомментироватьВ цитатник или сообщество
Рай77

Австралиец обнаружил питона в своём почтовом ящике. (Видео) — Рай77° - Экшн Ньюс

Вторник, 20 Декабря 2016 г. 11:05 (ссылка)
rai77.com/avstraliec-obnaru...10-56.html



Двухметровая змея с комфортом устроилась в укромном месте, но владелец жилища нарушил покой питона и вызвал работника службы Sunshine Coast Snake Catchers, специализирующейся на отлове ползучих гадов
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
Маками

Змейки ювелирные

Четверг, 08 Декабря 2016 г. 21:11 (ссылка)


Змеи в ювелирном искусстве-№ 2



4070986_6ba16ea254594c3dd055324cb0accbcf_1_ (389x468, 34Kb)4070986_2908cbab58084fda3107b21286603e0a (275x500, 22Kb)



Так как мой ребёнок ЗМЕЯ,( по знаку зодиака, а не по характеру!),



собираю картинки ювелирных изделий  

?????
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Deep Value, Vol.1 (Mixed By Markus Homm) (2016/FLAC)

Воскресенье, 27 Ноября 2016 г. 10:52 (ссылка)
djmp-3.com/dts/5213-va-deep...-flac.html

VA - Deep Value, Vol.1 (Mixed By Markus Homm) (2016/FLAC)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

Только самые лучшие хиты/Only the best hits, на первом месте хит парадов в АВСТРАЛИИ: DJ SNAKE – Let me love you (NRJ Ukraine) (2016)

Вторник, 11 Октября 2016 г. 23:34 (ссылка)
djmp-3.com/electronic/4535-...-2016.html

Только самые лучшие хиты/Only the best hits, на первом месте хит парадов в АВСТРАЛИИ: DJ SNAKE – Let me love you (NRJ Ukraine) (2016)


 

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

Сборник клипов - Россыпьююю. Часть 65 (2016/HD/Full HD/Ultra HD 4K)

Суббота, 24 Сентября 2016 г. 09:35 (ссылка)
djmp-3.com/hd720/4250-sborn...hd-4k.html

Сборник клипов - Россыпьююю. Часть 65 (2016/HD/Full HD/Ultra HD 4K)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Yellow Bikini, Vol. 1 (2016)

Среда, 24 Августа 2016 г. 12:03 (ссылка)
djmp-3.com/electronic/3731-...-2016.html

VA - Yellow Bikini, Vol. 1 (2016)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

VA - Mechanoids Carnival (2016)

Среда, 10 Августа 2016 г. 21:01 (ссылка)
djmp-3.com/electronic/3552-...-2016.html

VA - Mechanoids Carnival (2016)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

LUXEmusic Birthday Mix - DJ Tarantino & DJ Dyxanin (2016)

Вторник, 09 Августа 2016 г. 21:44 (ссылка)
djmp-3.com/club/3502-luxemu...-2016.html

LUXEmusic Birthday Mix - DJ Tarantino & DJ Dyxanin (2016)


 

Комментарии (0)КомментироватьВ цитатник или сообщество
DJmp-3

DJ Snake - Encore (2016/FLAC)

Пятница, 05 Августа 2016 г. 22:28 (ссылка)
djmp-3.com/dts/3441-dj-snak...-flac.html

DJ Snake - Encore (2016/FLAC)


 

Комментарии (0)КомментироватьВ цитатник или сообщество

Следующие 30  »

<snake - Самое интересное в блогах

Страницы: [1] 2 3 ..
.. 10

LiveInternet.Ru Ссылки: на главную|почта|знакомства|одноклассники|фото|открытки|тесты|чат
О проекте: помощь|контакты|разместить рекламу|версия для pda