Houseki Engine

2020 年 5 月—2020 年 7 月


本项目使用的全部资源均来自游戏 Aokana: Four Rhythm Across the Blue,且仅用于学习。


项目信息


UI 系统

UI 系统采用完全数据驱动的设计,可以解析 XML 文件来生成 UI 布局。

系统实现了四种 UI 元素:Canvas、Sprite、TextBox 和 Button。

系统还支持简单的 UI 动画与嵌套 UI 布局。


剧情系统

对于 Visual Novel 游戏而言,核心玩法由数据驱动,因此从文件解析数据对本项目十分重要。

该系统使用 CSV 而非 XML,因为在 Excel 中编辑 CSV 更方便。

剧情系统支持以下命令:

  • 解析单句对话
  • 切换背景
  • 切换背景音乐
  • 播放音效
  • 切换角色
  • 显示分支选项框
  • 跳转到其他剧情

#include "Game/Framework/UIScene.hpp"
#include "Game/Framework/UIElement.hpp"
#include "Game/Game.hpp"
#include "Game/GameCommon.hpp"
#include "Engine/Math/OBB2.hpp"
#include "Engine/Input/InputSystem.hpp"
#include "Engine/Core/DevConsole.hpp"
#include "Engine/Renderer/BitmapFont.hpp"

#include "Game/Framework/UISprite.hpp"
#include "Game/Framework/UIButton.hpp"
#include "Game/Framework/UITextBox.hpp"
#include "Game/Framework/UICanvas.hpp"
#include "Game/Framework/UIAnimation.hpp"
#include "Game/Framework/UIAnimationClip.hpp"


bool UIScene::LoadSceneFromXMLFile(char const* fileName)
{
	//m_elements.clear();

	m_rootElement = new UICanvas(Vec2(.5f, .5f), Vec2(0.f, 0.f), Vec2(UI_SIZE_X, UI_SIZE_Y));
	m_rootElement->SetName("Root");

	MyXMLDocument xmlDocument;
	xmlDocument.LoadFile(fileName);

	if (xmlDocument.ErrorID() != tinyxml2::XML_SUCCESS) {
		g_theConsole->PrintString(Rgba8::red, Stringf("Failed to Load the XML File %s", fileName));
		return false;
	}

	XMLElement* rootXMLElement = xmlDocument.RootElement();
	if (!rootXMLElement) {
		g_theConsole->PrintString(Rgba8::red, Stringf("XML File %s has no element.", fileName));
		return false;
	}

	CreateNestedElementsFromXMLElement(m_rootElement, rootXMLElement);

	g_theConsole->PrintString(Rgba8::green, Stringf("Successfully loaded scene from XML File %s.", fileName));
	return true;
}

UIElement* UIScene::LoadElementFromXMLFile(char const* fileName)
{
	MyXMLDocument xmlDocument;
	xmlDocument.LoadFile(fileName);

	if (xmlDocument.ErrorID() != tinyxml2::XML_SUCCESS) {
		g_theConsole->PrintString(Rgba8::red, Stringf("Failed to Load the XML File %s", fileName));
		return false;
	}

	XMLElement* rootXMLElement = xmlDocument.RootElement();
	if (!rootXMLElement) {
		g_theConsole->PrintString(Rgba8::red, Stringf("XML File %s has no element.", fileName));
		return false;
	}

	std::string eleName = rootXMLElement->Name();
	if (eleName == "Sprite")
		return CreateSpriteFromXMLElement(rootXMLElement);

	if (eleName == "Button")
		return CreateButtonFromXMLElement(rootXMLElement);

	if (eleName == "TextBox")
		return CreateTextBoxFromXMLElement(rootXMLElement);

	if (eleName == "Canvas")
		return CreateCanvasFromXMLElement(rootXMLElement);

	g_theConsole->PrintString(Rgba8::red, Stringf("Error in parsing file %s. Unknown element name.", fileName));
	return nullptr;
}

