[C#] 문자열로 조작 가능! 대화창 만들기!
안녕하십니까. 밤말팅입니다!
저번 시간에는 [은/는] 과 같이
앞 글자에 영향을 받는 조사를
변환시켜 보았는데요!
[C#]은는이가 타파!! 자연스러운 문장 만들기!
안녕하십니까 밤말팅입니다! 오늘은 가벼운 걸로 한 번 가져와보았습니다! 게임에 보면 꼭 이런 게 있곤 하죠 밤말팅(은)는 정의의 망치(을)를 얻었다! 한국어에 있는 조사는 "앞에 오는 종성"에
game-part-factory.tistory.com
은는이가 변환은 위 링크에서
확인하실 수 있습니다!
이번엔 변환에 그치지 말고!
대화창을 만들어봅시다!
<목표>
//외부에서 접근하는 모든 함수
//몇 번째 페이지를 읽게 할지 고릅니다.
TextReader.StartRead(int 읽을문장의 번호);
//매 프레임마다 불러 출력을 시도합니다.
TextReader.Update();
//변수를 등록하거나 변경합니다.
TextReader.globalVariable.Set(string 변수명, string 값);
//변수의 값을 가져옵니다.
TextReader.globalVariable.Get(string 변수명);
커맨드와 내용을 입력하면
천천히 출력해주는 프로그램을
만드는 것이 이번 목표입니다.
[Speed:10] 이 프로그램은 대화를
시뮬레이션하도록 제작되었습니다.
이렇게 쓰면
아래처럼 나오는 거죠.
제일 먼저 천천히 문자를 출력하는
기능을 만든 후에
커맨드 인식도 만들어봅시다!
시제품은 위의 파일을 다운로드하여
확인하실 수 있습니다.
들어있는 [CONTEXT.txt]를 변경하여
원하는 내용을 실행할 수 있습니다.
<네임스페이스 참조>
using System;
<구상>
매 업데이트 시기에 받아둔 문자열에서
다음 내용을 계산한 후에
결괏값을 한 글자씩 출력합니다.
더 출력할 값이 없다면
입력에 다음 계산 내용을 넣는 것으로
반복하면 한 글자씩 출력하는
프로그램을 만들 수 있을 겁니다.
그리고 이번에는 커맨드 종류를
5가지로 늘려보겠습니다!
저번에는 변숫값 출력과
조사 처리만 했었는데요,
출력, 대입, 조사, 조건, 선택지
로 나누어 봅시다.
[ 변수명 ]
해당 변수의 값을 가져옵니다.
[ 변수명 : 값 ]
해당 변수에 값을 대입합니다.
[ 종성 있음 / 종성 없음 ]
직전 글자에 종성이 있으면 왼쪽 값
종성이 없으면 오른쪽 값을 반환합니다.
[ 조건 ? 참 : 거짓 ]
조건을 확인합니다.
조건은 =, !=, <, >으로 확인합니다.
<=이나 >=, <>같은 조합도 됩니다.
조건에 변수명만 있으면
변수가 등록이 안되어 있거나
값이 false라면 거짓 부분 실행
등록이 되어 있고 false가 아니라면
참 부분을 실행합니다.
[ ( 대답 ) ? 선택 : 미선택 ]
선택지를 만듭니다.
현재 출력이 끝난 뒤에 선택하며
선택되면 선택 부분을 실행,
아니면 미선택 부분을 실행합니다.
한 문장에 8개까지 만들 수 있습니다.
[ ( 대답 ) ? 선택 ]
미선택 부분을 생략할 수도 있습니다.
그리고 예약어도 설정해둡시다.
Speed : 출력하는 속도를 정합니다.
(다음 출력까지 시간입니다.)
Next : 다음 읽을 줄을 정합니다.
PlayerName : 유저의 이름입니다.
TalkName : 상대의 이름입니다.
Wait : 출력을 기다립니다.
Input : 유저 입력을 받습니다.
일단은 이렇게만 만들어도 괜찮겠죠?
가봅시다!
<구현>
기반이 될 메인 클래스의 모습입니다.
모든 변수와 함수는
static으로 이루어질 것이구요
그래서 생성자는 필요가 없습니다~
내용은 크게
출력 부분, 커맨드 부분, 선택지 부분
세 가지로 나누면 되겠습니다!
그전에 우선 변수부터 보죠~
//현재 등록된 선택지 배열입니다.
//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());
};
}
첫 번째로 처리할 내용은
[ 은 / 는 ]의 형태입니다.
앞 글자를 미리 CommandChecker에서
인자로 받아오기 때문에
그 글자가 종성이 있는가만 확인해서
반환할 위치의 문자열을 내보냅시다.
[C#]은는이가 타파!! 자연스러운 문장 만들기!
안녕하십니까 밤말팅입니다! 오늘은 가벼운 걸로 한 번 가져와보았습니다! 게임에 보면 꼭 이런 게 있곤 하죠 밤말팅(은)는 정의의 망치(을)를 얻었다! 한국어에 있는 조사는 "앞에 오는 종성"에
game-part-factory.tistory.com
종성의 확인은 위 링크에서
확인하실 수 있습니다~
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));
};
}
예약어가 나오면 그냥 돌려주지만
Input이 나오면
잠깐 입력 창을 띄웁니다.
다른 변수를 찾을 땐
변수 리스트에서 가져옵시다!
다음은 조건식을 만들어 볼 건데요!
제일 길기 때문에 나눠서
설명해드리도록 할게요.
크게는 기호 확인, 조건 확인, 실행
세 가지로 갑니다.
일단 기본 세팅부터 갑시다.
else if(currentCommand == CommandType.Condition)
{
string arg0 = targetCommand.Substring(0, divideIndex).Trim();
string arg1 = targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1).Trim();
bool conditionCheck = false;
//여기에 아래 내용들이 들어갑니다.
}
일단 0번은 왼쪽
1번은 오른쪽 저장해 두고
conditionCheck로
결과가 참인지 확인합니다.
제일 먼저 왼쪽에서 하는
기호 확인과 조건 확인부터 합시다.
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());
};
실행 부분은 이미
커맨드 실행을 만들었기 때문에
왼쪽 오른쪽만 나누고
가공되지 않은 문자를
그대로 실행에 넘겨줍시다.
주의할 점은
[참 : 거짓]의 형태가
아닌 경우엔
그냥 전체를 실행합니다.
안 그러면 아무것도
실행하지 않게 됩니다~
else if(currentCommand == CommandType.Select)
{
//왼쪽에 있는 것은 "(내용)"의 형태이기 때문에
//앞뒤를 잘라서 "내용"으로 만듭니다.
string arg0 = targetCommand.Substring(0, divideIndex - 1).Trim();
arg0 = arg0.Substring(1, arg0.Length - 2).Trim();
//오른쪽은 그냥 둡니다.
string arg1 = targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1).Trim();
//오른쪽 구분용으로 놔둡시다.
int rightDivide = 0;
int rightCommandDepth = 0;
bool rightDivided = false;
//오른쪽을 ':'기준으로 나눕니다.
if(arg1.Length > 0)
{
for(int index = 0; index < arg1.Length; ++index)
{
if(arg1[index] == '[')
{
++rightCommandDepth;
}
else
{
--rightCommandDepth;
};
if(rightCommandDepth == 0 && arg1[index] == ':')
{
rightDivided = true;
rightDivide = index;
break;
};
};
};
//선택, 미선택 부분을 넣기 위해
//문자열을 선언해둡니다.
string onSelect;
string notSelect;
//나눠진 상태면 나눠서 넣습니다.
if (rightDivided)
{
onSelect = arg1.Substring(0, rightDivide).Trim();
notSelect = arg1.Substring(rightDivide + 1, arg1.Length - rightDivide - 1).Trim();
}
//나눠지지 않았으면 선택에 다 넣습니다.
else
{
onSelect = arg1;
notSelect = "";
};
//선택지를 만듭니다.
CreateSelection(arg0, onSelect, notSelect);
};
선택지는 형태가
[(선택지)?선택:미선택]
이런 모습이기 때문에
왼쪽은 괄호를 떼고
그대로 보관합니다.
오른쪽에서 ':'를 기준으로
문자열을 나눠서 보관합니다.
실제 실행은 선택할 때
할 것이기 때문에
지금은 문자열로만
보내도록 합시다.
만약 ':'가 없을 때에는
그냥 선택될 때 전체 지문이
실행되도록 조정합니다.
이렇게 하면 드디어!
커맨드 분류가 모두 끝났네요!
선택지 부분만 조금 건들면
끝입니다~
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);
};
}
선택은 모든 선택지를 확인해서
본인 번호와 일치하는지 본 후
선택 / 미선택 부분을 실행,
모든 선택지를 초기화한 뒤에
다음 문장을 실행해줍니다.
미리 다 만들어 두었더니
정말 편하네요!
마지막 이름 변경 함수는
그냥 이름을 바꿔주고
출력하는 것이 전부라
넘어가도록 하겠습니다~
<마치며>
간단하게 대화창만 만들려 했는데
커맨드가 들어가면서
생각보다 커지게 되었습니다.. ㅎㅎ
언젠가 스크립트 언어를
인식하는 기능을 넣을 때
도움이 좀 될 수도 있겠네요~
긴 글 읽어주셔서 감사드리고
다음 포스팅에서 뵙겠습니다!