На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Следуя инструкциям этого руководства, вы создадите традиционный «рогалик», используя игровой движок Phaser на JS+HTML5. Кстати, недавно мы публиковали обзор таких движков. В результате вы получите полнофункциональную игру в жанре «roguelike», запускаемую в браузере. (Под рогаликом мы подразумеваем одиночный рандомизированный пошаговый dungeon-crawler с одной жизнью.)
На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Примечание: хотя в этом руководстве и используются JavaScript, HTML и Phaser, вы можете использовать эти принципы для реализации на любом другом языке и движке.
Подготовка
Вам понадобятся текстовый редактор и браузер. Я использую Notepad++ и Google Chrome, но это не принципиально.
Затем вы должны скачать исходники и начать с папки init: она содержит файлы Phaser, HTML и JS, необходимые для нашей игры. Наш код мы будем писать в пустом файле rl.js.
Файл index.html file просто загружает Phaser и вышеупомянутый файл с кодом игры:
Сейчас для нашей игры мы будем использовать символы ASCII — впоследствии их можно заменить bitmap-графикой, но сейчас проще взять ASCII.
Давайте зададим несколько констант для размера шрифта, размера карты и количества персонажей:
// font size
var FONT = 32;
// map dimensions
var ROWS = 10;
var COLS = 15;
// number of actors per level, including player
var ACTORS = 10;
Также инициализируем Phaser и слушатели сигналов с клавиатуры, так как мы создаём пошаговую игру и хотим создавать действие после каждого нажатия клавиши:
// initialize phaser, call create() once done
var game = new Phaser.Game(COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, {
create: create
});
function create() {
// init keyboard commands
game.input.keyboard.addCallbacks(null, null, onKeyUp);
}
function onKeyUp(event) {
switch (event.keyCode) {
case Keyboard.LEFT:
case Keyboard.RIGHT:
case Keyboard.UP:
case Keyboard.DOWN:
}
}
Так как ширина стандартных моноширинных шрифтов равна 60% от высоты, мы зададим размер поля как 0.6 * размер шрифта * количество столбцов. Мы также говорим Phaser, что он должен вызвать нашу функцию create() сразу после завершения инициализации, когда инициализируется и управление с клавиатуры.
Можете взглянуть на нашу игру — правда, там пока и смотреть не на что:)
Карта
Клеточная карта отражает нашу игровую зону: дискретный двумерный массив клеток, представленных символами ASCII, которые могут изображать либо стену (#: блокирует перемещение), либо пол (.: не блокирует перемещение):
// the structure of the map
var map;
Давайте будем использовать простейшую форму процедурной генерации карт: каждая клетка принимает одно из двух значений случайным образом:
function initMap() {
// create a new random map
map = [];
for (var y = 0; y < ROWS; y++) {
var newRow = [];
for (var x = 0; x < COLS; x++) {
if (Math.random() > 0.8)
newRow.push('#');
else
newRow.push('.');
}
map.push(newRow);
}
}
Таким образом мы получим карту, примерно на 20% занятую стенами.
Мы инициализируем новую карту в функции create() сразу после запуска слушателей клавиатуры:
Можете посмотреть, что получилось — пока что мы всё равно не отрисовали наши карту.
Экран
Настало время вывести нашу карту на созданный экран:
// the ascii display, as a 2d array of characters
var asciidisplay;
function drawMap() {
for (var y = 0; y < ROWS; y++)
for (var x = 0; x < COLS; x++)
asciidisplay[y][x].content = map[y][x];
}
Тем не менее, перед отрисовкой карты экран нужно инициализировать. Вернёмся к функции create():
function create() {
// init keyboard commands
game.input.keyboard.addCallbacks(null, null, onKeyUp);
// initialize map
initMap();
// initialize screen
asciidisplay = [];
for (var y = 0; y < ROWS; y++) {
var newRow = [];
asciidisplay.push(newRow);
for (var x = 0; x < COLS; x++)
newRow.push( initCell('', x, y) );
}
drawMap();
}
function initCell(chr, x, y) {
// add a single cell in a given position to the ascii display
var style = { font: FONT + "px monospace", fill:"#fff"};
return game.add.text(FONT*0.6*x, FONT*y, chr, style);
}
Теперь при запуске проекта вы должны видеть случайную карту.
На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Персонажи
Теперь займёмся персонажами: нашим игроком и его врагами. Каждый персонаж будет объектом с тремя полями: координаты x и y и хитпоинты hp.
Мы будем хранить всех персонажей в массиве actorList (его первый элемент — игрок). Мы также будем хранить ассоциативный массив с позициями персонажей в качестве ключей для быстрого поиска; это поможет нам, когда мы займёмся перемещением и боёвкой.
// a list of all actors; 0 is the player
var player;
var actorList;
var livingEnemies;
// points to each actor in its position, for quick searching
var actorMap;
Мы создаём всех персонажей и рандомно размещаем их на свободных ячейках карты:
function randomInt(max) {
return Math.floor(Math.random() * max);
}
function initActors() {
// create actors at random locations
actorList = [];
actorMap = {};
for (var e=0; e<ACTORS; e++) {
// create new actor
var actor = { x:0, y:0, hp:e == 0?3:1 };
do {
// pick a random position that is both a floor and not occupied
actor.y=randomInt(ROWS);
actor.x=randomInt(COLS);
} while ( map[actor.y][actor.x] == '#' || actorMap[actor.y + "_" + actor.x] != null );
// add references to the actor to the actors list & map
actorMap[actor.y + "_" + actor.x]= actor;
actorList.push(actor);
}
// the player is the first actor in the list
player = actorList[0];
livingEnemies = ACTORS-1;
}
Настало время показать персонажей! Мы изобразим всех врагов буквой e, а игрока — количеством его хитпоинтов:
function drawActors() {
for (var a in actorList) {
if (actorList[a].hp > 0)
asciidisplay[actorList[a].y][actorList[a].x].content = a == 0?''+player.hp:'e';
}
}
Возьмём только что написанные функции и передадим их в create():
Наконец-то мы дошли до движухи! Так как в классических рогаликах персонажи атакуют друг друга при столкновении, мы обработаем это в функции moveTo(), которая принимает персонажа и направление (направление задаётся разностью координат x и y текущей и желаемой клеток):
function moveTo(actor, dir) {
// check if actor can move in the given direction
if (!canGo(actor,dir))
return false;
// moves actor to the new location
var newKey = (actor.y + dir.y) +'_' + (actor.x + dir.x);
// if the destination tile has an actor in it
if (actorMap[newKey] != null) {
//decrement hitpoints of the actor at the destination tile
var victim = actorMap[newKey];
victim.hp--;
// if it's dead remove its reference
if (victim.hp == 0) {
actorMap[newKey]= null;
actorList[actorList.indexOf(victim)]=null;
if(victim!=player) {
livingEnemies--;
if (livingEnemies == 0) {
// victory message
var victory = game.add.text(game.world.centerX, game.world.centerY, 'Victory!\nCtrl+r to restart', { fill : '#2e2', align: "center" } );
victory.anchor.setTo(0.5,0.5);
}
}
}
} else {
// remove reference to the actor's old position
actorMap[actor.y + '_' + actor.x]= null;
// update position
actor.y+=dir.y;
actor.x+=dir.x;
// add reference to the actor's new position
actorMap[actor.y + '_' + actor.x]=actor;
}
return true;
}
Вкратце:
Мы убеждаемся, что персонаж может переместиться в эту клетку.
Если в ней есть другой персонаж, мы атакуем его (и убиваем, если счётчик его хитпоинтов достигает нуля).
Если клетка пуста, мы перемещаемся в неё.
Заметим также, что мы выводим простое сообщение о победе после смерти последнего врага и возвращаем false или true в зависимости от того, валидно ли желаемое перемещение.
Теперь вернемся к функции onKeyUp() и изменим её так, чтобы при каждом нажатии клавиши мы стирали предыдущие положения персонажей (отрисовывая поверх них карту), перемещали игрока и снова отрисовывали персонажей:
function onKeyUp(event) {
// draw map to overwrite previous actors positions
drawMap();
// act on player input
var acted = false;
switch (event.keyCode) {
case Phaser.Keyboard.LEFT:
acted = moveTo(player, {x:-1, y:0});
break;
case Phaser.Keyboard.RIGHT:
acted = moveTo(player,{x:1, y:0});
break;
case Phaser.Keyboard.UP:
acted = moveTo(player, {x:0, y:-1});
break;
case Phaser.Keyboard.DOWN:
acted = moveTo(player, {x:0, y:1});
break;
}
// draw actors in new positions
drawActors();
}
Скоро мы введем переменную acted, чтобы узнать, должны ли после перемещения игрока действовать враги.
На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Простой ИИ
После того, как мы закончили с реализацией игрока, займёмся врагами. Напишем простой алгоритм поиска пути, по которому враг будут двигаться к игроку, если расстояние между ними не превышает шести шагов, а в противном случае будет перемещаться случайно. О различных алгоритмах поиска мы уже коротко рассказывали в нашей недавней статье.
Заметим, что противнику неважно, кого атаковать: таким образом, при правильном размещении противники будут уничтожать друг друга, пытаясь догнать игрока. Прям как в классическом Doom!
function aiAct(actor) {
var directions = [ { x: -1, y:0 }, { x:1, y:0 }, { x:0, y: -1 }, { x:0, y:1 } ];
var dx = player.x - actor.x;
var dy = player.y - actor.y;
// if player is far away, walk randomly
if (Math.abs(dx) + Math.abs(dy) > 6)
// try to walk in random directions until you succeed once
while (!moveTo(actor, directions[randomInt(directions.length)])) { };
// otherwise walk towards player
if (Math.abs(dx) > Math.abs(dy)) {
if (dx < 0) {
// left
moveTo(actor, directions[0]);
} else {
// right
moveTo(actor, directions[1]);
}
} else {
if (dy < 0) {
// up
moveTo(actor, directions[2]);
} else {
// down
moveTo(actor, directions[3]);
}
}
if (player.hp < 1) {
// game over message
var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart', { fill : '#e22', align: "center" } );
gameOver.anchor.setTo(0.5,0.5);
}
}
Также мы добавили сообщение, которое выводится на экран при смерти игрока.
Теперь нам осталось сделать так, чтобы враги перемещались с каждым ходом игрока. Дополним функцию onKeyUp():
function onKeyUp(event) {
...
// enemies act every time the player does
if (acted)
for (var enemy in actorList) {
// skip the player
if(enemy==0)
continue;
var e = actorList[enemy];
if (e != null)
aiAct(e);
}
// draw actors in new positions
drawActors();
}
На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Бонус: версия на Haxe
Изначально я писал это руководство на Haxe, кроссплатформенном языке, компилирующемся в JavaScript (и не только). Эту версию мы можете найти в папке haxe в исходниках.
Сперва вам потребуется установить компилятор haxe, после чего скомпилировать написанный в любом текстовом редакторе код, вызвав haxe build.hxml и дважды кликнув по файлу build.hxml. Я также добавил проект FlashDevelop, если вы предпочитаете пользоваться удобной IDE: просто откройте rl.hxproj и нажмите F5 для запуска.
Заключение
Вот и всё! Мы закончили создание простой roguelike-игры со случайной генерацией карты, движением, боёвкой, ИИ и условиями победы/поражения.