UISprite* UIScene::CreateSpriteFromXMLElement(XMLElement* e)
{
	std::string name = ParseXmlAttribute(*e, "Name", "Unknown Sprite");
	std::string texturePath = ParseXmlAttribute(*e, "Texture", "NULL");
	Vec2 position = ParseXmlAttribute(*e, "Position", Vec2(0, 0));
	Vec2 size = ParseXmlAttribute(*e, "Size", Vec2(100, 100));
	Vec2 pivot = ParseXmlAttribute(*e, "Pivot", Vec2(0.5f, 0.5f));
	float rotationDegrees = ParseXmlAttribute(*e, "Rotation", 0.f);
	Rgba8 tint = ParseXmlAttribute(*e, "Tint", Rgba8::white);
	bool isVisible = ParseXmlAttribute(*e, "Visible", true);
	bool isSelectable = ParseXmlAttribute(*e, "Selectable", false);

	std::string scaleStr = ParseXmlAttribute(*e, "Scale", "1");
	Vec2 scale = Vec2(1, 1);
	if (IsStringFloatFormat(scaleStr))
	{
		float s = ParseXmlAttribute(*e, "Scale", 1.f);
		scale = Vec2(s, s);
	}
	else if (Vec2::IsStringVec2Format(scaleStr.c_str()))
	{
		Vec2 s = ParseXmlAttribute(*e, "Scale", Vec2(1.f, 1.f));
		scale = s;
	}

	Texture* tex = g_theRenderer->CreateOrGetTextureFromFile(texturePath.c_str());
	UISprite* newSprite = new UISprite(tex, pivot, position, size);

	newSprite->SetName(name);
	newSprite->SetRotation(rotationDegrees);
	newSprite->SetVisibility(isVisible);
	newSprite->SetColor(tint);
	newSprite->SetIsSelectable(isSelectable);
	newSprite->SetScale(scale);

	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "Animation")
		{
			UIAnimation* anim = ParseAnimationElement(currentElement);
			newSprite->SetAnimation(anim);
		}
	}

	// Get Nested Elements
	CreateNestedElementsFromXMLElement(newSprite, e);

	return newSprite;
}

