Как создать JRPG: учебник для разработчиков игр
() translation by (you can also view the original English article)
Эта статья представляет собой обзор на сложном уровне создания JRPG (японской ролевой игры), такой как ранние игры серии Final Fantasy. Мы рассмотрим архитектуру и системы, которые составляют скелет JRPG, как управлять игровыми режимами, как использовать tilemaps для отображения мира и как программировать боевую систему RPG.
Примечание: Эта статья написана с использованием языка псевдокодирования, подобного Java, но эти концепции применимы к любой среде разработки игр.
Содержание
- Невероятное место рождения JRPG
- Обсуждение жанра
- Пять причин, по которым вы должны сделать JRPG
- Архитектура
- Управление игровым состоянием
- Карты
- Бой
- Обзор
Невероятное место рождения JRPG

В 1983 году Юджи Хорий, Коичи Накамура и Юкинобу Чида вылетели в Америку и посетили AppleFest'83, который собрал разработчиков, демонстрирующих свои последние творения для Apple II. Они были потрясены последней версией RPG под названием Wizardry.
По возвращении в Японию они решили создать Dragon Warrior, RPG, которая была бы похожа, но приспособлена для NES. Это был хит, определивший жанр JRPG. Воин Дракона не развился также хорошо в Америке, но через несколько лет появилась другая игра.
В 1987 году была выпущена оригинальная Final Fantasy, которая породила одну из самых продаваемых франшиз видеоигр на Земле, которая стала, по крайней мере на Западе, культовым JRPG.
Обсуждение жанра
Игровые жанры никогда не имеют точных определений - это скорее нечеткая коллекция соглашений. У РПГ, как правило, есть система повышения уровней, один или несколько персонажей с навыками и статистикой, оружием и доспехами, боевыми и разведывательными режимами и мощными повествованиями; прогресс игры часто достигается путем продвижения по карте.
Японские РПГ - это РПГ, созданные в форме Воина Дракона; они более линейны, бой часто пошаговый, и обычно там есть два типа карт: карта мира и локальная карта. Архаичные JRPG включают в себя Воина Дракона, Final Fantasy, Wild Arms, Phantasy Star и Chrono Trigger. Тип JRPG, о котором мы поговорим в этой статье, похож на раннюю Final Fantasy.



Пять причин, по которым вы должны сделать JRPG
1. Они выдержали испытание временем
Игры, такие как Final Fantasy VI и Chrono Trigger, по-прежнему очень приятны в игре. Если вы делаете JRPG, вы изучаете игровой формат который вне времени, к которому современные игроки все еще очень восприимчивы. Они создают отличную основу для добавления собственной «песни» и экспериментов - будь то в повествовании, подаче или механике. Это здорово, если вы можете сделать игру, в которую будут играть и наслаждаться десятилетиями после ее первого релиза!
2. Механика игры широко используется
Call of Duty, одна из самых популярных игр FPS в мире, использует элементы RPG; социальный игровой бум, окружающий FarmVille, был клоном RPG Harvest Moon для SNES; и даже гоночные игры, такие как Gran Turismo, имеют подсчёт уровней и баллов опыта.
3. Ограничения для творчества
Как писатель может быть запуган чистым листом бумаги, разработчик игры может оказаться парализованным большим количеством возможных вариантов при разработке новой игры. С JRPG у вас есть много готовых вариантов, поэтому у вас не будет такого ступора при выборе, вы можете следовать рекомендациям для большинства случаев и отклоняться от них в точках, которые вам важны.
4. Это можно сделать как сольный проект
Код для Final Fantasy был почти полностью написан одним программистом Насиром Гебелли, и он сделал это пока был на собрании! Благодаря современным инструментам и языкам гораздо проще создать такой тип игры. Самая большая часть RPG - это не программирование, это контент, но это не обязательно случай вашей игры. Если вы подправите его немного и сфокусируетесь на качестве, а не количестве, тогда JRPG - отличный сольный проект.
Наличие команды может помочь в создании любой игры, и вы, возможно, захотите передать на аутсорсинг создание графики и музыки, или использовать некоторые из отличных творческих ресурсов таких как opengameart.org. (Примечание редактора: Наш родной сайт GraphicRiver также продает спрайты для игр.)
5. Для прибыли!
JRPG обладают большим количеством поклонников, и ряд JRPG (например, представленных на рисунке ниже) хорошо зарекомендовали себя в качестве коммерческого продукта и доступны на таких платформах, как Steam.



