Как-то для своих некоторых планов мне потребовалось сделать небольшую песочницу в 2D пространстве с базовыми возможностями:

1. Передвижение по игровому миру
2. Физика при движении, столкновения
3. Создание блоков
4. Удаление блоков

Графическое исполнение меня не беспокоило, поэтому я решил оформить все в серых тонах, выглядит это так:

image

Подготовка


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

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

Первое, что требуется, инициализировать движок и вынести в переменные для быстрого доступа необходимые методы:

var pjs = new PointJS(640, 480, { // ширина, высота
	backgroundColor : '#4b4843' // цвет фона
});

pjs.system.initFullPage(); // полнокранный режим (тут ширина и высота изменилась под экран)

var game   = pjs.game;           // Менеджер игрового мира
var point  = pjs.vector.point;   // Конструктор точки/вектора
var camera = pjs.camera;         // Управление камерой
var brush  = pjs.brush;          // Методыпростого рисования
var OOP    = pjs.OOP;            // Общие ООП методы
var math   = pjs.math;           // Дополнительная математика

var key   = pjs.keyControl.initKeyControl(); // включим клавиатуру
var mouse = pjs.mouseControl.initMouseControl(); // включим мышь

var width  = game.getWH().w; // возьмем ширину 
var height = game.getWH().h; // и высоту экрана в переменные

var BW = 40, BH = 40; // размеры блоков, которые мы сможем создавать

pjs.system.initFPSCheck(); // включить счетчик fps

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

game.newLoopFromConstructor('myGame', function () {
 // game loop
});

Внутри этого конструктора нам надо определить обязательный метод update:

game.newLoopFromConstructor('myGame', function () {

  // init

  this.update = function () {
    // game loop
  }

});

Там, где указано // init, нам потребуется во внутренние переменные поместить игровой мир:

  var pPos; // переменная с первоначальной позицией нашего игрока

  var world = []; // массив с блоками, пока пуст
  
  // функцией ниже мы, на основе двумерного массива
  // восоздадим блоки в нужных позициях, опираться будем
  // на символы 0 и P, где первый - блок, второй - позиция игрока
  // сама же функция принимает ширину создаваемых объектов,
  // высоту, исходный массив, и функцию обработчик, которая срабатывает
  // на каждом символе.
  
  pjs.levels.forStringArray({w : BW, h : BH, source : [
    '0000000000000000000000000000',
    '0000000000000000000000000000',
    '0000000000000000000000000000',
    '0000000000000000000000000000',
    '000000        00000000000000',
    '000           00000000000000',
    '000  P    000000000000000000',
    '000       000000000000000000',
    '0000000000000000000000000000',
    '0000000000000000000000000000'
  ]}, function (S, X, Y, W, H) {
    if (S === '0') {                  // если текущий символ равен нулю
      world.push(game.newRectObject({ // то мы создадим блок
        x : X, y : Y,                 // позицию укажем из параметров
        w : W, h : H,                 // ширину и высоту - тоже
        fillColor : '#bcbcbc'         // цвет фона
      }));
    } else if (S === 'P') {           // если же символ равен английской букве P
      pPos = point(X, Y);             // то просто зафиксируем текущую позицию в переменную
    }
    
  });
  
  // а теперь создадим самого персонажа, тоже обычный блок
  var pl = game.newRectObject({
    x : pPos.x, y : pPos.y, // позицию берем из нашей переменной
    w : 30, h : 50,         // ширина и высота
    fillColor : 'white'     // цвет
  });

  var speed = point();      // переменная со скоростями по осям Х и Y

И самое главное — механика игрового цикла, и всё то, что заставит игровой мир «ожить»:

this.update = function () {

    // управление с клавиатуры, с кнопками, думаю, понятно
    if (key.isDown('A')) speed.x = -2;
    else if (key.isDown('D')) speed.x = 2;
    else speed.x = 0; // если никакая клавиша не нажата, то обнуляем скорость

    // скорость движения по оси Y, если нажати W, то ставим -7 (движение вверх)
    if (speed.x < 5) speed.y += 0.5; // постоянно меняем скорость движения, имитируя гравитацию
    if (key.isPress('W')) speed.y = -7;

    // рисуем нашего персонажа
    pl.draw();

    // распределим необъодимые массивы
    var collisionBlocks = []; // массив столкновений
    var drawBlocks = []; // массив для отрисовки
    var selBlocks = []; // массив выледенныех блоков

    // возьмем в переменные некоторые события
    var R = mouse.isPress('RIGHT'); // клик ЛКМ
    var L = mouse.isPress('LEFT');  // клик ПКМ
    var MP = mouse.getPosition();   // позиция мыши

    // позиция создаваемого блока
    // привяжем позицию мыши к сетке размером в один блок
    var createPos = point(BW * Math.floor(MP.x / BW), BH * Math.floor(MP.y / BH));

    // теперь идем циклом по всем объектам
    OOP.forArr(world, function (w, idW) { // принимает массив и обработчик
      if (w.isInCameraStatic()) { // если блок в пределах видимости
        drawBlocks.push(w); // добавим его в массив для отрисовки 
        
        // если блок близко к нашему игроку
        if (pl.getDistanceC(w.getPositionC()) < 80) {
          
          // то проверим, находится ли на нем мышь          
          if (mouse.isInStatic(w.getStaticBox())) {
            selBlocks.push(w); // если да, то добавим его в массив выбранных блоков
            if (L) { // если левый клик случился, то
              world.splice(idW, 1); // удалим блок из массива
            }
          }
        }
        
      }
    });
    
    // теперь пройдемся по тем блокам, что видно (которые попали в камерц)
    OOP.forArr(drawBlocks, function (d) {

      // изменяем прозрачность блока в зависимости от расстояния до игрока      
      d.setAlpha(1 - pl.getDistanceC(d.getPositionC()) / 250);
      // получается, что, чем дальше блок - тем более он невидим

      // рисуем то, что получилось
      d.draw();
      
      // если отрисованный блок близко к игроку, то добавим его к кандидатам
      // на столкновения
      if (pl.getDistanceC(d.getPositionC()) < 100) {
        collisionBlocks.push(d);
      }
      
    });
    
    // теперь пробежимся по выделенным блокам
    OOP.forArr(selBlocks, function (s) {
      brush.drawRect({ // и просто отрисуем вокруг них
        x : s.x, y : s.y,
        w : s.w, h : s.h,
        strokeColor : '#ac5a5a', // красную рамку
        strokeWidth : 2 // шириной 2 пикселя
      });
    });
    
    // отключим временно возможность создавать блоки
    var canCreate = false;
    var dist = pl.getDistanceC(MP); // замерим дистанцию курсора до игрока
    if (!selBlocks.length && dist > 50 && dist < 100) { // если нет выбранных блоков
      canCreate = true; // вернем возможность создавать блоки
      brush.drawRect({  // и отрисуем прямоугольник так, где потенциальный
        x : createPos.x, y : createPos.y, // блок будет создан
        w : BW, h : BH,
        strokeColor : '#69ac5a', // зеленым
        strokeWidth : 2
      });
    }
    
    // при левом клике мыши и возможности создавать блоки
    if (L && canCreate) {
      world.push(game.newRectObject({ // создадим новый объект
        x : createPos.x, y : createPos.y,
        w : BW, h : BH,
        fillColor : '#e2e2e2'
      }));
    }

    // теперь физика столкновений, используя эту функцию, мы сможем
    // двигать наш объект pl (игрок) с скоростью speed, а в качестве
    // препятствий будет массив collisionBlocks
    pjs.vector.moveCollision(pl, collisionBlocks, speed);
    
    // камерой будем следить за нашим игроков
    camera.follow(pl, 10);
    
    // и отрисуем fps
    brush.drawTextS({
      text : pjs.system.getFPS(),
      color : 'white',
      size : 50
    });
};

Это весь требуемый для игры код.

Результат: В браузере
Исходник: На GitHub

Для тех, кому удобнее и нагляднее смотреть видео-уроки, есть видео вариант:

ВидеоУрок 2D песочница на JavaScript

Комментарии (9)


  1. Vlad_IT
    29.10.2017 15:24
    +1

    Если сделать впереди себя вот такую дырку

    image


    1. Skaner Автор
      29.10.2017 17:06

      Ахах, и правда) Надо откалибровать будет, спасибо за замечание!)


    1. cool20141
      30.10.2017 13:03

      Еще если очень высоко забраться и спрыгнуть вниз, то ты просто пролетишь через все текстурки


      1. Skaner Автор
        30.10.2017 13:04

        надо ограничить вертикальную скорость в этом случае, так как камера не поспевает за персонажем видимо)


        1. Nefrace
          31.10.2017 19:30

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


          1. Skaner Автор
            31.10.2017 19:31

            Рейкаст можно поставить, по идее должно вылавливаться нормально)


  1. gera1080
    29.10.2017 17:06
    +1

    А меня одного удивляет упоминание в заголовке «… с нуля» и использование в проекте фреймворков и библиотек?


  1. xi-tauw
    30.10.2017 10:57

    Было интересно посмотреть как на js делают подобные вещи и наткнулся на проект: https://github.com/dissimulate/Clarity. Небольшой объем исходников и их простота позволяет за пару часов понять 99% кода.


  1. lekzd
    30.10.2017 22:06

    однобуквенные переменные это особый стиль или намек на далёкие времена Basic?
    if (L) { // если левый клик случился
    тут меня совсем порвало)

    и выучите про let и const, те, кому надо будет кроссбраузерность и так разберутся что делать