UIButton* UIScene::CreateButtonFromXMLElement(XMLElement* e)
{
	std::string name = ParseXmlAttribute(*e, "Name", "Unknown Button");
	std::string defaultTexturePath = ParseXmlAttribute(*e, "DefaultTexture", "NULL");
	std::string hoveredTexturePath = ParseXmlAttribute(*e, "HoveredTexture", "NULL");
	std::string clickedTexturePath = ParseXmlAttribute(*e, "ClickedTexture", "NULL");
	Vec2 position = ParseXmlAttribute(*e, "Position", Vec2(0, 0));
	Vec2 size = ParseXmlAttribute(*e, "Size", Vec2(100, 100));
	Vec2 pivot = ParseXmlAttribute(*e, "Pivot", Vec2(0.5f, 0.5f));
	float rotationDegrees = ParseXmlAttribute(*e, "Rotation", 0.f);
	Rgba8 defaultTint = ParseXmlAttribute(*e, "Tint", Rgba8::white);
	Rgba8 hoveredTint = ParseXmlAttribute(*e, "HoveredTint", Rgba8::white);
	Rgba8 clickedTint = ParseXmlAttribute(*e, "ClickedTint", Rgba8::white);
	bool isVisible = ParseXmlAttribute(*e, "Visible", true);
	bool isSelectable = ParseXmlAttribute(*e, "Selectable", true);

	std::string scaleStr = ParseXmlAttribute(*e, "Scale", "1");
	Vec2 scale = Vec2(1, 1);
	if (IsStringFloatFormat(scaleStr))
	{
		float s = ParseXmlAttribute(*e, "Scale", 1.f);
		scale = Vec2(s, s);
	}
	else if (Vec2::IsStringVec2Format(scaleStr.c_str()))
	{
		Vec2 s = ParseXmlAttribute(*e, "Scale", Vec2(1.f, 1.f));
		scale = s;
	}


	Texture* defaultTex = g_theRenderer->CreateOrGetTextureFromFile(defaultTexturePath.c_str());
	Texture* hoveredTex = nullptr;
	Texture* clickedTex = nullptr;

	if (hoveredTexturePath != "NULL")
		hoveredTex = g_theRenderer->CreateOrGetTextureFromFile(hoveredTexturePath.c_str());

	if (clickedTexturePath != "NULL")
		clickedTex = g_theRenderer->CreateOrGetTextureFromFile(clickedTexturePath.c_str());

	UIButton* newButton = new UIButton(defaultTex, pivot, position, size);

	newButton->SetName(name);
	newButton->SetHoveredTexture(hoveredTex);
	newButton->SetClickedTexture(clickedTex);

	newButton->SetRotation(rotationDegrees);
	newButton->SetVisibility(isVisible);
	newButton->SetColor(defaultTint);
	newButton->SetHoveredColor(hoveredTint);
	newButton->SetClickedColor(clickedTint);
	newButton->SetIsSelectable(isSelectable);
	newButton->SetScale(scale);


	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "Animation")
		{
			UIAnimation* anim = ParseAnimationElement(currentElement);
			newButton->SetAnimation(anim);
		}
	}

	Strings onClickedEvetStrs;
	std::string eventStrs = ParseXmlAttribute(*e, "OnClickedEvent", "NULL");
	if (eventStrs != "NULL")
	{
		onClickedEvetStrs = SplitStringOnDelimiter(eventStrs, ";");
		for (int i = 0; i < onClickedEvetStrs.size(); i++)
		{
			newButton->BindClickedEventFromString(onClickedEvetStrs[i]);
		}
	}

	Strings onHoveredEventStrs;
	eventStrs = ParseXmlAttribute(*e, "OnHoveredEvent", "NULL");
	if (eventStrs != "NULL")
	{
		onHoveredEventStrs = SplitStringOnDelimiter(eventStrs, ";");
		for (int i = 0; i < onHoveredEventStrs.size(); i++)
		{
			newButton->BindHoveredEventFromString(onHoveredEventStrs[i]);
		}
	}


	CreateNestedElementsFromXMLElement(newButton, e);

	return newButton;
}