Архитектура
JRPG сочетают в себе так много условий и механики, что типичную JRPG можно разбить на несколько систем:

В разработке программного обеспечения один шаблон используется снова и снова: многоуровневость. Это относится к тому, как системы программы относятся друг к другу, причем часто используемые уровни располагаются внизу, а уровни более тесно связанные с непосредственными задачами, располагаются вверху. JRPG не очень отличаются друг от друга и поэтому содержат несколько уровней - более низкие уровни имеют дело с основными графическими функциями, а верхние уровни - с квестами и персонажами.
Как видно из диаграммы выше, существует множество систем, позволяющих создать JRPG, но большинство систем можно сгруппировать в виде отдельных режимов игры. JRPG имеют очень четкие игровые режимы; там есть карта мира, локальная карта, боевой режим и несколько режимов меню. Эти режимы представляют собой почти полностью отдельные, самодостаточные фрагменты кода, что делает их простыми в разработке.
Режимы важны, но они не будут бесполезны без игрового контента. RPG содержит много файлов карт, определения монстров, строк диалога, скриптов для запуска роликов и кода геймплея для управления тем, как игрок продвигается. Рассказывая о том, как построить JRPG, в деталях, мы бы заполнили целую книгу, поэтому мы сосредоточимся на некоторых из наиболее важных частей. Управлять игровыми режимами критически важно для создания JRPG, так что это первая часть, которую мы изучим.
Управление игровым состоянием
На приведенном ниже изображении показан цикл игры, который вызывает функцию обновления для каждого кадра. Это «сердцебиение» игры, и почти все игры построены таким образом.



Вы когда-нибудь начали проект, но останавливались, потому что вам было слишком сложно добавлять новые функции или были сбиты с толку таинственными ошибками? Может быть, вы пытались втиснуть весь свой код в функцию обновления с небольшой структурой и обнаружили, что код стал непредсказуемым. Отличным решением этой проблемы будет разделение кода на разные игровые состояния, дающее более четкое представление о том, что происходит.
Обычный инструмент разработчика игр - это механизм состояний; он используется повсюду, для обработки анимации, меню, игрового потока, AI ... это важный инструмент для нашего набора. Для JRPG мы можем использовать механизм состояний для обработки различных режимов игры. Мы взглянем на обычный механизм состояний, а затем немного изменим его, чтобы сделать более подходящим для JRPG. Но сначала давайте рассмотрим общий игровой процесс, изображенный ниже.



