JRPG를 만드는 법: 입문
() translation by (you can also view the original English article)
이 글은 초기 파이널 판타지 게임들 같은 JRPG(일본식 롤플레잉 게임)를 만드는 과정을 개략적으로 살펴봅니다. JRPG의 뼈대를 이루는 구조와 시스템, 게임 모드의 관리 방법, 타일맵을 사용해 세계를 표시하는 방법, RPG 전투 시스템을 짜는 법을 살펴볼 겁니다.
주: 이 글에서는 자바와 닮은 가상의 프로그래밍 언어를 사용하는데 개념 자체는 어떤 개발 환경에도 적용할 수 있습니다.
목차
JRPG의 탄생

1983년 호리이 유지와 나카무라 코이치, 치다 유키노부는 미국으로 날아가 개발자들이 모여 애플 II용 소프트웨어들을 선보이는 애플페스트에 참석합니다. 세 사람은 위저드리라는 RPG의 최신판을 보고 감명을 받게 됩니다.
일본으로 돌아와 세 사람은 그와 유사하지만 패미콤 용으로 간편화된 RPG 드래곤 퀘스트를 만들기로 합니다. 드래곤 퀘스트는 대히트였고 JRPG 장르를 규정하게 됩니다. 드래곤 퀘스트는 미국에선 잘 되지 않았지만 몇 년 뒤 다른 게임이 미국에서 이름을 알립니다.
1987년, 최초의 파이널 판타지 게임이 출시되어 지구 상에서 가장 많이 팔린 비디오게임 프랜차이즈 중 하나를 탄생시키고, 적어도 서양에서는 JRPG의 상징이 되었습니다.
장르 토크
게임 장르는 엄격하게 규정되기보다는 관습의 모호한 집합에 가깝습니다. RPG는 대체로 레벨링 시스템과 스킬과 능력치를 지닌 하나 이상의 플레이어 캐릭터, 무기와 갑옷, 전투 모드와 탐험 모드, 강한 내러티브가 있고 게임은 맵을 나아감으로써 진행되는 경우가 흔합니다.
일본식 RPG들은 드래곤 퀘스트의 틀로 만들어진 RPG들입니다. 더 선형적이고, 전투는 흔히 턴제고, 보통 월드 맵과 에리어 맵 두 가지 종류의 맵이 있습니다. 드래곤 퀘스트와 파이널 판타지, 와일드 암즈, 판타시 스타, 크로노 트리거가 전형적인 JRPG입니다. 우리가 여기서 다룰 JRPG 유형은 초기 파이널 판타지에 가깝습니다.



JRPG를 만들어야 하는 다섯가지 이유
1. 세월을 견딘 장르다
파이널 판타지 VI과 크로노 트리거 같은 게임들은 오늘날에 플레이해도 즐길만합니다. JRPG를 만든다면 오늘날 플레이어들도 잘 이해하는 세월을 타지 않는 게임 형식을 배우게 됩니다. 스토리든 표현이든 시스템이든 여러분만의 아이디어나 실험을 덧붙이기에도 훌륭한 뼈대가 됩니다. 출시되고 수십 년 뒤에도 플레이되고 즐기는 게임을 만들 수 있다는 건 멋진 일입니다!
2. 게임 메커닉을 널리 응용할 수 있다
세계에서 가장 인기 있는 FPS 콜 오브 듀티는 RPG 요소를 활용합니다. 팜빌을 중심으로 하는 소셜 게임 붐은 기본적으로 슈퍼패미콤 RPG 목장이야기의 클론이었습니다. 그리고 심지어 그란 투리스모 같은 레이싱 게임에도 레벨과 경험치가 있습니다.
3. 제약이 독창성을 자극한다
작가가 비어있는 종이로 두려워할 수 있는 것처럼, 게임 개발자는 새로운 게임을 디자인할 때 가능한 선택의 큰 수 때문에 스스로 마비되는걸 발견할 수도 있습니다. JRPG는 여러분을 위해 많은 선택을 해주므로 선택 장애에 빠지지 않고 대부분의 대화를 자유롭게 따라가다가 중요한 부분에서만 선택하도록 합니다.
4. 솔로 프로젝트로 할 수 있습니다.
파이널 판타지는 나시르 게벨리라는 한 프로그래머가 거의 대부분을 코딩했으며, 그는 어셈블리로 작업했습니다. 현대의 도구와 언어를 사용하면 이런 종류의 게임을 만드는 것은 훨씬 쉽습니다. 대부분의 RPG의 가장 큰 부분은 프로그래밍이 아니라 컨텐츠입니다. - 그러나 이것이 여러분의 게임의 경우가 되어야만 하는 것은 아닙니다. 컨텐츠를 조금이라도 되돌아보고 양보다 질에 집중한다면 JRPG는 훌륭한 솔로 프로젝트입니다.
게임을 도울 수 있는 팀을 만들거나 아트와 음악을 아웃소싱하거나 opengameart.org 같은 곳에서 훌륭한 창조적인 에셋 일부를 사용하고 싶어할수도 있습니다. (편집주 주: 자매 사이트 GraphicRiver에서도 스프라이트 시트를 판매합니다.)
5. 이익을 위해서!
JRPG를 좋아하는 사람들이 있으며 많은 인디 JRPG(아래 그림에 나온 것과 같은)들이 상업적으로 성공하고 스팀 같은 플랫폼에서 판매합니다.