UITextBox* UIScene::CreateTextBoxFromXMLElement(XMLElement* e)
{
	std::string name = ParseXmlAttribute(*e, "Name", "Unknown TextBox");
	std::string text = ParseXmlAttribute(*e, "Text", "Random Text");

	Vec2 position = ParseXmlAttribute(*e, "Position", Vec2(0, 0));
	Vec2 size = ParseXmlAttribute(*e, "Size", Vec2(100, 100));
	Vec2 pivot = ParseXmlAttribute(*e, "Pivot", Vec2(0.5f, 0.5f));

	std::string scaleStr = ParseXmlAttribute(*e, "Scale", "1");
	Vec2 scale = Vec2(1, 1);
	if (IsStringFloatFormat(scaleStr))
	{
		float s = ParseXmlAttribute(*e, "Scale", 1.f);
		scale = Vec2(s, s);
	}
	else if (Vec2::IsStringVec2Format(scaleStr.c_str()))
	{
		Vec2 s = ParseXmlAttribute(*e, "Scale", Vec2(1.f, 1.f));
		scale = s;
	}

	std::string alignmentStr = ParseXmlAttribute(*e, "Alignment", "Center");
	Vec2 alignment = ALIGN_CENTER;
	if (alignmentStr == "TopLeft")
		alignment = ALIGN_TOPLEFT;
	if (alignmentStr == "TopCenter" || alignmentStr == "TopCentre")
		alignment = ALIGN_TOPCENTER;
	if (alignmentStr == "TopRight")
		alignment = ALIGN_TOPRIGHT;
	if (alignmentStr == "CenterLeft" || alignmentStr == "CentreLeft")
		alignment = ALIGN_CENTERLEFT;
	if (alignmentStr == "Center" || alignmentStr == "Centre")
		alignment = ALIGN_CENTER;
	if (alignmentStr == "CenterRight" || alignmentStr == "CentreRight")
		alignment = ALIGN_CENTERRIGHT;
	if (alignmentStr == "BottomLeft")
		alignment = ALIGN_BOTTOMLEFT;
	if (alignmentStr == "BottomCenter" || alignmentStr == "BottomCentre")
		alignment = ALIGN_BOTTOMCENTER;
	if (alignmentStr == "BottomRight")
		alignment = ALIGN_BOTTOMRIGHT;

	float rotationDegrees = ParseXmlAttribute(*e, "Rotation", 0.f);
	Rgba8 tint = ParseXmlAttribute(*e, "Tint", Rgba8::white);
	bool isVisible = ParseXmlAttribute(*e, "Visible", true);
	bool isSelectable = ParseXmlAttribute(*e, "Selectable", false);


	float fontSize = ParseXmlAttribute(*e, "FontSize", 18.f);
	float leading = ParseXmlAttribute(*e, "Leading", 0.f);

	UITextBox* newTextBox = new UITextBox(g_defaultFont, pivot, position, size);
	newTextBox->SetName(name);
	newTextBox->SetRotation(rotationDegrees);
	newTextBox->SetColor(tint);
	newTextBox->SetVisibility(isVisible);
	newTextBox->SetIsSelectable(isSelectable);
	newTextBox->SetScale(scale);
	newTextBox->SetLeading(leading);

	newTextBox->SetText(text);
	newTextBox->SetFontSize(fontSize);
	newTextBox->SetTextAlignment(alignment);


	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "Animation")
		{
			UIAnimation* anim = ParseAnimationElement(currentElement);
			newTextBox->SetAnimation(anim);
		}
	}

	CreateNestedElementsFromXMLElement(newTextBox, e);

	return newTextBox;
}

UICanvas* UIScene::CreateCanvasFromXMLElement(XMLElement* e)
{
	std::string name = ParseXmlAttribute(*e, "Name", "Unknown Canvas");

	Vec2 position = ParseXmlAttribute(*e, "Position", Vec2(0, 0));
	Vec2 size = ParseXmlAttribute(*e, "Size", Vec2(100, 100));
	Vec2 pivot = ParseXmlAttribute(*e, "Pivot", Vec2(0.5f, 0.5f));
	float rotationDegrees = ParseXmlAttribute(*e, "Rotation", 0.f);
	bool isVisible = ParseXmlAttribute(*e, "Visible", true);
	bool isSelectable = ParseXmlAttribute(*e, "Selectable", false);

	std::string scaleStr = ParseXmlAttribute(*e, "Scale", "1");
	Vec2 scale = Vec2(1, 1);
	if (IsStringFloatFormat(scaleStr))
	{
		float s = ParseXmlAttribute(*e, "Scale", 1.f);
		scale = Vec2(s, s);
	}
	else if (Vec2::IsStringVec2Format(scaleStr.c_str()))
	{
		Vec2 s = ParseXmlAttribute(*e, "Scale", Vec2(1.f, 1.f));
		scale = s;
	}

	UICanvas* newCanvas = new UICanvas(pivot, position, size);
	newCanvas->SetName(name);
	newCanvas->SetRotation(rotationDegrees);
	newCanvas->SetVisibility(isVisible);
	newCanvas->SetIsSelectable(isSelectable);
	newCanvas->SetScale(scale);

	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "Animation")
		{
			UIAnimation* anim = ParseAnimationElement(currentElement);
			newCanvas->SetAnimation(anim);
		}
	}


	CreateNestedElementsFromXMLElement(newCanvas, e);

	return newCanvas;
}

