Логотип проекта Unreal Engine

Муки диалоговые

Муки диалоговые

Йохохо и бутылка рому! В общем, поскольку у вашего покорного разламывается башка, он не нашел ничего лучше, чем настрочить небольшую заметку об истории борьбы и преодоления.

Предположим, что вы делаете квыст с кучей диалогов. Диалоги и без того сильно подрезаны синдромом массэффекта, потому как кое-кто решил, что пяти ответов хватит, и теперь нехочет перепиливать кучу систем под это дело, так вдобавок добавление одной фразы выглядит как-то так:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Теперь представьте, что за гаплык творится, когда таких диалогов становится полсотни, и в некоторых - по 15 вот такого типа блоков.

Будучи ленивой задницей, я решил, что мне надо ужать это до чего-то хотя бы вот такого:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Но как?

В данный момент диалоги лежат в виде индексированных текстовых файлов в папке Content\Text\<locale>\phr_<dialog_id>_<phrase_id>.txt

Чтобы пилить диалоги (и вообще всю текстовку) и не сойти с ума, пришлось запилить вот такую софтину:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Очевидно, что несложно добавить некоторый функционал, который бы помог решить проблему, но что, йолки, делать? Идея родилась следующая: поскольку данные о фразе диалога хранятся в унифицированной структуре:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Почему бы не повторить ее в виде своеобразного файла-манифеста, который и будет нести блок информации о фазе диалога? Сказано-сделано, на выходе имеем такой вот код на делфи для чтения/записи данных:

//Это записывает данные
procedure TForm3.Button1Click(Sender: TObject); 
var i,j:integer;
begin
  Memo1.Clear;
  //adding data
  if CheckBox1.Checked=true then Memo1.Text:=Memo1.Text+'1' else Memo1.Text:=Memo1.Text+'0';
  if CheckBox2.Checked=true then Memo1.Text:=Memo1.Text+'1' else Memo1.Text:=Memo1.Text+'0';
  if CheckBox3.Checked=true then Memo1.Text:=Memo1.Text+'1' else Memo1.Text:=Memo1.Text+'0';
  if CheckBox4.Checked=true then Memo1.Text:=Memo1.Text+'1' else Memo1.Text:=Memo1.Text+'0';
  if CheckBox5.Checked=true then Memo1.Text:=Memo1.Text+'1' else Memo1.Text:=Memo1.Text+'0';
  //adding data
  for j:=0 to 2 do
  for i:=0 to 4 do
  begin
    Memo1.Text:=Memo1.Text+StringGrid1.Cells[j,i]+';';
  end;
  Memo1.Lines.SaveToFile(contentloc+'text\phasemanifest_'+form1.edit2.text+'_'+Form1.Edit3.Text+'.txt');

end;


 //а это читает данные
procedure TForm3.Button2Click(Sender: TObject);
var ms,wrd:string;
    i,j:integer;
    k:integer;
begin
  
 Memo1.Lines.LoadFromFile(contentloc+'text\phasemanifest_'+form1.edit2.text+'_'+Form1.Edit3.Text+'.txt');
  ms:=memo1.Text;
  if ms[1]='1' then CheckBox1.Checked:=true else CheckBox1.Checked:=false;
  if ms[2]='1' then CheckBox2.Checked:=true else CheckBox2.Checked:=false;
  if ms[3]='1' then CheckBox3.Checked:=true else CheckBox3.Checked:=false;
  if ms[4]='1' then CheckBox4.Checked:=true else CheckBox4.Checked:=false;
  if ms[5]='1' then CheckBox5.Checked:=true else CheckBox5.Checked:=false;
  wrd:='';
  i:=0;
  j:=0;
  for k:=6 to length(ms) do
  begin
    if ms[k]=';' then
    begin
      StringGrid1.Cells[j,i]:=wrd;
      wrd:='';
      i:=i+1;
      if i>4 then
      begin
        i:=0;
        j:=j+1;
      end;
    end
    else
    begin
      wrd:=wrd+ms[k];
    end;
  end;

end;

Будучи редкостным лентяем, вместо использования заассайненного файла, я пользовал методы сохранения/загрузки на компоненте Memo, ибо нефиг.

Интерфейс вышел вот такой:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Справа как раз и можно глянуть на манифест в зашифрованном виде. Галочки с надписями use отвечают за указание на то, будет ли этот кусок добавлен в массивы флагов в конечной структуре.

Отлично! Дело сделано!

Ээээ... Стопэ. А как игра будет читать это дело?

В свое время я раскопал решения для удобного чтения текстовых файлов с диска в анрыловский апи:

//ЭТО В ХЕДЕРЕ
	UFUNCTION(BlueprintCallable, Category = "save")
		static bool FileSaveString(FString SaveTextB, FString FileNameB);

	UFUNCTION(BlueprintPure, Category = "save")
		static bool FileLoadString(FString FileNameA, FString& SaveTextA);
		
//ЭТО В ЦПП ФАЙЛЕ
bool Ucppfunctions::FileSaveString(FString SaveTextB, FString FileNameB)
{
	return FFileHelper::SaveStringToFile(SaveTextB, *(FPaths::ProjectDir() + FileNameB));
}

bool Ucppfunctions::FileLoadString(FString FileNameA, FString& SaveTextA)
{
	return FFileHelper::LoadFileToString(SaveTextA, *(FPaths::ProjectDir() + FileNameA));
}

Это, конечно, чудесно - я могу без проблем прочесть манифест... Но как его проанализировать и выдернуть данные? Ну, к счастью, тип FString прописан по-людски, и потому вполне работает вот такой код:

//ЭТО В ХЕДЕРЕ
UFUNCTION(BlueprintCallable, Category = "save")
		static void ProcessPhraseManifest(FString FManText, 
			bool& use0, bool& use1, bool& use2, bool& use3, bool& use4,
			bool& rfc0, bool& rfc1, bool& rfc2, bool& rfc3, bool& rfc4,
			int& rfid0, int& rfid1, int& rfid2, int& rfid3, int& rfid4, 
			int& rphid0, int& rphid1, int& rphid2, int& rphid3, int& rphid4);

//ЭТО В ЦППШКЕ
void Ucppfunctions::ProcessPhraseManifest(FString FManText,
	bool& use0, bool& use1, bool& use2, bool& use3, bool& use4,
	bool& rfc0, bool& rfc1, bool& rfc2, bool& rfc3, bool& rfc4,
	int& rfid0, int& rfid1, int& rfid2, int& rfid3, int& rfid4,
	int& rphid0, int& rphid1, int& rphid2, int& rphid3, int& rphid4)
{
	bool u0;
	bool u1;
	bool u2;
	bool u3;
	bool u4;
	if (FManText[0] == '0')
	{
		u0 = false;
	}
	else
	{
		u0 = true;
	}
	if (FManText[1] == '0')
	{
		u1 = false;
	}
	else
	{
		u1 = true;
	}
	if (FManText[2] == '0')
	{
		u2 = false;
	}
	else
	{
		u2 = true;
	}
	if (FManText[3] == '0')
	{
		u3 = false;
	}
	else
	{
		u3 = true;
	}
	if (FManText[4] == '0')
	{
		u4 = false;
	}
	else
	{
		u4 = true;
	}

	use0 = u0;
	use1 = u1;
	use2 = u2;
	use3 = u3;
	use4 = u4;

	int i = 5;
	int ph = 0;
	int itm = 0;
	int ll = FManText.FString::Len();

	FString wrd;

	while (i < ll - 1)
	{
		if (FManText[i] == ';')
		{
			if (ph == 0)
			{
				if (itm == 0)
				{
					if (wrd == "0") { rfc0 = false; }
					else { rfc0 = true; }
				}
				if (itm == 1)
				{
					if (wrd == "0") { rfc1 = false; }
					else { rfc1 = true; }
				}
				if (itm == 2)
				{
					if (wrd == "0") { rfc2 = false; }
					else { rfc2 = true; }
				}
				if (itm == 3)
				{
					if (wrd == "0") { rfc3 = false; }
					else { rfc3 = true; }
				}
				if (itm == 4)
				{
					if (wrd == "0") { rfc4 = false; }
					else { rfc4 = true; }
				}
			}
			if (ph == 1)
			{
				if (itm == 0)
				{
					rfid0=FCString::Atoi(*wrd);
				}
				if (itm == 1)
				{
					rfid1 = FCString::Atoi(*wrd);
				}
				if (itm == 2)
				{
					rfid2 = FCString::Atoi(*wrd);
				}
				if (itm == 3)
				{
					rfid3 = FCString::Atoi(*wrd);
				}
				if (itm == 4)
				{
					rfid4 = FCString::Atoi(*wrd);
				}
			}
			if (ph == 2)
			{
				if (itm == 0)
				{
					rphid0 = FCString::Atoi(*wrd);
				}
				if (itm == 1)
				{
					rphid1 = FCString::Atoi(*wrd);
				}
				if (itm == 2)
				{
					rphid2 = FCString::Atoi(*wrd);
				}
				if (itm == 3)
				{
					rphid3 = FCString::Atoi(*wrd);
				}
				if (itm == 4)
				{
					rphid4 = FCString::Atoi(*wrd);
				}
			}
			
			wrd = "";
			
			itm++;
			if (itm == 5) { itm = 0; ph++; }
		}
		else
		{
			wrd += FManText[i];
		}

		i++;
	}

	return;
	
}

Окай, дело. Громоздко, но работает, гном счастлив. Теперь остается добавить немного соли, сыра и сахару, в виде уже блюпринтовой функции, которая будет это дело обрабатывать. А точнее, даже двух. Первая читает данные из манифеста и пишет их в структуру:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Вторая ей помогает, формируя массивы на основе набора флагов:

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

Запускаем @ проверяем!

Муки диалоговые — Unreal Engine — DevTribe: Разработка игр (blueprints, C++, ue4, разработка игр, укрупнение, диалоговаясистема)

И вот таким вот образом я никоим образом не поправил себе состояние башки, но зато немного упростил жизнь с формированием блоков диалогов в моем magnum opus. Всем мира и корзинок!



Что то мне подсказывает что можно было бы обойтись более простой кровью... В виде набора хелпкрных функций для блупринта формировать граф диалога и потом использовать его. Хотя я не уверен (вообще без понятия как работает блюпринт) поэтому возможно это единственное решение.
Но я помню тогда еще в далекие времена моддинга вара диалоговая система у меня строилась на каком то таком коде

Int dialogId = CreateDialogForUnit(...)
AnswerId1 = CreateAnswerForDialog(dialogId,...)
Answer_AddOpenDialogAction(AnswerId, anotherDialogId)
AnswerId2 = CreateAnswerForDialog(dialogId,...)
Answer_AddAction(Answer2,...)

И потом оно все автоматически работало. Может быть можно было бы это обернуть в удобные хелперы для блюпринта. Единственный минус обязательно рекомпилить для фикса диалогов

Хех, там что так что эдак рекомпиляция понадобится, каждый диалог сейчас нуждается в индивидуальной настройке узловой структуры, в которую заливаются фразы. Ну и, как ни крути, задача выросла из попытки привести в чувство уже существующую, но до ужаса субоптимальную систему, не ломая ее - потому что на нее уже завязано немало контента. Как-то так =)

Дарин, ну т.е. сделать такой граф диалогов в блюпринте впринципе возможно?

Вполне можно, через ту же структуру. Хотя, наверное, удобнее было бы реализовывать это дело с помощью C++-класса вынесенного в отдельный объект-контроллер диалогов.

Дарин, ну не всегда удобно писать код для определения диалогов и настройки связей, особенно если есть возможно прокликивать и перетаскивать нужные блоки