이번엔 구를 그립시다!

 

 

원이 빙글빙글 도는 것은

 

보고만 있어도 즐겁죠!

 

 

 

[Math] 삼각함수로 원 그리기!

안녕하세요! 밤말팅입니다~ 최근 삼각함수를 사용해서 원을 그릴 일이 조금 있었는데요! 아무래도 함수만 가지고는 잘 와닿지 않는 부분이라서 헷갈려하시는 분들이 조금! 있으신 것 같아서ㅎ

game-part-factory.tistory.com

 

저번 시간에 저희는 삼각함수로

원을 그리는 데에 성공했습니다!

 

CosSin만으로 이뤄낸 성과죠!

 

오늘은 여기서 한 단계

 

더 나아갑니다!

 

 

이번엔

로 가봅시다!

 

3차원으로 이동해봅시다

위에서 보여드렸던 그림을

 

살짝 뉘여놓은 그림입니다!

 

그리고 을 하나 더 늘렸어요!

 

 

드디어 저희는 3차원으로 이동합니다!

 

원을 그릴 때처럼

 

"각도"를 이용해서 그릴 건데요!

 

각도 방향
90도
0도
-90도 아래

 

이런 느낌으로 잡아봅시다!

 

 

그렇지만 사실

3D로 넘어왔다고 해서

을 그리는 데에

다른 걸 사용하진 않습니다!

 

 

여전히 SinCos으로

그리기를 할 거에요!

 

 

 

위쪽으로 가는 Sin

이제 저희가 원래 사용하던 Sin

 

그대로 이용해서

 

만 위 아래로 맞췄어요!

별 다른 점은 없습니다!

 

그렇다면 이걸 적용하면

어떤 모양이 될까요?

 

 

 

수평에 적용된 애들은 지웠습니다!

뭔가 이상한 느낌이죠?

 

맘대로 늘어나고 줄어드는 것

같아보이기도 하고..

 

대각선으로만 도네요!

 

 

에서도 한 번 보죠!

 

 

 

 

에서 보면 이런 느낌입니다!

 

검은 선이 지금 Sin이에요!

 

오르내리고 있어서

수직선에 가리거나 보이거나 하죠?

 

그런데.. 위에서 볼 때에는

 

아까랑 변화가 없네요!

 

 

 

 

정확히 지금은 이런 모양으로

움직이는 중입니다!

 

원통과도 같다!

 

위 아래로 같이 움직이지만

 

문제는 위로 갈수록

구는 줄어들어야 하는데

 

줄어드는 부분이 없으니

 

원통처럼 움직이는 것이죠!

 

 

 

구는 위 아래가 더 작다!

그래서 저희는

수직 각도가 더 수록

 

원의 크기 자체를

줄여야 합니다!

 

 

다행히 이걸 위한

함수가 아직 남아있죠!

 

 

바로 수직 Cos입니다!

 

지금까지는 위아래로 Sin

적용을 했었죠?

 

 

 

적용한 현재 상태는

이렇습니다!

 

표시할 때 z를 높이라고 할게요!

 

 

x = Cos(수평각도)
y = Sin(수평각도)
z = Sin(수직각도)

 

라는 상태죠!

 

음.. 축 세 개를 모두 사용했는데

Cos(수직각도)는 어디에 넣어야할까요?

 

저희가 원의 크기를 줄이려고

생각했기 때문에

 

크기도 결국 Cos으로!

 

작아지는 양을 Cos으로

계산해볼 수 있습니다!

 

그래서 수평에서 계산했던 원

Cos의 크기대로 줄이기만

끝이에요!

 

대신 이제 코사인은

마이너스로도 간다!

음수로도 갈 수 있어서

절대값(Abs)으로 적용해줄게요!

 

여기선 크기음수일 수는

없으니까요!

 

 

x = Cos(수평각도) * Abs(Cos(수직각도))
y = Sin(수평각도) * Abs(Cos(수직각도))
z = Sin(수직각도)

 

위에서 그냥 Abs(Cos(수직각도))만 곱한 거죠!

 

그러면

 

자유분방

이런 형태가 됩니다!

 

음.. 아직 잘 모르겠네요!

 

 

지금은 수평각도수직각도

같은 속도로 움직이고 있어서

 

모양이 맘에 안드는데요!

 

직접 따로 돌리면

 

이런 모양입니다!


https://youtu.be/P-sPG7IEE7A

어떤가요?

 

직접 조종을 해보니 조금 더

그럴싸한 것 같네요!

 

결국 SinCos을 응용해서

원하는 방향을 가리키는

벡터 하나를 얻을 수 있었군요!

 

단지 3차원이라서

어디에다가 적용해야되는지

 

위치만 바뀌었을 뿐인 것 같네요!

 

자 그래서 간단하게

를 마쳐보았습니다!

 

혹시나 여기에서 더 궁금하신 점!

있으시면 남겨주시구요!

 

저는 그럼 또 다음 언젠가

다시뵙겠습니다~

 

읽어주셔서 감사합니다!!

안녕하세요! 밤말팅입니다~

 

최근 삼각함수를 사용해서 원을 그릴 일이

조금 있었는데요!

 

아무래도 함수만 가지고는

잘 와닿지 않는 부분이라서

 

헷갈려하시는 분들이 조금!

있으신 것 같아서ㅎㅎ

 

그림이랑 같이 들어갑니다!

 

귀여운 원!

동그라미입니다!

 

0도부터 360도까지

집어주면 돌아가는 녀석이죠!

 

이 녀석을 그리는 것이

목표입니다!

 

그런데 왜 삼각함수?

라고 생각하실 수 있으니까!

 

선을 좀 그어드릴게요!

 

 

삼각형 두 개가 된다!

 

선을 그어보았습니다!

 

자세히 보시면 삼각형이

두 개가 생긴 걸 볼 수 있죠!

 

둘 다 직각삼각형인 걸

확인하실 수 있습니다~

 

두 개의 각이 같은 삼각형

그러면 원이라고 하는 녀석에 있는

 

저 삼각형들은 아무튼 각도를 정해주면

 

크기가 어떻게 되었든 상관없이!

 

"비율"은 항상 똑같습니다!

 

위에 두 삼각형도 그냥

파란 삼각형을 복사해서

크기만 늘린 거예요! ㅎㅎ

 

높이 / 빗변

그래서 그 각도에 따라

비율은 항상 정해져 있으니!

 

그중에서도

높이 / 빗변

사인(Sin)

이라고 하고

 

그림으로는 이런 느낌!

각도에 따라 쭉 따라서

그리면 이렇게 됩니다!

 

실제로 반복된다는 느낌이죠?

 

이것은 0 ~ 2π 사이를

반복합니다!

 

 (2π * 반지름) 이 둘레니까!

반지름이 1인 원의

둘레를 나타내고

 

원의 둘레를 모두 돌았다면

다시 처음부터인 것이죠~

 

이런 값을 라디안이라고 합니다!

 

앞으로 자주 사용할 거예요!

 

 

 

 

..반복되는데

 

어쩌라는 거죠?

 

 

그래서 이걸

눈으로 보여드리기 위해

 

돌려보겠습니다!

 

 

 

이렇게!

이러면 좀 감이 오시나요?

 

시간에 따라 움직이다 보면

원의 "높이"와 똑같이

움직이고 있다는 것을

확인하실 수 있죠!

요겁니다 ㅎㅎ

 

좋아요! 내친김에 하나 더 하죠!

 

비슷해보이나요?

두 번째 그래프!

 

어.. 근데 아까랑 비슷해 보이네요?

 

요거는 조금 다른

코사인(Cos)

입니다!

 

물론 주기모양은 같지만!

 

시작을 1로 합니다!

 

너비 / 빗변

얘는 높이가 아니라

너비를 말하는 거거든요!

 

그래서 1로 시작하면 뭐가 되느냐?

 

살짝 돌려서

x축에다가 연결해봅니다!

 

느낌 오시나요?

 

돌립니다!

 

 

사인과 코사인의 콜라보

Sin은 0부터

Cos은 1부터

 

라고 하는 중요한 차이점으로

각각 원의 yx

 

표현할 수 있게 되는 거예요!

 

x = Cos( 값 )
y = Sin( 값 )

이런 느낌이 되는 것이죠!

 

값은 아까 말해드린 대로

0 ~ 2π

이구요!

 

라디안 각도
0 0
0.25π 45
0.5π 90
0.75π 135
1π 180
1.25π 225
1.5π 270
1.75π 315
2π 360

 

따져보면 이런 느낌입니다!

각도를 직접 쓰시고 싶으시면

각도 / 180 * π

(각도 * 0.0174533)

 

이렇게 쓰셔도 되구요!

 

반대로 라디안을 각도로

바꾸고 싶으시면

라디안 * 180 / π

(라디안 * 57.2958)

 

반대로 이렇게도 되겠죠!

 

 

그래서 가장 기본적인

삼각함수을 그리는

방법에 대해서 확인했습니다!

 

CosSin의 특징만 알면

되는 문제였네요!

 

좋습니다! 저희는 그럼 다음 장에선

 

이걸 가지고 3차원으로 가보도록 할게요!

 

 

 

다음 시간은

삼각함수로 구 그리기

입니다!

 

저번 시간에는 랜덤 박스를

포스팅했었습니다.

 

포스팅 중간에

이름을 검색하는 방법

나와 있었죠.

 

이런 식으로, 문자를 이어서

문자열을 찾는

Trie 자료구조였습니다.

 

이거면 모든 내용을 돌면서

문자열을 하나하나

대조할 필요가 없이

 

문자열 길이만큼의 참조

바로 원하는 내용에

갈 수 있게 도와주는

 

이정표 역할을 했었죠.

 

 

여기서 문자가 아닌

입력 키를 받으면

콤보 시스템을 만들 수 있습니다!

 

만들면서 Trie도 알아보도록 합시다~

 

<C# 코드>

ComboSystem.cs
0.01MB

다음과 같은 네임스페이스

참조가 필요합니다.

using System.Collections.Generic; //list<T>를 사용하기 위해서 참조

list만 쓰기 때문에

다른 list를 사용하실 분은

교체해주시면 되겠습니다!

 

<C++ 코드>

ComboSystem.h
0.00MB
ComboSystem.cpp
0.01MB

다음과 같은

헤더 참조가 필요합니다.

#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 "";
  };
}

대망의 콤보 구현 함수입니다!

이걸 위해 지금까지 달려왔죠~

 

근데 사실 생각보다 간단합니다.

 

Trie만 있으면 콤보는

이렇게 쉽게 완성되는 거네요!

 

 

천천히 살펴볼까요?

 

먼저 시간 안에 입력했는지

확인합니다.

 

시간을 넘었으면 null

직전 입력이 없으면 null

입력한 내용이 콤보에 없으면 null

 

이 세 가지의 경우를 뚫으면

다음 노드를 이어줍시다!

 

 

중간에 대기시간을 조정하구요.

 

 

위에서 null을 받았으면

콤보를 시작하는 키를 확인합니다.

 

전 콤보가 끊겨도

다시 시작할 수 있게 말이죠!

 

 

근데 다음 콤보에 있는 키도,

콤보 시작하는 키도 아니면

 

그냥 빈 문자열을 반환하구요.

 

둘 중 하나라도 해당한다면

 

그 노드를 lastChecked로 놓고

들어있는 문자열을 반환합니다.

 

물론 그래도 빈 문자열일 수 있지만

 

빈 문자열이면 아직 스킬이

나오지 않은 걸로 보면 되겠습니다!

 

 

 

<마치며>

이렇게 Trie를 사용한

콤보 시스템을 만들어 보았습니다!

 

Insert로 콤보를 넣고

Input으로 키 입력만 받으면

 

기본적인 콤보를 구현할 수 있겠네요!

 

물론 스킬 쓸 때 걸리는 시간도 있고

정지하는 시간도 있고 하지만

 

expiredTime을 조정해서

입력을 받으면 좋을 것 같습니다~

 

실시간으로 콤보를 넣고 뺄 수 있어서

 

튜토리얼에 콤보를

천천히 추가해 준다거나

 

조건을 만족하면

콤보를 얻는다거나

 

한 게임에 한 번만 쓸 수 있는

콤보를 만든다거나

 

유저가 직접

콤보를 만든다거나

 

 

다양한 곳에 응용할 수 있을 것 같네요!

 

만들기는 굉장히 쉬운 편이라

다양하게 응용해보시는 것도

좋을 것 같습니다!

 

 

글 읽어주셔서 정말 감사드리고

다음 포스팅에서 뵙겠습니다!

넣고, 뽑으면 끝나는 랜덤박스 만들기

 

안녕하세요! 밤말팅입니다.

 

이번에는 게임을 하면서

 

가장 흔하게 마주할 수 있는

 

"랜덤박스"

 

를 만들어보았습니다.

뭐가 나올지 두근두근해지는 그것

 

물론 굉장히 이슈가 되었던

 

물건이긴 하지만..

 

게임에서 확률

 

빼놓을 수 없는 것이라

 

 

언젠가는 필요할 것 같아서

 

한 번 만들어보게 되었네요!

 

 

<C#코드>

RandomBox.cs
0.05MB

C#코드는 다음과 같은

네임스페이스를 참조합니다.

using System.Collections.Generic; //List<T>를 위해 사용합니다.
using System; //랜덤기능을 위해 사용합니다.

사용에 제한이 있으신 분은

교체해서 사용하시면 되시겠습니다!

 

<C++ 코드>

RandomBox.h
0.01MB
RandomBox.cpp
0.03MB

C++코드는 다음과 같은

헤더파일을 참조합니다.

#include<vector>
#include<random>
#include<string>

역시나 위 헤더의 사용에

제한이 있으신 분은

교체하셔도 무방합니다.

 

 

<구성>

///랜덤박스의 아이템을 뽑아 문자열로 반환합니다. 뽑을 것이 없으면 nullptr을 반환합니다.
string Pick();

///원하는 아이템의 이름을 넣으면, 그 아이템의 정보를 가진 뷰어를 반환합니다.
ContentViewer GetViewer(string name);

///변동/부동확률의 모든 아이템을 보여주는 뷰어 리스트를 반환합니다.
///true는 변동, false는 부동확률을 나타냅니다.
vector<ContentViewer> GetViewerAll(bool wantFlexible);

///원하는 이름의 아이템을 삭제합니다.
void Delete(string name);

///원하는 아이템의 이름, 확률, 변동가능성을 설정하여 생성합니다. 아이템은 무한히 뽑을 수 있습니다.
bool Insert(string name, float wantRate, bool wantFlexible);

///원하는 아이템의 이름, 확률, 개수, 변동가능성을 설정하여 생성합니다. 아이템은 개수만큼만 뽑을 수 있습니다.
bool Insert(string name, float wantRate, unsigned int wantNumber, bool wantFlexible);

///원하는 아이템의 개수를 일정량 증가시킵니다.
void NumberAdd(string name, unsigned int wantNumber);

///원하는 아이템의 개수를 일정량 감소시킵니다.
void NumberSub(string name, unsigned int wantNumber);

///원하는 아이템의 개수를 원하는 개수로 변경합니다.
void SetNumber(string name, unsigned int wantNumber);

///원하는 아이템을 무한정 뽑을 수 있는지 정합니다.
void SetInfinity(string name, bool wantInfinity);

///원하는 아이템의 이름을 from에서 to로 변경합니다.
void SetName(string from, string to);

///원하는 아이템의 확률을 변경합니다.
void SetRate(string name, float wantRate);

///원하는 아이템의 변동 가능성을 변경합니다.
void SetFlexible(string name, bool wantFlexible);

///원하는 아이템의 활성화 여부를 결정합니다.
void SetAvailable(string name, bool wantAvailable);

랜덤박스의 함수들

소스코드에도 적혀있습니다!

랜덤박스의 내용물

구성은 아주 간단합니다.

 

Trie 자료구조라고 하죠

 

pi, te, to가 들어간 트라이

문자열을 쪼개서

 

밑으로 쭉 이어준 것인데요.

 

랜덤박스 내용물을

 

문자열로 만들 것이기 때문에

 

검색용으로 달아두었습니다.

 

확률은 부동과 변동이 있습니다.

그리고 확률이 두 가지가 있죠.

 

변하지 않는 확률 "부동 확률"과

 

변할 수 있는 확률"변동 확률"

 

두 개로 구성되어 있습니다.

 

 

부동확률의 입력

부동 확률은 변하지 않기 때문에

 

그대로 입력됩니다.

 

큰 확률부터 작은 확률까지

 

정렬되어 있어서

 

최소한의 참조로 확률을

 

구할 수 있게 해두었습니다.

 

변동 확률의 입력

변동확률은 부동확률을 빼고

 

남은 확률에다가 넣는 확률입니다.

 

부동확률이 늘어날수록

 

확률이 줄어들게 되겠죠?

 

변동확률 중에서 본인이 차지하는

 

비율만 유지하는 확률입니다.

 

혹시나 부동확률의 내용이

 

없어지거나, 비활성화 되거나

 

개수가 0이 되거나

 

이런 상황이 생겼을 때

 

남은 확률을 채워줄 수 있게

 

확률을 유연하게 조정하는

 

아이템들이라고 보시면 되겠습니다.

 

 

 

아이템 내부의 변수들

아이템들도 살펴보지 않으면

 

안되겠지요?

 

당연히 이름 들어가구요

 

확률 들어갑니다.

 

isFlexible은 위에 설명한

 

부동/변동의 설정이죠.

 

 

number는 남은 개수

 

isInfinity는 무한 여부입니다.

 

무한이면 그냥 뽑고

 

아니면 number를 확인해서

 

남은 개수가 있을 때에만

 

뽑아야겠네요!

 

 

isAvailable은 활성화 여부입니다.

 

뽑을 수 있는 상황일 때에도

 

이걸 꺼두면 나오지 않죠!

 

 

마지막 pickedTime은

 

지금까지 이 아이템이 뽑힌 횟수입니다.

 

있으면 편할 것 같아서 넣어놨네요!

 

<사용법>

//아이템을 뽑습니다.
string Pick();

아이템 뽑기입니다!

 

그냥 정해진 로직대로 뽑아주는

 

역할을 하구요.

 

뽑은 아이템의 이름을 반환합니다.

 

//아이템을 넣습니다.
bool Insert(string name, float wantRate, bool wantFlexible);
bool Insert(string name, float wantRate, unsigned int wantNumber, bool wantFlexible);

랜덤박스에 아이템을 넣는

 

함수입니다.

 

두 개의 차이점은 중간에

 

개수를 넣는다는 것인데요.

 

개수를 넣으면, 그 개수만큼만 뽑고

 

넣지 않으면 무한히 뽑을 수 있습니다.

 

//원하는 아이템 삭제
void Delete(string name);

넣는 것이 있다면

 

빼는 것도 있어야죠.

 

이름을 넣으면 해당하는

 

아이템을 없애줍니다.

 

//개수 증가
void NumberAdd(string name, unsigned int wantNumber);

//개수 감소
void NumberSub(string name, unsigned int wantNumber);

//개수 설정
void SetNumber(string name, unsigned int wantNumber);

//무한 설정
void SetInfinity(string name, bool wantInfinity);

//이름 설정
void SetName(string from, string to);

//확률 설정
void SetRate(string name, float wantRate);

//변동/부동 설정
void SetFlexible(string name, bool wantFlexible);

//활성화 설정
void SetAvailable(string name, bool wantAvailable);

아이템 설정을 바꾸어주는

 

함수들도 추가했구요.

 

//해당 아이템의 정보를 그대로 복사한 뷰어를 가져옵니다.
ContentViewer GetViewer(string name);
vector<ContentViewer> GetViewerAll(bool wantFlexible);

이것들은 아이템을 외부에 보여주는데

 

맘대로 바꾸는 것을 막기 위해

 

내용을 복사한 새로운 인스턴스

 

반환하도록 해두었습니다.

 

전부 가져오는 경우

 

변동/부동인지 확인해서

 

해당하는 아이템들만 가져옵니다.

 

 

<마치며>

문자열 기반으로 만들어서

 

어느 프로젝트에서나 쓸 수 있는

 

랜덤박스를 만들어보았습니다!

 

 

여기서 설명드리지 않았던

 

Trie구조나, 정렬된 리스트

 

함수 안에 모두 설명이 되어 있어서

 

혹시나 궁금하시다면

 

열어보셔도 좋겠네요!

 

 

그럼 다음 포스팅에서 뵙겠습니다!

문자열로 조작하는 대화창 만들기

 

 

이 글은 대화창 만들기에서 설명을 드린 곳에 들어간

 

실행기를 만든 전체 CS파일을 복사해서 올려드린 글입니다.

 

설명은 아래에서 보실 수 있습니다.

 

 

[C#] 문자열로 조작 가능! 대화창 만들기!

안녕하십니까. 밤말팅입니다! 저번 시간에는 [은/는] 과 같이 앞 글자에 영향을 받는 조사를 변환시켜 보았는데요! [C#]은는이가 타파!! 자연스러운 문장 만들기! 안녕하십니까 밤말팅입니다! 오

game-part-factory.tistory.com

 

 

스크립트 실행기.zip
0.09MB

using System;
using System.IO;

namespace LineReader
{
    class TextContainer
    {
        public static string[] text;
    }

    class Program
    {
        public static System.Diagnostics.Stopwatch time;

        private static System.Threading.Thread threadKeyRead;

        public static ConsoleKey currentKey = 0;

        static void Main(string[] args)
        {
            time = new System.Diagnostics.Stopwatch();
            time.Start();
            
            threadKeyRead = new System.Threading.Thread(delegate () {
                while (true)
                {
                    currentKey = Console.ReadKey(true).Key;
                };
            });
            threadKeyRead.Start();

            string textFilePath = Directory.GetCurrentDirectory() + @"\CONTEXT.txt";
            if (File.Exists(textFilePath))
            {
                TextContainer.text = File.ReadAllLines(textFilePath);

                TextReader.StartRead(0);
            }
            else
            {
                File.Create(textFilePath);
                Console.WriteLine("파일이 존재하지 않았습니다. 새로 생성된 CONTEXT.txt 파일을 이용하여 새로운 스크립트를 제작해주시기 바랍니다.");
            };

            while (true)
            {
                TextReader.Update();
            };
        }

        public class TextReader
        {
            private static SelectInformation[] currentSelection = new SelectInformation[8];

            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 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 StartRead(int page)
            {
                if (reading)
                {
                    return;
                };

                reading = true;
                skip = false;

                currentReadPage = page;
                nextReadPage = page + 1;

                calculatedText = "";
                showText = "";

                nextTime = 0;

                currentTextIndex = 0;
                currentReadIndex = 0;

                commandStartIndex = 0;
                commandDepth = 0;

                currentReadSpeed = globalReadSpeed;

                if (currentReadPage < TextContainer.text.Length)
                {
                    receiptText = TextContainer.text[currentReadPage];
                }
                else
                {
                    talkName = "<System>";
                    receiptText = "스크립트가 종료되었습니다.";
                    nextReadPage = page;
                };

                string showingName = talkName;
                Console.Clear();
                Console.WriteLine(showingName);
            }

            private static void OnRead()
            {
                if (skip || nextTime <= time.ElapsedMilliseconds)
                {
                    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;
                            commandDepth = 1;

                            while (currentTextIndex < receiptText.Length)
                            {
                                if (receiptText[currentTextIndex] == '[')
                                {
                                    ++commandDepth;
                                }
                                else if (receiptText[currentTextIndex] == ']')
                                {
                                    --commandDepth;
                                };

                                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;
                                }
                                else
                                {
                                    ++currentTextIndex;
                                };
                            };
                        }
                        else
                        {
                            calculatedText += receiptText[currentTextIndex];
                            ++currentTextIndex;
                        };
                    };

                    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 enum CommandType
            {
                GetVariable,
                Substitute,
                Postposition,
                Condition,
                Select
            }

            private static string CommandChecker(char preText, string targetCommand)
            {
                if (targetCommand.Length <= 2)
                {
                    return "";
                }
                else
                {
                    targetCommand = targetCommand.Substring(1, targetCommand.Length - 2);
                };

                int divideIndex = 0;
                int commandDepth = 0;
                
                CommandType currentCommand = CommandType.GetVariable;

                string returnText = "";

                for (int index = 0; index < targetCommand.Length; ++index)
                {
                    if (targetCommand[index] == '[' || targetCommand[index] == '(')
                    {
                        ++commandDepth;
                    }
                    else if (targetCommand[index] == ']' || targetCommand[index] == ')')
                    {
                        --commandDepth;
                    };

                    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;
                            };
                        };
                    };
                };

                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)
                {
                    string arg0 = CommandText(targetCommand.Substring(0, divideIndex).Trim());
                    string arg1 = CommandText(targetCommand.Substring(divideIndex + 1, targetCommand.Length - divideIndex - 1).Trim());

                    if (arg0 == "Speed")
                    {
                        int checkedNumber;

                        if (arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
                        {
                            currentReadSpeed = checkedNumber;
                        }
                        else
                        {
                            currentReadSpeed = globalReadSpeed;
                        };
                    }
                    else if (arg0 == "TalkName")
                    {
                        ChangeName(arg1);
                    }
                    else if(arg0 == "Wait")
                    {
                        int checkedNumber;

                        if (arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
                        {
                            nextTime = time.ElapsedMilliseconds + checkedNumber;
                        };
                    }
                    else if(arg0 == "Next")
                    {
                        int checkedNumber;

                        if(arg1.Length > 0 && int.TryParse(arg1, out checkedNumber))
                        {
                            if(arg1[0] == '+' || arg1[0] == '-')
                            {
                                nextReadPage = currentReadPage + checkedNumber;
                            }
                            else
                            {
                                nextReadPage = checkedNumber - 1;
                            };

                            if(nextReadPage < 0)
                            {
                                nextReadPage = 0;
                            };
                        };
                    }
                    else
                    {
                        globalVariable.Set(arg0, arg1);
                    };
                }
                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;
                    }
                    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));
                    };
                }
                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;

                    if(arg0.Length > 0)
                    {
                        int leftEnd = 0;
                        int rightStart = 0;

                        bool checkEqual = false;
                        bool checkNotEqual = false;
                        bool checkBigger = false;
                        bool checkSmaller = false;

                        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;
                                    };
                                    rightStart = index + 1;
                                }
                                else if(!checkNotEqual)
                                {
                                    checkEqual = true;
                                    if (leftEnd == 0)
                                    {
                                        leftEnd = index - 1;

                                        if(leftEnd < 0)
                                        {
                                            leftEnd = 0;
                                        };
                                    };
                                    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;
                            };
                        };

                        if(leftEnd == 0 && rightStart == 0)
                        {
                            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;
                                    };
                                };
                            };
                        };
                    }
                    else
                    {
                        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);
                };

                if (returnText == null || returnText.Length <= 0)
                {
                    return null;
                }
                else
                {
                    return CommandText(returnText);
                };
            }

            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;
                        };
                        ++commandDepth;
                    }
                    else if (returnText[index] == ']')
                    {
                        --commandDepth;
                        if (commandDepth <= 0)
                        {
                            checkedCommand = returnText.Substring(commandStartIndex, index - commandStartIndex + 1);

                            returnText = returnText.Remove(commandStartIndex, index - commandStartIndex + 1);
                            index = commandStartIndex;

                            if (checkedCommand != null)
                            {
                                if (index <= 0)
                                {
                                    index = 0;
                                    returnText = CommandChecker(' ', checkedCommand) + returnText;
                                }
                                else
                                {
                                    if (returnText != null && returnText.Length > 0)
                                    {
                                        returnText = returnText.Insert(index, CommandChecker(returnText[index - 1], checkedCommand));
                                    }
                                    else
                                    {
                                        returnText = CommandChecker(' ', checkedCommand);
                                    };
                                };
                            };
                                
                            commandDepth = 0;
                            commandStartIndex = 0;
                            --index;
                        };
                    };

                    if (returnText == null)
                    {
                        return null;
                    };
                };

                return returnText;
            }

            private static bool FinalConsonant(char wantCharacter)
            {
                if (wantCharacter < 44032 || wantCharacter > 55215)
                {
                    return false;
                };

                int checker = wantCharacter;

                checker -= 44032;

                return (checker % 28) > 0;
            }

            private static void CreateSelection(string wantContext, string wantOnSelect, string wantNotSelect)
            {
                if(currentSelectionNumber < currentSelection.Length)
                {
                    currentSelection[currentSelectionNumber] = new SelectInformation(wantContext, wantOnSelect, wantNotSelect);

                    ++currentSelectionNumber;
                };
            }

            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;
                    };

                    currentSelectionNumber = 0;

                    StartRead(nextReadPage);
                };
            }

            private static void ChangeName(string wantName)
            {
                talkName = wantName;

                Console.Clear();
                Console.WriteLine(talkName);
                Console.Write(showText);
            }
        }
    }

    public class SelectInformation
    {
        public string context;
        public string onSelectedAction;
        public string notSelectedAction;

        public SelectInformation(string wantContext, string wantOnSelectedAction, string wantNotSelectedAction)
        {
            context = wantContext;
            onSelectedAction = wantOnSelectedAction;
            notSelectedAction = wantNotSelectedAction;
        }
    }

    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;
        }

        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;
            };
        }

        public string Get(string wantName)
        {
            Variable target = Find(wantName);

            if (target != null)
            {
                return target.value;
            };

            return null;
        }

        public Variable Find(string wantName)
        {
            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;
                    };
                }
                else
                {
                    return null;
                };
            };

            return null;
        }
    }

    public class Variable
    {
        public string name;
        public string value;

        public Variable next;

        public Variable(string wantName, string wantValue)
        {
            name = wantName;
            value = wantValue;
            next = null;
        }
    }
}

'부품 설계도' 카테고리의 다른 글

[C#] 디아블로 스타일 인벤토리 전체 코드  (0) 2020.12.27
[C#]Bit_Builder 전체 코드  (0) 2020.11.15

문자열을 대화창에 재생해주는 기능을 만듭시다!

안녕하십니까. 밤말팅입니다!

 

저번 시간에는 [은/는] 과 같이

앞 글자에 영향을 받는 조사

변환시켜 보았는데요!

 

 

[C#]은는이가 타파!! 자연스러운 문장 만들기!

안녕하십니까 밤말팅입니다! 오늘은 가벼운 걸로 한 번 가져와보았습니다! 게임에 보면 꼭 이런 게 있곤 하죠 밤말팅(은)는 정의의 망치(을)를 얻었다! 한국어에 있는 조사는 "앞에 오는 종성"에

game-part-factory.tistory.com

은는이가 변환은 위 링크에서

확인하실 수 있습니다!

 

이번엔 변환에 그치지 말고!

 

대화창을 만들어봅시다!

 

<목표>

//외부에서 접근하는 모든 함수

//몇 번째 페이지를 읽게 할지 고릅니다.
TextReader.StartRead(int 읽을문장의 번호);

//매 프레임마다 불러 출력을 시도합니다.
TextReader.Update();

//변수를 등록하거나 변경합니다.
TextReader.globalVariable.Set(string 변수명, string 값);

//변수의 값을 가져옵니다.
TextReader.globalVariable.Get(string 변수명);

커맨드내용을 입력하면

천천히 출력해주는 프로그램을

만드는 것이 이번 목표입니다.

 

[Speed:10] 이 프로그램은 대화를

시뮬레이션하도록 제작되었습니다.

 

이렇게 쓰면

아래처럼 나오는 거죠.

이런 느낌으로 대화 출력

제일 먼저 천천히 문자를 출력하는

 

기능을 만든 후에

 

커맨드 인식도 만들어봅시다!

 

스크립트 실행기.zip
0.09MB

시제품은 위의 파일을 다운로드하여

확인하실 수 있습니다.

 

들어있는 [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);
  };
}

선택은 모든 선택지를 확인해서

 

본인 번호와 일치하는지 본 후

 

선택 / 미선택 부분을 실행,

 

모든 선택지를 초기화한 뒤에

 

다음 문장을 실행해줍니다.

 

 

미리 다 만들어 두었더니

 

정말 편하네요!

 

 

 

마지막 이름 변경 함수는

 

그냥 이름을 바꿔주고

 

출력하는 것이 전부라

 

넘어가도록 하겠습니다~

 

 

 

 

<마치며>

 

간단하게 대화창만 만들려 했는데

 

커맨드가 들어가면서

 

생각보다 커지게 되었습니다.. ㅎㅎ

 

 

언젠가 스크립트 언어를

 

인식하는 기능을 넣을 때

 

도움이 좀 될 수도 있겠네요~

 

 

 

긴 글 읽어주셔서 감사드리고

 

다음 포스팅에서 뵙겠습니다!

 

받침이 들어가는 조사를 커버해보자!

 

안녕하십니까 밤말팅입니다!

 

오늘은 가벼운 걸로 한 번 가져와보았습니다!

 

게임에 보면 꼭 이런 게 있곤 하죠

 

밤말팅(은)는 정의의 망치(을)를 얻었다!

 

한국어에 있는 조사는 "앞에 오는 종성"에 영향을 받는 것들이 있습니다.

 

종성이 있는 지만 판단하면 되는 아주 간단한 일이지만,

 

외국에서 수입된 프로그램인 경우에는 그냥 그대로 사용되는 것도 있구요

 

국산 프로그램 중에서도 제대로 커버가 되어있지 않은 경우도 가끔 보입니다~

 

오늘은 간단하게 조사를 처리해보도록 합시다!

 

ContextFixer.zip
0.08MB

완성본을 미리 시험해볼 수 있는 시제품입니다.

 

 

[이 클래스는 별도의 네임스페이스 참조가 필요하지 않습니다]

 

<목표>

은/는/이/가를 직접 표시하면 변환해줍니다.

[종성이 있을 때 / 종성이 없을 때]로 나누어서 입력을 하면

 

자동으로 앞 글자에 받침이 있는지 없는지 확인해서 입력해주는 함수를 만들어봅시다!

 

기왕 만드는 김에 대괄호를 사용하는 [커맨드]도 같이 넣어보도록 합시다.

 

<구상>

구상에 앞서 유니코드(UTF-8)로 되어있는 문자열을 사용한다는 것을 밝힙니다.
C#은 유니코드를 기본적으로 지원하기 때문에 char가 2바이트입니다.
다른 언어로 사용하실 땐, 종성 확인에 별도의 작업이 필요합니다.
EUC-KR이나 CP949의 경우 종성이 없는 글자의 규칙성이 없어 작업이 힘듭니다.

종성 없는 글자를 모아둔 상태

한글은 유니코드 번호 44032(가) ~ 55203(힣)에 등록되어있습니다.

 

매 28번째마다 종성이 들어있지 않은 문자가 나오는데요,

 

이걸 이용하면 쉽게 찾을 수 있겠죠!

 

커맨드는 딱 두 종류!

일단 문자열을 돌면서 '['와 ']'를 확인하고

 

가운데에 있는 내용을 확인할 겁니다.

 

가운데에 '/'이 있으면 조사로 넣고

 

없으면 커맨드로 쓰도록 하겠습니다!

 

<구현>

 

지금부터 필요한 부분은 딱 세 개 입니다!

 

커맨드를 확인하고 정보를 주기
받은 커맨드를 실행하기
종성을 확인하기

 

간단하죠?

 

커맨드가 있는지 확인해주는 애부터 시작해보도록 합시다!

 

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)는 종성의 역할을 하기 때문에

 

이 문자를 예외처리하면 일본어도 구현할 수 있겠죠!

 

 

 

그래도 일단은 이 세 개의 함수만으로 기본적인 구현은 끝입니다!

 

모두 static함수이기 때문에 아무 데에나 넣고 사용하시면 되겠네요~

 

 

 

<마치며>

 

이번 포스팅은 꽤나 작은 기능을 만들어봤습니다!

 

물론 여기서 끝낼 것은 아니구요, 이런 것들을 쌓아서 대화 기능을 만들 예정입니다.

 

대화를 넘기고, 선택지를 골라 다음 단계로 넘어가는 기능 등을 만들 것이기 때문에

 

관심이 있으시면 다음 포스팅을 확인해주러 오시면 감사드리겠습니다!

 

그럼 여기까지 쓰고 글을 마치겠습니다~

추억의 게임을 온라인 게임으로 만들어봅시다

 

 

설명이 나오기 전에, 해당 아이디어의 데모 영상을 먼저 보실 수도 있습니다!

 

안녕하십니까! 밤말팅입니다~

 

이번에는 게임 아이디어로 돌아오게 되었습니다.

 

이전에 있었던 게임의 개선방안을 생각해보고 짧은 시간동안에 그 데모버전까지 만들어보았습니다!

 

이번에 플래시 지원이 종료되면서, 플래시 게임을 다시 보게 되는 계기가 되었죠

 

그러면서 제가 항상 플래시 게임하면 생각하던 게임이 있었습니다.

 

 

원본 게임의 타이틀 이미지

바로 이누야샤 데몬 토너먼트인데요!

 

플래시 게임 중에서도 전략적인 부분이 부각되는 게임이라

 

특히나 재밌게 했었던 기억이 있네요~

 

전체적인 게임 화면

 

게임 내용을 한 번 살펴보도록 하죠.

 

위와 같은 게임 화면인데요, 하나씩 살펴보도록 합시다.

 

생명력과 에너지 바

상단에는 플레이어들의 생명력남은 에너지를 표현하는 바가 있습니다.

 

생명력이 0이 되는 경우에 사망하여 게임이 종료되고,

 

에너지는 기술을 사용할 때 소모하게 됩니다!

 

이건 흔한 개념이죠?

 

상대와 내 위치를 표시하는 맵

우측 하단에는 맵이 있죠

 

상대와 나의 위치를 표시하고 있습니다.

 

이것까지도 아주 익숙합니다.

 

칸은 가로 4칸, 세로 3칸이네요!

 

카드를 넣을 세 개의 공간

 

좌측 하단을 보시면 세 개의 칸이 있습니다

 

이게 이 게임의 특징이라고 볼 수 있는데요!

 

카드 세 개를 넣으면, 순서대로 하나씩 실행을 합니다.

 

상대가 어떤 카드를 넣었을지 생각하면서 가보는 거죠

 

이동용 카드

 

일단 이동용 카드가 있습니다.

 

같은 카드는 한 턴에 한 개만 놓을 수 있어서

 

한 턴에 같은 방향으로 여러번 갈 순 없지만

 

그래도 상대에게 다가가는 아주 기본적이지만 중요한 카드가 되겠네요!

 

공격용 카드

 

공격용 카드들입니다.

 

DM은 피해량을 나타내고

EN은 소모하는 에너지를 나타냅니다.

 

상대에게 적은 에너지로도 큰 피해를 주는 것이 중요하겠죠

 

그리고 오른 쪽에 3x3에 빨간색이 칠해져있는 것은

 

빨간색 부분에 상대가 있으면, 피해를 줄 수 있다는 이야기입니다.

 

범위를 표시하는 부분인 것이죠.

 

가운데 부분이 플레이어가 서 있는 부분이기 때문에

 

그걸 기준으로 생각해서 공격하시면 됩니다.

 

유틸용 카드

 

물론 공격만 있는 것은 아닙니다.

 

방어와, 회복도 있어야겠죠.

 

방어는 피해를 일정량 막아주고

 

에너지 회복은 말 그대로 에너지를 조금 채워줍니다.

 

 

이제 이걸로 모의게임을 돌려볼까요

 

두 명의 플레이어가 대전을 하는 모습을 한 턴만 그려봅시다

 

두 플레이어의 위치
두 플레이어가 고른 카드

왼쪽 플레이어의 경우에는 앞으로 이동해서

 

상대도 올 거라 예상을 해서

 

앞부분을 모두 커버하도록 두 가지의 공격을 사용했습니다.

 

 

오른쪽 플레이어는

 

강력한 피해를 주는 기술을 맞추기 위해서

 

아래로 이동한 뒤에 강력한 기술 하나를 사용했네요!

 

시작해보죠

 

첫 번째 기술

양쪽 플레이어 모두 서로에게 다가가기로 했네요

 

두 번째 기술

왼쪽 플레이어는 공격을 내질렀었는데요

 

아쉽게도 오른쪽 플레이어가 아래로 내려가버리는 탓에

 

맞추지 못하고 에너지만 쓰게 되었네요

 

세 번째 기술

세 번째 기술은 둘 다 공격을 했습니다.

 

왼쪽 플레이어는 더 많은 에너지를 썼는데

 

피해는 오히려 더 많이 입게 되었군요.

 

아쉬운 턴이 되었겠네요~

 

 

이런 식으로 돌아가는 게임입니다!

 

상대의 행동을 예측하면서 본인에게 불리한 부분을

 

오히려 유리하게 만들어 가는 것이 이 게임의 묘미입니다~

 

한 번에 세 개의 행동을 지정하기 때문에

 

잘못된 선택이 아주 큰 차이를 내게 되죠~

 

 

 

 

이 정도면 게임에 대한 설명은 끝난 것 같네요!

 

이제 게임을 뜯어고쳐봅시다!

 

 

 

< 게임의 개선 >

 

게임 방식을 눈여겨본 것이라 게임 방식을 바꿀 생각은 없었기 때문에

 

제가 주목한 부분은 공격이 단순하다는 것이었습니다.

 

캐릭터의 종류가 8개로, 적지 않은 수였지만

 

공격은 [범위], [데미지], [소모량]

 

딱 세 개로만 나뉘어졌습니다.

 

그래서 캐릭터가 바뀌어도 플레이가 크게 바뀌진 않았습니다.

 

그게 조금 아쉬웠기 때문에

 

저는 캐릭터의 특성에 맞춰 공격을 다양하게 만들려고 해보았습니다!

 

세 개의 속성이 서로를 찌른다

우선 캐릭터에 속성을 부여한 다음에

 

상대의 속성을 찌르도록 하죠.

 

물론 그냥 속성을 쥐어주면, 속성을 고른 것만으로 게임이 끝난 셈이 되기 때문에

 

세 개의 캐릭터를 골라서 시작하게 합시다~

 

게임이 시작한 뒤에도 카드를 써서 속성을 바꿀 수 있게하면 좋겠죠!

 

데모버전이라 불, 물, 나무 세 개만 만들었습니다!

 

이 세 개로 두 명이 싸우게 하려면 기술을 잘 배치해야겠네요~

 

 

[가로불] 가로 세 칸에 불을 질러 상대에게 피해를 줍니다

[세로불] 세로 세 칸에 불을 질러 상대에게 피해를 줍니다

[살치불] x자 모양으로 불을 질러 상대에게 피해를 줍니다

[완전연소] 내 주위에 있는 불을 완전히 태워 불에 닿은 적에게 큰 피해를 줍니다

 

첫 번째 캐릭터는 로 정했습니다.

 

불의 특징은 공격한 범위에 불이 남는다는 것입니다.

 

그래서 범위 공격이 주가 되며 낮은 데미지를 가지고 있습니다.

 

대신 불 위에 있는 적불 속성이 아니면 매 행동마다 불 속성 피해를 입습니다.

 

불은 필드에 불을 남긴다

 

불의 궁극기인 완전연소

 

불만 까는 것이 끝이면 안되겠죠?

 