В типичной JRPG вы, вероятно, начнете в режиме карты местности, где сможете свободно путешествовать по городу и взаимодействовать с его жителями. Вы можете уйти из города - здесь вы перейдёте в другой игровой режим и увидите карту мира.
Карта мира очень похожа на локальную карту, но в большем масштабе; вы можете увидеть горы и города, а не деревья и заборы. Если находясь на карте мира, вы вернетесь в город, режим вернется к карте местности.
На карте мира или местности вы можете открыть меню, чтобы проверить своих персонажей, а иногда на карте мира вы будете отправлены в бой. На приведенной выше диаграмме описаны эти режимы и переходы; это основной поток игрового процесса JRPG, и это то, с чего мы начнём создавать наши игровые состояния.
Управление сложностью с помощью механизма состояния
Механизм состояния, для наших целей, представляет собой кусок кода, который содержит все различные режимы наших игр, что позволяет нам перемещаться из одного режима в другой и обновлять и отображать все активные режимы.
В зависимости от языка реализации механизм состояния
обычно состоит из класса StateMachine
и интерфейса, IState
, который используется во всех состояниях.
Механизм состояния лучше всего описывается путем создания базовой системы в псевдокоде:
1 |
class StateMachine |
2 |
{
|
3 |
Map<String, IState> mStates = new Map<String, IState>(); |
4 |
IState mCurrentState = EmptyState; |
5 |
|
6 |
public void Update(float elapsedTime) |
7 |
{
|
8 |
mCurrentState.Update(elapsedTime); |
9 |
}
|
10 |
|
11 |
public void Render() |
12 |
{
|
13 |
mCurrentState.Render(); |
14 |
}
|
15 |
|
16 |
public void Change(String stateName, optional var params) |
17 |
{
|
18 |
mCurrentState.OnExit(); |
19 |
mCurrentState = mStates[stateName]; |
20 |
mCurrentState.OnEnter(params); |
21 |
}
|
22 |
|
23 |
public void Add(String name, IState state) |
24 |
{
|
25 |
mStates[name] = state; |
26 |
}
|
27 |
}
|
Код выше показывает простой механизм состояния без проверки ошибок.
Давайте посмотрим, как используется вышеуказанный механизм состояния в игре. В начале игры будет создан StateMachine
, добавлены все различные состояния игры и начальное состояние. Каждое состояние идентифицируется по имени String
, которое используется при вызове функции изменения состояния. Существует только одно активное состояние, mCurrentState
, и оно визуализируется и обновляется каждый игровой цикл.
Код может выглядеть так:
1 |
StateMachine gGameMode = new StateMachine(); |
2 |
|
3 |
// A state for each game mode
|
4 |
gGameMode.Add("mainmenu", new MainMenuState(gGameMode)); |
5 |
gGameMode.Add("localmap", new LocalMapState(gGameMode)); |
6 |
gGameMode.Add("worldmap", new WorldMapState(gGameMode)); |
7 |
gGameMode.Add("battle", new BattleState(gGameMode)); |
8 |
gGameMode.Add("ingamemenu", new InGameMenuState(gGameMode)); |
9 |
|
10 |
gGameMode.Change("mainmenu"); |
11 |
|
12 |
// Main Game Update Loop
|
13 |
public void Update() |
14 |
{
|
15 |
float elapsedTime = GetElapsedFrameTime(); |
16 |
gGameMode.Update(elapsedTime); |
17 |
gGameMode.Render(); |
18 |
}
|
В этом примере мы создаем все необходимые состояния, добавляем их в StateMachine
и устанавливаем начальное состояние в главное меню. Если мы запустим этот код, MainMenuState
будет отображаться и обновляться в первую очередь. Это меню, которое вы видите в большинстве игр при первой загрузке, с такими настройками, как Начать игру и Загрузить игру.
Когда пользователь выбирает Начать игру, MainMenuState
вызывает что-то вроде gGameMode.Change("localmap","map_001")
, а LocalMapState
становится новым текущим состоянием. Это состояние затем обновит и отобразит карту, позволяя игроку начать изучение игры.
На приведенной ниже диаграмме показана визуализация механизма состояния, перемещающегося между WorldMapState
и BattleState
. В игре это было бы эквивалентно игроку, который перемещается по всему миру, сражается с монстрами, переходит в боевой режим, а затем возвращается на карту.



Давайте быстро взглянем на интерфейс состояния и класс EmptyState
, который его реализует:
1 |
public interface IState |
2 |
{
|
3 |
public virtual void Update(float elapsedTime); |
4 |
public virtual void Render(); |
5 |
public virtual void OnEnter(); |
6 |
public virtual void OnExit(); |
7 |
}
|
8 |
|
9 |
public EmptyState : IState |
10 |
{
|
11 |
public void Update(float elapsedTime) |
12 |
{
|
13 |
// Nothing to update in the empty state.
|
14 |
}
|
15 |
|
16 |
public void Render() |
17 |
{
|
18 |
// Nothing to render in the empty state
|
19 |
}
|
20 |
|
21 |
public void OnEnter() |
22 |
{
|
23 |
// No action to take when the state is entered
|
24 |
}
|
25 |
|
26 |
public void OnExit() |
27 |
{
|
28 |
// No action to take when the state is exited
|
29 |
}
|
30 |
}
|
Интерфейс IState
требует, чтобы каждое состояние содержало четыре метода, прежде чем его можно будет использовать как состояние в механизме: Update()
, Render()
, OnEnter()
и OnExit()
.
Update()
и Render()
вызываются каждый кадр для текущего активного состояния; OnEnter()
и OnExit()
вызывается при изменении состояния. Это всё довольно просто. Теперь вы знаете это, что вы можете создавать все виды состояний для всех частей вашей игры.
Это основной механизм состояния. Это полезно для многих ситуаций, но при работе с игровыми режимами мы можем улучшить его! С текущей системой изменение состояния может иметь много накладных расходов - иногда при переходе на BattleState
мы хотим покинуть WorldState
, запустить битву, а затем вернуться в WorldState
в точной настройке, которая была до битвы. Такая операция может быть неуклюжей, используя стандартную машину состояний, которую мы описали. Лучшим решением было бы использовать стек состояний.
Сделать логику игры проще с помощью штатного стека
Мы можем переключить стандартную машину состояний в стек состояний, как показано на диаграмме ниже. Например, MainMenuState
сначала помещается в стек, в начале игры. Когда мы начинаем новую игру, LocalMapState
нажимается поверх нее. На данный момент MainMenuState
больше не отображается или не обновляется, а ждет, и мы готовы вернуться к нему.
Затем, если мы начнем битву, BattleState
будет нажата вверху; когда битва заканчивается, она соскользнула со стека, и мы можем вернуться на карту именно там, где мы остановились. Если мы умрем в игре, то LocalMapState
выскочит, и мы вернемся к MainMenuState
.
На приведенной ниже диаграмме дается визуализация стека состояний, показывающая, что InGameMenuState
помещается в стек, а затем выскакивает.



