Strixアクションゲームのサンプル

Strix Unity SDKをダウンロードしてStrixの初期設定を済ませた方は、このドキュメントの残りの部分を全て読んでSDKとStrixの使用方法を詳しく知りたいと思っているかもしれません。

しかし、実際にStrixを使用している既存のプロジェクトで基本的な説明を受けたいと思う方は、このページから始めましょう。このページはStrixActionGameSampleというサンプルゲームを使った一種のチュートリアルです。このサンプルはStrix Unity SDKのZIPファイルに入っています。

このチュートリアルを試すにはUnityを実行する必要がありますが、もうお持ちですよね!

サンプルフォルダー

サンプルはStrixUnitySDK > samples フォルダーにあります。Unity EditorでStrixActionGameSampleのUnityプロジェクトを読み込みましょう。

注釈

Strixのプラグインは既にStrixActionGameSampleプロジェクトにインポートしてあります。SDKのインポート手順に従ってご自分でインポートする必要はありません。

Unityに詳しい方はご自分の流儀でStrixActionGameSampleプロジェクトを用意してください。Unityプロジェクトの開き方に自信がない場合は、こちらのページを参考にしてください。

サンプルの説明

チュートリアルの最初に、SampleSceneを開きましょう。

View of the sample game

このサンプルに含まれているゲームは、プレイヤーキャラクター、NPC、基本的な環境を備えたシンプルなアクションゲームです。通信の接続処理は、StrixConnectUIプレハブを使って簡単に実現しています。

注釈

SampleSceneの開き方がよく分からない場合は付録を参照してください。

サーバー情報

Properties for StrixConnectGUI

StrixConnectUI > StrixConnectPanelオブジェクトを選択して、StrixConnectGUIスクリプトを見つけましょう。

HostPortApplication Idというプロパティがあります。これらのプロパティはサーバーの情報で、スクリプトがサーバーに接続するために必要となります。まだサーバーを設定していない場合は、Strix Cloudのセットアップの手順に従って設定してください。その後、Strix Cloudから該当する値を取得して、このコンポーネントのプロパティに値を設定します。

注釈

Hostは、Strix Cloudではマスターホスト名と呼ばれており、アプリケーションダッシュボードのサーバー数タブにあります。(これはインターネットのドメイン名で0123.game.strixcloud.netのようなものです。)

Portは、Strix Cloudでは常に9122です。

Application IDは、Strix Cloudのアプリケーションダッシュボードの情報タブにあります。

プレイ

サーバー情報を設定したら、サンプルを試してみることができます。プレイボタンを押して、テキストボックスにプレイヤー名を入力します。そして、「接続」ボタンをクリックしてサーバーに接続します。これで、マップを走り回り、NPCやゲームに参加した他のプレイヤーを撃つことができます。

接続

ゲームを試し終わったら終了しましょう。

サーバーへの接続は簡単です。お好きなテキストエディターでStrixConnectGUI.csスクリプトを開きます。

// 分かりやすいように一部の行を省略しています

public class StrixConnectGUI : MonoBehaviour {
    public string host = "127.0.0.1";
    public int port = 9122;
    public string applicationId = "00000000-0000-0000-0000-000000000000";
    public Level logLevel = Level.INFO;
    public InputField playerNameInputField;
    public Text statusText;
    public Button connectButton;
    public UnityEvent OnConnect;

    public void Connect() {
        LogManager.Instance.Filter = logLevel;

        StrixNetwork.instance.applicationId = applicationId;
        StrixNetwork.instance.playerName = playerNameInputField.text;
        StrixNetwork.instance.ConnectMasterServer(host, port, OnConnectCallback, OnConnectFailedCallback);

        statusText.text = "Connecting MasterServer " + host + ":" + port;

        connectButton.interactable = false;
    }

    private void OnConnectCallback(StrixNetworkConnectEventArgs args)
    {
        statusText.text = "Connection established";

        OnConnect.Invoke();

        gameObject.SetActive(false);
    }

    private void OnConnectFailedCallback(StrixNetworkConnectFailedEventArgs args) {
        string error = "";

        if (args.cause != null) {
            error = args.cause.Message;
        }

        statusText.text = "Connect " + host + ":" + port + " failed. " + error;
        connectButton.interactable = true;
    }
}

スクリプトは、MonoBehaviourを継承し、いくつかのプロパティを定義することから始まります。

ConnectメソッドではStrixNetworkapplicationIdplayerNameの値を設定します。その後、hostportを引数に使用してConnectMasterServerメソッドを呼び出します。これにより、アプリケーションのマスターサーバーへ接続します。

