#include<string> //문자열 관리를 위해 필요
#include<vector> //하위 노드의 관리를 위해 필요
역시나 다른 자료구조를
사용하시는 분은
해당 자료구조로 교체하셔도
무방합니다.
<목표>
키 입력을 받고
해당하는 스킬이 있을 때
그 스킬의 이름을 반환하는
클래스를 만듭시다!
<제작>
※이 강좌는 C#을 기준으로
작성되었습니다.
※언리얼과 유니티 엔진이
시간을 float으로 쓰기 때문에
float으로 만들었습니다.
///입력할 수 있는 키를 미리 표시해둡니다.
public enum EnumKey
{
UP,
DOWN,
FRONT,
BACK,
PUNCH,
KICK
}
우선 입력할 키를
미리 준비해둡시다.
필요에 따라서
넣어두시면 되겠습니다.
///콤보의 내용이 들어갈 노드입니다.
private class TrieNode
{
///이 노드가 가리키고 있는 키입니다.
EnumKey currentKey;
///이 노드에 할당된 기능이 있다면, 표시합니다.
public string currentName = "";
///이 노드의 상위 노드입니다.
TrieNode parent = null;
///이 노드의 하위 노드 리스트입니다.
List<TrieNode> childList = new List<TrieNode>();
///입력 키를 사용해서 새로운 노드를 추가합니다.
public TrieNode(EnumKey wantKey)
{
currentKey = wantKey;
}
}
노드의 구성입니다.
본인 노드를 만든 부모 노드
본인이 만든 자식 노드 리스트
두 종류에 이어져 있고
본인이 의미하는 입력 키
본인이 담고 있는 스킬 이름
이렇게 네 종류로 구성해두었습니다!
///하위 노드 중에서 해당하는 키를 가진 노드를 찾습니다.
public TrieNode FindChild(EnumKey wantKey)
{
for (int i = 0; i < childList.Count; ++i)
{
if (childList[i].currentKey == wantKey)
{
return childList[i];
};
};
return null;
}
제일 먼저 구성할 것은
자식 노드 확인입니다.
자식 노드를 전부 확인해서
원하는 키에 해당하는 노드가
있는지 확인하면
그 노드를 보여줍시다.
간단하죠?
///해당하는 키를 가진 하위 노드를 만듭니다.
public TrieNode NewChild(EnumKey wantKey)
{
TrieNode returnNode = FindChild(wantKey);
if(returnNode == null)
{
new TrieNode(wantKey);
returnNode.parent = this;
childList.Add(returnNode);
};
return returnNode;
}
뭘 생성하기도 전에
찾기부터 만든 이유는
중복 생성을 막기 위해서였습니다!
키가 들어오면, 이미 있는지 확인하고
없으면 새로운 노드를 만들죠~
이 때, 부모 노드가 본인이라고
알려준 후에
새로 만든 노드를 자식 노드 리스트에
추가를 해두도록 합시다.
///해당하는 배열과 순서가 일치하는 마지막 노드를 찾습니다.
public TrieNode Find(EnumKey[] wantArray, int index)
{
//내 다음 위치를 찾기 위해 1을 더합니다.
++index;
//다음 위치가 아직 남은 경우
if (index < wantArray.Length)
{
//다음 위치에 해당하는 키를 가진 하위 노드를 찾습니다.
TrieNode nextChild = FindChild(wantArray[index]);
//있다면, 그 노드에게 나머지 탐색을 요청합니다.
if (nextChild != null)
{
return nextChild.Find(wantArray , index);
}
//없다면, null을 반환합니다.
else
{
return null;
};
}
//다음 위치가 없는 경우
else
{
//마지막 키가 본인과 같으면, 본인이라고 알리고 아니면 null을 반환합니다.
if (wantArray[wantArray.Length - 1] == currentKey)
{
return this;
}
else
{
return null;
};
};
}
같은 순서로
배열 찾기를 만들어봅시다.
배열에 나열된 키를 그대로 쫓아서
배열 마지막까지 달립니다.
혹시라도 중간에 막히면
null을 반환하고
만약 끝까지 와도
마지막 키가 본인과 맞지 않으면
null을 반환합니다.
그림으로 보자면 이런 느낌입니다.
처음 이 함수가 입력될 0번째 노드는
루트에서 판별할 것이므로
0번은 건너 뛰고 판단합니다.
그래서 함수 시작할 때
위치에 1을 더하고 시작하는 것이고
더 나아갈 위치가 없으면
마지막으로 본인을 한 번 더
확인하고, 본인을 반환합시다.
///해당하는 배열의 순서에 맞춰 새로운 노드들을 만듭니다.
public void Insert(EnumKey[] wantArray, int index, string wantName)
{
//내 다음 위치를 찾기 위해 1을 더합니다.
++index;
//다음 위치가 존재하는 경우
if (index < wantArray.Length)
{
//다음 위치에 해당하는 키를 가진 하위 노드를 찾고, 없으면 새로 만듭니다.
TrieNode nextChild = NewChild(wantArray[index]);
//다음 위치의 하위 노드에게 계속 생성해달라고 요청합니다.
nextChild.Insert(wantArray, index, wantName);
}
//다음 위치가 없다면, 여기가 대상이므로 스킬이 없었을 때 새로운 스킬을 받습니다.
else if (currentName == "")
{
currentName = wantName;
};
}
배열 생성도 찾기와 같은 순서로
노드를 찾아 나갑시다.
찾기와 다른 점은
찾아서 없을 때마다
새로 생성한다는 점입니다!
///해당 노드를 삭제합니다.
public void Delete()
{
//스킬을 비웁니다.
currentName = "";
//하위 노드를 가지고 있지 않았을 때에만 실행합니다.
if (childList.Count <= 0)
{
//상위 노드가 있는 경우
if (parent != null)
{
//상위 노드가 가진 하위 노드가 이것뿐이라면, 그 노드도 삭제합니다.
if (parent.childList.Count <= 1 && parent.currentName == "")
{
parent.Delete();
}
//다른 하위 노드가 있었다면, 이 노드만 빼달라고 요청합니다.
else
{
parent.childList.Remove(this);
};
//상위 노드와 연결을 끊습니다.
parent = null;
};
//하위 노드와 연결을 끊습니다.
childList.Clear();
};
}
생성도 했으니 삭제도 해야죠!
삭제는 맨 마지막 노드에
요청을 하도록 하구요
삭제를 할 때에는
다음과 같은 상황을 고려해야 합니다.
-자식 노드가 있을 때-
이 때에는 본인이 중간이기 때문에
삭제되면 예기치 않은
연쇄삭제가 생기게 됩니다.
스킬 이름만 지우고 넘어갑시다.
-부모 노드가 없을 때-
본인에게 삭제 요청이 왔는데
부모 노드가 없다면
본인이 자료구조의
뿌리(root)이기 때문에
삭제되면 자료구조 사용에
문제가 생길 수 있습니다.
그래서 삭제를 처리하지 않습니다.
-부모 노드에 연결된 다른 노드가 있을 때-
다른 노드가 있다면
부모와 본인의 연결만 끊습니다.
-부모 노드에 본인만 있을 때-
본인만 연결된 부모 노드는
본인을 위해 만들어진 것이기 때문에
같은 라인으로 보고
연쇄 삭제를 요청합니다.
이러한 과정을 통해
삭제활동을 하도록 합니다.
///해당 노드에 이어진 모든 하위 노드들을 삭제합니다.
public void Destroy()
{
if(parent != null)
{
//하위 노드들에게 모두 삭제 요청을 돌립니다.
for (int i = 0; i < childList.Count; ++i)
{
childList[i].Destroy();
};
//상위, 하위 노드의 연결을 해제합니다.
parent = null;
childList.Clear();
};
}
한 단계 더 높은 삭제도
만들어 둘게요!
이건 콤보시스템을 삭제할 때
완전히 삭제하기 위해 만들었습니다~
뿌리 노드에 넣어주시면 됩니다!
트라이 삽입, 삭제, 검색
세 개면 다 만든 것 같으니
콤보 시스템도 만들어 봅시다~
class ComboSystem
{
///트라이 노드의 시작점(root)입니다.
private TrieNode rootTrie = new TrieNode(0);
///마지막으로 입력한 키의 노드입니다.
private TrieNode lastChecked = null;
///이 클래스가 기다려주는 한계 시간입니다.
private float expiredTime = 0.0f;
///입력을 받은 후, 다음 연계까지 몇 초의 시간을 기다려줄지 설정합니다.
private float limitInputTime = 0.0f;
///연계 대기 시간을 정해서 인스턴스를 생성합니다.
public ComboSystem(float limitTime)
{
limitInputTime = limitTime;
}
~ComboSystem()
{
rootTrie.Destroy();
}
}
콤보 시스템의 기본 구조입니다.
위에서 작성한 노드가
두 개 들어가 있습니다!
-rootTrie-
모든 노드의 시작 지점!
이 부분이 되겠네요!
여기는 어차피 다른 노드를 찾는
시작점이기 때문에
내용은 상관 없지만,
그래도 0번째 키를 넣어주었습니다.
쓰는 것이 아니기 때문에
그냥 아무렇게나 두셔도 됩니다!
-lastChecked-
마지막으로 확인한 노드!
이건 콤보시스템이기 때문에
마지막으로 입력한 값이
가장 중요하게 되겠는데요!
이걸 미리 저장해두어서
빠르게 접근하도록 합시다.
-expiredTime-
콤보를 사용하는 데에 있어서
한정 없이 입력을 기다릴 순 없죠~
입력한 시간이 여기에 기록된
시간보다 늦는다면
가차없이 처음부터
다시 입력하게 합시다~
-limitInputTime-
그럼 언제까지 기다리느냐?
를 기록하는 칸입니다.
여기에 적힌 시간만큼 기다립시다.
기본적으로 필요한 칸인 만큼
콤보시스템의 생성자도
이 칸의 입력을 필요로 하죠!
///한계 대기 시간을 다시 조정합니다.
public void SetTimeLimit(float wantTime)
{
limitInputTime = wantTime;
}
///다음 연계까지 기다려주는 시간을 직접 정합니다.
public void SetExpireTime(float wantTime)
{
expiredTime = wantTime;
}
이 시간에 관련된 두 변수는
그냥 설정할 수 있게 해두겠습니다~
///최상위 노드 중에서 원하는 키를 가진 노드를 찾습니다.
private TrieNode FindNodeStart(EnumKey wantKey)
{
return rootTrie.FindChild(wantKey);
}
일을 진행하기에 앞서
작업할 공간은 찾아야겠죠?
시작 노드 바로 밑에 달린
노드를 찾는 함수입니다!
///해당 배열의 순서와 같은 최하위 노드를 찾습니다.
private TrieNode FindNodeEnd(EnumKey[] wantArray)
{
//진입 노드를 찾습니다.
TrieNode targetNode = FindNodeStart(wantArray[0]);
//진입 노드가 존재한다면
if (targetNode != null)
{
//찾기 요청을 전달합니다.
targetNode = targetNode.Find(wantArray, 0);
//찾은 노드가 있다면 해당 노드를 반환합니다.
if (targetNode != null)
{
return targetNode;
};
};
//진입 노드가 없다면 그냥 null을 반환합니다.
return null;
}
시작 위치를 찾았다면
끝나는 위치도 찾아야죠!
진입할 노드를 찾아보고
그 노드에 Find함수를 실행합니다.
그럼 알아서 맨 밑에 있는
내용을 가져다 주겠죠~
///해당 배열의 순서에 맞게 스킬을 넣습니다. 이미 그 자리에 스킬이 있다면, 실패합니다.
public void Insert(EnumKey[] wantArray, string wantName)
{
//첫 번째 진입 노드를 찾습니다. 없으면 새로 만듭니다.
TrieNode targetNode = rootTrie.NewChild(wantArray[0]);
//진입 노드에 생성 요청을 전달합니다.
targetNode.Insert(wantArray, 0, wantName);
}
///해당 배열과 순서가 같은 스킬이 있다면, 해당 스킬을 제거합니다.
public void Delete(EnumKey[] wantArray)
{
//해당하는 스킬을 찾습니다.
TrieNode targetNode = FindNodeEnd(wantArray);
//있으면 제거합니다.
if(targetNode != null)
{
targetNode.Delete();
};
}
///해당 배열과 순서가 같은 스킬이 있다면, 그 스킬의 이름을 바꿉니다.
public void ChangeName(EnumKey[] wantArray, string wantName)
{
//해당하는 스킬을 찾습니다.
TrieNode targetNode = FindNodeEnd(wantArray);
//있으면 이름을 바꿉니다.
if (targetNode != null)
{
targetNode.currentName = wantName;
};
}
찾기를 만들었다면 이미 끝입니다!
Trie에서 모든 내용을 만들었기 때문에
찾은 대상에게 명령을 전달만 하면 됩니다~
Insert함수는 노드가 없을 때
새로 만든다는 선택지가
추가되었을 뿐이네요!
///유저가 키를 입력했을 때 실행하는 함수입니다.
public string Input(EnumKey inputKey, float inputTime)
{
//기다려주는 시간보다 입력한 시간이 더 빠르면
if(expiredTime >= inputTime)
{
//직전에 확인한 노드가 있었는지 봅니다.
if(lastChecked != null)
{
//있었다면, 하위 노드 중에 새로 입력된 키로 옮겨갑니다.
lastChecked = lastChecked.FindChild(inputKey);
};
}
//늦게 입력되었으면, 직전 노드를 비웁니다.
else
{
lastChecked = null;
};
//기다려주는 시간을 현재시간 + 한계시간으로 조정합니다.
expiredTime = inputTime + limitInputTime;
//확인할 노드가 없다면
if (lastChecked == null)
{
//최상단 노드들 중에서 해당하는 키를 확인하도록 합니다.
lastChecked = FindNodeStart(inputKey);
};
//모든 확인을 끝낸 뒤에 도출된 노드가 있다면
if(lastChecked != null)
{
//해당 노드의 이름을 받아옵니다.
return lastChecked.currentName;
}
//최상단 노드에도 해당하는 키가 없었다면, 빈 문자열을 반환합니다.
else
{
return "";
};
}
//외부에서 접근하는 모든 함수
//몇 번째 페이지를 읽게 할지 고릅니다.
TextReader.StartRead(int 읽을문장의 번호);
//매 프레임마다 불러 출력을 시도합니다.
TextReader.Update();
//변수를 등록하거나 변경합니다.
TextReader.globalVariable.Set(string 변수명, string 값);
//변수의 값을 가져옵니다.
TextReader.globalVariable.Get(string 변수명);
//현재 등록된 선택지 배열입니다.
//8개까지 등록할 수 있게 해두었습니다.
private static SelectInformation[] currentSelection = new SelectInformation[8];
//현재 등록된 변수입니다.
//VariableList는 변수 리스트로
//나중에 만들어보도록 합시다.
public static VariableList globalVariable = new VariableList();
//현재 표시되고 있는 이름과 대화 내용을 저장합니다.
private static string talkName = "???";
private static string showText = "";
//출력할 전체 문장입니다.
private static string receiptText = "";
//다음 내용까지 계산해둔 문장입니다.
//여기에서 대화 내용을 꺼내갑니다.
private static string calculatedText = "";
//다음 글자가 출력될 시간입니다.
private static long nextTime = 0;
//전체 문장에서 계산한 위치를 표시합니다.
private static int currentTextIndex = 0;
//계산된 문장에서 출력한 위치를 표시합니다.
private static int currentReadIndex = 0;
//현재 등록된 선택지의 수입니다.
private static int currentSelectionNumber = 0;
//글로벌 스피드는 기본적인 출력 속도를 나타냅니다.
//현재 스피드는 현재 문장의 출력 속도를 나타냅니다.
//문장이 시작할 때 글로벌 스피드에 맞춰지고
//스피드 커맨드가 나오면 바뀝니다.
private static int globalReadSpeed = 10;
private static int currentReadSpeed = 10;
//현재 읽는 페이지와 다음 페이지 위치를 저장합니다.
private static int currentReadPage = 0;
private static int nextReadPage = 0;
//커맨드를 확인할 때, 커맨드 속 커맨드를 위해서
//커맨드 시작 위치와, 겹친 개수를 저장합니다.
//추후 설명하도록 하겠습니다.
private static int commandStartIndex = 0;
private static int commandDepth = 0;
//현재 읽는 중이라고 표시합니다.
private static bool reading = false;
//속도를 무시하는지 확인합니다.
//대화를 진행하는 키 (이 프로젝트에서는 엔터나 스페이스바)
//를 출력 중에 입력하면 즉시
//해당 문장이 로드되게 합니다.
private static bool skip = false;
대강 설명을 해보면
선택지는 배열로
변수는 리스트로 저장됩니다.
입력받은 문장
계산한 문장
출력한 문장
세 개를 선언하고
계산된 위치와 출력한 위치를
따로 저장합니다.
출력 속도와 읽을 페이지
읽고 있는지, 스킵할지
커맨드를 읽는 중인지
확인하는 것으로
일단 끝입니다~
출력을 시작해봅시다
변수 선언을 끝냈으니 출력을 시작하도록
준비를 해봅시다!
public static void StartRead(int page)
{
//이미 읽고 있었다면, 취소합니다.
if (reading)
{
return;
};
//읽고 있다고 알립니다.
reading = true;
//스킵을 꺼둡니다.
skip = false;
//현재 받은 페이지 번호를 저장하고
//다음 페이지를 자동으로 조정합니다.
currentReadPage = page;
nextReadPage = page + 1;
//텍스트를 초기화합니다.
calculatedText = "";
showText = "";
//다음 출력 시간을 0으로 조정해서
//바로 첫 글자가 출력되게 합니다.
nextTime = 0;
//읽기 시작했기 때문에
//읽은 위치를 0으로 초기화합니다.
currentTextIndex = 0;
currentReadIndex = 0;
//커맨드가 시작된 위치를 0으로 초기화합니다.
//커맨드 개수도 초기화합니다.
commandStartIndex = 0;
commandDepth = 0;
//현재 읽는 속도를 기본 속도로 맞춥니다.
currentReadSpeed = globalReadSpeed;
//저는 TextContainer에서 문자열을 받아옵니다.
//받아올 위치를 여기에 넣으시면 되겠습니다!
if (currentReadPage < TextContainer.text.Length)
{
receiptText = TextContainer.text[currentReadPage];
}
//만약 받아올 문자열이 없는 경우에
//스크립트 종료 문장을 출력합니다.
//지금은 출력하는 기능만 있어서 이렇게 만들었는데
//실제 적용은 다르게 하셔도 됩니다~
else
{
talkName = "<System>";
receiptText = "스크립트가 종료되었습니다.";
nextReadPage = page;
};
//상대 이름을 초기화합니다.
string showingName = talkName;
//지금은 콘솔로 출력하기 때문에
//화면을 모두 지우고 상대 이름을 써주겠습니다.
Console.Clear();
Console.WriteLine(showingName);
}
출력 준비는 초기화로 시작합시다~
이미 읽는 중이면 취소를 하고
아니면 변수를 초기화시켜줍니다.
그리고 여기선 페이지 번호를 받아서
TextContainer에 있는 값을 받는데요,
다른 곳에서 정보를 받아오시는 분은
이 부분을 수정하시면 되겠습니다!
매 프레임 실행할 부분
public static void Update()
{
//지금 출력 중인 경우
if (reading)
{
//엔터 키나 스페이스 바를 누른 경우
if(currentKey == ConsoleKey.Enter || currentKey == ConsoleKey.Spacebar)
{
//눌린 키를 초기화시킵니다.
currentKey = 0;
//스킵을 활성화합니다.
skip = true;
};
//출력 함수를 실행합니다.
OnRead();
}
//출력 중이 아닌 경우
else
{
//엔터나 스페이스바를 누른 경우
if(currentKey == ConsoleKey.Enter || currentKey == ConsoleKey.Spacebar)
{
//등록된 선택지가 없을 때
if(currentSelectionNumber <= 0)
{
//키를 초기화합니다.
currentKey = 0;
//다음 페이지를 실행합니다.
StartRead(nextReadPage);
};
}
//선택지가 있을 때 다른 키를 누르면
//해당 위치의 선택지를 고릅니다.
else if(currentKey == ConsoleKey.A)
{
ChoiceSelection(0);
}
else if (currentKey == ConsoleKey.S)
{
ChoiceSelection(1);
}
else if (currentKey == ConsoleKey.D)
{
ChoiceSelection(2);
}
else if (currentKey == ConsoleKey.F)
{
ChoiceSelection(3);
}
else if (currentKey == ConsoleKey.Z)
{
ChoiceSelection(4);
}
else if (currentKey == ConsoleKey.X)
{
ChoiceSelection(5);
}
else if (currentKey == ConsoleKey.C)
{
ChoiceSelection(6);
}
else if (currentKey == ConsoleKey.V)
{
ChoiceSelection(7);
};
};
}
매 프레임마다 실행할
업데이트 함수입니다.
[출력 중]이면
스킵 버튼 확인만 해보고
바로 출력 함수를 실행해주고
[출력 중이 아닐 때]는
스킵 버튼을 누르면
선택지 없을 때 다음 문장 출력
다른 버튼을 누르면
해당 위치의 선택지를
선택하도록 합시다~
만약 선택지 선택을
다른 방법으로 구현하시면
이 부분은 그냥 스킵만
구현해두시면 되겠습니다!
실제 출력하는 함수
public static void OnRead()
{
if (skip || nextTime <= time.ElapsedMilliseconds)
{
if(계산된 글자를 모두 출력 못 했을 경우)
else if(입력된 글자를 모두 계산 못 했을 경우);
if(모두 계산이 끝났고, 모두 출력이 끝난 경우);
};
}
이런 구조로 만들 겁니다!
우선 스킵이 켜져 있거나
다음 출력 시간을 넘었을 때
실행할 거구요
모두 출력이 됐는지 확인
모두 계산이 됐는지 확인
모두 끝났는지 확인
세 가지 순서로 갑니다~
if (currentReadIndex < calculatedText.Length)
{
//현재 위치의 텍스트를 출력 텍스트에 추가합니다.
showText += calculatedText[currentReadIndex];
//콘솔이기 때문에, 직접 출력해주도록 하겠습니다.
Console.Write(calculatedText[currentReadIndex]);
//읽는 위치를 한 칸 뒤로 옮겨놓습니다.
++currentReadIndex;
//다음 출력 시간을 현재시간 + 읽기 속도로 저장해둡니다.
nextTime = time.ElapsedMilliseconds + currentReadSpeed;
}
출력한 위치가 계산된 길이보다 작을 때
아직 출력할 것이 남았기 때문에
이 부분을 실행합니다~
한 글자만 출력한 뒤에
다음 출력 시간을 조정해둡니다.
else if (currentTextIndex < receiptText.Length)
{
//'['가 나오면 커맨드가 시작한 것으로 봅니다.
if (receiptText[currentTextIndex] == '[')
{
//커맨드가 시작한 위치를 저장해 둡니다.
commandStartIndex = currentTextIndex;
//다음 문자를 읽기 위해 위치를 조정합니다.
++currentTextIndex;
//현재 커맨드 깊이는 1입니다.
//']'가 1개 더 나와야 커맨드가 끝난다는 뜻입니다.
commandDepth = 1;
//입력받은 텍스트가 끝날 때까지 반복합니다.
while (currentTextIndex < receiptText.Length)
{
//'['가 나오면 깊이를 1 더합니다.
if (receiptText[currentTextIndex] == '[')
{
++commandDepth;
}
//']'가 나오면 깊이를 1 뺍니다.
else if (receiptText[currentTextIndex] == ']')
{
--commandDepth;
};
//깊이가 0이 된 경우에 커맨드가 끝났다고 봅니다.
if (commandDepth <= 0)
{
//앞에 계산된 텍스트가 있는 경우
if (calculatedText.Length - 1 > 0)
{
//마지막 글자와, 커맨드 전체를 확인해서 계산된 글자에 넣습니다.
//커맨드 확인은 나중에 설명드리도록 하겠습니다.
calculatedText += CommandChecker(calculatedText[calculatedText.Length - 1], receiptText.Substring(commandStartIndex, currentTextIndex - commandStartIndex + 1));
}
//앞에 계산된 텍스트가 없는 경우
//똑같이 커맨드를 넣는데, 마지막 글자가 공백인 것으로 합니다.
else
{
calculatedText += CommandChecker(' ', receiptText.Substring(commandStartIndex, currentTextIndex - commandStartIndex + 1));
};
//다음 글자를 읽게 하기 위해 위치를 옮겨둡니다.
++currentTextIndex;
break;
}
//커맨드 깊이가 아직 1이상인 경우
//커맨드가 끝나지 않았기 때문에 계속 갑니다.
else
{
++currentTextIndex;
};
};
}
//커맨드가 아닌 경우에는
//현재 글자를 계산된 문장에 추가합니다.
else
{
calculatedText += receiptText[currentTextIndex];
++currentTextIndex;
};
};
계산된 문장이 끝나고
아직 계산할 문장이 남은 경우
실행하는 부분입니다.
커맨드가 아닌 일반 글자인 경우
그냥 한 글자 추가를 하지만
커맨드에 돌입했을 땐 달라집니다.
[ PlayerName : 밤말팅 ]
같은 모습이면 '['면 시작, ']'면 끝으로
처리하기만 해도 돼서 참 편하겠지만
이걸로는 그냥 정해진 걸로만
실행이 되겠지요?
[ PlayerName : [Input] ]
이런 것도 처리해야 할 겁니다.
커맨드 읽기는 나중에 할 거라서
이걸 처리해서 보내주는 것만 해보죠.
'['이 나오면 깊이에 1을 더합니다.
1 2
[ PlayerName : [Input] ]
']'이 나오면 깊이에 1을 뺍니다.
1 2 1 0
[ PlayerName : [Input] ]
0이 된 순간에 처음 '['위치부터
현재 위치까지 문자열을 넘겨주면
커맨드 확인 함수가 알아서 해줄 테니
일단은 여기까지 해둡시다.
if (currentTextIndex >= receiptText.Length && currentReadIndex >= calculatedText.Length)
{
//읽기가 끝났다고 알립니다.
reading = false;
//다음 문장이 스킵되지 않게 꺼둡니다.
skip = false;
Console.Write('\n');
//선택지 개수에 맞게 선택지 표시를 합니다.
if (currentSelectionNumber > 0)
{
Console.WriteLine("A : " + currentSelection[0].context);
};
if (currentSelectionNumber > 1)
{
Console.WriteLine("S : " + currentSelection[1].context);
};
if (currentSelectionNumber > 2)
{
Console.WriteLine("D : " + currentSelection[2].context);
};
if (currentSelectionNumber > 3)
{
Console.WriteLine("F : " + currentSelection[3].context);
};
if (currentSelectionNumber > 4)
{
Console.WriteLine("Z : " + currentSelection[4].context);
};
if (currentSelectionNumber > 5)
{
Console.WriteLine("X : " + currentSelection[5].context);
};
if (currentSelectionNumber > 6)
{
Console.WriteLine("C : " + currentSelection[6].context);
};
if (currentSelectionNumber > 7)
{
Console.WriteLine("V : " + currentSelection[7].context);
};
return;
};
계산할 문장도 없고
출력할 문장도 없어진 상태에서
실행할 부분입니다.
읽기 끝났다고 알려준 후
스킵을 끄고
선택지 표시하는 것이 끝입니다.
별거 없죠?
이걸로 출력 부분은 끝입니다.
인식한 커맨드를 실행합니다.
private static string CommandText(string targetText)
{
//결과값을 저장할 문자열입니다.
//우선은 받아 온 그대로 넣어둡시다.
string returnText = targetText;
//커맨드 깊이를 저장합시다.
int commandStartIndex = 0;
int commandDepth = 0;
//문자열에서 추출한 커맨드를 저장할 공간입니다.
string checkedCommand;
//문자열 전체를 돌면서 확인해봅시다.
for (int index = 0; index < returnText.Length; ++index)
{
//'['가 나타나면
if (returnText[index] == '[')
{
//커맨드 시작 위치가 지정이 안되어있으면 지정합니다.
if(commandStartIndex == 0)
{
commandStartIndex = index;
};
//커맨드 깊이를 1 늘립니다.
++commandDepth;
}
//']'가 나타나면
else if (returnText[index] == ']')
{
//커맨드 깊이를 1 줄여봅니다.
--commandDepth;
//커맨드가 끝났다고 판단되었을 때
if (commandDepth <= 0)
{
//커맨드를 저장해둡니다.
checkedCommand = returnText.Substring(commandStartIndex, index - commandStartIndex + 1);
//반환할 텍스트에서 해당 커맨드 부분을 제거합니다.
returnText = returnText.Remove(commandStartIndex, index - commandStartIndex + 1);
//커맨드 시작위치로 이동합니다.
//이렇게 해야 놓치는 글자가 없이 확인 가능합니다.
index = commandStartIndex;
//저장된 커맨드가 null값이 아닐 때에만 실행합니다.
if (checkedCommand != null)
{
//시작 위치가 0 이하인 경우
if (index <= 0)
{
//위치를 0으로 보정한 후
index = 0;
//반환할 텍스트 앞부분에 커맨드의 반환값을 넣어둡니다.
returnText = CommandChecker(' ', checkedCommand) + returnText;
}
//시작 위치가 0보다 큰 경우
else
{
//반환할 문자열의 길이가 0보다 큰 경우
if (returnText != null && returnText.Length > 0)
{
//문자열에 커맨드 반환값을 넣습니다.
returnText = returnText.Insert(index, CommandChecker(returnText[index - 1], checkedCommand));
}
//반환할 문자열이 없는 경우
else
{
//커맨드 반환값을 반환할 문자열로 지정합니다.
returnText = CommandChecker(' ', checkedCommand);
};
};
};
//검색 위치를 한 칸 앞으로 이동해서
//커맨드로 인해 생겨난 글자도 확인합니다.
//위치를 최소 0으로 맞춰놓았고
//반복문에서 +1이 되기 때문에
//음수로 내려가도 음수 배열을 참조하지 않습니다.
--index;
};
};
//모든 작업이 끝난 뒤에 남은 텍스트가 없으면
//반복문 중간에도 null을 반환하고 끝냅니다.
if (returnText == null)
{
return null;
};
};
//정상적으로 모두 끝났다면 완료된 문자열을 반환합니다.
return returnText;
}
문자열을 받아서 커맨드 부분을 실행한 뒤
나온 결과 문자열을 반환하는 함수입니다.
커맨드 부분만 추출해서 실행하고
다시 넣어주고 있습니다.
문자열에 저장한다는 특성상
여러 가지 커맨드를 실행시킨 후
결과를 모두 하나로 합쳐주는 역할을
하고 있습니다!
그래서 CommandText인 것이죠~
합쳐 줄 함수를 만들었으니
개별로 확인할 함수도 만들어야겠죠?
위에 나온 CommandChecker를
확인해보도록 합시다!
private static string CommandChecker(char preText, string targetCommand)
{
//받은 글자가 2개 이하라면 []만 있거나
//제대로 받은 것이 아니기 때문에 공백을 반환합니다.
if (targetCommand.Length <= 2)
{
return "";
}
//2개 이상이면 앞 뒤 []를 자릅니다.
else
{
targetCommand = targetCommand.Substring(1, targetCommand.Length - 2);
};
//커맨드가 나뉘어지는 부분을 저장합니다.
int divideIndex = 0;
//현재 커맨드 외에 다른 커맨드가 있으면
//인식 대상에서 제외하기 위해서
//커맨드 깊이를 계산합니다.
int commandDepth = 0;
//인식한 커맨드가 어떤 커맨드인지 분류합니다.
CommandType currentCommand = CommandType.GetVariable;
//반환할 텍스트를 저장합니다.
string returnText = "";
//커맨드 분류와 실행을
//이 부분에 넣습니다.
//결과물을 한 번 더 공정해서 내보냅니다.
if (returnText == null || returnText.Length <= 0)
{
return null;
}
else
{
return CommandText(returnText);
};
}
CommandChecker의 앞부분입니다.
[] 안에 한 글자라도 있어야
커맨드로 인식할 수 있겠죠?
[]를 포함하기 때문에
2글자를 초과할 때만 넘겨줍니다.
제일 바깥쪽의 []는 방해가 되기 때문에
넘겨줄 때 자르면서 시작합시다.
[ 왼쪽 ? 오른쪽 ]
[ 왼쪽 : 오른쪽 ]
? 나 : 같은 커맨드 기점이 나오면
위치를 저장해두도록 합시다.
그리고 분류와 실행이 끝나면
혹시 모르니 한 번 더 가공해주는 것으로
함수를 끝내면 되겠습니다!
분류된 커맨드는 다음과 같습니다.
private enum CommandType
{
//변수 값 반환
GetVariable,
//변수 값 대입
Substitute,
//조사 표현
Postposition,
//조건문 확인
Condition,
//선택지 생성
Select
}
반환, 대입, 조사, 조건, 선택
다섯 개로 해두고
분류 작업을 시작합시다.
for (int index = 0; index < targetCommand.Length; ++index)
{
//'['나 '('가 나오면 깊이 추가
//']'나 ')'가 나오면 한 칸 빠져나옵니다.
if (targetCommand[index] == '[' || targetCommand[index] == '(')
{
++commandDepth;
}
else if (targetCommand[index] == ']' || targetCommand[index] == ')')
{
--commandDepth;
};
//깊이가 0일 때만 열기.
//순수 본인 커맨드만 읽도록 합시다.
if (commandDepth == 0)
{
// '/'가 있으면 [은/는]과 같이 조사 표현입니다.
if (targetCommand[index] == '/')
{
currentCommand = CommandType.Postposition;
divideIndex = index;
break;
}
// ':'가 있으면 대입입니다.
else if (targetCommand[index] == ':')
{
currentCommand = CommandType.Substitute;
divideIndex = index;
break;
}
// '?'가 있으면 두 가지 상황으로 나뉩니다.
else if (targetCommand[index] == '?')
{
//일단 나뉜 부분은 공통이기 때문에
//여기서 나뉘다고 알립니다.
divideIndex = index;
//왼쪽 부분만 떼어옵니다.
string leftArgument = targetCommand.Substring(0, divideIndex).Trim();
//왼쪽 부분의 맨 처음이 '('일 때, 맨 마지막이 ')'인 것이 확인되면
//현재 커맨드는 선택지임을 알립니다.
if (leftArgument.Length > 0 && leftArgument[0] == '(' && leftArgument[leftArgument.Length - 1] == ')')
{
currentCommand = CommandType.Select;
break;
}
// '( 내용 )' 형태가 아닌 경우
//현재 커맨드가 조건문임을 알립니다.
else
{
currentCommand = CommandType.Condition;
break;
};
};
};
};
[ [조건?참:거짓] : TRUE ]
이런 커맨드를 처리를 할 때
안에 있는 커맨드는 무시를 하고
현재 커맨드가 무엇인지
알아야 하기 때문에
[ ] 안은 무시하기로 합시다.
그럼 커맨드 깊이가 0일 때만 읽어야겠죠!
깊이가 0이고 처음 나온 기호를 사용합시다.
[ 종성 있을 때 / 종성 없을 때]
[변수명 : 값]
형태는 그냥 바로 확인하면 되지만
[ 조건 ? 참 : 거짓]
[ (선택지) ? 선택시 : 미선택 ]
같은 경우에는 처음이 ?로 같습니다.
그래서 왼쪽 부분을 확인해서
(내용)의 형태로 되어있는지만
확인해주면 되겠습니다~
if (currentCommand == CommandType.Postposition)
{
//함수 시작에 받아온 직전 문자를 사용합니다.
//직전 문자가 종성이 있으면 왼쪽, 없으면 오른쪽을 반환합니다.
//반환할 때, 혹시 모르니 커맨드를 읽어본 후에 반환합시다.
if (FinalConsonant(preText))
{
returnText = CommandText(targetCommand.Substring(0, divideIndex).Trim());
}
else
{
returnText = CommandText(targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1).Trim());
};
}
else if (currentCommand == CommandType.Substitute)
{
//0번은 왼쪽입니다.
string arg0 = CommandText(targetCommand.Substring(0, divideIndex).Trim());
//1번은 오른쪽입니다.
string arg1 = CommandText(targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1).Trim());
//왼쪽 문자열을 구분해서 사용합니다.
//미리 예약된 변수는 분류해서 넣어줍시다.
// Speed에 대입을 시도한 경우
if (arg0 == "Speed")
{
//문자열을 숫자로 바꾸기 위해 int를 선언해둡니다.
int checkedNumber;
//오른쪽 값을 숫자로 바꿉니다.
if (arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
{
//바꾼 값을 현재 읽기 속도에 대입합니다.
currentReadSpeed = checkedNumber;
}
//오른쪽 값이 비어있거나 숫자가 아닌 경우
else
{
//기본 읽는 속도를 대입해놓습니다.
currentReadSpeed = globalReadSpeed;
};
}
// TalkName에 대입할 경우
else if (arg0 == "TalkName")
{
//오른쪽 문자열을 그대로 대상 이름에 넣습니다.
ChangeName(arg1);
}
// Wait에 대입할 경우
else if(arg0 == "Wait")
{
//문자열을 숫자로 저장하기 위해 int를 선언해둡니다.
int checkedNumber;
//오른쪽에 숫자가 있는 경우
if (arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
{
//다음 출력 시간을
//현재 시간 + 대기 시간으로 조정합니다.
nextTime = time.ElapsedMilliseconds + checkedNumber;
};
}
// Next에 대입할 경우
else if(arg0 == "Next")
{
//숫자로 만듭시다.
int checkedNumber;
if(arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
{
//맨 앞 글자가 +나 -라면
//현재 페이지에 +나 -한 값을 다음 페이지로 둡니다.
if(arg1[0] == '+' || arg1[0] == '-')
{
nextReadPage = currentReadPage + checkedNumber;
}
//그냥 숫자만 있으면 바로 대입합니다.
//숫자에 -1이 된 이유는 메모장 줄 읽는 것과
//배열의 위치가 달라서 그런 것이니
//이 부분은 조정하시면 됩니다!
else
{
nextReadPage = checkedNumber - 1;
};
//혹시나 페이지가 0 이하로 떨어지면 0으로 보정합니다.
if(nextReadPage < 0)
{
nextReadPage = 0;
};
};
}
//예약어에 속하지 않는 경우에는
//변수 목록에 추가를 시도합니다.
else
{
globalVariable.Set(arg0, arg1);
};
}
두 번째로 처리할 내용은
[ 변수 : 값 ] 형태입니다.
왼쪽 변수에 오른쪽 값을 넣으면
끝나는 것이기 때문에
짧게 끝날 수도 있겠지만
예약어를 미리 둬서
일반 변수 쓰는 걸 좀 줄여 봅시다!
글자로 변수를 찾는 것은
생각보다 시간이 걸리니까요!
예약어 처리는
각 예약어마다 다르기 때문에
위에서 확인을 해보시고
딱 한 줄만 되어있는 부분을
파헤쳐 봅시다.
변수가 들어갈 리스트
그러면 잠시 클래스를 옮겨가야 합니다.
변수를 저장할 리스트이구요!
[단일 연결 리스트]로 만들어봅시다!
public class VariableList
{
//첫 번째 원소를 가리킵니다.
public Variable start;
//마지막 원소를 가리킵니다.
public Variable end;
//마지막으로 사용한 원소를 가리킵니다.
public Variable lastContactVariable;
//원소의 개수를 받아올 수 있게 읽기 전용으로 둡니다.
public int count { get { return realCount; } }
//실제 원소 개수는 여기서 기록합니다.
private int realCount;
//생성자는 그냥 모두 초기화시킵니다.
public VariableList()
{
start = null;
end = null;
lastContactVariable = null;
lastContactIndex = 0;
}
}
처음과 끝 원소와
마지막으로 접근한 원소를 저장하고
개수만 표시하는
아주 간단한 구조입니다.
각 원소에는
변수명, 값, 다음 변수 주소
딱 세 개만 쓰여있죠.
일단 변수를 찾아오는 것부터 시작합시다.
변수를 확인해봅시다.
public Variable Find(string wantName)
{
//리스트에 남은 것이 없으면 바로 null을 반환합니다.
if (count <= 0)
{
return null;
};
//마지막으로 사용한 원소를 먼저 확인합니다.
if (lastContactVariable.name == wantName)
{
return lastContactVariable;
};
//변수 검색을 시작하기 위해서
//처음 변수를 등록합니다.
Variable check = start;
//모든 원소를 돌면서 확인해봅시다.
for (int index = 0; index < count; ++index)
{
//대상이 없는지는 확인한 후에
if (check != null)
{
//현재 확인 중인 원소의 이름을 확인합니다.
if (check.name == wantName)
{
//마지막으로 접근한 위치를 저장합니다.
lastContactVariable = check;
//현재 확인한 원소를 반환합니다.
return check;
}
//아니면 다음 원소로 갑니다.
else
{
check = check.next;
};
}
//대상이 없으면 바로 null을 반환합니다.
else
{
return null;
};
};
//다 돌아도 없으면 null을 반환합니다.
return null;
}
우선 마지막으로 접근한 원소를 보고
없으면 돌면서
이름을 대조해보고
맞으면 반환하는 것이 끝입니다!
public string Get(string wantName)
{
Variable target = Find(wantName);
if (target != null)
{
return target.value;
};
return null;
}
찾기가 끝났으면 쉬운 것이
값을 받아오는 것이죠!
변수가 있으면 해당 위치의 값을
그대로 가져옵니다.
변수를 넣어봅시다.
public void Set(string wantName, string wantValue)
{
//대상을 찾아옵니다.
Variable target = Find(wantName);
//대상이 없으면
if(target == null)
{
//새로운 변수를 만듭니다.
target = new Variable(wantName, wantValue);
//마지막 원소가 있다면
if (end != null)
{
//마지막 원소의 다음 원소로
//새로 만든 원소를 넣습니다.
end.next = target;
}
//마지막 원소가 없다면
//첫 번째 원소도 없는 것이기 때문에
//첫 번째 원소를 새로운 원소로 등록합니다.
else
{
start = target;
};
//마지막 원소로 등록합니다.
end = target;
//마지막으로 접근한 원소를 조정합니다.
lastContactVariable = target;
//원소 개수 표시를 늘립니다.
++realCount;
}
//찾은 대상이 있으면 대상의 값을 바꿉니다.
else
{
target.value = wantValue;
};
}
변수 입력은 변수 확인을 하고
이미 있는 변수면
값만 살짝 바꿔주고
없었던 변수라면
새로 만들어 등록해줍니다.
다시 커맨드 확인으로
그럼 변수 입력을 끝냈으니
다시 커맨드로 돌아갑시다~
else if (currentCommand == CommandType.GetVariable)
{
//각 예약어마다 변수를 돌려줍니다.
if (targetCommand == "Speed")
{
returnText = Convert.ToString(currentReadSpeed);
}
else if (targetCommand == "TalkName")
{
returnText = talkName;
}
else if(targetCommand == "Next")
{
returnText = Convert.ToString(nextReadPage);
}
else if(targetCommand == "Wait")
{
returnText = null;
}
// Input명령이 나오면 입력 창으로 이동합니다.
else if(targetCommand == "Input")
{
//입력을 보여줍니다.
Console.Write('\n');
Console.WriteLine("입력 :");
returnText = Console.ReadLine();
//입력이 끝나면 원래 보여주던 내용을 다시 보여줍니다.
Console.Clear();
Console.WriteLine(talkName);
Console.Write(showText);
}
//예약어가 없으면 등록된 변수가 있는지 확인합니다.
else
{
returnText = globalVariable.Get(CommandText(targetCommand));
};
}
if(arg0.Length > 0)
{
//기호의 왼쪽 끝입니다.
int leftEnd = 0;
//기호의 오른쪽 끝입니다.
int rightStart = 0;
// '='가 있는지 확인합니다.
bool checkEqual = false;
// '!='인지 확인합니다.
bool checkNotEqual = false;
// '>'인지 확인합니다.
bool checkBigger = false;
// '<'인지 확인합니다.
bool checkSmaller = false;
//여기에 기호 확인과
//조건 확인이 들어갑니다.
}
//조건이 없었을 땐, 무조건 참으로 계산합니다.
else
{
conditionCheck = true;
};
조건식 부분을 확인하는
기본 부분입니다.
기호는 두 개 이상이 될 수 있어서
시작과 끝 부분을 저장해둡시다.
=, !=, <, > 여부를 확인할
bool값을 선언해두겠습니다.
for (int index = 0; index < arg0.Length; ++index)
{
// '='가 나왔을 때
if (arg0[index] == '=')
{
//바로 앞에 '!'가 있으면
if(index > 0 && arg0[index - 1] == '!')
{
//같지 않다고 확인합니다.
checkNotEqual = true;
//왼쪽 끝이 맞춰져 있지 않으면
//'!' 전이 왼쪽 끝이라고 알립니다.
if(leftEnd == 0)
{
leftEnd = index - 2;
if(leftEnd < 0)
{
leftEnd = 0;
};
};
//오른쪽 끝을 조정합니다.
rightStart = index + 1;
}
//바로 앞에 '!'가 없고 같지 않아야 한다고 등록되지 않았을 때
else if(!checkNotEqual)
{
//등호를 켭니다.
checkEqual = true;
//왼쪽 끝과 오른쪽 끝을 조정합니다.
if (leftEnd == 0)
{
leftEnd = index - 1;
};
rightStart = index + 1;
};
}
//부등호도 같은 방식으로 등록합니다.
else if(arg0[index] == '>')
{
checkBigger = true;
if (leftEnd == 0)
{
leftEnd = index - 1;
};
rightStart = index + 1;
}
else if (arg0[index] == '<')
{
checkSmaller = true;
if (leftEnd == 0)
{
leftEnd = index - 1;
};
rightStart = index + 1;
};
};
기호 확인은 위치를 맞추는 것도
같이 하도록 합니다.
조건식에서 왼쪽과 오른쪽을
구분하기 위해 기호가 나왔을 때
위치를 조금 조정합니다.
그리고 각 기호에 맞는
스위치를 켜주는 것으로
기호 확인은 끝입니다.
//양쪽 다 위치가 0인 경우
//기호가 없는 것으로 판단합니다.
if(leftEnd == 0 && rightStart == 0)
{
//조건식에 이름만 있는 것으로 보고
//해당 이름을 가진 변수를 받아옵니다.
//해당 변수가 있고, 값이 false가 아니면 true로 칩니다.
if (globalVariable.Get(CommandText(arg0)) != null && globalVariable.Get(CommandText(arg0)).ToLower() != "false")
{
conditionCheck = true;
};
}
//기호를 발견한 경우
else
{
//변수의 값을 받아옵니다.
//변수 이름을 조정하기 위해서 커맨드를 모두 실행해둡니다.
string leftText = CommandText(arg0.Substring(0, leftEnd).Trim());
string leftValue = globalVariable.Get(leftText);
if (leftValue == null)
{
leftValue = leftText;
};
//오른쪽은 확인할 값입니다.
//역시 커맨드를 실행해둔 후 값을 받아옵니다.
string rightText = CommandText(arg0.Substring(rightStart, arg0.Length - rightStart).Trim());
string rightValue = globalVariable.Get(rightText);
if (rightValue == null)
{
rightValue = rightText;
};
//확인할 스위치에 맞는 연산을 진행합니다.
if (checkEqual && leftValue == rightValue)
{
conditionCheck = true;
}
else if (checkNotEqual && leftValue != rightValue)
{
conditionCheck = true;
};
//같거나 같지 않거나 상관 없이
//부등호는 그대로 진행합니다.
if (checkBigger || checkSmaller)
{
//왼쪽과 오른쪽은 모두 숫자여야합니다.
int leftNumber;
int rightNumber;
if (int.TryParse(leftValue, out leftNumber) && int.TryParse(rightValue, out rightNumber))
{
//해당 토큰에 맞는 연산을 진행합니다.
if (checkBigger && leftNumber > rightNumber)
{
conditionCheck = true;
};
if (checkSmaller && leftNumber < rightNumber)
{
conditionCheck = true;
};
};
};
};
조건 확인은 간단합니다.
이미 스위치를 받아왔기 때문에
양쪽 값을 직접 연산해줘야 하는데요
'='와 '!='는 양립할 수 없기 때문에
둘 중 하나만 해주고
부등호는 다른 것과 상관없이
언제나 확인합니다.
조건을 확인하는 중에
어떤 것이라도 통과하면
참이 되는 시스템입니다!
그렇게 하면 >=나 <=도
둘 중 하나만 적용되면
통과할 수 있겠죠?
극단적으로 <>로 하면
!=와 같은 역할이 되고
<=>는 항상 참이 되겠죠?
이런 느낌으로 만들어보았습니다~
//나뉘는 위치를 표시합니다.
int resultDivideIndex = -4;
//커맨드 깊이를 저장합니다.
int rightCommandDepth = 0;
//다른 커맨드가 닿지 않는 ':'를 찾아서
//나뉘는 위치로 설정합니다.
for(int index = 0; index < arg1.Length; ++index)
{
if(arg1[index] == '[')
{
++rightCommandDepth;
}
else if(arg1[index] == ']')
{
--rightCommandDepth;
}
else if(arg1[index] == ':' && rightCommandDepth == 0)
{
resultDivideIndex = index;
break;
};
};
//값이 참이거나, 나뉘지 않았을 경우
if (conditionCheck || resultDivideIndex < 0)
{
//나뉘지 않아서 왔을 때는 문자열을 그대로 반환합니다.
if(resultDivideIndex < 0)
{
return CommandText(arg1.Trim());
}
//참이어서 왔을 때에는 참 부분만 반환합니다.
else
{
return CommandText(arg1.Substring(0, resultDivideIndex).Trim());
};
}
//거짓일 경우
else
{
//거짓 부분을 반환합니다.
return CommandText(arg1.Substring(resultDivideIndex + 1, arg1.Length - resultDivideIndex - 1).Trim());
};
private static void CreateSelection(string wantContext, string wantOnSelect, string wantNotSelect)
{
//현재 생성된 선택지 개수가
//최대치에 도달하지 않았을 때에만 추가합시다.
if(currentSelectionNumber < currentSelection.Length)
{
//선택지 개수의 위치에 새로운 선택지를 만들면
//선택지 맨 마지막 부분에 놓은 것과 마찬가지 효과를 낼 수 있습니다.
currentSelection[currentSelectionNumber] = new SelectInformation(wantContext, wantOnSelect, wantNotSelect);
//선택지 개수를 하나 늘려줍니다.
++currentSelectionNumber;
};
}
SelectionInformation
이라는 것이 나오는데요
지문, 선택, 미선택
세 개의 변수로 이루어져 있어서
선택지 커맨드에 있는 것을
그대로 저장해주는 클래스입니다.
이걸 선택지 마지막 위치에
생성해 주기만 하면
선택지 생성은 끝이죠~
선택지를 선택해봅시다.
private static void ChoiceSelection(int wantNumber)
{
//읽기가 끝난 상태에서 받은 선택지 번호가
//존재하는 선택지 범위 내에 있어야 실행합니다.
if (!reading && wantNumber < currentSelectionNumber)
{
//모든 선택지를 돌면서 확인합니다.
for(int index = 0; index < currentSelectionNumber; ++index)
{
//만약 선택지 누락이 발생하면 멈춥니다.
if(currentSelection[wantNumber] == null)
{
break;
};
//유저가 고른 선택지가 맞다면
if(index == wantNumber)
{
//선택 시 실행할 내용을 실행합니다.
if(currentSelection[wantNumber].onSelectedAction != null)
{
CommandText(currentSelection[wantNumber].onSelectedAction);
};
}
//다른 선택지들은
else
{
//선택되지 않았을 때 실행할 내용을 실행합니다.
if (currentSelection[wantNumber].notSelectedAction != null)
{
CommandText(currentSelection[wantNumber].notSelectedAction);
};
};
//작업이 끝나면 선택지를 지웁니다.
currentSelection[index] = null;
};
//선택지를 모두 지웠기 때문에
//개수를 0으로 맞춥니다.
currentSelectionNumber = 0;
//바로 다음 지문으로 넘어가도록 합시다.
StartRead(nextReadPage);
};
}
public static string CommandText(string targetText)
{
//반환해 줄 문자열을 저장해둡시다.
string returnText = targetText;
//커맨드가 시작하는 위치를 저장합니다.
int commandStartIndex = 0;
//커맨드가 시작되었다고 표시합니다.
bool commandReading = false;
//해당 문자열을 돌면서 확인합니다.
for(int index = 0; index < returnText.Length; ++index)
{
//'['가 나오면 커맨드가 시작되는 것으로 판단합니다.
if(returnText[index] == '[')
{
//커맨드 시작 위치가 현재 위치라고 알립니다.
commandStartIndex = index;
//커맨드가 시작되었다고 알립니다.
commandReading = true;
}
//커맨드가 시작되어 있는 상태에서, ']'가 나오면 커맨드가 끝났다고 판단합니다.
else if(commandReading && returnText[index] == ']')
{
//커맨드 시작 위치부터 현재 위치까지의 문자열을 저장합니다.
string checkedCommand = returnText.Substring(commandStartIndex, index - commandStartIndex + 1);
//커맨드 부분을 문자열에서 지웁니다.
returnText = returnText.Remove(commandStartIndex, index - commandStartIndex + 1);
//확인 중인 문자열 위치를 조정합니다.
index = commandStartIndex;
//커맨드 실행 후에 나온 결과물을 위치에 넣습니다.
returnText = returnText.Insert(index, CommandChecker(returnText[index - 1], checkedCommand));
//결과물을 넣은 후에, 한 칸 앞으로 이동해서 변환한 부분부터 읽게 합니다.
--index;
//커맨드 읽기가 끝났다고 알립니다.
commandReading = false;
};
};
//조정이 끝난 문자열을 반환합니다.
return returnText;
}
간단합니다!
문자열 돌면서 확인하는 거구요
'['가 나오면 위치를 저장, 커맨드 블록에 들어왔다고 알려놓은 뒤
']'가 나오면 저장된 위치부터 지금 위치까지를 떼오면 됩니다!
떼오면서 잠깐 확인하는 위치를 조정하는 것이 끝이네요~
public static string CommandChecker(char preText, string targetCommand)
{
//대상이 2글자보다 작다면, 내용이 없는 것으로 공백을 반환합니다.
if(targetCommand.Length <= 2)
{
return "";
}
//2글자보다 크다면, 내용이 있으므로 맨 앞 뒤에 있는 대괄호를 자릅시다.
else
{
targetCommand = targetCommand.Substring(1, targetCommand.Length - 2);
};
//대괄호를 자르고 넘어온 내용물을 확인합니다.
switch (targetCommand)
{
//저는 Info라는 클래스에서 내용물을 받는 걸로 만들어놓았습니다.
//여기서 원하는 내용을 실행하셔도 되고
//다른 곳에서 내용을 받아오셔도 됩니다!
//그냥 커맨드 구분이기 때문에 마음대로 쓰시면 되겠습니다~
case "PlayerName":
return Info.playerName;
case "PlayerClass":
return Info.playerClass;
case "TargetLocation":
return Info.targetLocation;
//여기가 핵심적인 부분입니다.
//확인되지 않은 커맨드인 경우에
//가운데에 '/'이 있는지 확인해서 나누어주도록 합시다.
default:
//나뉘어지는 위치를 저장하는 곳입니다.
int divideIndex = 0;
//나뉘어지는지 확인하는 곳입니다.
bool isDivide = false;
//문자열을 돌면서 '/'를 찾습니다.
for(int index = 0; index < targetCommand.Length; ++index)
{
//'/'를 찾은 경우에 나뉘어진다고 알린 후, 위치를 저장합니다.
if (targetCommand[index] == '/')
{
isDivide = true;
divideIndex = index;
break;
};
};
//나뉘어지는 것이 확인된 경우
if (isDivide)
{
//직전 문자에 종성이 있는지 확인합니다.
if (FinalConsonant(preText))
{
//종성이 있으면 왼쪽 문자열을 반환합니다.
return targetCommand.Substring(0, divideIndex);
}
else
{
//종성이 없으면 오른쪽 문자열을 반환합니다.
return targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1);
};
}
//나뉘어지지 않으면 확인되지 않은 커맨드이므로 공백을 반환합니다.
else
{
return "";
};
};
}
이제 커맨드를 실행해보도록 합시다!
커맨드를 실행하는 함수는 커맨드 들어오기 직전 문자와 커맨드 블럭을 받아옵니다.
[커맨드]의 형태로 받아오기 때문에 앞 뒤를 자를 건데요!
[]같은 걸로 들어오면 바로 돌려보내주도록 합시다.
자른 뒤에는 switch문을 사용해서 커맨드를 걸러내고
등록된 커맨드의 경우 그냥 실행을 해줍니다~
아닌 경우만 조금 생각해보죠!
[아무렇게나 쓰인 커맨드]가 들어왔을 때에는 당연히 아무 것도 하지 않아야 하기 때문에
[이렇게/저렇게]같은 형태로 되어있는지 확인을 해주도록 합니다.
'/'의 위치를 확인하고 저장한 후에, 있다고 알려주기만 하면 되겠죠?
종성이 있으면 왼쪽 / 오른쪽 애를 가져다 주면 되는데!
종성이 있는지 체크만 하면 되겠네요~
가봅시다!
public static bool FinalConsonant(char wantCharacter)
{
//한글의 범위를 넘어서면 종성이 없다고 알립니다.
if(wantCharacter < 44032 || wantCharacter > 55215)
{
return false;
};
//대상 문자를 조정하기 위해 저장해둡니다.
int checker = wantCharacter;
//한글의 시작 위치인 44032를 빼서 0으로 맞춥시다.
checker -= 44032;
//28로 나누어서 나머지가 있으면 종성이 있는 것으로 칩니다.
return (checker % 28) > 0;
}
핵심 부품인데 생각보다 뭐가 없네요?
유니코드는 간격이 일정해서 생각보다 구현이 쉽습니다!
44032를 기준점인 0으로 잡고
28번째마다 종성이 없는 글자가 나오기 때문에
28로 나눠서 나머지가 0인 애들만 false로 주면 되겠죠!
한글이 아닌 경우에는 무조건 false를 반환하도록 하고 있는데요,
만약 다른 언어도 원하는 경우 추가하시면 됩니다.
예를 들어 일본어 っ(12387), ん(12435),ッ(12483), ン(12531)는 종성의 역할을 하기 때문에