チュートリアル: 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;
  }
}

前のページ: ARDKとゲームロジックの使用

続き: MessagingManagerによるメッセージの送受信