궁극기인 완전연소를 사용해서 불타는 필드에 있던 적에게 큰 피해를 주는 것이

 

주된 전법이 되겠습니다!

 

물론 누가 불을 붙힌 건지는 확인하지 않기 때문

 

양쪽 모두 불 속성인 경우에는 완전연소각을 누가 잘 보는가가 중요하겠죠?

 

불 속성 범위공격은 항상 자기자신의 위치를 포함하기 때문에 조심해야겠네요!

 

불 속성 공격이 나무를 쓰지 못 할 정도로 압박할 수 있어서,

 

적절히 스스로 견제받을 수 있는 스타일로 고안했습니다.

 

 

[휩쓸기] 위 아래에 있는 적을 물로 밀어내며 피해를 줍니다

[파도] 위쪽 세 칸에 있는 적들을 물로 밀어내며 피해를 줍니다.

[범람] 내 주위 여덟칸에 있는 적을 물로 밀어내며 피해를 줍니다.

[서핑] 두 칸을 파도타며 내려갑니다. 닿은 적을 물로 밀어내며 피해를 줍니다.

 

두 번째 캐릭터는 입니다.

 

불을 카운터하는 캐릭터이기 때문에

 

필드에 있는 불을 끄는 것이 필요하다고 봤습니다.

 

필드에 있는 불을 제거하면서, 상대를 밀어내는 공격

비슷하게 범위공격이 있는 물이지만, 그 범위로 불을 끕니다!

 

동시에 상대를 밀어내는 공격을 합니다.

 

본인도 밀려나는 강한 물살을 가진 캐릭터라서

 

본인이 범위에 포함되는 기술은 하나밖에 없습니다!

 

밀어낼 때, 밀려날 공간이 없으면 추가 피해를 줍니다.

 

벽에 부딪혔다는 설정이죠.

 

세로축이 더 좁기 때문에, 세로축 공격을 위주로 합니다.

 

불은 물에 추가 데미지를 입기 때문에, 벽에 부딪히기까지 하면 굉장히 큰 피해를 입겠죠.

 

그리고 공격을 하면 불을 꺼버리기 때문

 

나무가 양분을 얻을 때 도움이 되는 속성입니다.

 

나무

[만개] 십자 모양으로 뿌리를 뻗어 상대에게 피해를 주고 그 절반만큼 회복합니다.

범위 내에 불이 붙어있으면 피해를 입습니다.

[흡수] 땅 속에 있는 영양분을 흡수하여 생명력을 회복합니다. 내 위치에 불이 붙어 있으면

회복하는 대신 피해를 입습니다.

[반격] 상대의 공격에 대비합니다. 상대의 기술로 피해를 입으면 그 피해를 상대에게 돌려줍니다.

[동화] 내 위치에 있는 상대를 흡수하여 큰 피해를 입히고 피해의 절반만큼 회복합니다.

 

마지막으로 기획한 캐릭터는 나무입니다.

 

물에게 밀려나지 않아서 큰 데미지를 입지 않는 대신

 

불에게 많은 데미지를 입는 속성입니다.

 

불은 깔아놓는 플레이스타일이라서, 타격이 크겠죠!

 

 

기술을 보면...

 

다른 캐릭터에 비해 텍스트가 좀 기네요!

 

생명력을 회복할 수 있는 유일한 캐릭터이지만, 위험부담이 있는 캐릭터입니다~

 

만개를 써서 피해를 입는 모습

만개는 넓은 범위를 가진 공격기에다가 준수한 데미지를 가지고, 데미지 절반을 회복하지만

 

공격 범위에 불이 깔려있는 경우에 닿은 칸마다 화상 피해를 입습니다.

 

위와 같은 상황에선 회복량보다 피해량이 훨씬 크게 되겠죠?

 

흡수도 마찬가지로, 내 위치에 불이 있으면 오히려 데미지를 입게 됩니다!

 

회복하려면 상황을 잘 봐야겠죠?

 

만약 상대가 물로 불을 막 꺼준 상태라면, 물에게 큰 피해를 주고 큰 회복을 할 수도 있겠네요

 

많은 마나를 소모하지만 데미지를 완전히 반사하는 반격

반격완벽하게 공격을 막고, 오히려 상대에게 데미지를 돌려주는

 

방어의 완전 상위호환인 기술이지만, 마나가 많이 소모되는 기술입니다.

 

똑같이 마나가 많이 소모되고 상대 상성에 따라 거의 즉사에 가까운 데미지를 주는 공격이지만

 

상대와 같은 칸에 있어야 맞출 수 있는 기술인 동화와 비슷하게

 

상대의 속성과 행동을 제대로 파악했을 때, 큰 이득을 얻을 수 있는 기술이죠.

 

 

 

제대로 쓰려면 실력이 필요한 캐릭터이지만, 생명력 회복을 위해서 자주 사용해야하기 때문에

 

계속 나와서 물 견제를 해주는 캐릭터가 되겠네요~

 

 

 

이렇게 캐릭터를 만든 것은 좋은데..

 

가장 중요한 사항이 있겠죠?

 

그게 이 글의 제목이기도 하구요!

 

온라인 지원

이 게임은 상대와의 수 싸움이 중요한 게임입니다.

 

컴퓨터랑만 수 싸움을 하는 것은 한계가 있겠죠?

 

이 게임을 온라인 지원하는 것이 중요하다고 생각했습니다!

 

간단하게 리눅스로 서버를 만들어서 지인과 함께 플레이를 해봤습니다~

 

만약 데모버전 말고 진짜로 만들게 된다면

 

필드에 숨는 땅 속성,

본인을 강화하는 금 속성,

씨앗을 심는 나무 속성,

움직이는 비 구름을 만드는 물 속성

폭발로 기동성을 살리는 불 속성

 

뭐 이런 느낌으로 여러 캐릭터를 만들고

 

그래픽도 날림으로 말고, 진짜 제대로 그려보고도 싶네요~

 

 

 

 

 

이번 데모는 2주간 진행했는데요,

 

코딩하면서 그림도 그리고, 캐릭터 기획도 해보고 나름 재밌었던 시간이었던 것 같습니다!

 

그럼 저는 이번 데모를 시연한 영상을 남기고 글을 마쳐보겠습니다~

 

긴 글 읽어주셔서 감사합니다!

 

 

+ Recent posts