Unity as a LibraryでAndroid->Unity間のやりとりで、いちいちブリッジコード書くの面倒。。。
いい感じにやれないかなと思い、localhostでhttpServer立てて動かしてみた。
Android側の実装
Android側は無難にRetrofit(gson)で雑に実装した。DIとかちゃんとするのはまた今度。
// HomeFragment.kt(抜粋)
val retrofit = Retrofit.Builder()
.baseUrl("http://localhost:8080/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service: UnityBridgeService = retrofit.create(UnityBridgeService::class.java)
val coroutineScope = CoroutineScope(context = Dispatchers.Main)
textView.setOnClickListener {
coroutineScope.launch {
try {
val res = service.ping("test")
Log.d("HomeFragment", res.message)
showToast(res.message)
} catch (e: Exception) {
Log.d("HomeFragment", e.localizedMessage)
}
}
}
Unity側の実装(async/await)で動かしてみる
Unity側はSystem.NetのHttpListenerでいけそうなので、以下を参考に適当に実装した。
- UnityでHTTPリクエストを処理してみる - Qita
- HttpListenerContext.Response プロパティ - Microsoft Docs
- Listen for Http Requests with Unity - gist
// HttpServer.cs(async/await版)
using System.IO;
using System.Net;
using UnityEngine;
public class HttpServer : MonoBehaviour
{
readonly HttpListener _httpListener = new HttpListener();
public int port = 8080;
public string path = "/";
public bool startOnAwake = true;
// TODO: Listenerが死んだときのことを考えないといけない
void Start()
{
_httpListener.Prefixes.Add("http://*:" + port + path);
if (startOnAwake)
{
StartServer();
}
}
async void StartServer()
{
_httpListener.Start();
while (true)
{
var context = await _httpListener.GetContextAsync();
Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");
if (context.Request.HttpMethod == "POST")
{
var dataText = await new StreamReader(context.Request.InputStream,
context.Request.ContentEncoding).ReadToEndAsync();
Debug.Log(dataText);
}
var response = context.Response;
response.StatusCode = 200;
var buffer = System.Text.Encoding.UTF8.GetBytes("{\"message\": \"OK\"}");
response.ContentType = "application/json";
response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
await output.WriteAsync(buffer, 0, buffer.Length);
output.Close();
context.Response.Close();
}
}
void StopServer()
{
_httpListener.Stop();
}
void OnDestroy()
{
StopServer();
}
}
とりあえずUnityEditorで起動して、ローカルからcurlしてみたら動いてそう。
$ curl -i -X GET http://localhost:8080/
HTTP/1.1 200 OK
Content-Type: application/json
Server: Mono-HTTPAPI/1.0
Date: Sat, 13 Jun 2020 04:19:11 GMT
Content-Length: 15
Keep-Alive: timeout=15,max=100
{"message": "OK"}⏎
LibraryExportしてAndroid端末にinstallして起動。 ローカルからAndroid端末にcurlしたらresponse帰ってきた。
$ curl -X GET http://192.168.0.10:8080/
{"message": "OK"}⏎
しかし、実機のAndroidからRetrofitでHttpRequestしてみたら、エラーになった。 どうやらUnityPlayerが存在するActivityがフォアグラウンドじゃないと動かない。 GameObjectがactiveじゃないと駄目っぽい(あんまりわかってないがそんな気がする)
Android側のエラー(単純に応答がこなくてtimeoutしてる)
2020-06-13 14:16:47.758 9734-9734/com.unity.mynativeapp E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.unity.mynativeapp, PID: 9734
java.net.SocketTimeoutException: timeout
at okio.Okio$4.newTimeoutException(Okio.java:232)
at okio.AsyncTimeout.exit(AsyncTimeout.java:286)
at okio.AsyncTimeout$2.read(AsyncTimeout.java:241)
at okio.RealBufferedSource.indexOf(RealBufferedSource.java:358)
at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:230)
at okhttp3.internal.http1.Http1ExchangeCodec.readHeaderLine(Http1ExchangeCodec.java:242)
at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.java:213)
at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.java:115)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:94)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:43)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:94)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:88)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:229)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:172)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Caused by: java.net.SocketException: Socket closed
async/awaitをやめてThreadにする
ThreadでListenerのWhileループの処理を行うようにしてみた。
// HttpServer.cs(Thread版)
using System.IO;
using System.Net;
using System.Threading;
using UnityEngine;
public class HttpServer : MonoBehaviour
{
readonly HttpListener _httpListener = new HttpListener();
Thread _listenerThread;
public int port = 8080;
public string path = "/";
public bool startOnAwake = true;
// TODO: Listenerが死んだときのことを考えないといけない
void Start()
{
_httpListener.Prefixes.Add("http://*:" + port + path);
if (startOnAwake)
{
_httpListener.Start();
_listenerThread = new Thread(StartServer);
_listenerThread.Start();
}
}
async void StartServer()
{
while (true)
{
var context = await _httpListener.GetContextAsync();
Debug.Log($"{context.Request.HttpMethod} Request path: {context.Request.RawUrl}");
if (context.Request.HttpMethod == "POST")
{
var dataText = await new StreamReader(context.Request.InputStream,
context.Request.ContentEncoding).ReadToEndAsync();
Debug.Log(dataText);
}
var response = context.Response;
response.StatusCode = 200;
var buffer = System.Text.Encoding.UTF8.GetBytes("{\"message\": \"OK\"}");
response.ContentType = "application/json";
response.ContentLength64 = buffer.Length;
var output = response.OutputStream;
await output.WriteAsync(buffer, 0, buffer.Length);
output.Close();
context.Response.Close();
}
}
void StopServer()
{
_httpListener.Stop();
}
void OnDestroy()
{
StopServer();
}
}
Threadの管理をちゃんとしないといけなくて大変そうだが、とりあえず動いた 🎉
Android側のLog
2020-06-13 14:29:29.351 10587-10587/com.unity.mynativeapp D/HomeFragment: OK
Unity側のLog
2020-06-13 14:18:16.447 9466-9663/com.unity.mynativeapp I/Unity: GET Request path: /ping/test
HttpServer:.ctor()
System.Threading.ContextCallback:Invoke(Object)
System.Threading.ExecutionContext:RunInternal(ExecutionContext, ContextCallback, Object, Boolean)
System.Runtime.CompilerServices.MoveNextRunner:Run()
System.Action:Invoke()
System.Threading.SendOrPostCallback:Invoke(Object)
UnityEngine.UnitySynchronizationContext:Exec()
UnityEngine.UnitySynchronizationContext:Exec()
(Filename: ./Runtime/Export/Debug/Debug.bindings.h Line: 35)
まとめ
- Uniy上httpServerを立ててAndroid側からrequestすることはできそう
- Listenerのserve処理はThreadで動かさないとUnityPlayerがあるActivityがbackgroundに遷移すると動かないので注意
- 実際運用していくと色々大変そうではある(Listenerがお亡くなりになったときにどうするのか?とか)
- Threadちゃんと管理するの大変そう(小並感)
- UnityPlayerを起動しないとListenできないのでそのへんうまいことやる必要がありそう
処理の流れイメージ図
その他メモ
- 逆にUnity -> AndroidのhttpRequestもできるか?(むしろこっちのほうがほしい説?認証情報もらったりなどなど)
- HttpRequestじゃなくてSQLiteとかでうまいことやれないかな?とか思った