PhotonUnityNetwork(PUN)を使って、全ユーザで同一画像を表示させる話

目的
----
PhotonUnityNetwork(PUN)を使って、同一Photonサーバに接続している全ユーザに対して同一の画像を表示させることを目的としています。
**同じ画像をダウンロードして表示させるのは当然として、各ユーザが任意の画像をアップロードしてそれを全ユーザに共有することも含めます。**


ユーザが自由に画像をアップロード、ダウンロードして共有できると、オンラインゲームでユーザが好きな画像を共有できるわけですし、なんか応用が効きそうな雰囲気が出ません？
サーバ周りの知識がまだ乏しいので実際は更に効率の良いやり方が他にありそうだけどとりあえず自分はこれでやりました。という感じでよろしくお願いします。更に良いやり方があればコメント欄で教えてくれると嬉しいです！

必要なもの
-------
・WebDAVサーバ(画像を簡単にアップロード、ダウンロードさせるのに大事です。)
・PUN(AssetStoreからダウンロードしてください。)
・Unity(言わずもがな)

前提
-----
Unityにプロジェクトが作ってあって、こんな感じに、Canvasとその子要素にImageがあるとします。
![CropperCapture[1053].png](https://qiita-image-store.s3.amazonaws.com/0/88657/1de82d3e-e0a4-b892-12d2-a1c5115f802f.png)
**またImageには、PhotonViewが既にAddComponentされているものとします。**

また、今回の話を理解するのに、若干Photonの知識を要すると思います。
まずダウンロードの話から始めます。その後にアップロードを含めた話も。

何をするのか(ダウンロード編)
----
・WebDAVサーバを作る
・サーバから画像をDLして、Imageに対して画像を適用する処理を書く。
・PUNRPC属性を付与したメソッドを用意して、リモートからの命令を受け取って上記処理を行う処理を書く。
・適当なトリガーを用意して、各プレイヤーに上記処理を行うように命令を出す。
ということをします。

**(つまり、サーバから特定の画像をダウンロードしてそれを表示させるように全ユーザに指示することで、同一画像を共有するということです。)**

WebDAVサーバを作る
----
[RaspberryPi - RaspiにNASを作ってAndroidで動画を見る話とその他メモ](http://qiita.com/CST_negi/items/898cebf08ea96a420a7b)
ここの下半分に書きました。

RaspberryPi上にApacheが乗っていて、そこにWebDAVサーバが作ってあります。
また、BASIC認証がかかっています。

・サーバから画像をDLして、Imageに対して画像を適用する処理を書く。
----

``` SharedImageManager.cs
using UnityEngine;
using System.Collections;

public class SharedImageManager : Photon.MonoBehaviour
{
    private UnityEngine.UI.Image sharedImage;
	// Use this for initialization
	void Start ()
	{
	    sharedImage = GetComponent<UnityEngine.UI.Image>();
	}

    private IEnumerator DownloadImage(string url)
    {
        var www = new WWW(url);
        while (!www.isDone)
        {
            yield return www;
        }
        Debug.Log("DL Completed");
        AttachImage(www); //画像のダウンロードが完了したらAttachImageが呼ばれる。
    }

    private void AttachImage(WWW www)
    {
        var tex = www.texture;
        var spr = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(tex.width/2,tex.height/2));
        sharedImage.sprite = spr;
    }
}
```
そんなに変なことはしてないですね。
・与えられたURLから画像をダウンロード（コルーチンなので画像のDL完了を待つことができる）
・画像のダウンロードが完了したらwww.texture(Texture2D型)からSpriteを作る
・sharedImage.spriteに対して作ったSpriteを適用させる。

このクラスをCanvasの子要素である、Imageに対してAddComponentしておきます。
SharedImageManagerクラスが**Photon.MonoBehaviour**からの継承となっていますが、その理由は次が関係しています。

・PUNRPC属性を付与したメソッドを用意して、リモートからの命令を受け取って上記処理を行う処理を書く。
----
``` SharedImageManager.cs
    [PunRPC]
    public void StartSharing(string url)
    {
        StartCoroutine(DownloadImage(url));
    }
```
先ほど書いたクラスにこれを追加します。
・PUNRPC属性を付与して、このメソッドを他のユーザが呼び出した時にそれを受け付けられるようにします。

・適当なトリガーを用意して、各プレイヤーに上記処理を行うように命令を出す。
----
``` SharedImageManager.cs
  void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
        //WebDAVサーバに置いた画像ファイルのURLをここで指定
        var args = new object[] { "http://example.com/test.jpg" };
        photonView.RPC(
           "StartSharing",                  // メソッド名
           PhotonTargets.All,          // ネットワークプレイヤー全員に対して呼び出す
           args);
        }
    }
```
このトリガー自体はなんでもいいです。今回僕はRキーを押したら全ユーザに処理を行うように命令を出すようにしました。

**・args変数には呼び出すメソッドの引数を指定します。**
今回はStartSharingメソッドには引数が画像URLの引数のみなのでそれだけを指定します。
**・photonView.RPCメソッドによって、リモートユーザに対してもメソッドを呼び出します。**
第一引数はメソッド名
第二引数は同一のPhotonサーバに接続している人の誰に送るかという指定
第三引数は第一引数で指定したメソッドの引数を指定
ということです。

このURL指定部分を適当なネットに転がっている画像ファイルのURLにしても動くと思うので試してみてください。
**なんでWebDAVサーバにしたかは次から解説します。**


何をするのか(アップロード編)
---
WebDAVサーバを作った理由は、画像のダウンロードとアップロードが楽にできるからです。特に後者が楽なのは嬉しい。
というわけで
・Unityのプロジェクト内にあるStreamingAssetsの中の画像ファイルをWebDAVサーバにアップロードする
ということをします。



Unityのプロジェクト内にあるStreamingAssetsの中の画像ファイルをWebDAVサーバにアップロードする
----

``` FileUploadManager.cs

using System.IO;
using System.Net;
using System.Text;
using UnityEngine;
using System.Collections;

public class FileUploadManager : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	    if (Input.GetKeyDown(KeyCode.Q))
	    {
	        var filePath = Application.streamingAssetsPath + @"\unityJpg.jpg";
	        filePath = filePath.Replace('/', '\\');
            //公開されているWebDAVサーバのURLとローカルにある画像ファイルを引数に指定している。
            UploadFile("http://example.com/webdav",filePath);
        }

	}

    public void UploadFile(string url,string filePath)
    {
        // HttpWebRequestを初期化し、認証とメゾットを設定
        var webReq = (HttpWebRequest)WebRequest.Create(url + @"\test.jpg");
        webReq.Credentials = new NetworkCredential("WebDAVサーバのUserName", "そのPassword");
        webReq.Method = WebRequestMethods.File.UploadFile;

        // アップロードするファイルをオープンし、HttpWebRequestにサイズを設定
        FileStream inStrm = new FileStream(filePath, FileMode.Open);
        webReq.ContentLength = inStrm.Length;

        // ファイルをバッファに読み込み・サーバーへ書き込み
        using (Stream outStrm = webReq.GetRequestStream())
        {
            var bufSize = 1048576; // 1MB
            var buffer = new byte[bufSize];
            var bytesRead = 0;

            while ((bytesRead = inStrm.Read(buffer, 0, bufSize)) > 0)
                outStrm.Write(buffer, 0, bytesRead);

            inStrm.Close();

            outStrm.Flush();
            outStrm.Close();
        }

        // 結果取得（今回はヘッダとステータス）
        var res = (HttpWebResponse)webReq.GetResponse();
        Debug.Log(res.Headers.ToString());
        Debug.Log("Status Code:" + res.StatusCode);
        res.Close();
    }
}

```
(参考：http://blogs.gine.jp/taka/archives/1376 )
これはQキーを押すことでStreamingAssets以下にある「unityJpg.jpg」という画像ファイルをアップロードしています。
ローカルにおけるファイルの場所は別にStreamingAssets以下にある必要はなくて、ここを変更すれば、ゲーム画面にドラッグした画像とかにも出来そうな予感がします。

最後に
----
本記事で大事なのはダウンロード部分ですので、それを踏まえてもしアップロードしたいという欲求があったらアップロードの方を参考にするという感じが良いと思います。
こんな感じになります。(テストプロジェクトなのでミクがいますが気にしないでください。)
![CropperCapture[1057].png](https://qiita-image-store.s3.amazonaws.com/0/88657/46465ee7-37ee-873b-8d6a-e61559b2d5e2.png)


以下はおまけです。

サーバを使わずともPhotonで完結させればいいのではという議論
-----
最初は

``` PunRPCManager.cs
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
	    var tex2d = (Texture2D) Resources.Load("tekitounaImage");
	    byte[] pngData = tex2d.EncodeToPNG();
	    var args = new object[] {pngData,tex2d.width,tex2d.height};
        
        photonView.RPC(
        "RPCFunc",                  // メソッド名
        PhotonTargets.All,          // ネットワークプレイヤー全員に対して呼び出す
        args);
	    }
    }

    [PunRPC]
    public void RPCFunc(byte[] textureByteArray, int width, int height, PhotonMessageInfo info)
    {

        var tex = new Texture2D(width, height);
        tex.LoadImage(textureByteArray);
        Debug.Log(tex.name);
        GetComponent<Renderer>().material.mainTexture = tex;

    }
```
これはSpaceキーを押すと、PhotonRPCを用いて、
・画像をByte列にエンコード
・全ユーザに対して、Byte列とPNGの縦と横の数値情報を渡す
・各ユーザはそれら情報からLoadImageによって画像を復元する。
・それを使って何かする。(このコードではMaterialのMainTextureにしています。)

で、送れるかなーと思ったんですけど、500KBのPNGを使ったところ正常に動作しなかったのでこれは何か工夫が必要だなと思いました。
これは予感なのですが、これで大規模なファイルとか送り合ってると死んでしまうと思ったので大人しくサーバ使ったほうがいいなという感じです。

-----

以上です。