UIAnimation* UIScene::ParseAnimationElement(XMLElement* e)
{
	UIAnimation* newAnim = new UIAnimation();
	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "AnimationClip")
		{
			UIAnimationClip* newAnimClip = ParseAnimationClipElement(currentElement);
			newAnim->AppendClip(*newAnimClip);
		}
	}

	std::string defaultAnimStr = ParseXmlAttribute(*e, "DefaultAnimation", "");
	newAnim->SetCurrentClipByName(defaultAnimStr);

	return newAnim;
}

UIAnimationClip* UIScene::ParseAnimationClipElement(XMLElement* e)
{
	std::string animClipName = ParseXmlAttribute(*e, "Name", "Unknown Clip");
	UIAnimationClip* newAnimClip = new UIAnimationClip(animClipName);

	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		std::string eleName = currentElement->Name();
		if (eleName == "Frame")
		{
			float t = ParseXmlAttribute(*currentElement, "Time", -1.f);
			if (t < 0.f)
			{
				// Do something here
			}

			for (const XMLAttribute* attr = currentElement->FirstAttribute(); attr != NULL; attr = attr->Next())
			{
				std::string attrName = attr->Name();
				if (attrName == "Position")
				{
					Vec2 pos = ParseXmlAttribute(*currentElement, "Position", Vec2::Zero());
					newAnimClip->SetPositionAtTime(t, pos);
				}
				else if (attrName == "Rotation")
				{
					float rotationDegrees = ParseXmlAttribute(*currentElement, "Rotation", 0.f);
					newAnimClip->SetRotationAtTime(t, rotationDegrees);
				}
				else if (attrName == "Scale")
				{
					Vec2 scale = ParseXmlAttribute(*currentElement, "Scale", Vec2::Zero());
					newAnimClip->SetScaleAtTime(t, scale);
				}
				else if (attrName == "Color")
				{
					Rgba8 color = ParseXmlAttribute(*currentElement, "Color", Rgba8::white);
					newAnimClip->SetColorAtTime(t, color);
				}

			}
		}
	}

	return newAnimClip;
}

void UIScene::CreateNestedElementsFromXMLElement(UIElement* rootElement, XMLElement* e)
{
	for (XMLElement* currentElement = e->FirstChildElement(); currentElement != NULL; currentElement = currentElement->NextSiblingElement())
	{
		// Sprite
		std::string elementType = currentElement->Name();
		if (elementType == "Sprite")
		{
			UISprite* newSprite = CreateSpriteFromXMLElement(currentElement);
			newSprite->m_parentElement = rootElement;
			rootElement->m_childElements.push_back(newSprite);
		}

		else if (elementType == "Button")
		{
			UIButton* newButton = CreateButtonFromXMLElement(currentElement);
			newButton->m_parentElement = rootElement;
			rootElement->m_childElements.push_back(newButton);
		}

		else if (elementType == "TextBox")
		{
			UITextBox* newTextBox = CreateTextBoxFromXMLElement(currentElement);
			newTextBox->m_parentElement = rootElement;
			rootElement->m_childElements.push_back(newTextBox);
		}

		else if (elementType == "Canvas")
		{
			UICanvas* newCanvas = CreateCanvasFromXMLElement(currentElement);
			newCanvas->m_parentElement = rootElement;
			rootElement->m_childElements.push_back(newCanvas);
		}

		else {
			if (elementType != "Animation")
				g_theConsole->PrintString(Rgba8::yellow, Stringf("Warning! Unknown element from XML File."));
		}
	}
}

UIElement* UIScene::GetUIElementByName(std::string name)
{
	std::vector elements = GetUIElements(m_rootElement);

	for (int i = 0; i < elements.size(); i++)
	{
		if (elements[i]->m_name == name)
			return elements[i];
	}
	return nullptr;
}

