JavaScript

Перемещение персонажа на спрайтах с помощью JavaScript часть 2

Spread the love

Перевод: Martin HimmelMoving a Sprite Sheet Character with JavaScript

Продолжаем анимировать движения персонажа! 😅

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

Начало

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

let img = new Image();
img.src = 'https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png';
img.onload = function() {
  window.requestAnimationFrame(gameLoop);
};

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');

const SCALE = 2;
const WIDTH = 16;
const HEIGHT = 18;
const SCALED_WIDTH = SCALE * WIDTH;
const SCALED_HEIGHT = SCALE * HEIGHT;

function drawFrame(frameX, frameY, canvasX, canvasY) {
  ctx.drawImage(img,
                frameX * WIDTH, frameY * HEIGHT, WIDTH, HEIGHT,
                canvasX, canvasY, SCALED_WIDTH, SCALED_HEIGHT);
}

const CYCLE_LOOP = [0, 1, 0, 2];
let currentLoopIndex = 0;
let frameCount = 0;
let currentDirection = 0;

function gameLoop() {

  window.requestAnimationFrame(gameLoop);
}
  1. Функция init была переименована в gameLoop.
  2. Функция step была удалена.
  3. Чтобы цикл продолжался, window.requestAnimationFrame (gameLoop); вызывается в конце gameLoop.
  4. В соответствии с соглашениями о константах, все константы полностью написаны в верхнем регистре.

Получение пользовательского ввода

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

let keyPresses = {};

window.addEventListener('keydown', keyDownListener, false);
function keyDownListener(event) {
  keyPresses[event.key] = true;
}

window.addEventListener('keyup', keyUpListener, false);
function keyUpListener(event) {
  keyPresses[event.key] = false;
}

function gameLoop() {
  // ...
}

Перемещение персонажа

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

Для начала мы будем использовать только первый кадр персонажа, обращенного вниз. Нам также нужно отслеживать позиции x и y персонажа. Мы также должны добавить константу MOVEMENT_SPEED, чтобы мы могли легко изменить ее позже. Она будет означать количество пикселей, перемещаемых за кадр анимации.

const MOVEMENT_SPEED = 1;
let positionX = 0;
let positionY = 0;

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (keyPresses.w) {
    positionY -= MOVEMENT_SPEED;
  } else if (keyPresses.s) {
    positionY += MOVEMENT_SPEED;
  }
  if (keyPresses.a) {
    positionX -= MOVEMENT_SPEED;
  } else if (keyPresses.d) {
    positionX += MOVEMENT_SPEED;
  }

  drawFrame(0, 0, positionX, positionY);
  window.requestAnimationFrame(gameLoop);
}

Теперь у нас есть подвижный персонаж!

Примечание. Изначально использовались клавиши со стрелками, но из-за прокрутки страницы при нажатии вверх и вниз вместо них использовались клавиши WASD. Однако вы можете использовать любую комбинацию клавиш.

Изменение направления

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

const FACING_DOWN = 0;
const FACING_UP = 1;
const FACING_LEFT = 2;
const FACING_RIGHT = 3;
let currentDirection = FACING_DOWN;

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

// Inside gameLoop
if (keyPresses.w) {
  positionY -= MOVEMENT_SPEED;
  currentDirection = FACING_UP;
} else if (keyPresses.s) {
  positionY += MOVEMENT_SPEED;
  currentDirection = FACING_DOWN;
}

if (keyPresses.a) {
  positionX -= MOVEMENT_SPEED;
  currentDirection = FACING_LEFT;
} else if (keyPresses.d) {
  positionX += MOVEMENT_SPEED;
  currentDirection = FACING_RIGHT;
}

drawFrame(0, currentDirection, positionX, positionY);

Теперь у нас персонаж меняет направления. Давайте добавим разные кадры для этих направлений. Мы по-прежнему будем придерживаться шаблона 0, 1, 0, 2 кадра для нашей анимации ходьбы. Для этого мы можем вернуть ссылку на CYCLE_LOOP [currentLoopIndex] в нашем вызове drawFrame.

drawFrame(CYCLE_LOOP[currentLoopIndex], currentDirection, positionX, positionY);

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

