код на gitlab, игра на pages

вступление

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

Как только мяч коснется зеленой точки, игра считается оконченной и раунд завершен. Игра доступна на github pages.

Игра была написана с учетом моделирования ИИ. Таким образом, общий дизайн этой игры (по крайней мере, движка) будет заключаться в том, чтобы сделать ее как можно более универсальной. Это означало бы разработку игрового движка, отвечающего за игровую физику, такую ​​как движение игрока и столкновение объектов.

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

Большая часть кода доступна здесь, на gitlab,

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

представление объектов на сцене

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

const game = {
    ball: { x: 0.8, y: 0.5, s: global.size.ball },
    play: { x: 0.9, y: 0.5, s: global.size.play },
    goal: { x: 0.2, y: 0.5, s: global.size.goal },
};

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

const global = {
    stage: {
        w: 400, h: 300,
    },
    size: {
        ball: 0.05,
        goal: 0.02,
        play: 0.1,
    }
};

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

html
    head
        link(href='style.css', rel='stylesheet')
        script(src='script.js')
    body
        h2 Blue circle will travel towards the cursor
        p Move blue circle to push red ball to green dot
        .game
            .goal
            .ball
            .play

Как и в большинстве игр, которые мы создали ранее, в этой игре будет использоваться тот же трюк position: absolute для отображения. Объектам будет присвоен собственный цвет, а атрибуты CSS left и top будут использоваться для позиционирования объекта (sass).

$clr-bg: #ccc;
$clr-ball: #c00;
$clr-goal: #0c0;
$clr-play: #00c;
body {
    text-align: center;
}
.game {
    background: $clr-bg;
    position: relative; 
    overflow: hidden;
    margin: 0 auto;
    .ball, .play, .goal {
        position: absolute;
        border-radius: 50%;
    }
    .ball { background: $clr-ball; }
    .play { background: $clr-play; }
    .goal { background: $clr-goal; }
}

Наконец, javascript, который фактически отображает объекты, изменяя атрибуты CSS. Здесь я вложу все в функцию update(). Эта функция будет вызываться для каждого кадра и будет отвечать за обновление положения объектов и отрисовку их на сцену, чем мы сейчас и занимаемся.

function $(css, ele=null) {
    if (!ele) { ele = document; }
    return [].slice.call(ele.querySelectorAll(css));
}
function update() {
    function draw(ele, info) {
        ele.style.top = `${(info.y - info.s/2)*100}%`;
        ele.style.left = `${(info.x - info.s/2)*100}%`;
        ele.style.width = `${info.s*100}%`;
        ele.style.paddingBottom = `${info.s*100}%`;
    }
    // NOTE: drawing everything
    draw($('.ball')[0], game.ball);
    draw($('.goal')[0], game.goal);
    draw($('.play')[0], game.play);
    const stage = $('.game')[0];
    stage.style.width = global.stage.w + 'px';
    stage.style.height = global.stage.h + 'px';
    setTimeout(update, 1000/60);
}

Каждый объект в основном будет иметь 3 переменные, которые будут использоваться для отображения — положение x, положение y и размер объекта (x, y и s). Поскольку позиция xy представляет собой центр объекта, нам придется сократить его вдвое.

Еще один прием, который мы использовали, — это paddingBottom для определения высоты. Это использует ширину для расчетов в процентах, что позволит объекту быть круглым.

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

математические функции для двигателя

Математические функции — сердце любого игрового движка. Как и в большинстве игр, в которых используются круглые объекты, решающее значение имеют расстояние и азимут между объектами. Это все фундаментальные геометрические расчеты, которые относительно легко реализовать с помощью некоторых базовых математических знаний средней школы (dist() и bearing()).

function dist(a, b) {
    return Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2);
}
function bearing(a, b) {
    // NOTE: returns angle of b from a
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    const angle = Math.atan(Math.abs(dy/dx));
    if (dy<0) {
        return (dx>0) ? angle : Math.PI-angle;
    } else {
        return (dx<0) ? Math.PI+angle : (2*Math.PI)-angle;
    }
}

Функции более высокого порядка, такие как расширение точки и обнаружение столкновений, также реализованы в игровом движке. extend() будет использоваться для перемещения объектов в нужное место (удар по мячу и т. д.).

function extend(pt, dist, angle) {
    // NOTE: given old pt, return new pt
    const dx = Math.abs(Math.cos(angle)*dist);
    const dy = Math.abs(Math.sin(angle)*dist);
    if (angle<Math.PI/2) { return { x: pt.x+dx, y: pt.y-dy }; }
    if (angle<Math.PI) { return { x: pt.x-dx, y: pt.y-dy }; }
    if (angle<Math.PI*3/2) { return { x: pt.x-dx, y: pt.y+dy }; }
    return { x: pt.x+dx, y: pt.y+dy };
}
function isCollide(a, b) {
    // NOTE: assumes a, b are points with {x, y}
    return dist(a, b) < (a.s + b.s)/2;
}

Самые сообразительные из вас поняли бы, что здесь я принял сознательное дизайнерское решение. Все координаты xy и размер указаны в процентах. Нигде нет фиксированного замера экрана, почему так? Это должно облегчить искусственное обучение позже, когда нам придется уменьшить масштаб игры, чтобы отобразить их все сразу. Используя все в процентной форме, мы можем масштабировать сцену, никак не влияя на игровой процесс.

игровая механика (отскок мяча от игрока)

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

function update() {
    // NOTE: kick ball away from player
    if (isCollide(game.ball, game.play)) {
        const angle = bearing(game.play, game.ball);
        const dist = (game.play.s + game.ball.s)/2;
        const pt = extend(game.play, dist*global.stage.buffer, angle);
        game.ball.x = pt.x;
        game.ball.y = pt.y;
    }
    // ...
}

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

обработка событий мыши и управление игроком

Функция цикла обновления должна быть следующей:

  1. переместить игрока
  2. выполнять все автоматические обновления (удары по мячу, проверка границ и т. д.)
  3. отображать обновленные позиции объекта

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

const global = {
    mouse: { x: 0, y: 0 },
    ...
};
...
function onMouseMove(evt) {
    global.mouse.x = evt.layerX / global.stage.w;
    global.mouse.y = evt.layerY / global.stage.h;
}
window.onload = () => {
    $('.game')[0].addEventListener('mousemove', onMouseMove);
};

Функция onMouseMove в основном определяет относительное положение курсора относительно сцены и записывает это положение в global.mouse. Затем это будет использоваться для перемещения игрока в цикле обновления.

function update() {
    // NOTE: move player towards cursor
    const pt = extend(game.play, global.stage.speed,
        bearing(game.play, global.mouse));
    game.play.x = pt.x; game.play.y = pt.y;
    ...
}

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

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

окончание игры после касания ворот

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

function update() {
    if (game.state.done) { 
        return game.state.time; 
    } else {
        game.state.time += 1;
    }
    ...
}

В самом начале объяснения, когда мы создавали игровой объект, мы сказали, что игровой объект должен хранить все состояние игры. В этом случае это будет логическое значение (game.state.done), определяющее, окончена ли игра. Мы также добавили рядом game.state.time для хранения количества кадров, прошедших для игры. Это будет полезно, когда дело доходит до подсчета очков. Если игроку требуется больше времени, чтобы доставить мяч к воротам, он, как правило, не так хорош.

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

Итак, вот некоторые другие функции, которые мы не вдавались в подробности в этой статье.

extras — проверка границ

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

дополнительно — скользящий шар

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

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

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

Все это доступно на gitlab. И игра доступна для игры в моем блоге