Теперь у нас есть идея, как работает стек, давайте посмотрим на некоторый код для его реализации:
1 |
public class StateStack |
2 |
{
|
3 |
Map<String, IState> mStates = new Map<String, IState>(); |
4 |
List<IState> mStack = List<IState>(); |
5 |
|
6 |
public void Update(float elapsedTime) |
7 |
{
|
8 |
IState top = mStack.Top() |
9 |
top.Update(elapsedTime) |
10 |
}
|
11 |
|
12 |
public void Render() |
13 |
{
|
14 |
IState top = mStack.Top() |
15 |
top.Render() |
16 |
}
|
17 |
|
18 |
public void Push(String name) |
19 |
{
|
20 |
IState state = mStates[name]; |
21 |
mStack.Push(state); |
22 |
}
|
23 |
|
24 |
public IState Pop() |
25 |
{
|
26 |
return mStack.Pop(); |
27 |
}
|
28 |
}
|
Этот вышеуказанный код стека состояния не имеет проверки ошибок и довольно прост. Состояния могут быть перенесены в стек с помощью вызова Push()
и выведены вызовом Pop()
, а состояние на самой вершине стека - это тот, который обновлен и отображен.
Использование подхода на основе стека хорош для меню, и с небольшой модификацией его также можно использовать для диалоговых окон и уведомлений. Если вы чувствуете себя авантюристом, тогда вы можете комбинировать оба и иметь конечный автомат, который также поддерживает стеки.
Использование StateMachine
, StateStack
или некоторая комбинация двух создает отличную структуру для создания вашей RPG.
Следующие действия:
- Внедрите код конечного автомата на ваш любимый язык программирования.
- Создайте
MenuMenuState
иGameState
, наследующие отIState
. - Задайте основное состояние меню как начальное состояние.
- Оба состояния отображают разные изображения.
- При нажатии кнопки измените состояние из главного меню в состояние игры.
Карты
Карты описывают мир; пустыни, космические корабли и джунгли могут быть представлены с использованием плитки. Тканевая карта - это способ использования ограниченного количества небольших изображений для создания большего. На приведенной ниже диаграмме показано, как это работает:



Вышеприведенная диаграмма состоит из трех частей: палитры плитки, визуализации того, как построена плитка, и окончательной карты, отображаемой на экран.
Плитка плитки - это совокупность всех плиток, используемых для создания карты. Каждая плитка в палитре уникально идентифицируется целым числом. Например, плитка номер 1 - трава; обратите внимание на места, где он используется при визуализации в виде плитки.
Типичная карта - это всего лишь массив чисел, каждый из которых относится к плитке в палитре. Если бы мы хотели создать карту, полную травы, мы могли бы просто иметь большой массив, заполненный номером 1, и когда мы отобрали эти плитки, мы увидели бы карту травы, составленную из многих мелких травяных плит. Палитра плитки обычно загружается как одна большая текстура, содержащая много меньших фрагментов, но каждая запись в палитре может так же легко быть ее собственным графическим файлом.
Причина, по которой мы этого не делаем, - просто для простоты и эффективности. Если у вас есть массив целых чисел, это один непрерывный блок памяти. Если у вас массив массивов, то это один блок памяти для первого массива, который содержит указатели, причем каждый указатель указывает на ряд фрагментов. Это направление может замедлить работу - и поскольку мы рисуем карту каждого кадра, тем быстрее, тем лучше!
Давайте рассмотрим некоторый код для описания карты плитки:
1 |
//
|
2 |
// Takes a texture map of multiple tiles and breaks it up into
|
3 |
// individual images of 32 x 32.
|
4 |
// The final array will look like:
|
5 |
// gTilePalette[1] = Image // Our first grass tile
|
6 |
// gTilePalette[2] = Image // Second grass tile variant
|
7 |
// ..
|
8 |
// gTilePalette[15] = Image // Rock and grass tile
|
9 |
//
|
10 |
Array gTilePalette = SliceTexture("grass_tiles.png", 32, 32) |
11 |
|
12 |
gMap1Width = 10 |
13 |
gMap1Height = 10 |
14 |
Array gMap1Layer1 = new Array() |
15 |
[
|
16 |
2, 2, 7, 3, 11, 11, 11, 12, 2, 2, |
17 |
1, 1, 10, 11, 11, 4, 11, 12, 2, 2, |
18 |
2, 1, 13, 5, 11, 11, 11, 4, 8, 2, |
19 |
1, 2, 1, 10, 11, 11, 11, 11, 11, 9, |
20 |
10, 11, 12, 13, 5, 11, 11, 11, 11, 4, |
21 |
13, 14, 15, 1, 10, 11, 11, 11, 11, 6, |
22 |
2, 2, 2, 2, 13, 14, 11, 11, 11, 11, |
23 |
2, 2, 2, 2, 2, 2, 11, 11, 11, 11, |
24 |
2, 2, 2, 2, 2, 2, 5, 11, 11, 11, |
25 |
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, |
26 |
];
|
Сравните приведенный выше код с диаграммой, и совершенно ясно, как плитка построена из небольшой серии плиток. Как только карта описана так, мы можем написать простую функцию рендеринга, чтобы нарисовать ее на экране. Точные детали функции будут меняться в зависимости от параметров настройки видоискателя и чертежа. Ниже показана наша функция рендеринга.
1 |
static int TilePixelSize = 32; |
2 |
|
3 |
// Draws a tilemap from the top left, at pixel position x, y
|
4 |
// x, y - the pixel position the map will be rendered from
|
5 |
// map - the map to render
|
6 |
// width - the width of the map in tiles
|
7 |
public void RenderMap(int x, int y, Array map, int mapWidth) |
8 |
{
|
9 |
// Start by indexing the top left most tile
|
10 |
int tileColumn = 1; |
11 |
int tileRow = 1; |
12 |
|
13 |
for(int i = 1; map.Count(); i++) |
14 |
{
|
15 |
// Minus 1 so that the first tile draws at 0, 0
|
16 |
int pixelPosX = x + (tileColumn - 1) * TilePixelSize; |
17 |
int pixelPosY = y + (tileRow - 1) * TilePixelSize; |
18 |
|
19 |
RenderImage(x, y, gTilePalette[gMap1Layer1[i]]); |
20 |
|
21 |
// Advance to the next tile
|
22 |
tileColumn += 1; |
23 |
if(tileColumn > mapWidth) |
24 |
{
|
25 |
tileColumn = 1; |
26 |
tileRow += 1; |
27 |
}
|
28 |
}
|
29 |
}
|
30 |
|
31 |
-- How it's used in the main update loop |
32 |
public void Update() |
33 |
{
|
34 |
// Actually draw a map on screen
|
35 |
RenderMap(0, 0, gMap1Layer1, gMap1Width) |
36 |
}
|
Карта, которую мы использовали до сих пор, является довольно простой; большинство JRPG будут использовать несколько слоев tilemaps для создания более интересных сцен. На приведенной ниже диаграмме показана наша первая карта, в которую добавлено еще три слоя, что приводит к гораздо более приятной карте.