아키텍쳐
JRPG는 많은 규칙과 매커니즘을 공유하므로 일반적인 JRPG를 여러 시스템으로 나눌 수 있습니다:

소프트웨어 개발에서 반복적으로 보이는 하나의 패턴이 있습니다: 계층화. 이것은 프로그램의 시스템이 각각의 위에 올라가는 방식을 말하며 밑에는 넓게 적용가능한 레이어가 있고 위로 가까워질수록 근처의 문제를 더 밀접하게 처리하는 레이어가 있습니다. JRPG는 차이가 없으며 몇개의 레이어로 볼 수 있습니다 - 낮은 레이어는 기본적인 그래픽 함수를 다루고 높은 레이어는 퀘스트와 캐릭터 스텟을 다룹니다.
위의 아키텍쳐 다이어그램에서 볼 수 있듯이 JRPG를 구성하는 시스템은 많지만 대부분의 시스템이 게임의 개별 모드로 그룹화할 수 있습니다. JRPG는 매우 다른 게임 모드를 가집니다; 월드 맵, 지역 맵, 전투 모드와 몇몇 메뉴 모드를 가집니다. 이런 모드들은 대부분 완전히 갈라져 있고, 코드 조각으로 분리되어 있으며, 각 코드 조각은 간단하게 개발할 수 있습니다.
모드는 중요하지만 게임 컨텐츠 없이는 쓸모없습니다. RPG는 많은 맵 파일, 몬스터 정의, 대화 라인, 컷씬을 실행할 스크립트와 플레이어 진행 방법을 제어하는 게임플레이 코드를 포함합니다. JRPG를 만드는 방법에 대해서 자세하게 다루면 책 한권을 채울 것이므로 중요한 부분 몇몇에만 집중할 것입니다. 게임 모드를 깔끔하게 다루는 것은 관리 가능한 JRPG를 제작하는데 중요하므로 이 시스템을 우선 다룰 것입니다.
게임 상태 관리하기
아래 이미지는 게임 루프가 쏟아져 나가는 것을 보여주며 매 프레임마다 update 함수를 호출합니다. 이것은 게임의 심장 박동이며 거의 모든 게임이 이런 식으로 구성됩니다.