const FRAME_LIMIT = 12;

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let hasMoved = false;

  if (keyPresses.w) {
    positionY -= MOVEMENT_SPEED;
    currentDirection = FACING_UP;
    hasMoved = true;
  } else if (keyPresses.s) {
    positionY += MOVEMENT_SPEED;
    currentDirection = FACING_DOWN;
    hasMoved = true;
  }

  if (keyPresses.a) {
    positionX -= MOVEMENT_SPEED;
    currentDirection = FACING_LEFT;
    hasMoved = true;
  } else if (keyPresses.d) {
    positionX += MOVEMENT_SPEED;
    currentDirection = FACING_RIGHT;
    hasMoved = true;
  }

  if (hasMoved) {
    frameCount++;
    if (frameCount >= FRAME_LIMIT) {
      frameCount = 0;
      currentLoopIndex++;
      if (currentLoopIndex >= CYCLE_LOOP.length) {
        currentLoopIndex = 0;
      }
    }
  }

  drawFrame(CYCLE_LOOP[currentLoopIndex], currentDirection, positionX, positionY);
  window.requestAnimationFrame(gameLoop);
}

Вот оно! Персонаж перемещается по canvas, меняет направление и циклически проходит все кадры анимации.

Небольшая уборка

Прежде чем мы продолжим, давайте сделаем небольшой рефакторинг:

const SCALE = 2;
const WIDTH = 16;
const HEIGHT = 18;
const SCALED_WIDTH = SCALE * WIDTH;
const SCALED_HEIGHT = SCALE * HEIGHT;
const CYCLE_LOOP = [0, 1, 0, 2];
const FACING_DOWN = 0;
const FACING_UP = 1;
const FACING_LEFT = 2;
const FACING_RIGHT = 3;
const FRAME_LIMIT = 12;
const MOVEMENT_SPEED = 1;

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let keyPresses = {};
let currentDirection = FACING_DOWN;
let currentLoopIndex = 0;
let frameCount = 0;
let positionX = 0;
let positionY = 0;
let img = new Image();

window.addEventListener('keydown', keyDownListener);
function keyDownListener(event) {
    keyPresses[event.key] = true;
}

window.addEventListener('keyup', keyUpListener);
function keyUpListener(event) {
    keyPresses[event.key] = false;
}

function loadImage() {
  img.src = 'https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png';
  img.onload = function() {
    window.requestAnimationFrame(gameLoop);
  };
}

function drawFrame(frameX, frameY, canvasX, canvasY) {
  ctx.drawImage(img,
                frameX * WIDTH, frameY * HEIGHT, WIDTH, HEIGHT,
                canvasX, canvasY, SCALED_WIDTH, SCALED_HEIGHT);
}

loadImage();

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let hasMoved = false;

  if (keyPresses.w) {
    moveCharacter(0, -MOVEMENT_SPEED, FACING_UP);
    hasMoved = true;
  } else if (keyPresses.s) {
    moveCharacter(0, MOVEMENT_SPEED, FACING_DOWN);
    hasMoved = true;
  }

  if (keyPresses.a) {
    moveCharacter(-MOVEMENT_SPEED, 0, FACING_LEFT);
    hasMoved = true;
  } else if (keyPresses.d) {
    moveCharacter(MOVEMENT_SPEED, 0, FACING_RIGHT);
    hasMoved = true;
  }

  if (hasMoved) {
    frameCount++;
    if (frameCount >= FRAME_LIMIT) {
      frameCount = 0;
      currentLoopIndex++;
      if (currentLoopIndex >= CYCLE_LOOP.length) {
        currentLoopIndex = 0;
      }
    }
  }

  drawFrame(CYCLE_LOOP[currentLoopIndex], currentDirection, positionX, positionY);
  window.requestAnimationFrame(gameLoop);
}

function moveCharacter(deltaX, deltaY, direction) {
  positionX += deltaX;
  positionY += deltaY;
  currentDirection = direction;
}

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

Держаться в границах

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

Наше обнаружение столкновений выглядит примерно так:

  1. Левый край персонажа касается или проходит за левый край canvas?
  2. Правый край персонажа касается или проходит за правый край canvas?
  3. Верхний край персонажа касается или проходит за верхний край canvas?
  4. Нижний край персонажа касается или проходит за нижний край canvas?

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

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

function moveCharacter(deltaX, deltaY, direction) {
  if (positionX + deltaX > 0 && positionX + SCALED_WIDTH + deltaX < canvas.width) {
    positionX += deltaX;
  }
  if (positionY + deltaY > 0 && positionY + SCALED_HEIGHT + deltaY < canvas.height) {
    positionY += deltaY;
  }
  currentDirection = direction;
}

Важно помнить, что positionX и positionY относятся к левому верхнему углу персонажа. Из-за этого positionX + SCALED_WIDTH дает нам правый край персонажа, а positionX + SCALED_HEIGHT дает нам нижний край персонажа.

Имея это в виду, вот как проверки переводятся в соответствие с вопросами выше:

  1. positionX + deltaX > 0 проверяет столкновение с левым краем.
  2. positionX + SCALED_WIDTH + deltaX < canvas.width проверяет столкновение с правым краем.
  3. positionY + deltaY > 0 проверяет столкновение с верхним краем.
  4. positionY + SCALED_HEIGHT + deltaY < canvas.height проверяет столкновение нижнего края.

