Статьи

(ab) Использование d3.js для создания игры в понг

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

Но D3 — это нечто большее. Это мощная библиотека манипуляций с SVG . Да, некоторые люди скажут: «Но вам не нужна библиотека манипуляций с SVG! Вы можете просто написать SVG, как вы делаете HTML » . Эти люди глупы и, вероятно, пишут свои собственные функции манипулирования временем.

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

Вы можете поиграть в игру здесь , и увидеть код здесь .

Хотя это был быстрый проект, поэтому он не работает в Firefox из-за странной ошибки в размере холста SVG (мне нужно было это распространить по всему экрану), и некоторые люди говорили мне, что перетаскивание весла не работает на рабочем столе. Работал на меня. пожимание плечами

Положить его вместе

Сделать такую ​​игру с помощью D3 не так уж и много.

Для начала нам нужен минимальный HTML:

01.
<!DOCTYPE html>
02.
<meta charset="utf-8">
03.
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
04.
<title>D3 pong</title>
05.
<link href='http://fonts.googleapis.com/css?family=Overlock' rel='stylesheet' type='text/css'>
06.
<link rel="stylesheet" href="style.css">
07.
  
08.
<main></main>
09.
  
10.
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
11.
<script src="d3-pong.js"></script>

HTML не интересен. Наша игра перейдет в <main></main>тег. Остальное касается загрузки необходимых файлов и указания мобильным браузерам не вести себя смешно.

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

01.
html, body, main {
02.
    height: 100%;
03.
    padding: 0;
04.
    margin: 0;
05.
    -webkit-box-sizing: border-box;
06.
    -moz-box-sizing: border-box;
07.
    box-sizing: border-box;
08.
}
09.
  
10.
svg {
11.
    width: 100%;
12.
    height: 99%; /* gets rid of scrollbar */
13.
}
14.
  
15.
text {
16.
    font-family: 'Overlock', cursive;
17.
    font-size: 1.5em;
18.
}
19.
  
20.
line {
21.
    stroke: black;
22.
    stroke-width: 2;
23.
}
24.
  
25.
.area {
26.
    fill: white;
27.
    stroke: "red";
28.
}

Базовые элементы

После этой наземной работы начинается самое интересное.

Мы создаем элемент SVG, определяем некоторые полезные поля и вспомогательную функцию, которая превращает свойства css как "10px"числа.

01.
var svg = d3.select("main")
02.
            .append("svg"),
03.
        margin = {top: 10,
04.
                  right: 10,
05.
                  bottom: 10,
06.
                  left: 10},
07.
        parse = function (N) {
08.
            return Number(N.replace("px", ""));
09.
        };

Просто.

ScreenФункция всегда будет сказать нам , сколько места у нас есть.

1.
var Screen = function () {
2.
            return {
3.
                width: parse(svg.style("width")),
4.
                height: parse(svg.style("height"))
5.
            };
6.
        };

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

01.
Paddle = function (which) {
02.
            var width = 5,
03.
                area = svg.append('rect')
04.
                    .classed('area', true)
05.
                    .attr({width: width*7}),
06.
                paddle = svg.append('rect')
07.
                    .classed('paddle', true)
08.
                    .classed(which+"_paddle", true)
09.
                    .attr({width: 5}),
10.
                update = function (x, y) {
11.
                    var height = Screen().height*0.15;
12.
  
13.
                    paddle.attr({
14.
                        x: x,
15.
                        y: y,
16.
                        height: height
17.
                    });
18.
                    area.attr({
19.
                        x: x-width*5/2,
20.
                        y: y,
21.
                        height: height
22.
                    });
23.
                    return update;
24.
                };

Поскольку пальцы толстые, а весла тонкие, мы определили, areaчто это больше, чем фактическое весло. Это будет использоваться в качестве ручки перетаскивания. Тогда у нас есть paddleсамо по себе — оба они просто SVG-прямоугольники.

updateФункция си немного более интересно, но не так много. Просто занимает x, yпозицию и обновления paddleи area.

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

Все, что от нас требуется, это определить, что на самом деле происходит, когда что-то затягивают

Это идет внутри Paddleфункции.

// make paddle draggable
            var drag = d3.behavior.drag()
                    .on("drag", function () {
                        var y = parse(area.attr("y")),
                            height = Screen().height*0.1;
 
                        update(parse(paddle.attr("x")),
                               Math.max(margin.top, 
                                        Math.min(parse(paddle.attr("y"))+d3.event.dy,
                                                 Screen().height-margin.bottom-height)));
 
                    })
                    .origin(function () {
                        return {x: parse(area.attr("x")),
                                y: parse(area.attr("y"))};
                    });
 
            area.call(drag);
 
            return update;
        },

"drag"Событие представляет собой любой тип либо мыши или сенсорного события , которые могут представлять перетаскивание. В обратном вызове мы просто вызываем updateфункцию с новыми координатами. Все это Math.maxи Math.minчепуха гарантируют, что весла не могут быть вытащены из экрана.

.originэто то, что нужно для правильного расчета позиций. Лучше всего просто установить его на текущую позицию нашего элемента.