프로젝트를 시작했지만 새로운 기능을 추가하는 것이 너무 어렵거나 이상한 버그 때문에 괴로워서 중지한 적이 있나요? 아마 작은 구조체와 update 함수 안에 모든 코드를 집어넣어 코드가 엉망이 된 것을 발견했을지도 모릅니다. 이런 종류의 문제에 대한 훌륭한 해결 방법은 코드를 각각의 다른 게임 상태에 따라 나누어 무엇이 발생했는지를 훨씬 선명하게 보여주도록 하는 것입니다.
일반적인 게임 개발 도구는 상태 머신입니다; 이것은 애니메이션, 메뉴, 게임 흐름, 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
인터페이스는 상태 머신 내의 상태로 사용 가능한 4개의 메소드 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를 구축하는 훌륭한 구조체가 생성됩니다.
다음 작업:
- 상태 머신 코드를 좋아하는 프로그래밍 언어로 구현해봅시다.
-
IState
를 상속받은MenuMenuState
와GameState
를 생성합시다. - 기본 상태로 메인 메뉴 상태를 설정합니다.
- 두 상태가 각각 다른 이미지를 렌더링하도록 합니다.
- 버튼을 누르면 메인 메뉴에서 게임 상태로 변경되도록 합니다.
맵
맵은 세계를 표현합니다; 사막, 우주선, 정글은 타일맵을 사용하여 표현할 수 있습니다. 타일맵은 제한된 수의 작은 이미지를 사용하여 보다 큰 것을 만드는 방법입니다. 아래 다이어그램은 이것이 어떻게 작동하는지를 보여줍니다:



위의 다이어그램에는 타일 팔레트, 타일맵 구성 방법의 시각화, 화면에 렌더링되는 최종 맵이라는 세가지 부분이 있습니다.
타일 팔레트는 맵을 만드는데 사용되는 모든 타일의 모음입니다. 팔레트의 각 타일은 정수로 고유하게 식별됩니다. 예를 들어 타일 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는 더 재미있는 장면을 만들기 위해 타일 맵의 다중 레이어를 사용합니다. 아래 다이어그램은 우리의 첫번째 맵을 보여주며, 세 개의 레이어가 추가되어 보다 즐거운 맵으로 되었습니다.



이전에 보았듯이 각 타일맵은 단순히 숫자 배열이므로 전체 레이어된 맵은 해당 배열의 배열로 만들 수 있습니다. 물론 타일맵을 렌더링하는 것은 게임에 탐색을 추가하는 첫번째 단계일 뿐입니다; 맵에는 충돌에 대한 정보, 이동하는 엔티티에 대한 지원, 트리거를 사용하는 기본 상호작용 역시 필요합니다.
트리거는 플레이어가 어떤 행동을 수행하여 "발동"할때에만 작동하는 코드 조각입니다. 트리거가 인식할 수 있는 액션은 아주 많습니다. 예를 들어 플레이어 캐릭터를 타일 위로 움직이는 것이 액션을 트리거할 수도 있습니다 - 이는 일반적으로 출입구, 텔레포터 혹은 맵의 가장자리 타일로 이동했을 때 발생합니다. 캐릭터를 실내 맵, 월드 맵이나 관련된 지역 맵으로 텔레포트하기 위해 트리거를 이런 타일에 배치할 수도 있습니다.
다른 트리거는 "사용" 버튼이 눌렸는지를 감지합니다. 예를 들어 플레이어가 표지판 위로 이동하여 "사용"을 누르면 트리거가 실행되고 표지판의 텍스트를 보여주는 대화상자가 표시됩니다. 트리거는 모든 장소에서 지도를 연결하고 상호작용을 제공하는데 사용됩니다.
JRPG는 종종 매우 상세하고 복잡한 지도가 많기 때문에 손으로 직접 작성하지 말고 타일맵 에디터를 사용하는 것을 추천합니다. 이미 존재하는 우수한 솔루션 중 하나를 사용하거나 자신만의 솔루션을 사용할 수 있습니다. 기존 도구를 사용하고 싶다면 이 예제 맵들을 만드는데 사용한 Tiled 툴을 추천합니다.
다음 작업:
- Tiled를 받습니다.
- opengameart.org에서 타일을 얻습니다.
- 맵을 만들고 게임에 로드한다.
- 플레이어 캐릭터를 추가합니다.
- 캐릭터를 타일에서 타일로 이동합니다.
- 캐릭터를 타일에서 타일로 부드럽게 이동하도록 합니다.
- 충돌 감지를 추가합니다. (새 레이어를 사용해 충돌 정보를 저장할 수 있습니다)
- 맵을 전환하는 간단한 트리거를 추가합니다.
- 표지판을 읽는 트리거를 추가합니다 - 대화 상자를 추가하기 위해 이전에 말한 상태 스택을 사용하는 것을 고려해봅시다.
- "게임 시작" 옵션이 있는 메인 메뉴 상태와 로컬 맵 상태를 만들고 둘을 연결합시다.
- 맵과 NPC를 추가하고 간단한 퀘스트를 추가합시다 - 상상력을 무료로 활용하세요.
전투
드디어 전투입니다! JRPG에서 전투가 없다면 어떻게 되겠습니까? 전투는 많은 게임이 혁신을 선택하는 곳이며, 새로운 스킬 시스템, 새로운 전투 구조, 다른 주문 시스템을 소개하는 곳입니다 - 다양한 종류가 있습니다.
대부분의 전투 시스템은 한번에 하나의 전투원만 행동을 할 수 있는 턴 기반 구조를 사용합니다. 아주 처음의 턴 기반 전투 시스템은 단순했습니다. 모든 개체가 순서대로 진행하는 것이였죠. 플레이어의 턴, 적의 턴, 플레이어의 턴, 적의 턴... 이것은 전술과 전략에 더 많은 여유를 제공하는 보다 복잡한 시스템으로 신속하게 연결되었습니다.
우리는 전투원이 똑같은 숫자의 턴을 가질 필요가 없는 액티브 타임 기반 전투 시스템을 살펴볼 것입니다. 더 빠른 엔티티는 더 많은 턴을 가지며 행한 액션의 종류 또한 턴의 길이에 영향을 미칩니다. 예를 들어 단검을 휘두른 전사는 20초가 걸리지만 몬스터를 소환한 위저드는 2분이 걸리겠죠.