ConnectMasterServer関数の他の2つの引数OnConnectCallbackOnConnectFailedCallbackに注目してください。これらは、接続が成功したか失敗したかに応じて呼び出されるメソッドです。ご覧の通り、OnConnectCallbackOnConnectUnityEventを呼び出してゲームを進めます。

OnClick event of ConnectButton

Unity Editorに戻り、StrixConnectUI > StrixConnectPanel > Horizontal > ConnectButtonを見て、上記のスクリプトのConnectメソッドを呼び出すOn Clickイベントを確認しましょう。

OnConnect event on StrixConnectGUI

StrixConnectPanelではStrixConnectGUIスクリプトコンポーネントの中にあるOnConnect UnityEventを確認できます。このスクリプトを見ると、接続が成功するとそれが起動されることが分かります。これにバインドされているのは、StrixEnterRoom.EnterRoomメソッドで、次のスクリプトコンポーネントの中にあるStrix Enter Roomスクリプトを開くと見ることができます。

// 分かりやすいように一部の行を省略しています

public class StrixEnterRoom : MonoBehaviour {

    public int capacity = 4;
    public string roomName = "New Room";
    public UnityEvent onRoomEntered;
    public UnityEvent onRoomEnterFailed;

    public void EnterRoom() {
        StrixNetwork.instance.JoinRandomRoom(StrixNetwork.instance.playerName, args => {
            onRoomEntered.Invoke();
        }, args => {
            CreateRoom();
        });
    }

    private void CreateRoom() {
        RoomProperties roomProperties = new RoomProperties {
            capacity = capacity,
            name = roomName
        };

        RoomMemberProperties memberProperties = new RoomMemberProperties {
            name = StrixNetwork.instance.playerName
        };


        StrixNetwork.instance.CreateRoom(roomProperties, memberProperties, args => {
            onRoomEntered.Invoke();
        }, args => {
            onRoomEnterFailed.Invoke();
        });
    }
}

EnterRoomメソッドは、マスターサーバーへの接続が成功したときに呼び出され、その後すぐにJoinRandomRoomを呼び出してルームへの接続を試みます。この呼び出しは、以前に設定されたプレイヤー名に加え、成功と失敗のハンドラーを受け取ります。成功するとonRoomEnteredUnityEventを起動します。失敗するとCreateRoomメソッドを呼び出します。

CreateRoomメソッドは、指定された定員とルーム名でRoomPropertiesオブジェクトを、プレイヤーの名前でRoomMemberPropertiesオブジェクトを、それぞれ作成します。これらは、Strixでルームを作成するために必要なプロパティです。次に、StrixNetworkインスタンスのCreateRoomを適切な引数で呼び出します。

ここでも、成功と失敗のコールバックを使用します(Strixの関数ではこれをたくさん目にするでしょう)。(なお、このサンプルでは、それらがトリガーするイベントにはロジックが含まれていません)。

接続する簡単な方法は次の通りです。

  1. マスターサーバーに接続します。

  2. ルームに参加します。

  3. それが失敗した場合は、ルームを作成します(ルームを作成すると、作成者はいつでも自動的にそのルームに参加します)。

複製

Strixのクライアント間ではオブジェクトの複製が非常に簡単に行えます。unitychanキャラクターにはStrix Replicatorコンポーネントを追加してあります。これにより、このキャラクターがクライアント間で複製されます。これはプレイヤーキャラクターなので、自分のキャラクターがゲームワールドにいる他のプレイヤーに見えるようになります。

複製されるシーンには、他にNPCBallという2つのオブジェクトがあります。これらにもStrix Replicatorコンポーネントがあります。ただし、重要な違いが2点あります。

Instantiable By set to Room Owner

第1に、これら2つのオブジェクトでは、レプリケーターのInstantiable By(インスタンス化できるプレイヤー)プロパティをRoom Owner(ルームオーナー)に設定してあります。これは、ルームオーナーのみがオブジェクトを複製するということをStrixに伝えます。他のプレイヤーがルームに参加したとき、そのプレイヤーは自分のオブジェクトを他のクライアントに複製しません。その結果、ルームオーナーのオブジェクトのレプリカのみが表示されるようになります。

第2にConnection Closed Behaviour(接続が閉じられたときの動作)をChange Ownership(オーナー変更)に設定してあります。このオブジェクトをインスタンス化するクライアントの接続が閉じられると、Strixはこのオブジェクトのオーナーを変更します。

