Как-то для своих некоторых планов мне потребовалось сделать небольшую песочницу в 2D пространстве с базовыми возможностями:
1. Передвижение по игровому миру
2. Физика при движении, столкновения
3. Создание блоков
4. Удаление блоков
Графическое исполнение меня не беспокоило, поэтому я решил оформить все в серых тонах, выглядит это так:
Для работы используется JavaScript, так как проект нужен для демонстрации, и затачивать его под платформы не было желания, да и подразумевался быстрый просмотр результата.
В качестве рендерера я использую PointJS.
Весь код песочницы я уместил в один файл, так как писать много и не подразумевалось.
Для этого я создал файл game.js, все исходные коды, появляющиеся в статье, находятся внутри этого файла и изложены по порядку. Итоговый вариант и живой запуск в конце статьи.
Первое, что требуется, инициализировать движок и вынести в переменные для быстрого доступа необходимые методы:
После инициализации нам потребуется создать единственный игровой цикл, внутри которого будет существовать наш игровой мир.
Внутри этого конструктора нам надо определить обязательный метод update:
Там, где указано // init, нам потребуется во внутренние переменные поместить игровой мир:
И самое главное — механика игрового цикла, и всё то, что заставит игровой мир «ожить»:
Это весь требуемый для игры код.
Результат: В браузере
Исходник: На GitHub
Для тех, кому удобнее и нагляднее смотреть видео-уроки, есть видео вариант:
1. Передвижение по игровому миру
2. Физика при движении, столкновения
3. Создание блоков
4. Удаление блоков
Графическое исполнение меня не беспокоило, поэтому я решил оформить все в серых тонах, выглядит это так:
Подготовка
Для работы используется 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
Vlad_IT
Skaner Автор
Ахах, и правда) Надо откалибровать будет, спасибо за замечание!)
cool20141
Еще если очень высоко забраться и спрыгнуть вниз, то ты просто пролетишь через все текстурки
Skaner Автор
надо ограничить вертикальную скорость в этом случае, так как камера не поспевает за персонажем видимо)
Nefrace
Дело не в камере, а как раз в высокой скорости, из-за которой персонаж пролетает все блоки, «не успев» проверить коллизию с ними. И тут, да, либо ограничивать скорость, либо проверять столкновения иными способами, учитывающими скорость.
Skaner Автор
Рейкаст можно поставить, по идее должно вылавливаться нормально)
gera1080
А меня одного удивляет упоминание в заголовке «… с нуля» и использование в проекте фреймворков и библиотек?
xi-tauw
Было интересно посмотреть как на js делают подобные вещи и наткнулся на проект: https://github.com/dissimulate/Clarity. Небольшой объем исходников и их простота позволяет за пару часов понять 99% кода.
lekzd
однобуквенные переменные это особый стиль или намек на далёкие времена Basic?
if (L) { // если левый клик случился
тут меня совсем порвало)
и выучите про let и const, те, кому надо будет кроссбраузерность и так разберутся что делать