クリックで動かす
このサンプルでは、プレイヤーがポイント&クリック方式だけでキャラクターをコントロールできる、トップダウンのゲーム作成について説明します。Bolt
のインストールおよびプロジェクトへの設定がまだの場合は、まずはじめよう を参照してください。
このサンプルでは、UnityのスタンダードアセットであるThirdPersonCharacter
をベースとして使用します。これはアニメーションを使用してよくできており、Navigation System経由でコントロールできます。このチュートリアルは3セクションに分かれていて、それぞれ (i) レベルの作成、(ii) キャラクターの設定、(iii) Boltを設定しキャラクターを管理する、となります。
このサンプルで作成した成果物は、 Bolt Samples repositoryでご確認いただけます。
レベルの作成
このセクションでは、以下のようなレベルの構築が目的です。このレベルでは障害物と、キャラクターがスポーンする開始地点をいくつか作成します。好きなところに障害物を置いてください。
以下がレベル作成に使用される主な階層です。
「地面」として使用する平面を準備します。
地面にボックスをいくつか設置します。
プレイヤーからイベントを受信するため
EventSystem
オブジェクトを追加します。以下の手順でシーンの中にスポーンポイントをいくつか追加します。
- ポイントの把握用に空のオブジェクトを作成します。
- ホルダーの中にもう1つ空のオブジェクトを作成します。
Spawn
と名前を付け、Respawn
のTag
を設定します。
; - これでこのオブジェクトを複製し、全体的に適切に配置できるようになりました。
- スポーンポイントにアイコンを割り当て、シーン上で目立つようにできます。
地面や障害物にマテリアルを追加して、シーンを増強できます。ゲームの実行時に差別化を図れます。
今度はGameObject
を設定してキャラクターが向かうべき地点をアップデートします。以下の手順でおこないます。
- 空の
GameObject
を作成し、TargetPointer
と名付けます。; Player
が見てわかるように、小さな球
をこのオブジェクトの子として追加し、そこに透明のマテリアル
を作成します。これは、視覚情報として使います。- 新しいスクリプトを作成し、
PlaceTarget
と名付けます。このスクリプトはターゲットの位置をコントロールするのに使います。
以下に表示しているのが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
がクリックの通常のベクトルに従って、正しい場所に移動するはずです。
次はナビゲーションシステムの設定をしてキャラクターにナビゲーションメッシュを作成します。とても簡単です。以下を参照してください。
Window/Navigation
でNavigation
タブを開きます。Unityのナビゲーションがよくわからない場合は、こちらのリンクを参照してください。- 以下の手順でナビゲーションエリアを設定します。
- 最新の
Object
タブでキャラクターが歩ける場所と歩けない場所を設定します。 Ground
オブジェクトを選択し、Navigation Static
としてチェックしてWalkable
に設定します。- シーン内の障害物を全て選択し、
Navigation Static
としてチェックしてNot Walkable
に設定します。
- 最新の
- 全ての障害物に設定ができたら、
NavMesh
を作成します。Bake
タブのBake
ボタンをクリックすると、フォルダとNavMesh
ファイルがシーンロケーションの隣に作成されます。
これらの手順を完了すると、以下のようなNavMesh
が完成しています。青いエリアはWalkable
で、シーン内の障害物によってメッシュが制限されていることに留意してください。この制限されたエリアには、キャラクターが踏み込めないようになっています。
これでレベルの準備が整いました。歩けるエリアや障害物をもう少しアレンジしてみてもいいですね。ただ、変更した場合は再度Bakeすることを忘れないでください。(Navigation
ウィンドウのBake
ボタンです。)
キャラクターの設定
このサンプルでは、メインのキャラクターとしてThirdPersonCharacter
、中でもAIThirdPersonController
バージョンを使用します。( スタンダードアセットから取得できます。)このキャラクターのデザインはEthan
というモデルから作られました。サングラスをしてかっこいいですね!では、早速Unityのプロジェクトにパッケージのダウンロードとインポートをおこないましょう。
以下の画像のような、AIThirdPersonController
プレハブのある構造を取得します。このプレハブのコピーを作成して他のフォルダに移してください。任意の名前を付けられますが、ここではEthanClickToMove
とします。なぜコピーが必要かというと、オリジナルオブジェクトのコンポーネントに変更を加えるので、バックアップがあったほうが安心だからです。
まず、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
をゲームに統合して、マルチプレイヤーゲームにします。
Bolt 統合
ゲームが動作し、キャラクターが歩き回ってターゲットを追いかけることができるようになり、プロジェクトにBolt
を統合する準備が整いました。ページ上部でも記載しましたが、Bolt
をプロジェクトに設定していない場合は、まずはじめようチュートリアルを参照してください。
アセットの設定
Bolt
では全てのエンティティはStates
を使用してデータを同期します。このサンプルでも同様です。Window/Bolt/Assets
で新しいState
を作成しClickToMoveState
と名前を付け、以下の手順に従って進めてください。
- 新しい
Transform
プロパティを作成してタイプをTransform
に設定します。 - ここでネットワーク経由で同期するため、Playerが使用しているアニメーションから、プロパティをいくつかインポートする必要があります。
Import Mecanim Parameters
内でThirdPersonAnimatorController
アセットを選択します。Import
ボタンをクリックします。- 全てのパラメータが適切にインポートされます。
すると、State
が以下のような設定になっているかと思います。
クライアント
からサーバー
へデータを送信する方法も必要ですが、これはBolt
のコマンド
機能を使用しておこないます。コマンドは、Playerがキャラクターの制御をするためにおこなった入力を表すものですが、サーバー
がゲームの統一性を保つのにクライアント
へ訂正を送信するためにも使用されます。
先ほどClickToMoveState
を作成したのと同じウインドウで、右クリックして新しくコマンド
を作成しClickToMoveCommand
と名前をつけます。以下の手順に従ってください。
新しい
Input
をclick
という名前で作成し、タイプをVector
に設定します。これはPlayerにより設定されたターゲットの位置を送信するのに使用されます。新しい
Result
をposition
という名前で作成し、タイプをVector
に設定します。これはキャラクターの位置を、Server
上のあらゆる場所に設定します。
これでBolt Assets
ウィンドウを閉じ、 Assets/Bolt/Compile Assembly
でコマンドを実行してBolt
のコンパイルができるようになりました。BoltCompiler: Success!
メッセージが コンソール に表示されたら、次の手順に進みます。
Bolt
を使用してPlayerを複製するため、Bolt Entity
コンポーネントを追加して先ほど作成したState
を設定します。以下の手順に従って進めてください。
EthanClickToMove
プレハブを選択します。- 新しく
Bolt Entity
コンポーネントを追加します。 State
ドロップダウンでIClickToMoveState
をクリックします。- (
Assets/Bolt/Compile Assembly
で)Bolt
をコンパイルし、ライブラリがこのエンティティを認識するようにします。(Id
で信号が送られます。)
ネットワークスクリプト
このセッションではスクリプトをいくつか追加・修正して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
となります。(これは作成中のシーンの名前に合うように変更してください。) SceneLoadLocalDone
とSceneLoadRemoteDone
の2つのコールバックを使用しており、両方ともゲームシーンの読み込みが完了したときに呼び出されます。この瞬間を使って、ランダムな位置でEthanClickToMove
プレハブのコピーをインスタンス化します。(先で作成していた Respawn ポイントを使用します。)- ゲームホスト(サーバー)上でしか実行されないので、シーンがローカルに読み込まれたとき、プレイヤーがエンティティの制御(
player.TakeControl()
)をおこないます。そしてクライアント上でシーンが読みこまれたとき、ホストはリモートプレイヤー(player.AssignControl(connection)
)に制御を許可します。
Bolt
を使用してコマンドの送信ができるように、コントローラースクリプトをアップデートします。ClickToMoveController
スクリプトを開いて以下に従って変更を加えましょう。
Bolt
からイベントコールバックを受信するため、EntityEventListener
を拡張する必要があります。拡張によってライブラリからユーティリティへのアクセスが可能になります。IClickToMoveState
がジェネリック型の引数として含まれている点に留意してください。これで特定のState
からのデータのみを処理することが保証されます。
C#
public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
// ...
}
Attached()
コールバックを使用すると、キャラクターデータをBolt
に結び付けられるようになります。今回のケースではTransform
とAnimator
の全ての情報を同期します。
C#
// ...
public override void Attached()
{
state.SetTransforms(state.Transform, transform);
state.SetAnimator(GetComponentInChildren<Animator>());
}
// ...
- ローカルの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
メソッドはTransform
をVector3
ではなく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/Scenes
にBolt Debug
ウィンドウが開き、サーバー
として作動するようにエディターをセットし、クライアントの数を設定してDebug Start
をクリックします。しばらくすると、自動的にクライアントが開始しUnityエディターがゲームホストとして動作します。
お楽しみください!
チュートリアルは以上です!
Back to top