const void UIScene::Update()
{
	UpdateMouseRaycast();

	m_rootElement->Update();
}

const void UIScene::UpdateMouseRaycast()
{
	std::vector elements = GetUIElements(m_rootElement);

	// Clear all raycast info
	for (int i = 0; i < elements.size(); i++)
	{
		UIElement* currentElement = elements[i];
		if (currentElement)
			currentElement->SetIsHovered(false);
	}

	Vec2 mouseNormalizedPos = g_theInput->GetMouseNormalizedClientPos();
	Vec2 mousePos = mouseNormalizedPos * Vec2(UI_SIZE_X, UI_SIZE_Y);
	for (int i = elements.size() - 1; i >= 0; i--)
	{
		UIElement* currentElement = elements[i];
		if (!currentElement) continue;

		OBB2 boundingBox = currentElement->GetBoundingBox();
		if (boundingBox.IsPointInside(mousePos) && currentElement->IsSelectable())
		{
			currentElement->SetIsHovered(true);
			break;
		}

	}
}

const void UIScene::Render() const
{
	m_rootElement->Render();
}

const void UIScene::DebugRender() const
{
	m_rootElement->DebugRender();


	std::string elementsStr = GetElementsStrsForDebug(m_rootElement);
	VertexList vertexes;

	Vec2 dimensions = g_defaultFont->GetDimensionsForText2D(elementsStr, 31.5f);
	AppendVertsForAABB2D(vertexes, AABB2(50, 1000 + 31.5f - dimensions.y, 50 + dimensions.x, 1000 + 31.5f), Rgba8(255, 255, 255, 100));
	g_theRenderer->BindTexture(nullptr);
	g_theRenderer->DrawVertexArray(vertexes);

	vertexes.clear();
	g_defaultFont->AddVertsForText2D(vertexes, Vec2(50, 1000), 31.5f, elementsStr, Rgba8::black);
	g_theRenderer->BindTexture(g_defaultFont->GetTexture());
	g_theRenderer->DrawVertexArray(vertexes);


}

const void UIScene::AppendElement(UIElement* element, UIElement* parentElement)
{
	if (element == nullptr) return;

	std::vector elements = GetUIElements(m_rootElement);

	for (int i = 0; i < elements.size(); i++)
	{
		if (elements[i] == element)
		{
			g_theConsole->PrintString(Rgba8::red, "WARNING! APPENDING AN ELEMENT THAT'S ALREADY IN SCENE");
			return;
		}
	}

	for (int i = 0; i < elements.size(); i++)
	{
		if (elements[i] == parentElement)
		{
			parentElement->m_childElements.push_back(element);
			element->m_parentElement = parentElement;
			return;
		}
	}

	m_rootElement->m_childElements.push_back(element);
	element->m_parentElement = m_rootElement;

	return;

}

const void UIScene::RemoveElement(UIElement* element)
{
	std::vector elements = GetUIElements(m_rootElement);

	for (int i = 0; i < elements.size(); i++)
	{
		if (elements[i] == element)
		{
			if (elements[i]->m_parentElement)
			{
				for (int j = 0; j < elements[i]->m_parentElement->m_childElements.size(); j++)
					if (elements[i]->m_parentElement->m_childElements[j] == element)
						elements[i]->m_parentElement->m_childElements[j] = nullptr;
			}
		}
	}
}

const void UIScene::RemoveElementByName(std::string name)
{
	UIElement* ele = GetUIElementByName(name);
	if (ele != nullptr)
		RemoveElement(ele);
}

std::vector UIScene::GetUIElements(UIElement* rootElement) const
{
	std::vector elements;
	elements.push_back(rootElement);
	for (int i = 0; i < rootElement->m_childElements.size(); i++)
	{
		if (!rootElement->m_childElements[i])
			continue;
		std::vector childElements = GetUIElements(rootElement->m_childElements[i]);
		elements.insert(elements.end(), childElements.begin(), childElements.end());
	}

	return elements;
}