Последние дополнение

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

В функции gameLoop прямо перед вызовом drawFrame добавим проверку:

if (!hasMoved) {
    currentLoopIndex = 0;
}

Супер! Теперь персонаж всегда будет в естественном положении стоя, когда не движется.

Конечный результат

Итоговый фрагмент кода:

const SCALE = 2;
const WIDTH = 16;
const HEIGHT = 18;
const SCALED_WIDTH = SCALE * WIDTH;
const SCALED_HEIGHT = SCALE * HEIGHT;
const CYCLE_LOOP = [0, 1, 0, 2];
const FACING_DOWN = 0;
const FACING_UP = 1;
const FACING_LEFT = 2;
const FACING_RIGHT = 3;
const FRAME_LIMIT = 12;
const MOVEMENT_SPEED = 1;

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let keyPresses = {};
let currentDirection = FACING_DOWN;
let currentLoopIndex = 0;
let frameCount = 0;
let positionX = 0;
let positionY = 0;
let img = new Image();

window.addEventListener('keydown', keyDownListener);
function keyDownListener(event) {
    keyPresses[event.key] = true;
}

window.addEventListener('keyup', keyUpListener);
function keyUpListener(event) {
    keyPresses[event.key] = false;
}

function loadImage() {
  img.src = 'https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png';
  img.onload = function() {
    window.requestAnimationFrame(gameLoop);
  };
}

function drawFrame(frameX, frameY, canvasX, canvasY) {
  ctx.drawImage(img,
                frameX * WIDTH, frameY * HEIGHT, WIDTH, HEIGHT,
                canvasX, canvasY, SCALED_WIDTH, SCALED_HEIGHT);
}

loadImage();

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let hasMoved = false;

  if (keyPresses.w) {
    moveCharacter(0, -MOVEMENT_SPEED, FACING_UP);
    hasMoved = true;
  } else if (keyPresses.s) {
    moveCharacter(0, MOVEMENT_SPEED, FACING_DOWN);
    hasMoved = true;
  }

  if (keyPresses.a) {
    moveCharacter(-MOVEMENT_SPEED, 0, FACING_LEFT);
    hasMoved = true;
  } else if (keyPresses.d) {
    moveCharacter(MOVEMENT_SPEED, 0, FACING_RIGHT);
    hasMoved = true;
  }

  if (hasMoved) {
    frameCount++;
    if (frameCount >= FRAME_LIMIT) {
      frameCount = 0;
      currentLoopIndex++;
      if (currentLoopIndex >= CYCLE_LOOP.length) {
        currentLoopIndex = 0;
      }
    }
  }

  if (!hasMoved) {
    currentLoopIndex = 0;
  }

  drawFrame(CYCLE_LOOP[currentLoopIndex], currentDirection, positionX, positionY);
  window.requestAnimationFrame(gameLoop);
}

function moveCharacter(deltaX, deltaY, direction) {
  if (positionX + deltaX > 0 && positionX + SCALED_WIDTH + deltaX < canvas.width) {
    positionX += deltaX;
  }
  if (positionY + deltaY > 0 && positionY + SCALED_HEIGHT + deltaY < canvas.height) {
    positionY += deltaY;
  }
  currentDirection = direction;
}

Была ли вам полезна эта статья?
[10 / 4.2]

Spread the love
Editorial Team

View Comments

  • Спасибо за интересную и информативную статью. Я попытался использовать нажатие на button кнопки по onclick для передвижения но у меня не получилось, надеюськак то это победить. также интересна возможность перемешения персонажа на фиксированное количество пикселей.

    вот так это конечно работает но это же не хорошо)

    function KeySend(dir) {
      var ke = new KeyboardEvent("keydown", {key:dir, bubbles: true});
      document.dispatchEvent(ke);
    }
    function KeySende(dir) {
      var ke = new KeyboardEvent("keyup", {key:dir, bubbles: true});
      document.dispatchEvent(ke);
    }
    
    <p id="Movelink">
    <button class="MoveBtn" onmousedown="KeySend('a')" onmouseup="KeySende('a')">Влево</button> 
    <button class="MoveBtn" onmousedown="KeySend('w')" onmouseup="KeySende('w')">Вверх</button> 
    <button class="MoveBtn" onmousedown="KeySend('s')" onmouseup="KeySende('s')">Вниз</button> 
    <button class="MoveBtn" onmousedown="KeySend('d')" onmouseup="KeySende('d')">Вправо</button></p>
    
    • Что вам мешает сделать так же как описание в статье, через addEventListener?

  • Как добавить проверку столкновения с другими объектами?

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago