概要
何かしらのログを送り続けて表示してくれるようなツールが欲しいと思い作りました
わざわざUnityで作るようなものでもないと思いつつ、モデルやロジックを使い回したかったので...
実装
通信部分
HTTPサーバー部分は以下の記事を参考に実装しています
ただし、WebGLではスレッドを使えないので少しだけ改造しています
(Unity6ではマルチスレッド対応済みなので、プロジェクトのバージョンを挙げられる方は上記の記事を参考にしてください!)
通信部分コード
HttpListener.cs
using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
namespace HttpDataTool
{
public class UnityHttpListener : MonoBehaviour
{
private HttpListener _listener;
private const string Domain = "localhost";
private const int Port = 8080;
private const string ContentType = "application/json";
private void Start()
{
_listener = new HttpListener();
_listener.Prefixes.Add("http://" + Domain + ":" + Port + "/");
_listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
_listener.Start();
// Unity6以前はWebGLでマルチスレッドが使えないため、コルーチンで非同期処理を行う
// (メインスレッドで処理される)
StartCoroutine(StartListenerCoroutine());
Debug.Log("Server Started");
}
private void OnDestroy()
{
_listener.Stop();
}
private IEnumerator StartListenerCoroutine()
{
while (_listener.IsListening)
{
var result = _listener.GetContextAsync();
yield return new WaitUntil(() => result.IsCompleted);
ListenerCallback(result.Result);
}
}
private void ListenerCallback(HttpListenerContext context)
{
var request = context.Request;
Debug.Log($"Method: {request.HttpMethod}, Url: {request.Url}");
try
{
Action<HttpListenerRequest> process = context.Request.HttpMethod switch
{
var s when s == HttpMethod.Get.Method => HttpMethodProcessor.Get,
var s when s == HttpMethod.Post.Method => HttpMethodProcessor.Post,
_ => throw new Exception("Method Not Allowed")
};
process.Invoke(request);
// 何か情報を返すことを想定していないので、処理成功時は固定のレスポンスを返す
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.ContentType = ContentType;
using var writer = new StreamWriter(context.Response.OutputStream, Encoding.UTF8);
writer.Write(JsonUtility.ToJson(new { message = "Success" }));
}
catch (Exception e)
{
// 処理中の全例外はExceptionで処理する
ReturnInternalError(context.Response, e);
}
}
private static void ReturnInternalError(HttpListenerResponse response, Exception cause)
{
Debug.LogError(cause);
response.StatusCode = (int)HttpStatusCode.InternalServerError;
response.ContentType = ContentType;
try
{
using (var writer = new StreamWriter(response.OutputStream, Encoding.UTF8))
writer.Write(JsonUtility.ToJson(cause));
response.Close();
}
catch (Exception e)
{
Debug.LogError(e);
response.Abort();
}
}
}
}
HttpMethodProcessor.cs
using System;
using System.Net;
using System.Text;
using UnityEngine;
namespace HttpDataTool
{
public static class HttpMethodProcessor
{
public static void Get(HttpListenerRequest request)
{
// GETはヘルスチェックのみで使用
if (request.Url.AbsolutePath != "/health")
throw new Exception("Not Found");
}
public static void Post(HttpListenerRequest request)
{
switch (request.Url.AbsolutePath)
{
case "/api_log":
var body = request.RequestBody();
DataContainer.AddData(JsonUtility.FromJson<Data>(body));
break;
default:
throw new Exception("Not Found");
}
}
private static string RequestBody(this HttpListenerRequest request)
{
var body = request.InputStream;
var bodyBytes = new byte[request.ContentLength64];
body.Read(bodyBytes, 0, bodyBytes.Length);
return Encoding.UTF8.GetString(bodyBytes);
}
}
}
データ部分
今回はAPIを叩いた時のログを受け取る前提です
DataContainerは用途に応じて RemoveAllData
とかFilterData
とかを追加で実装しましょう
データ部分コード
Data.cs
using System;
namespace HttpDataTool
{
[Serializable]
public class Data
{
public string endpoint;
public string method;
public int responseCode;
public string response;
public string request;
}
}
DataContainer.cs
using System.Collections.Generic;
using UnityEngine.Events;
namespace HttpDataTool
{
public static class DataContainer
{
public static List<Data> DataList { get; } = new();
public static readonly UnityEvent OnDataChanged = new();
public static void AddData(Data data)
{
DataList.Add(data);
OnDataChanged.Invoke();
}
}
}
表示部分
EnhancedScrollerを使っていますが、持っていない場合は軽量なリストビューを別途用意してください
UIToolを使う際には、ListView用に書き換える必要があります
表示部分コード
ListView.cs
using EnhancedUI.EnhancedScroller;
using UnityEngine;
namespace HttpDataTool
{
public class ListView : MonoBehaviour, IEnhancedScrollerDelegate
{
private EnhancedScroller _scroller;
[SerializeField] private EnhancedScrollerCellView cellViewPrefab;
[SerializeField] private DetailViewer detailViewer;
private void Start()
{
_scroller = GetComponent<EnhancedScroller>();
_scroller.Delegate = this;
_scroller.ReloadData();
detailViewer.SetData(null);
DataContainer.OnDataChanged.AddListener(() => _scroller.ReloadData());
}
public int GetNumberOfCells(EnhancedScroller _)
=> DataContainer.DataList.Count;
public float GetCellViewSize(EnhancedScroller _, int dataIndex)
=> 120;
private void OnSelected(Data data)
=> detailViewer.SetData(data);
public EnhancedScrollerCellView GetCellView(EnhancedScroller scroller, int dataIndex, int cellIndex)
{
var cellView = scroller.GetCellView(cellViewPrefab) as CellView;
if (cellView == null) return cellView;
cellView.SetData(DataContainer.DataList[dataIndex]);
cellView.Selected = OnSelected;
return cellView;
}
}
}
CellView.cs
using EnhancedUI.EnhancedScroller;
using UnityEngine;
using TMPro;
namespace HttpDataTool
{
public class CellView : EnhancedScrollerCellView
{
[SerializeField] private TMP_Text methodTypeText;
[SerializeField] private TMP_Text endpointText;
private Data _data;
public delegate void SelectedDelegate(Data data);
public SelectedDelegate Selected;
public void SetData(Data data)
{
methodTypeText.text = data.method;
endpointText.text = data.endpoint;
_data = data;
}
public void OnSelected()
{
Selected?.Invoke(_data);
}
}
}
DetailViewer.cs
using UnityEngine;
using TMPro;
namespace HttpDataTool
{
public class DetailViewer : MonoBehaviour
{
[SerializeField] private TMP_Text endpointText;
[SerializeField] private TMP_Text methodTypeText;
[SerializeField] private TMP_Text responseCodeText;
[SerializeField] private TMP_Text requestText;
[SerializeField] private TMP_Text responseText;
public void SetData(Data data)
{
if (data == null)
{
gameObject.SetActive(false);
return;
}
gameObject.SetActive(true);
endpointText.text = data.endpoint;
methodTypeText.text = data.method;
responseCodeText.text = data.responseCode.ToString();
requestText.text = data.request;
responseText.text = data.response;
}
}
}