std::string UIScene::GetElementsStrsForDebug(UIElement* rootElement, int level) const
{
	std::string elementStrs;

	for (int i = 0; i < level; i++)
		elementStrs += "  ";
	elementStrs += rootElement->m_name + "\n";

	for (int i = 0; i < rootElement->m_childElements.size(); i++)
	{
		if (!rootElement->m_childElements[i])
			continue;
		elementStrs += GetElementsStrsForDebug(rootElement->m_childElements[i], level + 1);
	}

	return elementStrs;
}
            

UIScene

UIScene 是整个 UI 系统的核心,既能解析单个布局文件,也能实时管理 UI 元素。


Scenario

Scenario 代码用于解析文件中的剧情数据并将其保存在该类中,关卡管理器可以从这些文件获取游戏数据。

#include "Game/Scenario.hpp"
#include "ThirdParty/fast-cpp-csv-parser/csv.h"
#include "Engine/Core/FileUtils.hpp"

std::vector Scenario::m_scenarioList;

Scenario::Scenario(std::string fileName)
{
	m_name = GetNameFromFilePath(fileName);
	m_fileName = fileName;
	LoadScenario();
}

void Scenario::LoadScenario()
{
	m_commands.clear();
	io::CSVReader<9,
		io::trim_chars<' ', '\t'>,
		io::double_quote_escape<',', '"'>,
		io::throw_on_overflow,
		io::no_comment> in(m_fileName);
	in.read_header(io::ignore_extra_column, "Index", "Event", "Character", "Name", "Cloth", "Emote", "Dialog", "Audio", "Args");

	std::string indexStr, eventStr, character, name, cloth, emote, dialog, audio, argsStr;
	while (in.read_row(indexStr, eventStr, character, name, cloth, emote, dialog, audio, argsStr))
	{
		ScenarioCommandData d;
		d.m_index = indexStr;
		d.m_event = eventStr;
		d.m_character = character;
		d.m_name = name;
		d.m_cloth = cloth;
		d.m_emote = emote;
		d.m_dialog = dialog;
		d.m_audio = audio;
		d.m_args = SplitStringOnDelimiter(argsStr, ';');

		m_commands.push_back(d);
	}
}

void Scenario::CreateAllScenariosFromFile()
{
	Strings scenarioFileNames = GetFileNamesInFolder("Data/Gameplay/", "*.csv");

	for (int i = 0; i < scenarioFileNames.size(); i++)
	{
		CreateOrGetScenarioFromFile("Data/Gameplay/" + scenarioFileNames[i]);
	}

}

Scenario* Scenario::CreateOrGetScenarioFromFile(std::string fileName)
{
	for (int i = 0; i < m_scenarioList.size(); i++)
	{
		if (m_scenarioList[i]->m_fileName == fileName)
			return m_scenarioList[i];
	}
	Scenario* newScenario = new Scenario(fileName);
	m_scenarioList.push_back(newScenario);
	return newScenario;
}

Scenario* Scenario::GetScenarioByName(std::string name)
{
	for (int i = 0; i < m_scenarioList.size(); i++)
	{
		if (m_scenarioList[i]->m_name == name)
			return m_scenarioList[i];
	}
	return nullptr;
}

            

项目复盘


做得好的地方

  • 成功从零开发了一套简易 UI 系统
  • 成功基于这套系统开发了一款游戏

需要改进的地方

  • 项目开发期间的时间估算不够准确,资源编辑和规划所花的时间超出预期。
  • 动画系统实现难度较高,而我在制作动画时没有预留足够时间。

我的收获

  • 开发自己喜爱的引擎和游戏是一件非常享受的事
  • 从零开发 UI 系统并不算难,但必须重视细节
  • 动画系统实现难度较高,下次开发时应预留更多时间。