PUN Classic (v1)、PUN 2、Boltはメンテナンスモードとなっております。Unity2022についてはPUN 2でサポートいたしますが、新機能が追加されることはありません。お客様のPUNプロジェクトおよびBoltプロジェクトが停止することはなく、将来にわたってパフォーマンス性能が落ちることはありません。 今後の新しいプロジェクトについては、Photon FusionまたはQuantumへ切り替えていただくようよろしくお願いいたします。

クリックで動かす

このサンプルでは、プレイヤーがポイント&クリック方式だけでキャラクターをコントロールできる、トップダウンのゲーム作成について説明します。Boltのインストールおよびプロジェクトへの設定がまだの場合は、まずはじめよう を参照してください。

このサンプルでは、UnityのスタンダードアセットであるThirdPersonCharacterをベースとして使用します。これはアニメーションを使用してよくできており、Navigation System経由でコントロールできます。このチュートリアルは3セクションに分かれていて、それぞれ (i) レベルの作成、(ii) キャラクターの設定、(iii) Boltを設定しキャラクターを管理する、となります。

このサンプルで作成した成果物は、 Bolt Samples repositoryでご確認いただけます。

レベルの作成

このセクションでは、以下のようなレベルの構築が目的です。このレベルでは障害物と、キャラクターがスポーンする開始地点をいくつか作成します。好きなところに障害物を置いてください。

Click to Move Level
クリックで動かす レベル

以下がレベル作成に使用される主な階層です。

  1. 「地面」として使用する平面を準備します。

  2. 地面にボックスをいくつか設置します。

  3. プレイヤーからイベントを受信するためEventSystemオブジェクトを追加します。

  4. 以下の手順でシーンの中にスポーンポイントをいくつか追加します。

    1. ポイントの把握用に空のオブジェクトを作成します。
    2. ホルダーの中にもう1つ空のオブジェクトを作成します。Spawnと名前を付け、 RespawnTagを設定します。
      ;
    3. これでこのオブジェクトを複製し、全体的に適切に配置できるようになりました。
    4. スポーンポイントにアイコンを割り当て、シーン上で目立つようにできます。
  5. 地面や障害物にマテリアルを追加して、シーンを増強できます。ゲームの実行時に差別化を図れます。

Click to Move Level
クリックで動かす レベル

今度はGameObjectを設定してキャラクターが向かうべき地点をアップデートします。以下の手順でおこないます。

  1. 空のGameObjectを作成し、TargetPointerと名付けます。;
  2. Playerが見てわかるように、小さなをこのオブジェクトの子として追加し、そこに透明のマテリアルを作成します。これは、視覚情報として使います。
  3. 新しいスクリプトを作成し、PlaceTargetと名付けます。このスクリプトはターゲットの位置をコントロールするのに使います。
TargetPointer GameObject
`TargetPointer` GameObject.

以下に表示しているのがPlaceTargetスクリプトです。コードはシンプルで、各UpdateループごとにプレイヤーからのClick入力を確認し、この位置から、シーン上で何かに衝突するまでレイトレーシングします。衝突した情報でTargetPointer位置が変更され、UpdateTargetイベントが発生します。(このイベントは、後でプレイヤーに登録されます。)

C#

public class PlaceTarget : MonoBehaviour
{
    public float surfaceOffset = 0.2f;
    public event Action<Transform> UpdateTarget;

    Vector3 lastPosition = Vector3.zero;

    private void Update()
    {
        if (!Input.GetMouseButtonDown(0))
        {
            return;
        }

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (!Physics.Raycast(ray, out hit))
        {
            return;
        }

        transform.position = hit.point + hit.normal * surfaceOffset;

        if (lastPosition != transform.position)
        {
            lastPosition = transform.position;

            if (UpdateTarget != null)
            {
                UpdateTarget(transform);
            }
        }
    }
}

スクリプトが整ったら、ゲームを実行してオブジェクトをクリックしましょう。TargetPointerがクリックの通常のベクトルに従って、正しい場所に移動するはずです。

次はナビゲーションシステムの設定をしてキャラクターにナビゲーションメッシュを作成します。とても簡単です。以下を参照してください。

  1. Window/NavigationNavigationタブを開きます。Unityのナビゲーションがよくわからない場合は、こちらのリンクを参照してください。
  2. 以下の手順でナビゲーションエリアを設定します。
    1. 最新のObjectタブでキャラクターが歩ける場所と歩けない場所を設定します。
    2. Groundオブジェクトを選択し、 Navigation StaticとしてチェックしてWalkableに設定します。
    3. シーン内の障害物を全て選択し、Navigation StaticとしてチェックしてNot Walkableに設定します。
  3. 全ての障害物に設定ができたら、NavMeshを作成します。BakeタブのBakeボタンをクリックすると、フォルダとNavMeshファイルがシーンロケーションの隣に作成されます。

