チュートリアル: Pong: ゲームロジックとARイベント
ARDKの機能を使用して作った、ARマルチプレイヤー版『Pong』のUnityプロジェクト例です。このチュートリアルでは、プロジェクトを正しく機能させるためのUnity上での各ステップやC#スクリプトの使用法をご確認いただけます。この使用例では低レベルメッセージを使ってプレイヤー間のデータ送信を行っています。このプロジェクトの別バージョンでは、プレイヤー同期用のメッセージ送受信プロセスを能率化する高レベルAPIオブジェクト(HLAPI)のセットアップおよび使用例が示されています (こちら) 。
ゲームロジックとARイベント
セッションとゲームオブジェクトがすべて作成されたので、いよいよゲームロジックに移ります。ボールの動きは BallBehaviour
スクリプトで処理するため、 GameController
では基本的な移動や衝突のロジックとスコアの更新のみを処理します。
一貫性を保つために、ゲームのほとんどのオブジェクトやステータスを操作できるのはホストのみです。それに対し、その他のピアはホストから送信されたメッセージをリッスンする必要があります。
ホストのコントローラーでは、目標に到達するたびに、 BallBehaviour
のスクリプトによって最初のメソッドである GoalScored(String color)
が呼び出されます。スコアが更新された後、スコアを更新するように、非ホストのコントローラーにもメッセージが送信されます。
ボールの位置は、ホストのみ操作することができます。そして、その位置を含むメッセージが非ホストのプレイヤーに送信されます。 MessagingManager
はホストから新しい位置を受け取るたびに SetBallLocation(Vector3 position)
を呼び出し、非ホストプレイヤーのためにボールの位置を設定します。
最後に、 Update()
関数はフレームごとに呼び出されるUnityイベントです。どちらのプレイヤーも同期されているものの、ゲームが開始されていない場合は、ホストがヒットテストを実行し、検出された平面上にゲームオブジェクトをスポーンすることができます。ゲーム開始後は、フレームごとに、ボールとプレイヤーのアバターとの距離を計算し、両者が衝突した場合(両者の半径は0.25m、衝突は0.5m以下)は、ボールをヒット角度に応じた方向にバウンドさせます。プレイヤーがホストである場合、ヒットは即座に実行されます。それ以外の場合は、ホストにヒットを実行させるためのヒットベクトルを含むメッセージがホストに送信されます。最近ヒットしたフィールドとヒットがロックアウトしたフィールドは、複数のヒットメッセージが連続して送信されるのを防ぐために使用されます。
// Reset the ball when a goal is scored, increase score for player that scored // Only the host should call this method internal void GoalScored(string color) { // color param is the color of the goal that the ball went into // we score points by getting the ball in our opponent's goal if (color == "red") { Debug.Log("Point scored for team blue"); BlueScore += 1; } else { Debug.Log("Point scored for team red"); RedScore += 1; } score.text = string.Format("Score: {0} - {1}", RedScore, BlueScore); _messagingManager.GoalScored(color); } // Set the ball location for non-host players internal void SetBallLocation(Vector3 position) { if (!_isGameStarted) _isGameStarted = true; _ball.transform.position = position; } // Every frame, detect if you have hit the ball // If so, either bounce the ball (if host) or tell host to bounce the ball private void Update() { if (_isSynced && !_isGameStarted && _isHost) { if (PlatformAgnosticInput.touchCount <= 0) return; var touch = PlatformAgnosticInput.GetTouch(0); if (touch.phase == TouchPhase.Began) { var startGameDistance = Vector2.Distance ( touch.position, new Vector2(startGameButton.transform.position.x, startGameButton.transform.position.y) ); if (startGameDistance > 80) FindFieldLocation(touch); } } if (!_isGameStarted) return; if (_recentlyHit) { _hitLockout += 1; if (_hitLockout >= 15) { _recentlyHit = false; _hitLockout = 0; } } var ballDistance = Vector3.Distance(_player.transform.position, _ball.transform.position); if (ballDistance > .5 || _recentlyHit) return; Debug.Log("We hit the ball!"); var bounceDirection = _ball.transform.position - _player.transform.position; bounceDirection = Vector3.Normalize(bounceDirection); _recentlyHit = true; if (_isHost) _ballBehaviour.Hit(bounceDirection); else _messagingManager.BallHitByPlayer(_host, bounceDirection); }
イベント
セッションの共有ARのステータスに関するアップデートを受け取るには、いくつかの ARNetworking
イベントをサブスクライブします。
OnFrameUpdated
イベントは、ローカルプレイヤーの位置をフレームごとに取得するために使用されます( ARSession
オブジェクトに存在し、シングルプレイヤーのARセッションでも利用できます)。ユーティリティクラス MatrixUtils
は、位置と回転のデータを含む Matrix4x4
から Vector3
の位置を抽出するために使用されます。同様に、イベント OnPeerPoseReceived
は、相手の位置を取得するために使用されます。 OnPeerStateReceived
イベントは、ローカルピアと、同じセッションのそれ以外のピアの両方の同期状態の更新時に発生します。
// Every updated frame, get our location from the frame data and move the local player's avatar private void OnFrameUpdated(FrameUpdatedArgs args) { _location = MatrixUtils.PositionFromMatrix(args.Frame.Camera.Transform); if (_player == null) return; var playerPos = _player.transform.position; playerPos.x = _location.x; _player.transform.position = playerPos; } private void OnPeerStateReceived(PeerStateReceivedArgs args) { if (_self.Identifier == args.Peer.Identifier) UpdateOwnState(args); else UpdatePeerState(args); } private void UpdatePeerState(PeerStateReceivedArgs args) { if (args.State == PeerState.Stable) { _isSynced = true; if (_isHost) startGameButton.SetActive(true); } } private void UpdateOwnState(PeerStateReceivedArgs args) { string message = args.State.ToString(); score.text = message; Debug.Log("We reached state " + message); } // Upon receiving a peer's location data, take its location and move its avatar private void OnPeerPoseReceived(PeerPoseReceivedArgs args) { if (_opponent == null) return; var peerLocation = MatrixUtils.PositionFromMatrix(args.Pose); var opponentPosition = _opponent.transform.position; opponentPosition.x = peerLocation.x; _opponent.transform.position = opponentPosition; }
また、すべてのコールバックを削除し、 MessagingManager
を破棄するための初期化イベントと破棄イベントがあります。
private void OnDidConnect(ConnectedArgs args) { _self = args.Self; _host = args.Host; _isHost = args.IsHost; } private void OnDestroy() { ARNetworkingFactory.ARNetworkingInitialized -= OnAnyARNetworkingSessionInitialized; if (_arNetworking != null) { _arNetworking.PeerPoseReceived -= OnPeerPoseReceived; _arNetworking.PeerStateReceived -= OnPeerStateReceived; _arNetworking.ARSession.FrameUpdated -= OnFrameUpdated; _arNetworking.Networking.Connected -= OnDidConnect; } if (_messagingManager != null) { _messagingManager.Destroy(); _messagingManager = null; } }
BallBehaviour
ボールの動作に移ります。このスクリプトはBallプレハブにアタッチされ、ボールがインスタンス化される際に作成されます。ほとんどの場合、このスクリプトは純粋なゲームロジックであり、ARDKとはあまり関係がありません。また、ボールの位置はフレームごとに更新され、メッセージとして非ホストに送信されるため、これらのメソッドのほとんどを呼び出すことができるのはホストのみです。
セットアップと参考情報
このセクションでは、フィールドの境界線、初速、位置などのパラメーターについて説明しますが、ほとんどがゲーム上級者を対象としています。ボールをスポーンする位置はキャッシュに保存され、目標に到達するたびにフィールドの中心に戻されます。
public class BallBehaviour: MonoBehaviour { internal GameController Controller = null; private Vector3 _pos; // Left and right boundaries of the field, in meters private float _lrBound = 2.5f; // Forward and backwards boundaries of the field, in meters private float _fbBound = 2.5f; // Initial velocity, in meters per second private float _initialVelocity = 1.0f; private Vector3 _velocity; // Cache the floor level, so the ball is reset properly private Vector3 _initialPosition; // Flags for whether the game has started and if the local player is the host private bool _isGameStarted; private bool _isHost; /// Reference to the messaging manager private MessagingManager _messagingManager; // Store the start location of the ball private void Start() { _initialPosition = transform.position; }
GameStart
このメソッドは、ボールのインスタンス化後に GameController
によって呼び出されます。ローカルプレイヤーがホストかどうかについての情報と、最初の位置などの関連情報が設定され、ローカルプレイヤーがホストの場合は MessagingManager
と速度が設定されます。
// Set up the initial conditions internal void GameStart(bool isHost, MessagingManager messagingManager) { _isHost = isHost; _isGameStarted = true; _initialPosition = transform.position; if (!_isHost) return; _messagingManager = messagingManager; _velocity = new Vector3(_initialVelocity, 0, _initialVelocity); }
Hit
このメソッドは、(自分自身がヒットしたとき、またはホスト以外からメッセージを受け取ったときに)ホストの GameController
によって呼び出されます。これにより、ボールは適切な方向にバウンドし、速度は10%増加します。
// Signal that the ball has been hit, with a unit vector representing the new direction internal void Hit(Vector3 direction) { if (!_isGameStarted || !_isHost) return; _velocity = direction * _initialVelocity; _initialVelocity *= 1.1f; }
Unityイベント
Update()
はフレームごとに呼び出され、速度をもとにボールの位置を更新します。続いて、 MessagingManager
を使用して、ボールの位置を非ホストのプレイヤーに送信します。ボールが境界を越えた場合は、次のフレームで境界内に収まるように正しい速度を反転します。
OnTriggerEnter(Collider other)
は、ボールがゴールに入るたびに呼び出されます。ボールの位置と速度がリセットされたら、スコアを更新する GameController
メソッドが呼び出され、スコアを更新するメッセージが非ホストに送信されます。
// Perform movement, send position to non-host player private void Update() { if (!_isGameStarted || !_isHost) return; _pos = gameObject.transform.position; _pos.x += _velocity.x * Time.deltaTime; _pos.z += _velocity.z * Time.deltaTime; transform.position = _pos; _messagingManager.BroadcastBallPosition(_pos); if (_pos.x > _initialPosition.x + _lrBound) _velocity.x = -_initialVelocity; else if (_pos.x < _initialPosition.x - _lrBound) _velocity.x = _initialVelocity; if (_pos.z > _initialPosition.z + _fbBound) _velocity.z = -_initialVelocity; else if (_pos.z < _initialPosition.z - _fbBound) _velocity.z = _initialVelocity; } // Signal to host that a goal has been scored private void OnTriggerEnter(Collider other) { if (!_isGameStarted || !_isHost) return; _initialVelocity = 1.0f; _velocity = new Vector3(0, 0, _initialVelocity); transform.position = _initialPosition; switch (other.gameObject.tag) { case "RedGoal": Controller.GoalScored("red"); break; case "BlueGoal": Controller.GoalScored("blue"); break; } }