このドキュメントでは、新しいネットワーキング・システムを利用して新規のマルチプレイヤー・プロジェクトをゼロからセットアップする方法を説明しています。この手順は一般的なものですが、一度始めれば、さまざまな種類のマルチプレイヤーゲーム向けにカスタマイズ可能です。
まず始めに、空の Unity プロジェクトを新規で作成してください。
最初のステップは、プロジェクト内に NetworkManager オブジェクトを作成することです。
詳しくは NetworkManager を使用する を参照してください。
次のステップは、ゲーム内でプレイヤーを表す Unity Prefab のセットアップです。デフォルトでは NetworkManager は、プレイヤーごとに1つ、オブジェクトのインスタンスを作成します。これは、プレイヤープレハブをコピーすることで行われます。この例では、プレイヤーオブジェクトは単純な立方体です。
プレイヤーオブジェクト を参照してください。
作成されたプレイヤープレハブは、ネットワークシステムに登録される必要があります。
ここまで来たらプロジェクトを保存しましょう。メニューを File -> Save Project と進み、プロジェクトを保存します。シーンも保存してください。このシーンを “offline” シーンと名付けましょう。
ゲーム機能の開発でまず最初に行うのは、プレイヤーオブジェクトを動かすことです。始めはネットワークを使わずに行うので、シングルプレイヤーモードでのみ機能します。
using UnityEngine;
public class PlayerMove : MonoBehaviour
{
void Update()
{
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
これでキューブが矢印キーやタッチパッドで制御できるようになります。この時点ではキューブはクライアント上でのみ動きます ― ネットワーク化されていない状態です。
プロジェクトを再度保存してください。
Play ボタンをクリックするとエディターがプレイモードに入ります。NetworkManagerHUD デフォルト ユーザー インターフェースが表示されます。
“Host” を押すと、ゲームのホストとしてゲームが開始されます。プレイヤーオブジェクトが作成され、HUD はサーバーがアクティブであることを示す状態に変化します。このゲームは、サーバーであると同時にクライアントでもある、「ホスト」として実行されます。
ネットワークシステムの概念 を参照してください。
方向キーを押すとプレイヤー キューブ オブジェクトが動きます。
エディターで Stop ボタンを押すとプレイモードが終了します。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
}
}
この時点では、ゲーム内のプレイヤーはすべて白色なため、ユーザーはどれが自分のキューブなのか見分けることができません。プレイヤーを識別できるようにするために、ローカルプレイヤーのキューブを赤色にしてみましょう。
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
この関数は、ローカルプレイヤーにのみ、そのクライアント上で呼び出されます。この関数によって、ユーザー自身のキューブが赤く表示されます。OnStartLocalPlayer 関数は、ローカルプレイヤーのみの初期化を行うのに適した場所です。(例えばカメラと入力の設定など。)
NetworkBehaviour のベースクラスには、便利な仮想関数が他にもあります。オブジェクトの Spawn(生成) を参照してください。
マルチプレイヤーゲームで頻繁に使用される機能のひとつに、弾の発射があります。このセクションでは、参考例に非ネットワークの弾を追加していきます。弾のネットワーク化に関しては、次のセクションで説明します。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
// 弾丸のプレハブから弾丸のオブジェクトを作成
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
// プレイヤーの前から弾丸を移動する
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
// 2 秒後に弾丸を消す
Destroy(bullet, 2.0f);
}
}
このセクションでは、参考例の弾にネットワークを追加していきます。
using UnityEngine;
using UnityEngine.Networking;
public class PlayerMove : NetworkBehaviour
{
public GameObject bulletPrefab;
public override void OnStartLocalPlayer()
{
GetComponent<MeshRenderer>().material.color = Color.red;
}
[Command]
void CmdFire()
{
// [Command] はサーバー上で実行
// 弾丸オブジェクトをローカルに作成
var bullet = (GameObject)Instantiate(
bulletPrefab,
transform.position - transform.forward,
Quaternion.identity);
bullet.GetComponent<Rigidbody>().velocity = -transform.forward*4;
//クライアント上に弾丸を生成
NetworkServer.Spawn(bullet);
// サーバー上で弾丸が破棄されると、自動的にクライアント上で破棄されます
Destroy(bullet, 2.0f);
}
void Update()
{
if (!isLocalPlayer)
return;
var x = Input.GetAxis("Horizontal")*0.1f;
var z = Input.GetAxis("Vertical")*0.1f;
transform.Translate(x, 0, z);
if (Input.GetKeyDown(KeyCode.Space))
{
// コマンド関数はクライアントから呼び出されますが、サーバー上で実行されます。
CmdFire();
}
}
}
このコードは、サーバー上で弾を発射するのに [Command] を使用しています。詳しくは リモートアクション をご参照ください。
衝突処理を追加して、弾がプレイヤーキューブオブジェクトに当たったときに消えるようにします。
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
Destroy(gameObject);
}
}
}
これで、プレイヤーオブジェクトに当たった弾が破壊されるようになりました。弾はネットワークで管理された生成オブジェクトなので、サーバー上で破壊されるとクライアント上でも破壊されます。
弾と関連してもうひとつ一般的な機能は、プレイヤーオブジェクトの「体力」(Health/ヒットポイント)プロパティーです。これは満タンな状態でスタートし、プレイヤーが弾に当たってダメージを受けると低下します。このセクションでは、プレイヤーオブジェクトに非ネットワークの「体力」プロパティーを追加します。
using UnityEngine;
public class Combat : MonoBehaviour
{
public const int maxHealth = 100;
public int health = maxHealth;
public void TakeDamage(int amount)
{
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
弾が当たったときに TakeDamage スクリプトを呼び出すには、弾のスクリプトが更新される必要があります。 * 弾(Bullet)のスクリプトを開きます。 * 衝突処理関数内で Combat スクリプトから TakeDamage() への呼び出しを追加してください。
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitPlayer = hit.GetComponent<PlayerMove>();
if (hitPlayer != null)
{
var combat = hit.GetComponent<Combat>();
combat.TakeDamage(10);
Destroy(gameObject);
}
}
}
これで、弾に当たったときにプレイヤーオブジェクトの体力が減るようになりました。ただし、これはゲーム内では確認することができません。簡単な体力ゲージを追加する必要があります。
下記の長いコードは、古い GUI システムを使用したものです。これを使用しますが、ネットワーキングとはあまり関係がないので、ここでは説明は省略します。
using UnityEngine;
using System.Collections;
public class HealthBar : MonoBehaviour
{
GUIStyle healthStyle;
GUIStyle backStyle;
Combat combat;
void Awake()
{
combat = GetComponent<Combat>();
}
void OnGUI()
{
InitStyles();
// Health Bar を描画
Vector3 pos = Camera.main.WorldToScreenPoint(transform.position);
// Health Bar の背景を描画
GUI.color = Color.grey;
GUI.backgroundColor = Color.grey;
GUI.Box(new Rect(pos.x-26, Screen.height - pos.y + 20, Combat.maxHealth/2, 7), ".", backStyle);
// Health Bar に体力値を描画
GUI.color = Color.green;
GUI.backgroundColor = Color.green;
GUI.Box(new Rect(pos.x-25, Screen.height - pos.y + 21, combat.health/2, 5), ".", healthStyle);
}
void InitStyles()
{
if( healthStyle == null )
{
healthStyle = new GUIStyle( GUI.skin.box );
healthStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 1f, 0f, 1.0f ) );
}
if( backStyle == null )
{
backStyle = new GUIStyle( GUI.skin.box );
backStyle.normal.background = MakeTex( 2, 2, new Color( 0f, 0f, 0f, 1.0f ) );
}
}
Texture2D MakeTex( int width, int height, Color col )
{
Color[] pix = new Color[width * height];
for( int i = 0; i < pix.Length; ++i )
{
pix[ i ] = col;
}
Texture2D result = new Texture2D( width, height );
result.SetPixels( pix );
result.Apply();
return result;
}
}
この時点で、体力の変化はクライアントとホストそれぞれに独立して適用されるようになっています。つまりプレイヤーごとに異なる体力値を持てるということです。しかし、体力値はサーバー上のみで適用されて、その変化がクライアントに反映されるという形にする必要があります。これは、体力の「サーバー権限」と呼ばれます。
SyncVar に関する詳細は、ステートの同期 を参照してください。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = 0;
Debug.Log("Dead!");
}
}
}
この時点では、プレイヤーの体力がゼロになっても、ログメッセージが表示される以外は何も起こりません。よりゲームらしくするために、体力がゼロになったらプレイヤーがスタート地点に戻されて体力が満タンに戻るようにしましょう。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
health = maxHealth;
// サーバー上で呼び出され、クライアント上で実行されます
RpcRespawn();
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// 0 の位置に戻ります
transform.position = Vector3.zero;
}
}
}
このゲームでは、プレイヤーオブジェクトの位置がクライアントによって制御されています。プレイヤーオブジェクトはクライアント上で「ローカル権限」を持っています。もしプレイヤーの位置が直接サーバーによってスタート位置に設定されたとしたら、クライアントがそれをオーバーライドしてしまいます。クライアントが権限を持っているからです。これを回避するために、サーバーから権限を持つクライアントに対して、プレイヤーをスタート位置に移動するように命令が送られるようになっています。
クライアントがホストに接続するとプレイヤーオブジェクトが生成さますが、ほとんどのゲームでは、ゲーム世界の中に敵などのノンプレイヤー・オブジェクトが存在しています。このセクションでは、射撃したり倒したりすることのできるノンプレイヤー・オブジェクトの Spawner(生成機)を追加していきます。
using UnityEngine;
using UnityEngine.Networking;
public class EnemySpawner : NetworkBehaviour {
public GameObject enemyPrefab;
public int numEnemies;
public override void OnStartServer()
{
for (int i=0; i < numEnemies; i++)
{
var pos = new Vector3(
Random.Range(-8.0f, 8.0f),
0.2f,
Random.Range(-8.0f, 8.0f)
);
var rotation = Quaternion.Euler( Random.Range(0,180), Random.Range(0,180), Random.Range(0,180));
var enemy = (GameObject)Instantiate(enemyPrefab, pos, rotation);
NetworkServer.Spawn(enemy);
}
}
}
上記が完了したら、Enemy のプレハブを作成してください。
弾のスクリプトは、プレイヤーに対してのみ機能するように設定されています。これを、Combat スクリプトを持つすべてのオブジェクトに対して機能するように更新しましょう。
using UnityEngine;
public class Bullet : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
var hit = collision.gameObject;
var hitCombat = hit.GetComponent<Combat>();
if (hitCombat != null)
{
hitCombat.TakeDamage(10);
Destroy(gameObject);
}
}
}
EnemySpawner を Enemy オブジェクトと関連付けます。
敵のテストを行います。
敵もプレイヤー同様に、弾に撃たれて体力が低下し、再生成されることも可能ですが、敵の場合は体力がゼロになると再生成の代わりに破壊されるのが通常です。
using UnityEngine;
using UnityEngine.Networking;
public class Combat : NetworkBehaviour
{
public const int maxHealth = 100;
public bool destroyOnDeath;
[SyncVar]
public int health = maxHealth;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
if (health <= 0)
{
if (destroyOnDeath)
{
Destroy(gameObject);
}
else
{
health = maxHealth;
// サーバー上で呼び出され、クライアント上で実行されます
RpcRespawn();
}
}
}
[ClientRpc]
void RpcRespawn()
{
if (isLocalPlayer)
{
// 0 の位置へ戻ります
transform.position = Vector3.zero;
}
}
}
これで、敵の体力がゼロになると破壊されるようになりました。プレイヤーの場合は再生成されます。
この時点では、すべてのプレイヤーがゼロ地点に生成されます。これではプレイヤー同士が重なってしまうことがありえます。したがって、プレイヤーは異なる位置に生成されるほうが好ましいと言えます。NetworkStartPosition コンポーネントを使ってこの設定を行ってみましょう。
空の GameObject を新規で作成します。
作成したオブジェクトの名前を “Pos1” に変更します。
Add Component ボタンを選択し、NetworkStartPosition コンポーネントを追加してください。
Pos1 オブジェクトを (–3,0,0) の位置に移動してください。
空の GameObject をもう1つ作成してください。
作成したオブジェクトの名前を “Pos2” に変更してください。
Add Component ボタンを選択し、NetworkStartPosition コンポーネントを追加してください。
Pos2 オブジェクトを (3,0,0) の位置に移動してください。
NetworkManager を見付けて選択してください。
“Spawn Info” の折り畳みメニューを開きます。
“Player Spawn Method” を “Round Robin” に変更してください。
ゲームのビルドと実行を行ってください。
プレイヤーオブジェクトが、ゼロ地点ではなく Pos1 オブジェクトと Pos2 オブジェクトの位置に作成されるようになりました。