これらの手順を完了すると、以下のようなNavMeshが完成しています。青いエリアはWalkableで、シーン内の障害物によってメッシュが制限されていることに留意してください。この制限されたエリアには、キャラクターが踏み込めないようになっています。

Level NavMesh
レベル *NavMesh*

これでレベルの準備が整いました。歩けるエリアや障害物をもう少しアレンジしてみてもいいですね。ただ、変更した場合は再度Bakeすることを忘れないでください。(NavigationウィンドウのBakeボタンです。)

キャラクターの設定

このサンプルでは、メインのキャラクターとしてThirdPersonCharacter、中でもAIThirdPersonControllerバージョンを使用します。( スタンダードアセットから取得できます。)このキャラクターのデザインはEthanというモデルから作られました。サングラスをしてかっこいいですね!では、早速Unityのプロジェクトにパッケージのダウンロードとインポートをおこないましょう。

以下の画像のような、AIThirdPersonControllerプレハブのある構造を取得します。このプレハブのコピーを作成して他のフォルダに移してください。任意の名前を付けられますが、ここではEthanClickToMoveとします。なぜコピーが必要かというと、オリジナルオブジェクトのコンポーネントに変更を加えるので、バックアップがあったほうが安心だからです。

Game Character
ゲームキャラクター

まず、TargetPointerでの変更をリッスンする簡潔なスクリプトを作成し、プレイヤーをその位置まで送ります。キャラクターに付随している必要があります。Start()メソッドで、TargetPointerオブジェクトを見つけ、PlaceTargetコンポーネントのリファレンスを取得後、UpdateTargetイベントに登録し、イベントが実行されたらすぐに、SetTarget呼び出しをリッスンするGameObject上の全てのスクリプトにメッセージを送信します。今回のケースではAICharacterControlスクリプトがデータを受信して、それに従って動作します。

C#

public class ClickToMoveController : MonoBehaviour
{
    void Start()
    {
        var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

        placeTarget.UpdateTarget += (newTarget) =>
        {
            gameObject.SendMessage("SetTarget", newTarget);
        };
    }
}

キャラクタープレハブのコピーをシーンに入れてゲームを実行してみると、以下の動画のようにクリックした場所へキャラクターを走らせることができるようになります。これで次の手順へ進む準備ができました。Boltをゲームに統合して、マルチプレイヤーゲームにします。

Game Character running by Click
クリックで走るゲームキャラクター

Bolt 統合

ゲームが動作し、キャラクターが歩き回ってターゲットを追いかけることができるようになり、プロジェクトにBoltを統合する準備が整いました。ページ上部でも記載しましたが、Boltをプロジェクトに設定していない場合は、まずはじめようチュートリアルを参照してください。

アセットの設定

Boltでは全てのエンティティはStatesを使用してデータを同期します。このサンプルでも同様です。Window/Bolt/Assetsで新しいStateを作成しClickToMoveStateと名前を付け、以下の手順に従って進めてください。

  1. 新しいTransformプロパティを作成してタイプをTransformに設定します。
  2. ここでネットワーク経由で同期するため、Playerが使用しているアニメーションから、プロパティをいくつかインポートする必要があります。
    1. Import Mecanim Parameters内でThirdPersonAnimatorControllerアセットを選択します。
    2. Importボタンをクリックします。
    3. 全てのパラメータが適切にインポートされます。

すると、Stateが以下のような設定になっているかと思います。

Bolt State Creation
Bolt State 作成

クライアントからサーバーへデータを送信する方法も必要ですが、これはBoltコマンド機能を使用しておこないます。コマンドは、Playerがキャラクターの制御をするためにおこなった入力を表すものですが、サーバーがゲームの統一性を保つのにクライアントへ訂正を送信するためにも使用されます。

先ほどClickToMoveStateを作成したのと同じウインドウで、右クリックして新しくコマンドを作成しClickToMoveCommandと名前をつけます。以下の手順に従ってください。

  1. 新しいInputclickという名前で作成し、タイプをVectorに設定します。これはPlayerにより設定されたターゲットの位置を送信するのに使用されます。

  2. 新しいResultpositionという名前で作成し、タイプをVectorに設定します。これはキャラクターの位置を、Server上のあらゆる場所に設定します。

Bolt State on Character
CharacterのBolt State

