LoginSignup
6
7

More than 1 year has passed since last update.

Unity+Node.jsで1対1の簡単なオンラインシューティングゲームを作った話 Unity編

Last updated at Posted at 2019-05-26

この記事に関わる記事一覧

導入編
Node.js編
Unity編(現在の記事)

はじめに

Node.jsを使ってみたく、実際に手を動かして何か作ってみるのが一番だと思い、
自分なりにプレイヤーのマッチングや同期の取り方を考えてやってみようと思いました。

Node.js(サーバー側)よりもUnity(クライアント側)で苦戦しました...

私はNode.js,リアルタイム通信の知識がそこまであるわけではないので、
素人なりにどう考えて実装していったかの記録を残していきたいと思い記事を書き始めました。

コードの書き方に正解はないと思うので、これから書いていく記事を通して
今回作成したゲームの作り方の概念だけ書いていこうかなと思います。

成果物

ソースコード公開しました!
クライアントプロジェクト(Unity)
サーバープロジェクト(Node.js)

実行方法

1.対戦するのには2つクライアントが必要なので、あらかじめビルドしておきます
2.サーバープロジェクトのindex.jsをnodeで実行
3.クライアントプロジェクトのMenuシーンと1でビルドしたアプリを実行
4.両アプリでマッチング開始

操作方法

ドラッグ:移動
タップ :自機が向いている方向に弾を発射

スクリーンショット

IMG_5512.PNG

IMG_5514.PNG

UDPManager

まず、通信するクラスが必要だったので作成しました。
作成したクラスはJSONで送受信データを扱います。
シングルトンです。

UDPManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using MiniJSON;

public class UDPManager : MonoBehaviour
{
    private static UDPManager instance = null;
    public static UDPManager Instance
    {
        get
        {
            if (instance)
            {
                return instance;
            }
            else
            {
                instance = GameObject.FindObjectOfType<UDPManager>();
                if (!instance)
                {
                    var newObj = new GameObject();
                    newObj.name = "UDPManager";
                    newObj.AddComponent<UDPManager>();
                    Instantiate(newObj);
                    instance = newObj.GetComponent<UDPManager>();
                }
                return instance;
            }
        }
    }

    [SerializeField]
    string host = "localhost";
    [SerializeField]
    int port = 33333;

    private UdpClient client;
    private Thread thread;

    public delegate void MessageReceived(JsonNode jsonNode, string jsonStr);
    public event MessageReceived messageReceived;

    private void Awake()
    {
        if (this != Instance)
        {
            Destroy(this.gameObject);
            return;
        }

        DontDestroyOnLoad(this.gameObject);
    }

    void Start()
    {
        client = new UdpClient();
        client.Connect(host, port);

        thread = new Thread(new ThreadStart(ThreadMethod));
        thread.Start();
    }

    void Update()
    {

    }

    private void OnApplicationFocus(bool focus)
    {
        if (client == null)
            return;

        if(focus)
        {
            client.Connect(host, port);
        }
    }

    void OnApplicationQuit()
    {
        client.Close();
        thread.Abort();

        if (this == Instance) instance = null;
    }

    private void ThreadMethod()
    {
        while (true)
        {
            if (!client.Client.Connected)
                client.Connect(host, port);

            IPEndPoint remoteEP = null;
            byte[] data = client.Receive(ref remoteEP);
            string text = Encoding.UTF8.GetString(data);

            if (messageReceived != null)
            {
                JsonNode jsonNode = JsonNode.Parse(text);
                messageReceived(jsonNode, text);
            }

            Debug.Log("GET:" + text);
        }
    }

    public void SendJson(string jsonStr)
    {
        if(!client.Client.Connected)
            client.Connect(host, port);

        byte[] dgram = Encoding.UTF8.GetBytes(jsonStr);
        client.Send(dgram, dgram.Length);

        Debug.Log("SEND:" + jsonStr);
    }
}

hostにはサーバーのアドレスを入れます。

JSONを扱うためにMiniJsonとそのパラメーターを扱いやすくする、
Koki IbukuroさんのJsonNodeを使用させていただきました。

シングルトンなので

UDPManager udpManager = UDPManager.Instance;

でどこからでも取得できています。
オブジェクトとして生成されていなかったとしても、自動的に生成されるように実装されています。

以下使い方です。

JSONの送信

string jsonStr = "{\"type\":\"greet\",\"msg\":\"hello\"}";
UDPManager.Instance.SendJson(jsonStr);

JSONの受信

//1.受信処理をするクラスを作成
void OnReceiveMessage(JsonNode jsonNode, string jsonStr)
{
 //処理
}

//2.関数をUDPManagerに登録
UDPManager.Instance.messageReceived += OnReceiveMessage;

//3.オブジェクトが破棄されたときに関数の登録を解除するようにしておく
//OnDestroyはMonoBehaviourが破棄されるときに自動的に呼ばれる関数
void OnDestroy()
{
 UDPManager.Instance.messageReceived -= OnReceiveMessage;
}

あとはUDPManagerがメッセージを受信するたびに、OnReceiveMessageを呼び出します。
注意しなければならないことが一つありまして、
OnReceiveMessage内はUnityのメインスレッドで実行されないので、
オブジェクトの生成や位置の変更などオブジェクトの操作ができなくなります。

なので今回私は以下のようにして実行しました。

//処理を入れておくキューを生成しておく
Queue<Action> mainThreadQueue = new Queue<Action>();

//処理を追加
Action action = () => {
 //処理
};
mainThreadQueue.Enqueue(action);

//Update内で実行
void Update()
{
  while(mainThreadQueue.Count > 0)
  {
    Action action = mainThreadQueue.Dequeue();
    action();
  }
}

UDPManager.csは自己責任でご自由に使っていただいて大丈夫です。

マッチング

マッチングは以下のようなシーケンスで実装しました。
Matching.png

draw.ioを使って書いてみました。
UML詳しくないので、書き方が間違っているかもしれませんが、
今回のゲームのマッチングの流れが伝われば幸いです。

Unityで行なっている処理としては以下の流れです
1.マッチング開始したことをサーバーに伝える
2.サーバーから自分のユーザー情報を受け取る
3.マッチングが成立情報をサーバーから受け取りシーン遷移
3.マッチングが成立しなかった情報をサーバーから受け取る

基本的にサーバーがマッチングの処理を行なってくれるので、
クライアント側ではメッセージの送受信ができれば実装できると思います。
サーバー側の処理についてはNode.js編をご参照ください。

入力情報・位置・回転同期

この記事を書くために自分の書いたコードを見直してみましたが、
コードが汚い状態になっておりまとめるのが難しい状態であると感じました...

自分の考えが整理されていることと、コードの綺麗さは比例するのかもしれませんね...
入力の同期だけでサンプルを作り、別の記事で詳しく説明したいと思います。

最後に

肝心な部分が説明できない記事になってしまい申し訳ございません。
導入編
Node.js編
はスムーズに記事を書くことができたのですが、今回は何を書いて良いか分からなくなってしまいました。
ソースコードを全て載せたとしても説明が難しいです...
恐らくこれは自分の考えが整理されていないからだと思います。

Node.jsとUnityを使ったチュートリアル的なサンプルを次回から作成して説明していきたいと思います。

今回の記事でUDPManagerの内容は参考になると思います。
それ以外の内容はあまり参考にならないと思いますが...

これからも記事を書くことを習慣にしていきたいと思いますので、よろしくお願いいたします!!

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7