Как мы видели ранее, каждый tilemap - это всего лишь массив чисел, и поэтому полноразмерная карта может быть сделана из массива этих массивов. Конечно, создание tilemap на самом деле является лишь первым шагом в добавлении исследования в вашу игру; карты также должны иметь информацию о столкновении, поддержку движущихся объектов вокруг и базовую интерактивность с использованием триггеров.
Триггер - это фрагмент кода, который запускается только тогда, когда игрок «запускает» его, выполняя некоторые действия. Существует множество действий, которые может распознать триггер. Например, перемещение персонажа игрока на плитку может вызвать действие - это обычно происходит при перемещении по дверному проему, телепорту или кромке плитки карты. Триггеры могут быть помещены на эти плитки, чтобы телепортировать персонажа на карту внутреннего пространства, карту мира или соответствующую локальную карту.
Другой триггер может зависеть от нажатия кнопки «Использовать». Например, если игрок поднимается до знака и нажимает «использовать», тогда запускается триггер и отображается диалоговое окно с текстом знака. Триггеры используются повсюду, чтобы помочь сшить карты вместе и обеспечить интерактивность.
В JRPG часто есть много довольно подробных и сложных карт, поэтому я рекомендую вам не пытаться их делать вручную, гораздо лучше использовать редактор tilemap. Вы можете использовать один из превосходных бесплатных существующих решений или сворачивать свои собственные. Если вы хотите попробовать существующий инструмент, я обязательно рекомендую проверить Tiled, который является инструментом, который я использовал для создания этих примеров карт.
Следующие действия:
- Создаём плитки.
- Скачивайте готовые плитки на opengameart.org.
- Создайте карту и загрузите ее в свою игру.
- Добавьте персонаж игрока.
- Передвигаем персонаж с плитки на плитку.
- Плавный переход персонажа с плитки на плитку.
- Добавляем детектор столкновения (вы можете использовать отдельный слой для хранения информации о взаимодействиях).
- Добавьте простой триггер для обмена картами.
- Добавьте триггер для чтения знаков - использование стека состояний, о котором мы говорили ранее, чтобы показать диалоговое окно.
- Сделайте главное меню с опцией «Начать игру» и локальным состоянием карты и объедините их вместе.
- Разработайте несколько карт, добавьте несколько NPC, попробуйте простой квест-квест - пусть ваше воображение бежит бесплатно!
Бой
Наконец, на битву! Какая JRPG без боя? Сражение - это то место, где множество игр предпочитают вводить новшества, внедряя новые системы навыков, новую боевую структуру или различные системы заклинаний - есть довольно много вариаций.
Большинство боевых систем используют пошаговую структуру, и только один боец разрешает действовать одновременно. Самые первые пошаговые боевые системы были простыми, и каждый персонаж получал ход: ход игрока, ход врага, ход игрока, ход врага и т.д. Это быстро уступило место более сложным системам, предлагающим больше возможностей для тактики и стратегии.
Мы внимательно рассмотрим боевые системы на основе активного времени, где комбатанты не обязательно получают равное количество оборотов. Более быстрые объекты могут получать больше оборотов, а тип действия также влияет на продолжительность прохождения. Например, воин, рубящий кинжалом, может занять 20 секунд, но волшебник, призывающий монстра, может занять две минуты.