これでBolt Assetsウィンドウを閉じ、 Assets/Bolt/Compile Assemblyでコマンドを実行してBoltのコンパイルができるようになりました。BoltCompiler: Success!メッセージが コンソール に表示されたら、次の手順に進みます。

Boltを使用してPlayerを複製するため、Bolt Entityコンポーネントを追加して先ほど作成したStateを設定します。以下の手順に従って進めてください。

  1. EthanClickToMoveプレハブを選択します。
  2. 新しくBolt Entityコンポーネントを追加します。
  3. StateドロップダウンでIClickToMoveStateをクリックします。
  4. (Assets/Bolt/Compile Assemblyで)Boltをコンパイルし、ライブラリがこのエンティティを認識するようにします。(Idで信号が送られます。)
Bolt State on Character
Bolt State on Character.

ネットワークスクリプト

このセッションではスクリプトをいくつか追加・修正してBoltがアセットの認識、プレイヤーのスポーンおよび必要なデータの同期をするようにします。

まずはじめに、ネットワークゲームマネジャースクリプトを作成します。これはサーバーとクライアントの両方でゲーム上のプレハブのインスタンス化をつかさどります。下記のようにClickToMoveNetworkCallbacksを作成してください。

C#

[BoltGlobalBehaviour(BoltNetworkModes.Server, "ClickToMoveGameScene")]
public class ClickToMoveNetworkCallbacks : Bolt.GlobalEventListener
{
    public override void SceneLoadLocalDone(string scene)
    {
        var player = InstantiateEntity();
        player.TakeControl();
    }

    public override void SceneLoadRemoteDone(BoltConnection connection)
    {
        var player = InstantiateEntity();
        player.AssignControl(connection);
    }

    private BoltEntity InstantiateEntity()
    {
        GameObject[] respawnPoints = GameObject.FindGameObjectsWithTag("Respawn");

        var respawn = respawnPoints[Random.Range(0, respawnPoints.Length)];
        return BoltNetwork.Instantiate(BoltPrefabs.EthanClickToMove, respawn.transform.position, Quaternion.identity);
    }
}

このコードに関する注意点をいくつか以下に挙げます。

  • このコードが実行されるのはサーバー上のみかつ、ゲームシーンが読みこまれたときです。今回のケースではClickToMoveGameSceneとなります。(これは作成中のシーンの名前に合うように変更してください。)
  • SceneLoadLocalDoneSceneLoadRemoteDoneの2つのコールバックを使用しており、両方ともゲームシーンの読み込みが完了したときに呼び出されます。この瞬間を使って、ランダムな位置でEthanClickToMoveプレハブのコピーをインスタンス化します。(先で作成していた Respawn ポイントを使用します。)
  • ゲームホスト(サーバー)上でしか実行されないので、シーンがローカルに読み込まれたとき、プレイヤーがエンティティの制御(player.TakeControl())をおこないます。そしてクライアント上でシーンが読みこまれたとき、ホストはリモートプレイヤー(player.AssignControl(connection))に制御を許可します。

Boltを使用してコマンドの送信ができるように、コントローラースクリプトをアップデートします。ClickToMoveControllerスクリプトを開いて以下に従って変更を加えましょう。

  1. Boltからイベントコールバックを受信するため、EntityEventListenerを拡張する必要があります。拡張によってライブラリからユーティリティへのアクセスが可能になります。IClickToMoveStateがジェネリック型の引数として含まれている点に留意してください。これで特定のStateからのデータのみを処理することが保証されます。

C#

public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
    // ...
}
  1. Attached()コールバックを使用すると、キャラクターデータをBoltに結び付けられるようになります。今回のケースではTransformAnimatorの全ての情報を同期します。

C#

// ...
public override void Attached()
{
    state.SetTransforms(state.Transform, transform);
    state.SetAnimator(GetComponentInChildren<Animator>());
}
// ...
  1. ローカルのPlayerにBolt Entityの制御権を与え、コマンドの送信ができるようになります。これを活用するため、あと3つのコールバックをオーバーライドします。
  • プレイヤーがエンティティに対して制御権できるようになるとControlGainedメソッドが実行されます。このメソッドは、ローカルのTargetPointerを探しUpdateTargetコールバックをフックするのに使用します。以前におこなったことと同様ですが、ここではtarget transformは後にとっておきます。

C#

// ...
public Transform destination;

