[Unity Server] # 4 - Simulation on headless server

qweasfjbv·2025년 2월 12일

UnityServer

목록 보기
4/5

개요


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

목표는 위처럼 그래프를 그리는 것입니다.
(위는 TIS-100, 아래는 Opus Magnum의 결과창입니다.)

구현


Structs

    #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 는 그래프를 그리기위한 결과 데이터입니다.


Server에 DB 세팅

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

Simulation은 Unity 클라이언트 코드와 비슷하게 작성할 수 있습니다.
하지만, 주의해야 할 점이 몇가지 있습니다.

  • Server는 Client 여러 개를 동시에 Handle합니다. 하지만, Simulation은 동시에 수행하지 못합니다.
  • Graphic 관련 코드들이 에러가 발생할 수도 있습니다.
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);
}

우선 간단하게 시뮬레이션을 돌리고 기다렸다가 끝나는 코루틴으로 테스트 해보겠습니다.

마무리


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

  • 서버는 큐에 데이터를 넣고 시뮬레이션은 큐에서 데이터를 가져와서 비동기적으로 처리하도록 해야합니다.
  • 네트워크 환경에서 작동하는지 확인해야 합니다.
  • 데이터를 받은 후에, MySql의 데이터를 가공해서 클라이언트로 보내야합니다.
  • 클라이언트에서는 받은 데이터로 그래프를 그려야합니다.

0개의 댓글