Unity(4/5)로 시각 소설 만들기 - 변수와 상태 관리

우리는 문자를 어떻게 표시하고 숨기는지 배웠다.이번에 우리는 변수와 게임 자체의 상태 처리를 연구할 것이다.여느 때와 마찬가지로 사용 가능한 예시 파일here을 사용하거나 자신의 게임을 개발할 수 있다.우리 시작합시다!
이 예시 항목은 게임Guilt Free의 재료를 사용했다. 이 게임은 한 사람이 음식 장애와 싸우고 있다는 것을 묘사했다.이런 종류의 이야기가 당신에게 적합하지 않을 수도 있다고 생각되면 파일을 사용하십시오.

가변 상태


잉크가 변수를 도입하고 이야기의 발전에 따라 그 값을 바꿀 수 있다는 것을 기억할 수도 있다.그리고 이야기의 특정한 부분, 그렇지 않으면 차단되는 부분, 예를 들어 당신의 매력 수준을 바탕으로 하는 각종 대화상자 옵션 등을 표시할 수 있습니다. 어떤 경우 저희 C 코드에서 이 값을 방문하면 유용할 수 있습니다. 예를 들어 유저가 상처를 입었을 때 건강 표시줄을 업데이트할 수 있습니다.이를 실현하는 방법은 다음과 같이 Story 객체에 variable_state 속성을 사용합니다._story.variablesState["variable_name"]제 ink 파일에 변수가 있습니다. 이 변수는 이야기의 발전에 따라 달라집니다.게임이 시작될 때 값을 표시할 코드를 추가합니다.이렇게 하려면 InkManager의 함수Start()를 업데이트해야 합니다.
void Start()
{
  _characterManager = FindObjectOfType<CharacterManager>();

  StartStory();

  var relationshipStrength = (int)_story.variablesState["relationship_strength"];

  var mentalHealth = (int)_story.variablesState["mental_health"];

  Debug.Log($"Logging ink variables. Relationship strength: {relationshipStrength}, mental health: {mentalHealth}");
}
보시다시피 우리는 _story.variablesState를 통해 모든 변수에 접근할 수 있습니다.변수 이름이 ink의 변수 이름과 완전히 같은지 확인하십시오.런을 눌렀을 때, 컨트롤러에 이 값을 표시해야 합니다.

현재, 우리는 이 코드를 하나의 단독 함수에 넣을 수 있다. 우리는 언제든지 그것을 호출해서 현재 값을 검사할 수 있지만, 우리는 언제 그것을 검사할지 어떻게 알 수 있습니까?저희가 5분마다 하나요?매번 우리가 새로운 선을 표시합니까?어쩌면.. 아니, 그건 안 돼.만약 우리가 감청기를 가지고 있다면, 그것은 잉크에서 값을 바꿀 때 이 값을 업데이트할 것이다. 그렇지?다행히도 우리는 이 점을 해낼 수 있다!
_story.ObserveVariable("relationship_strength", (arg, value) =>
{
  Debug.Log($"Value updated. Relationship strength: {value}");
});

_story.ObserveVariable("mental_health", (arg, value) =>
{
  Debug.Log($"Value updated. Mental health: {value}");
});
이 예에서 arg 매개 변수는 업데이트된 변수의 이름이 됩니다.보시다시피 변수 관찰기를 설정하고 값이 바뀔 때마다 실행할 동작을 지정할 수 있습니다.우리는 그것으로 사용자 인터페이스를 업데이트하거나 서로 다른 정서적인 음악을 재생하기 시작할 수 있다.네가 그것을 써서 무엇을 하든지 간에 그것은 매우 유용할 것이다.그러나 관찰자는 시작할 때 호출되지 않는다는 것을 명심하십시오. 따라서 변수의 초기 값을 알고 싶으면 변수 상태 속성을 사용해야 합니다.이 문제를 처리하기 위해 InkManager를 업데이트합니다.우선, 매번 업데이트를 기록할 개인 setter가 있는 속성을 추가합니다.
public int RelationshipStrength
{
  get => _relationshipStrength;
  private set
  { 
    Debug.Log($"Updating RelationshipStrength value. Old value: {_relationshipStrength}, new value: {value}");
    _relationshipStrength = value;
  }
}

private int _mentalHealth;
public int MentalHealth
{
  get => _mentalHealth;
  private set
  {
    Debug.Log($"Updating MentalHealth value. Old value: {_mentalHealth}, new value: {value}");
    _mentalHealth = value;
  }
}
다음에 변수 업데이트를 처리하는 새로운 함수를 추가합니다.나중에 바로 전화할게StartStory().
private void InitializeVariables()
{
  RelationshipStrength = (int)_story.variablesState\["relationship_strength"];
  MentalHealth = (int)_story.variablesState\["mental_health"];

  _story.ObserveVariable("relationship_strength", (arg, value) => 
  {
    RelationshipStrength = (int)value;
  });

  _story.ObserveVariable("mental_health", (arg, value) =>
  {
    MentalHealth = (int)value;
  });
}
이제 우리는 C 코드에서 시종일관 최신 값을 제공해야 한다.

게임 상태


우리는 이미 대부분의 기본적인 시각적 신기한 기능을 소개했기 때문에 다음에 우리는 우리의 게임을 어떻게 저장하고 불러오는지 보아야 한다.
이를 위해, 우리는 새로운 관리자 스크립트를 도입하여, 게임 상태와 관련된 모든 논리를 책임질 것이다.다음 단계에 어떤 기능을 소개해야 할지 프레임워크를 만들어 봅시다.
public class GameStateManager : MonoBehaviour
{
  private InkManager _inkManager;
  private CharacterManager _characterManager;

  private void Start()
  {
    _inkManager = FindObjectOfType<InkManager>();
    _characterManager = FindObjectOfType<CharacterManager>();
  }

  public void StartGame()
  {
  }

  public void SaveGame()
  {
    // Here we will collect all the data from other managers and save it to a file
  }

  public void LoadGame()
  {
    // Here we will load data from a file and make it available to other managers
  }

  public void ExitGame()
  {
  }
}
게임에 대한 모든 정보를 저장하려면 SaveData 클래스가 필요합니다.[Serializable] 속성으로 그것을 표시해야 합니다. 왜냐하면 우리는 그것이 포함하는 정보를 서열화해야 하기 때문입니다.현재, 우리는 ink story 상태만 주목하지만, 잠시 후에 더 많은 데이터를 추가할 것입니다.
[Serializable]
public class SaveData
{
  public string InkStoryState;
}

구출 게임


우선 게임을 살리는 데 중점을 두자.Ink은 상태를 저장하고 로드하는 매우 간단한 방법을 제공합니다.var storyState = _story.state.ToJson(); // export game state for saving _story.state.LoadJson(storyState); // load state이 기능을 사용하여 현재 스토리 상태를 GameState Manager에 전달합니다.InkManager에 다음 메서드를 추가합니다.
public string GetStoryState()
{
  return _story.state.ToJson();
}
게임의 논리를 저장하기 위해 GameState Manager로 돌아가겠습니다.다음과 같은 두 가지 기능이 필요합니다.
public void SaveGame()
{
  SaveData save = CreateSaveGameObject();
  var bf = new BinaryFormatter();

  var savePath = Application.persistentDataPath + "/savedata.save";

  FileStream file = File.Create(savePath); // creates a file at the specified location

  bf.Serialize(file, save); // writes the content of SaveData object into the file

  file.Close();

  Debug.Log("Game saved");

}

private SaveData CreateSaveGameObject()
{
  return new SaveData
  {
    InkStoryState = _inkManager.GetStoryState(),
  };
}
보시다시피, SaveGame() 함수는 다른 helper 함수로 SaveData 대상을 만들고, 지정한 위치에서 새 저장 파일로 서열화합니다.Application.persistentDataPath는 폴더를 가리키는 경로로 실행 간에 데이터를 저장할 수 있습니다.정확한 위치는 플랫폼에 따라 다릅니다here.
이 코드를 테스트해 봅시다.화면에 새 단추를 추가해서 이 기능에 연결해야 합니다.스크립트는 다음과 같습니다.
public class SaveGameButtonScript : MonoBehaviour
{
  GameStateManager _gameStateManager;

  void Start()
  {
    _gameStateManager = FindObjectOfType<GameStateManager>();

    if (_gameStateManager == null)
    {
      Debug.LogError("Game State Manager was not found!");
    }
  }

  public void OnClick()
  {
    _gameStateManager?.SaveGame();
  }
}

장면에 GameStateManager 스크립트를 저장할 새 객체를 만들어야 합니다.계층은 다음과 같아야 합니다.

우리들은 우리의 코드가 유효하다는 것을 확보할 것이다.플레이를 누르고 게임을 저장해 보십시오.너는 너의 게임기에서 새로운 회선을 보게 될 것이다.필요하면 영구 데이터 폴더 (check its path here 에 저장된 파일을 볼 수 있는지 확인할 수 있습니다.

게임 로드

LoadGame() 기능을 실현할 때가 되었다.
public void LoadGame()
{
  var savePath = Application.persistentDataPath + "/savedata.save";

  if (File.Exists(savePath))
  {
    BinaryFormatter bf = new BinaryFormatter();

    FileStream file = File.Open(savePath, FileMode.Open);
    file.Position = 0;

    SaveData save = (SaveData)bf.Deserialize(file);

    file.Close();

    InkManager.LoadState(save.InkStoryState);

    StartGame();
  }
  else
  {
    Debug.Log("No game saved!");
  }
}
Load() 함수는 같은 FileStream과 BinaryFormatter 클래스를 사용하여 저장된 파일을 처리합니다.우리는 먼저 파일이 존재하는지 확인하고, 만약 존재한다면, 우리는 그 내용을 SaveData 대상으로 반서열화할 것이다.그런 다음 ink 상태를 InkManager에 전달하고 게임을 시작합니다.InkManager에서 정적 함수를 사용합니다.이것은 불러오는 것이 서로 다른 장면(메인 메뉴)에서 진행되기 때문에 장면 사이의 상태가 변하지 않기를 바랍니다.InkManager를 변경한 다음 이 기능을 수행합니다.
private static string _loadedState;

public static void LoadState(string state)
{
  _loadedState = state;
}

private void StartStory()
{
  _story = new Story(_inkJsonAsset.text);

  if (!string.IsNullOrEmpty(_loadedState))
  {
    _story?.state?.LoadJson(_loadedState);

    _loadedState = null;
  }

  // external function bindings etc.
}
보시다시피, LoadState() 함수는 불러오는 상태로 사유 정적 변수를 업데이트합니다.그리고 우리는 StartStory() 함수에서 그것을 검사할 것이다.만약 불러오는 상태가 존재한다면, 우리는 그것을 사용하여 이야기를 초기화하고 변수를 제거하여, 장래에 발생할 수 있는 혼동을 피할 것입니다.만약 없다면, 우리는 평상시대로 계속해서 새로운 이야기를 할 것이다.
메인 메뉴를 추가해서 모든 내용을 한데 묶읍시다.계속해서 버튼으로 새 장면을 만들어서 새 게임을 시작하거나 기존 게임을 불러오거나 완전히 종료합니다.

각 버튼에는 스크립트가 필요합니다.GameState Manager 함수를 호출하기 때문에 매우 비슷합니다.
public class LoadGameButtonScript : MonoBehaviour
{
  GameStateManager _gameStateManager;

  void Start()
  {
    _gameStateManager = FindObjectOfType<GameStateManager>();

    if (_gameStateManager == null)
    {
      Debug.LogError("Game State Manager was not found!");
    }
  }

  public void OnClick()
  {
    _gameStateManager?.LoadGame();
  }
}
새 게임 단추의 스크립트는 _gameStateManager?.StartGame(),'게임 종료'단추는 _gameStateManager?.ExitGame()를 호출합니다.이 스크립트를 편집기의 단추에 추가하고 관리자 자체로 새 대상을 추가해야 합니다.
이제 남은 GameStateManager 기능을 구현해 보겠습니다.
public void StartGame()
{
  UnityEngine.SceneManagement.SceneManager.LoadScene("MainScene");
}

public void ExitGame()
{
  Application.Quit();
}
계속, 재생 모드로 들어갑니다.너는 지금 너의 게임을 저장하고 불러올 수 있을 거야!
하지만 작은 문제 하나를 알아차릴 수도 있어...

문자 상태


당신이 게임을 불러올 때, 우리의 캐릭터는 더 이상 표시되지 않습니다!우리는 잉크 상태만 처리하지만 문자는 처리하지 않습니다.우리 이제 이거 바꾸자.
우선, 우리는 문자를 저장하고 불러올 때 필요한 모든 정보를 저장하는 데이터 용기로 새로운 클래스를 만들 것입니다.
[Serializable]
public class CharacterData
{
  public CharacterPosition Position { get; set; }

  public CharacterName Name { get; set; }

  public CharacterMood Mood { get; set; }
}
다음에 Character 스크립트에 함수를 만들면 캐릭터마다 CharacterData 대상을 되돌려줍니다.
public CharacterData GetCharacterData()
{
  return new CharacterData
  {
    Name = Name,
    Position = Position,
    Mood = Mood
  };
}
우리가 캐릭터 상태를 처리해야 하는 곳은 Character Manager다.필요한 코드를 추가합시다.이는 InkManager와 협력하는 방식과 유사합니다.
public List<CharacterData> GetVisibleCharacters()
{
  var visibleCharacters = _characters.Where(x => x.IsShowing).ToList();

  var characterDataList = new List<CharacterData>();

  foreach (var character in visibleCharacters)
  {
    characterDataList.Add(character.GetCharacterData());
  }

  return characterDataList;
}
GetVisibleCharacters()는 게임을 저장할 때 사용한다.우선, 현재 보이는 문자를 선택한 다음, 새로 추가된 GetCharacterData() 함수로 새 목록을 채웁니다. 이 목록은 GameState Manager에 되돌아옵니다.
다음에, 우리는 불러오는 상태의 코드를 추가해야 한다.
private static List<CharacterData> _loadedCharacters;

public static void LoadState(List<CharacterData> characters)
{
  _loadedCharacters = characters;
}

private void Start()
{
  _characters = new List<Character>();

  if (_loadedCharacters != null)
  {
    RestoreState();
  }
}

private void RestoreState()
{
  foreach (var character in _loadedCharacters)
  {
    ShowCharacter(character.Name, character.Position, character.Mood);
  }

  _loadedCharacters = null;
}
보시다시피 정적 상하문을 다시 사용합니다. 이번에는 채우기 위해서입니다. _loadedCharacters나는 또 RestoreState() 함수를 추가했는데, 이것은 Start()에서 호출된 것이다.만약 우리가 모든 문자를 복원하려면, 이 함수는 모든 문자를 훑어보고 ShowCharacter() 함수를 사용하여 화면에 그것들을 표시합니다.
GameState Manager와 SaveData를 업데이트해야 합니다.
후자는 지금 이렇게 보여요.
[Serializable]
public class SaveData
{
  public string InkStoryState;
  public List<CharacterData> Characters;
}
GameState Manager 내부는 크게 달라지지 않습니다.우리는 CreateSaveGameObject() 함수와 LoadGame() 함수에 문자에 특정한 줄을 추가했는지 확인하기만 하면 된다.
private SaveData CreateSaveGameObject()
{
  return new SaveData
  {
    InkStoryState = _inkManager.GetStoryState(),
    Characters = _characterManager.GetVisibleCharacters()
  };
}

public void LoadGame()
{
  var savePath = Application.persistentDataPath + "/savedata.save";

  if (File.Exists(savePath))
  {
    // file loading code

    InkManager.LoadState(save.InkStoryState);
    CharacterManager.LoadState(save.Characters);

    StartGame();
  }
  else
  {
    Debug.Log("No game saved!");
  }
}
계속하고 플레이를 누릅니다.이제 스토리지 시스템이 정상적으로 작동할 수 있습니다!

마무리


오늘은 여기까지.나는 네가 지금 너의 시각 소설을 위해 견고한 기초를 닦고 모든 것이 형성되기를 바란다.여느 때와 마찬가지로, 이 강좌의 제 코드를 보고 싶으면, 방문할 수 있습니다. here도움이 필요하시면 언제든지 여기나 인터넷으로 연락 주세요.
이 강좌에서 사용하는 모든 시각 자원과 정령은 나와 엘리카가 게임을 위해 그린 것이다Guilt Free.

좋은 웹페이지 즐겨찾기