public override void ControlGained()
{
    var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

    placeTarget.UpdateTarget += (newTarget) =>
    {
        destination = newTarget;
    };
}
// ...
  • SimulateControllerコールバック内でのみ新しいコマンドをサーバーに送信できます。ここで新しく ClickToMoveCommandを作成して方向を設定し(このケースではclickパラメータ)、entity.QueueInputを呼び出して実行に備えキュー登録します。送信しているのはコマンドの位置ベクトルだけという点に気を付けてください。

C#

// ...
public override void SimulateController()
{
    if (destination != null)
    {
        IClickToMoveCommandInput input = ClickToMoveCommand.Create();
        input.click = destination.position;
        entity.QueueInput(input);
    }
}
// ...
  • ExecuteCommandではいろいろな「魔法」が起こります。このコールバック内ではPlayerから送信された全てのコマンドを処理し、クライアントとサーバーの両方でそのコマンドを実行し、clickパラメータが読み込まれメッセージを介してAICharacterControlに送信されます。それに加えてサーバーが訂正をクライアントに返信し、キャラクターが全てのクライアント上で同じ位置にいる様にします。

C#

// ...
public override void ExecuteCommand(Command command, bool resetState)
{
    ClickToMoveCommand cmd = (ClickToMoveCommand)command;

    if (resetState)
    {
        // owner has sent a correction to the controller
        transform.position = cmd.Result.position;
    }
    else
    {
        if (cmd.Input.click != Vector3.zero)
        {
            gameObject.SendMessage("SetTarget", cmd.Input.click);
        }

        cmd.Result.position = transform.position;
    }
}
// ...

全ての変更が完了したら、ゲームを実行してPlayerがシーン上を走り回るようになるまでもう一息です。参考のため、以下に完全なClickToMoveControllerスクリプトを載せておきます。

C#

public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
    public Transform destination;

    public override void Attached()
    {
        state.SetTransforms(state.Transform, transform);
        state.SetAnimator(GetComponentInChildren<Animator>());
    }

    public override void ControlGained()
    {
        var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

        placeTarget.UpdateTarget += (newTarget) =>
        {
            destination = newTarget;
        };
    }

    public override void SimulateController()
    {
        if (destination != null)
        {
            IClickToMoveCommandInput input = ClickToMoveCommand.Create();
            input.click = destination.position;
            entity.QueueInput(input);
        }
    }

    public override void ExecuteCommand(Command command, bool resetState)
    {
        ClickToMoveCommand cmd = (ClickToMoveCommand)command;

        if (resetState)
        {
            //owner has sent a correction to the controller
            transform.position = cmd.Result.position;
        }
        else
        {
            if (cmd.Input.click != Vector3.zero)
            {
                gameObject.SendMessage("SetTarget", cmd.Input.click);
            }

            cmd.Result.position = transform.position;
        }
    }
}

変更すべき点がもう1つあります。お気づきかもしれませんが、AICharacterControl.SetTargetメソッドはTransformVector3ではなくTransformとして受信します。現在、Boltでは完全な Transformを1つのパラメータで送信することはサポートされていませんが(位置と回転を2つのVector3変数に送れば可能です)、ターゲットの位置のみがわかればいいので、問題ではありません。AICharacterControlスクリプトを開いて、以下のように修正してください。

C#

public class AICharacterControl : MonoBehaviour
{
    // Add this new field
    public Vector3 targetPosition;

    // ...

    // Modify the Update method
    private void Update()
    {
        if (target != null)
            agent.SetDestination(target.position);

        // Here we pass our new Vector3 target
        if (targetPosition != Vector3.zero)
            agent.SetDestination(targetPosition);

        if (agent.remainingDistance > agent.stoppingDistance)
            character.Move(agent.desiredVelocity, false, false);
        else
            character.Move(Vector3.zero, false, false);
    }

    // ...

    // Add this new overload
    public void SetTarget(Vector3 target)
    {
        this.targetPosition = target;
    }
}

ゲームを実行

準備が整いました!今まで説明してきた手順は、このサンプルを動作させるためのものでした。では実行してみましょう。簡単なのは、Bolt Debugユーティリティを使用することです。まず、Build Settings/Scenes In Buildをゲームシーンに追加し、Bolt (Assets/Bolt/Compile Assembly)をコンパイルしてシーンを認識するようにしておきます。

これを実行するとすぐに Window/Bolt/ScenesBolt Debugウィンドウが開き、サーバーとして作動するようにエディターをセットし、クライアントの数を設定してDebug Startをクリックします。しばらくすると、自動的にクライアントが開始しUnityエディターがゲームホストとして動作します。

Debug Start the Game
Debug Start the Game.

お楽しみください!

Game Running
ゲーム実行中

チュートリアルは以上です!

Back to top