area.call(drag);активирует перетаскиваемое поведение на нашем перетаскиваемом area.

Далее — функция, которая ведет счет.

// generates a score, returns function for updating value and repositioning score
        Score = function (x) {
            var value = 0,
                score = svg.append('text')
                    .text(value);
 
            return function f(inc) {
                value += inc;
 
                score.text(value)
                    .attr({x: Screen().width*x,
                           y: margin.top*3});
                return f;
            };
        },

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

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

// generates middle line, returns function for updating position
        Middle = function () {
            var line = svg.append('line');
 
            return function f() {
                var screen = Screen();
 
                line.attr({
                    x1: screen.width/2,
                    y1: margin.top,
                    x2: screen.width/2,
                    y2: screen.height-margin.bottom
                });
                return f;
            };

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

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

Ball = function () {
            var R = 5,
                ball = svg.append('circle')
                    .classed("ball", true)
                    .attr({r: R,
                           cx: Screen().width/2,
                           cy: Screen().height/2}),
                scale = d3.scale.linear().domain([0, 1]).range([-1, 1]),
                vector = {x: scale(Math.random()),
                          y: scale(Math.random())},
                speed = 7;

Мы начали с простых вещей — рисование мяча, определение случайного вектора и увеличение скорости, которая хорошо смотрелась на моем экране.

Логика столкновений более привлекательна.

var hit_paddle = function (y, paddle) {
                return y-R > parse(paddle.attr("y")) && y+R < parse(paddle.attr("y"))+parse(paddle.attr("height"));
            },
            collisions = function () {
                var x = parse(ball.attr("cx")),
                    y = parse(ball.attr("cy")),
                    left_p = d3.select(".left_paddle"),
                    right_p = d3.select(".right_paddle");
 
                // collision with top or bottom
                if (y-R < margin.top || y+R > Screen().height-margin.bottom) {
                    vector.y = -vector.y;
                }
 
                // bounce off right paddle or score
                if (x+R > parse(right_p.attr("x"))) {
                    if (hit_paddle(y, right_p)) {
                        vector.x = -vector.x;
                    }else{
                        return "left";
                    }
                }
 
                // bounce off left paddle or score
                if (x-R < 
                    parse(left_p.attr("x"))+parse(left_p.attr("width"))) {
                    if (hit_paddle(y, left_p)) {
                        vector.x = -vector.x;
                    }else{
                        return "right";
                    }
                }
 
                return false;
            };

Hokay.

hit_paddleэто вспомогательная функция, которая сообщает нам, касается ли мяч весла — положение весла минус радиус шара. Просто.

collisions выглядит волосатым, но очень повторяющимся

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

Последняя часть Paddleфункции — это функция, которая выполняет шаг анимации.

return function f(left, right, delta_t) {
                var screen = Screen(),
                    // this should pretend we have 100 fps
                    fps = delta_t > 0 ? (delta_t/1000)/100 : 1; 
 
                ball.attr({
                    cx: parse(ball.attr("cx"))+vector.x*speed*fps,
                    cy: parse(ball.attr("cy"))+vector.y*speed*fps
                });
 
                var scored = collisions();
 
                if (scored) {
                    if (scored == "left") {
                        left.score(1);
                    }else{
                        right.score(1);
                    }
                    return true;
                }
 
                return false;
            };
        };

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

Основной бит

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

 // generate starting scene
    var left = {score: Score(0.25)(0),
                paddle: Paddle("left")(margin.left, Screen().height/2)},
        right = {score: Score(0.75)(0),
                paddle: Paddle("right")(Screen().width-margin.right, Screen().height/2)},
        middle = Middle()(),
        ball = Ball();

leftи rightудерживайте счет каждого игрока и функции обновления весла, и middleи ballявляются средней линией и мячом.

Мы также должны реагировать на изменение размеров окна. Это скрытно отражает и изменения ориентации.

    // detect window resize events (also captures orientation changes)
    d3.select(window).on('resize', function () {
        var screen = Screen();
 
        left.score(0);
        left.paddle(margin.left, screen.height/2);
        right.score(0);
        right.paddle(screen.width-margin.right, screen.height/2);
 
        middle();
    });

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

И наконец, мы запускаем анимацию.

 // start animation timer that runs until a player scores
    // then reset ball and start again
    function run() {
        var last_time = Date.now();
        d3.timer(function () {
 
            var now = Date.now(),
                scored = ball(left, right, now-last_time),
                last_time = now;
 
            if (scored) {
                d3.select(".ball").remove();
                ball = Ball();
                run();
            }
            return scored;
        }, 500);
    };
    run();

Мы использовали d3.timerдля создания пользовательского цикла анимации, привязанного к скорости графики устройства пользователя. Чтобы противостоять этому, мы вводим дельту времени в нашу ballфункцию анимации, чтобы создать видимость постоянной скорости.

Фактические разработчики игр сказали мне, что я должен сделать это, и я сделал это.

Когда пользователь забивает, мы сбрасываем мяч в его текущую позицию и перезапускаем все. d3.timerОсновной цикл работает до тех пор, пока функция продолжает возвращаться false. Мы позаботились об этом, вернувшись scored.

Плавник

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