На приведенном выше снимке показан боевой режим в типичном JRPG. Управляемые игроком персонажи справа, вражеские персонажи слева, а текстовое поле внизу показывает информацию о комбатантах.
В начале боя на сцену добавляются спрайты с монстрами и игроками, а затем принимается решение о том, какой порядок сущности совершают свои очереди. Это решение может частично зависеть от того, как был начат бой: если игрок попал в засаду, монстры будут атаковать сначала, иначе он будет основываться на одной из статистических данных, таких как скорость.
Все, что делает игрок или монстры, - это действие: атака - это действие, использование магии - это действие, и даже принятие решения о том, какое действие нужно предпринять, - это действие! Порядок действий лучше всего отслеживать с использованием очереди. Действие вверху - это действие, которое будет происходить дальше, если только это не ускорит действие. Каждое действие будет иметь обратный отсчет, который уменьшается по мере прохождения каждого кадра.
Боевой поток управляется с использованием конечного автомата с двумя состояниями; одно состояние, чтобы пометить действия и другое состояние, чтобы выполнить верхнее действие, когда придет время. Как всегда, лучший способ понять что-то - это посмотреть на код. В следующем примере реализовано базовое состояние боя с очередью действий:
1 |
class BattleState : IState |
2 |
{
|
3 |
List<IAction> mActions = List<IAction>(); |
4 |
List<Entity> mEntities = List<Entity>(); |
5 |
|
6 |
StateMachine mBattleStates = new StateMachine(); |
7 |
|
8 |
public static bool SortByTime(Action a, Action b) |
9 |
{
|
10 |
return a.TimeRemaining() > b.TimeRemaining() |
11 |
}
|
12 |
|
13 |
public BattleState() |
14 |
{
|
15 |
mBattleStates.Add("tick", new BattleTick(mBattleStates, mActions)); |
16 |
mBattleStates.Add("execute", new BattleExecute(mBattleStates, mActions)); |
17 |
}
|
18 |
|
19 |
public void OnEnter(var params) |
20 |
{
|
21 |
mBattleStates.Change("tick"); |
22 |
|
23 |
//
|
24 |
// Get a decision action for every entity in the action queue
|
25 |
// The sort it so the quickest actions are the top
|
26 |
//
|
27 |
|
28 |
mEntities = params.entities; |
29 |
|
30 |
foreach(Entity e in mEntities) |
31 |
{
|
32 |
if(e.playerControlled) |
33 |
{
|
34 |
PlayerDecide action = new PlayerDecide(e, e.Speed()); |
35 |
mActions.Add(action); |
36 |
}
|
37 |
else
|
38 |
{
|
39 |
AIDecide action = new AIDecide(e, e.Speed()); |
40 |
mActions.Add(action); |
41 |
}
|
42 |
}
|
43 |
|
44 |
Sort(mActions, BattleState::SortByTime); |
45 |
}
|
46 |
|
47 |
public void Update(float elapsedTime) |
48 |
{
|
49 |
mBattleStates.Update(elapsedTime); |
50 |
}
|
51 |
|
52 |
public void Render() |
53 |
{
|
54 |
// Draw the scene, gui, characters, animations etc
|
55 |
|
56 |
mBattleState.Render(); |
57 |
}
|
58 |
|
59 |
public void OnExit() |
60 |
{
|
61 |
|
62 |
}
|
63 |
}
|
В приведенном выше коде показано управление потоком боевого режима с использованием простого конечного автомата и очереди действий. Начнем с того, что все сущности, участвующие в битве, добавили в очередь действие принятия решения.
Принятие решения для игрока приведет к появлению меню с устойчивыми параметрами RPG Attack, Magic и Item; как только игрок принимает решение о действии, действие принятия решения удаляется из очереди и добавляется новое выбранное действие.
Принятие решения для ИИ проверяет сцену и решает, что делать дальше (используя что-то вроде дерева поведения, дерева решений или аналогичной техники), а затем оно также удалит его действие принятия решения и добавит его новое действие в очередь.
Класс BattleTick
контролирует обновление действий, как показано ниже:
1 |
class BattleTick : IState |
2 |
{
|
3 |
StateMachine mStateMachine; |
4 |
List<IAction> mActions; |
5 |
|
6 |
public BattleTick(StateMachine stateMachine, List<IAction> actions) |
7 |
: mStateMachine(stateMachine), mActions(action) |
8 |
{
|
9 |
}
|
10 |
|
11 |
// Things may happen in these functions but nothing we're interested in.
|
12 |
public void OnEnter() {} |
13 |
public void OnExit() {} |
14 |
public void Render() {} |
15 |
|
16 |
public void Update(float elapsedTime) |
17 |
{
|
18 |
foreach(Action a in mActions) |
19 |
{
|
20 |
a.Update(elapsedTime); |
21 |
}
|
22 |
|
23 |
if(mActions.Top().IsReady()) |
24 |
{
|
25 |
Action top = mActions.Pop(); |
26 |
mStateMachine:Change("execute", top); |
27 |
}
|
28 |
}
|
29 |
}
|
BattleTick
является подчиненным состоянием состояния BattleMode, и он просто галочки, пока обратный отсчет верхнего действия не равен нулю. Затем он выдает верхнее действие из очереди и переходит в состояние выполнения.