これら2つの設定は、ボールとNPCにとって重要です。プレイヤーキャラクターについては、どのクライアントにも、各クライアントごとに1人のプレイヤーキャラクターが必要です。しかしボールやNPCはプレイヤーが共有するゲームワールドのオブジェクトであるため、各クライアントに必要なオブジェクトは1つのみです。上記のオプションを使用すると、各クライアントでオブジェクトが1つのみインスタンス化されます(ルームオーナーのみがそれらのオブジェクトのオーナーであるため)。また、ルームオーナーの接続が切断された場合でも、オーナー権限が別のプレイヤーに移譲されます。

これは、プレイヤーのオブジェクトとワールドの(サーバーの)オブジェクトとの重要な違いです。

移動

StrixはStrix Replicatorコンポーネントを持つオブジェクトを複製しますが、その位置を自動的に同期することはありません。位置を同期するために、NPCとunitychanキャラクターにはStrix Movement Synchronizerが付いています。Strix Movement Synchronizerは、キャラクターの移動などを滑らかに同期するために使用します。

ボールにはStrix Transform Syncを付けてあります。このコンポーネントは単純なオブジェクトに適したシンプルな移動のロジックを提供するもので、加速の影響を考慮した移動の補間処理を行いません。

これらのコンポーネントの設定を自由にいじって、機能を確認してみてください。実際のゲームでは、他のクライアントでの動きをスムーズにするために、これらの設定の調整が必要になることがあります。

アニメーション

unitychanとNPCの両方にAnimatorがあり、Strixはこれらを同期する方法も提供します。Strix Animation Syncコンポーネントは、Animatorが指定されるとアニメーションの同期を実行します。

ゲームプレイの同期

Strixがオブジェクト、動き、アニメーションを複製する方法を理解したところで、Strixがゲームプレイアクションを複製する方法を調べます。これはサードパーソンシューティングゲームですから、プレイヤーたちは弾を撃ちたいはずです。

このゲームには、体力と弾丸という2つの重要な要素があります。プレイヤーがマウスの左ボタンをクリックするとキャラクターは弾丸を発射します。弾丸はキャラクターに当たって体力を減少させる可能性があります。

unitychanオブジェクトには、FireBulletというスクリプトがあります。

public class FireBullet : StrixBehaviour {
    public GameObject bullet;
    private PlayerStatus playerStatus;

    // Use this for initialization
    void Start () {
        playerStatus = GetComponent<PlayerStatus>();
    }

    // Update is called once per frame
    void Update () {
        if (!isLocal) {
            return;
        }

        if (playerStatus != null && playerStatus.health <= 0) {
            return;
        }

        if (Input.GetButtonDown("Fire1")) {
            GameObject instance = Instantiate(bullet);
            Transform firePos = transform.Find("FirePos");

            BulletControl bulletControl = instance.GetComponent<BulletControl>();
            bulletControl.owner = gameObject;

            instance.transform.position = firePos.position;
            instance.transform.rotation = firePos.rotation;
        }
    }
}

このスクリプトはStrixBehaviourクラスを継承しています。StrixBehaviourクラス自体は、Unityの標準のMonoBehaviourを継承していますが、Strixを使用するためのいくつかの追加機能を提供します。

スクリプトは毎フレーム更新をチェックしますが、isLocalプロパティを使用して、これがローカルオブジェクトで実行されているのか複製されたオブジェクトで実行されているのかを判定します。この判定は重要です。他のクライアントが、別のプレイヤーがオーナーであるレプリカのこのロジックにアクセスすると困るからです。アクセスをオブジェクトのオーナーであるプレイヤーに限定する必要があります。

このスクリプトでは、発射ボタンが押されているかどうかをチェックし、押されていれば弾丸とBulletControlオブジェクトを作成します。

BulletControl.csを見ると、BulletControlが弾丸の移動を処理していることが分かりますが、そこには重要な命中のロジックも含まれています。

// 41~43行目
if (playerStatus != null && playerStatus.isLocal) {
    playerStatus.RpcToRoomOwner("OnHit");
}