위 스크린샷은 일반적은 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의 안정적인 옵션인 공격, 마법, 아이템이 있는 메뉴를 표시할 것입니다; 플레이어가 액션을 결정하면 결정 액션은 큐에서 삭제되고 새로 선택된 액션이 추가됩니다.
AI의 추가 액션은 씬을 검사하고 다음 할 것을 결정합니다 (비헤이비어 트리, 결정 트리나 비슷한 테크닉을 사용합시다) 그 다음 똑같이 선택 액션을 삭제하고 새로운 액션을 큐에 추가합니다.
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이 될 때까지 틱합니다. それから、キューから一番上のactionを取り出して、actionの実行状態に移ります。



위의 다이어그램은 전투가 시작될 때의 액션 큐를 보여줍니다. 아무도 아직 액션을 취하지 않았으며 모두는 결정을 할 시간을 받았습니다.
자이언트 플랜트의 카운트다운은 0이므로 다음 틱에서 AIDecide
액션을 실행할 것입니다. 이 경우 AIDecide
액션은 몬스터가 공격하도록 하는 결과를 낼 것입니다. 공격 액션은 거의 즉시 이루어지며 두번째 작업으로 큐에 추가됩니다.
BattleTick
의 다음 반복에서 플레이어는 드워프 "마크"가 취해야 할 액션을 고르게 될 것이며, 그것은 큐를 다시 변경할 것입니다. 다음 BattleTick
의 반복시에 식물은 드워프 중 하나를 공격할 것입니다. 공격 액션은 큐에서 제거되고 BattelExecute
상태로 전달되며, 이것은 모든 필요한 전투 계산과 함께 식물의 공격을 애니메이트할 것이다.
몬스터 공격이 완료되면 또다른 몬스터의 AIDecide
액션이 큐에 추가될 것입니다. BattleState
는 전투가 끝날 때까지 이런 식으로 이어질 것입니다.
전투 중 엔티티가 죽으면 모든 관련된 액션이 큐에서 삭제될 것입니다 - 죽은 몬스터가 게임중에 갑자기 나타나 공격하기를 원치 않기 때문이죠. (좀비나 언데드 류를 만들려고 의도한게 아니라면 말입니다!)
액션 큐와 간단한 상태 머신은 전투 시스템의 심장이며 이제는 둘이 어떻게 잘 어울리지는지를 잘 알고 있어야 합니다. 스탠드얼론 솔루션으로는 충분하지 않지만 보다 완벽하고 복잡한 것을 만들기 위한 템플릿으로는 사용할 수 있을 것입니다. 액션과 상태는 전투의 복잡성을 관리하고 확장과 개발을 쉽게 하는 좋은 가상화입니다.
다음 작업:
-
BattleExecute
상태를 작성합니다. -
BattleMenuState
나AnimationState
와 같은 더 많은 상태를 추가합니다. - 백그라운드와 기본 체력 스텟을 가진 적을 렌더링합니다.
- 간단한 공격 액션을 작성하고 공격을 주고받는 간단한 전투를 실행합니다.
- 엔티티에게 특별한 스킬이나 마법을 줍니다.
- 적의 체력이 25% 이하일때 스스로에게 힐을 하도록 합니다.
- 배틀 상태를 시작할 월드 맵을 생성합니다.
- 전리품과 얻은 경험치를 보여주는
BattleOver
상태를 만듭니다.
리뷰
우리는 JRPG를 만드는 방법에 대해 살펴보고 몇몇 흥미로운 디테일에 대해 다루었습니다. 상태 머신이나 스택을 사용해 코드를 구축하는 방법, 타일맵과 레이어로 월드를 표시하는 방법, 액션 큐와 상태 머신을 사용해 전투의 흐름을 조종하는 방법을 다루었습니다. 우리가 다룬 기능들은 뭔가를 만들고 개발하는 것을 시작하기에 좋습니다.
그러나 아직 다루지 않은 것들이 많습니다. 완벽한 JRPG를 만드는데는 경험치 및 레벨링 시스템, 게임의 세이브 및 로드, 메뉴의 많은 GUI 코드, 기본 애니메이션과 특별한 효과, 컷씬을 다룰 상태, 전투 요소(수면, 독, 원소 보너스 및 저항 같은) 같은 것들이 들어갑니다.
그러나 게임이 이 모든 것들을 넣어야 할 필요는 없습니다. 투 더 문은 맵 이동과 대화뿐입니다. 게임을 만들어 나가면서 조금씩 새로운 기능을 추가할 수 있습니다.
여기서 나아가야 할 곳
어떤 게임의 만들기 가장 어려운 곳은 완료하는 것입니다. 그러므로 작은 미니 RPG를 시작하는 것을 생각해보세요; 던전 탈출, 1개의 퀘스트, 그리고 만드는 겁니다. 막혔다고 생각되면 게임의 범위를 줄여 간단하게 하고 완료해봅시다. 아마 여러분은 다양한 새롭고 재미있는 아이디어를 개발하면서 발견할 수 있습니다. 이건 좋습니다. 써봅시다. 그러나 게임의 범위를 늘리고픈 충동 혹은 더 심하게는 새로운 것을 시작하고 싶은 충동에는 저항하세요.
JRPG를 만드는 것은 어렵습니다; JRPG에는 많은 시스템이 있으며, 좋은 맵 없이는 어디서부터 태클을 걸어야 될지 아는 것부터 어렵게 됩니다. 한걸음씩 JRPG를 만들기 위한 가이드는 책을 채울지도 모릅니다. 다행이도 저는 책을 작성중입니다. 만약 JRPG 제작에 대한 더 자세한 내용을 원하신다면 확인해보세요.
사용한 리소스
많은 리소스와 Creative Commons 에셋이 이 문서를 작성하는데 도움을 주었습니다.
- Zabin, Daneeklu, Jetrel, Hyptosis, Redshrike, Bertram이 만든 타운 맵 아트
- MrBeast의 월드 맵 아트
- Delfos의 배경 아트
- 저스틴 니콜의 초상화 아트
- Blarumyrran의 몬스터 아트
- Lorc의 Gift of Knowledge 아이콘과 Omar Alvardo의 다크 우드 패턴
- 맵 생성을 위한 Tiled
- 컨셉을 위한 저의 RPG를 만드는 방법 책. JRPG의 창조에 대한 더 자세한 내용을 원한다면 확인해보세요.