На приведенной выше диаграмме показана очередь действий в начале битвы. Никто еще не предпринял никаких действий, и к их времени все приказано принять решение.
У гигантского завода есть обратный отсчет 0, поэтому на
следующем тике он выполняет свое действие AIDecide
. В этом случае действие AIDecide
приводит к тому, что монстр решает атаковать. Действие атаки почти мгновенно и добавляется обратно в очередь в качестве второго действия.
На следующей итерации BattleTick
игрок сможет выбрать, какое действие должен предпринять его карлик «Марк», который снова изменит очередь. После следующей итерации BattleTick
после этого завод атакует одного из гномов. Действие атаки будет удалено из очереди и передано в состояние BattleExecute
, и оно будет анимировать атакующее устройство, а также выполнить все необходимые боевые вычисления.
Как только атака монстра будет закончена, в очередь для монстра добавится другое действие AIDecide
. BattleState
будет продолжать этот путь до конца боя.
Если какой-либо объект умирает во время боя, все его действия нужно удалить из очереди - мы не хотим, чтобы мертвые монстры внезапно реанимировали и атаковали во время игры (если только мы не намерены создавать зомби или какую-то нежить!).
Очередь действий и простой конечный автомат являются сердцем боевой системы, и теперь вы должны хорошо чувствовать, как она сочетается. Это недостаточно полно, чтобы быть автономным решением, но его можно использовать в качестве шаблона для создания чего-то более полноценного и сложного. Действия и состояния - это хорошая абстракция, которая помогает управлять сложностью боя и облегчает ее расширение и развитие.
Следующие действия:
- Напишите состояние
BattleExecute
. - Возможно, добавьте больше состояний, таких как
BattleMenuState
иAnimationState
. - Извлечь фоны и врагов с базовыми характеристиками здоровья.
- Напишите простое действие атак и выполните простую битву за торговые атаки.
- Дайте сущности особые навыки или магию.
- Сделайте врага, который излечит себя, если здоровье ниже 25%.
- Создайте карту мира для запуска состояния битвы.
- Создайте состояние
BattleOver
, которое показывает добычу и прирост XP.
Обзор
У нас был высокий уровень взгляда на то, как сделать JRPG, погрузившись в некоторые из более интересных деталей. Мы рассмотрели, как структурировать код с использованием конечного автомата или стека, как использовать tilemaps и слои для отображения нашего мира и как управлять потоком боя с помощью очереди действий и конечного автомата. Функции, которые мы рассмотрели, создают хорошую основу для развития и развития.
Но есть и многое, что не было охвачено вообще. Создание полноценного JRPG включает в себя системы XP и выравнивания, сохранение и загрузку игры, множество графического интерфейса для меню, основные анимации и специальные эффекты, состояния для обработки роликов, боевой механики (такие как сон, позиция, стихийные бонусы и сопротивления) , чтобы назвать несколько вещей!
Однако вам не нужны все эти вещи для игры; Для Луны в основном было только исследование карт и диалог. Вы можете постепенно добавлять новые функции, когда вы делаете свою игру.
Куда пойти отсюда
Самая сложная часть создания любой игры заканчивается, поэтому начинайте с малого, подумайте о мини-RPG; выйдите из подземелья, один квест, и затем создайте. Если вы обнаружите, что вы застреваете, уменьшите масштаб игры, упростите ее и завершите. Вы можете обнаружить, что по мере развития вы получаете множество новых и захватывающих идей, что хорошо, записывайте их, но сопротивляйтесь стремлению увеличить объем вашей игры или, что еще хуже, начать новую.
Сделать JRPG сложно; существует множество систем, и без хорошей карты может быть трудно понять, с чем сначала справиться. Тщательное пошаговое руководство по созданию JRPG заполнило бы книгу! К счастью, я пишу эту книгу, поэтому, если вы хотите получить более подробное руководство по созданию JRPG, пожалуйста, проверьте это.
Используемые ресурсы
Для того, чтобы помочь собрать эту статью, был использован ряд ресурсов и активов для коллективного использования:
- Городская карта Искусство Забина, Даниеклу, Джетрель, Гиптозис, Редшир, Бертрам.
- Мировая карта Art by MrBeast.
- Предшествующий уровень техники Delfos.
- Искусство Потрайт Джастина Никол.
- Монстры от Blarumyrran.
- Значок «Знак Знаний» по рисунку Лорка и Темного дерева Омара Альвардо.
- Плитка для создания карты.
- Понятия из моей книги Как сделать RPG. Если вы хотите более подробное освещение создания JRPG, проверьте его.