命中がローカルオブジェクトで起きたことを確認した上でRpcToRoomOwner(\"OnHit\")を呼び出します。

RPC(リモートプロシージャコール)は他のクライアントにメッセージを送信し、そのマシン上のオブジェクトのメソッドを呼び出すようにクライアントに指示します。これは、クライアント間でアクションを同期するのに非常に役立ちます。

ここで、BulletControlクラスは、ルームのオーナーにメッセージを送信して、hitObjectのPlayerStatusオブジェクトのOnHitメソッドを呼び出すように指示します。このhitObjectはルームオーナーとは別のマシンにあるかもしれませんが、PlayerStatusStrixBehaviourであり、キャラクターにはStrixReplicatorがあるため、ルームオーナーのゲーム内でどのオブジェクトのメソッドを呼び出すべきなのかがStrixに分かります。

つまり、弾丸を発射してキャラクターに命中すると、ルームオーナーのゲーム内にある複製されたキャラクターのPlayerStatusOnHitメソッドが呼び出されることになります。

次に、PlayerStatusスクリプトを見てみましょう。

// 分かりやすいように一部の行を省略しています

public class PlayerStatus : StrixBehaviour {
    [StrixSyncField]
    public int health = 100;
    public int maxHealth = 100;
    public float recoverTime = 3;
    private Animator animator;
    private float deadTime = 0;

    // Use this for initialization
    void Start() {
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update() {
        if (!isLocal) {
            return;
        }

        if (health <= 0 && Time.time >= deadTime + recoverTime) {
            RpcToAll("SetHealth", maxHealth);
        }
    }

    [StrixRpc]
    public void OnHit() {
        int value = health - 10;

        if (value < 0) {
            value = 0;
        }

        RpcToAll("SetHealth", value);
    }

    [StrixRpc]
    public void SetHealth(int value) {
        if (value < 0) {
            value = 0;
        } else if (value > maxHealth) {
            value = maxHealth;
        }

        animator.SetInteger("Health", value);

        if (animator != null) {
            if (value < health) {
                if (value <= 0) {
                    animator.SetTrigger("Dead");
                } else {
                    animator.SetTrigger("Damaged");
                }
            }

            if (value > 0 && health <= 0) {
                Respawn();
            }
        }

        if(health != value) {
            if (value <= 0) {
                deadTime = Time.time;
            }
        }

        health = value;
    }

    private void Respawn() {
        if (!CompareTag("Player") && isLocal) {
            transform.position = new Vector3(Random.Range(-40.0f, 40.0f), 2, Random.Range(-40.0f, 40.0f));
        }
    }
}

OnHitメソッドを見る前に、healthフィールドを調べることにしましょう。この変数は、キャラクターの体力を表しますが、上に[StrixSyncField]という属性があります。[StrixSyncField]は、この変数をオブジェクトとレプリカの間で同期するようにStrixに指示します。これにより、オリジナルのオブジェクトの体力がレプリカに確実に複製されます。これは、Strixを使用して値を同期する本当に簡単な方法です。

ここでOnHitメソッドを見ると、そちらには[StrixRpc]という属性があることが分かります。これはこのメソッドをStrixに登録し、RPCを使用して呼び出せるようにします。

OnHitは、ルームオーナーのワールド内のレプリカキャラクターで呼び出されます。このメソッドは体力値から一定のダメージを差し引いたあと、別のRPCを送信します。今度は全てのクライアントでSetHealthを呼び出します。これはレプリカのメソッドを呼び出しているため、この体力値の更新は、実はこれがルームオーナーがオーナーであるキャラクターだった場合を除き、自動的に全てのクライアントに同期されることはありません。

注釈

RpcToAllメソッドは、RPCを全てのクライアントに送信します。これにはRPCを送信したクライアントも含みます。

StrixのRPCは引数を取ることができるため、SetHealthは命中したキャラクターの新しい体力値を送信します。このメソッドはキャラクターの全てのレプリカのPlayerStatusについて呼び出されます。体力値を設定し、死亡と再生成のロジックを処理します。

要点は次の通りです。

  1. プレイヤーは自分のワールドで弾丸オブジェクトを発射します。

  2. 弾丸が別のキャラクターと衝突すると、StrixはルームオーナーにRPCを送信し、そのワールドで弾丸がいずれかのキャラクターに命中したことを伝えます。

  3. ルームオーナーはキャラクターからダメージを差し引き、この変更を全てのレプリカとそのキャラクターのオリジナルにブロードキャストします。

命中の検出はローカル処理として行うため、精度を保証できます。ダメージの計算をルームオーナーに限定することにより、同期ずれの問題を防止しています。なお、この命中の処理には合計でn + 1個のメッセージが必要です(nはゲームに参加しているクライアント(プレイヤー)の数です)。

結論

Strixは、ネットワークゲーム用に複数の機能を提供します。このサンプルで、これらの機能の概要を見てきましたが、全てを詳しく説明しているわけではありません。Strixの仕組みや、Strixを使用してゲームで何かを実装する方法について関心があれば、このドキュメントの続きをお読みください。