데이터를 주고받을 수 있게 되었으니, 시뮬레이션을 위한 데이터를 넘겨주고 시뮬레이션을 돌리는 코드를 작성해보겠습니다.


목표는 위처럼 그래프를 그리는 것입니다.
(위는 TIS-100, 아래는 Opus Magnum의 결과창입니다.)
#region Server Containers
/// <summary>
/// Client에서 서버에 보낼 시뮬레이션 데이터
/// </summary>
public class ClientSimulationData
{
public int userId;
public StageSaveData saveData;
}
/// <summary>
/// 서버에서 클라이언트로 보낼 결과 데이터
/// </summary>
public class ServerResultData
{
public NormalizedResultData[] datas;
public ServerResultData()
{
datas = new NormalizedResultData[Managers.Resource.GetStageCount()];
for (int i = 0; i < datas.Count(); i++)
{
datas[i].cycleGraphs = Enumerable.Repeat(0, Constants.COUNT_GRAPH_MAX).ToArray();
datas[i].buttonGraphs = Enumerable.Repeat(0, Constants.COUNT_GRAPH_MAX).ToArray();
datas[i].killGraphs = Enumerable.Repeat(0, Constants.COUNT_GRAPH_MAX).ToArray();
}
}
public void InsertData(int stageIdx, int cycleCnt, int btnCnt, int killCnt)
{
int cycleMax = Managers.Resource.GetStageInfo(stageIdx).maxCounts[0];
int btnMax = Managers.Resource.GetStageInfo(stageIdx).maxCounts[1];
int killMax = Managers.Resource.GetStageInfo(stageIdx).maxCounts[2];
int cycleIdx = GetBarIdx(cycleCnt, cycleMax);
int btnIdx = GetBarIdx(btnCnt, btnMax);
int killIdx = GetBarIdx(killCnt, killMax);
if (cycleIdx > 0) datas[stageIdx].cycleGraphs[cycleIdx]++;
if (btnIdx > 0) datas[stageIdx].buttonGraphs[btnIdx]++;
if (killIdx > 0) datas[stageIdx].killGraphs[killIdx]++;
}
private int GetBarIdx(int cnt, int max)
{
if (max == 0) return -1;
return Mathf.Min((cnt * Constants.COUNT_GRAPH_MAX / max), Constants.COUNT_GRAPH_MAX - 1);
}
}
public class NormalizedResultData
{
public int[] cycleGraphs;
public int[] buttonGraphs;
public int[] killGraphs;
}
#endregion
우선 데이터들을 담을 컨테이너들부터 선언해줍니다.
ClientSimulationData 는 UserId와 SaveData를 보내서 시뮬레이션을 돌리고, 중복된 결과를 저장하지 않도록 합니다.
ServerResultData 는 그래프를 그리기위한 결과 데이터입니다.
sudo apt update
sudo apt install mysql-server
sudo systemctl start mysql
sudo mysql -u root -p
해당 명령어들로 Ubuntu Server에 mysql을 실행시켜줍니다.
CREATE DATABASE game_results;
USE game_results;
CREATE TABLE results (
UserId VARCHAR(255),
StageIdx INT,
CycleCount INT,
ButtonCount INT,
KillCount INT,
PRIMARY KEY (UserId, StageIdx)
);
위와 같이 간단한 DB 테이블을 만들어 준 뒤에, Unity에서 접근하도록 하겠습니다.
mysqlConnector를 설치한 후에, MySql.Data.dll 파일을 Assets/Plugins/ 위치에 넣습니다.
Assembly 'Assets/Plugins/MySql.Data.dll' will not be loaded due to errors:
Unable to resolve reference 'Zstandard.Net'. Is the assembly missing or incompatible with the current platform?
Reference validation can be disabled in the Plugin Inspector.
Unable to resolve reference 'K4os.Compression.LZ4.Streams'. Is the assembly missing or incompatible with the current platform?
Reference validation can be disabled in the Plugin Inspector.
...
위와 같은 에러가 발생할 수도 있는데, 저기 보이시는 Zstandard.Net, K4os.Compression.LZ4.Streams 와 같은 dll들을 전부 설치해주어야 합니다. NuGetForUnity 와 같은 툴 사용하시면 편리합니다.
( https://github.com/GlitchEnzo/NuGetForUnity )
private string connectionString = "Server=localhost; Database=human_results; User ID=root; Password=;";
public void InsertTestData()
{
using (MySqlConnection conn = new MySqlConnection(connectionString))
{
try
{
conn.Open();
Debug.Log("Connection Opened.");
string query = @"
INSERT INTO results (UserId, StageIdx, CycleCount, ButtonCount, KillCount)
VALUES (@UserId, @StageIdx, @CycleCount, @ButtonCount, @KillCount)
ON DUPLICATE KEY UPDATE
CycleCount = LEAST(CycleCount, @CycleCount),
ButtonCount = LEAST(ButtonCount, @ButtonCount),
KillCount = LEAST(KillCount, @KillCount);
";
using (MySqlCommand cmd = new MySqlCommand(query, conn))
{
cmd.Parameters.AddWithValue("@UserId", 1);
cmd.Parameters.AddWithValue("@StageIdx", 1);
cmd.Parameters.AddWithValue("@CycleCount", 1);
cmd.Parameters.AddWithValue("@ButtonCount", 2);
cmd.Parameters.AddWithValue("@KillCount", 3);
cmd.ExecuteNonQuery();
Debug.Log("Test data inserted successfully.");
}
}
catch (MySqlException ex)
{
Debug.LogError("Error: " + ex.Message);
}
}
}
DB에 Insert 하는 명령어를 짠 뒤에 간단하게 테스트를 해보았습니다.


DB에 잘 들어가는 모습을 확인할 수 있습니다.
Simulation은 Unity 클라이언트 코드와 비슷하게 작성할 수 있습니다.
하지만, 주의해야 할 점이 몇가지 있습니다.
private bool isSimulEnd = true;
public IEnumerator RunSimulation()
{
// if (dataQueue.Count <= 0) yield break;
Debug.Log("Start Simulation");
// HACK - Wait for Awake(), Start() ...
yield return new WaitForSeconds(2.0f);
isSimulEnd = false;
// HACK - temp datas
ClientSimulationData data = new ClientSimulationData();
data.stageIdx = 0;
data.saveData = Managers.Data.GetGridDatas(0, 0);
// Load map and simulate
MapManager.Instance.LoadStage(data.stageIdx, data.saveData);
MapManager.Instance.AddPersonWith1x(0f);
GameManagerEx.Instance.SetExeType(ExecuteType.Play);
// Wait until simuation ended
yield return new WaitUntil(() => isSimulEnd);
}
public void OnSimulationEnd(GameResultInfo info, bool isSuccess)
{
SimulationResult result = new SimulationResult(0, info);
isSimulEnd = true;
if (isSuccess)
InsertResultData(result);
}
우선 간단하게 시뮬레이션을 돌리고 기다렸다가 끝나는 코루틴으로 테스트 해보겠습니다.


우선 로컬 데이터의 테스트를 완료했습니다.
다음 목표는 다음과 같습니다.