アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

【Unity】Starter Assets – Third Person Character Controllerをオンライン対応する【Photon Cloud PUN2】

こんにちは!ジェイです。昨日Unity公式からでた新しいアセット、Starter Assets – First Person Character ControllerとThird Person Character Controllerの紹介と使い方を解説しました。

今日はそれらを使ってPhotonCloudPUN2でオンライン対応させます。

記事内広告

準備

まず、こちらの記事のTPS版のセットアップ方法を参考にThird Person Character Controllerを自分の好きなモデルに差し替えて使えるようにします。

Third Person Character Controller

PhotonPUN2

次にPhotonPUN2の準備をします。

以上でオンライン対応までの準備ができました。これから実際にロビーから入室までのシーンを作って、対応させます。

タイトル画面の作成

  • 以下のスクリプトを作成し、空のGameObjectに貼り付けます。
  • SceneManager.LoadScene(“Game");の部分を移行したいシーン名に変える

たったこれだけで簡易ロビーの実装ができるので、オンラインゲームのプロトタイプを作成するにはもってこいです!

using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.SceneManagement;

public class CTitleManager : MonoBehaviourPunCallbacks
{
    private string RoomName = "";
    Rect LobbyGuiRect = new Rect(0, 0, 300, 200);

    void Update()
    {
        LobbyGuiRect.x = Screen.width / 4;
        LobbyGuiRect.y = Screen.height / 4;
        LobbyGuiRect.width = Screen.width / 2;
        LobbyGuiRect.height = Screen.height / 4;
    }
    // 接続したらロビーに入室する
    public override void OnConnectedToMaster()
    {
        PhotonNetwork.JoinLobby();
    }
    //GetRoomListは一定時間ごとに更新され、その更新のタイミングで実行する処理
    //ルームリストに更新があった時(ロビーに入ってるときのみ呼ばれる)
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        RoomList = roomList;
    }
    List<RoomInfo> RoomList = new List<RoomInfo>();
    void RoomNameUIWindow(int window_id)
    {
        // ルーム名の入力
        GUILayout.BeginHorizontal();
        GUILayout.Label("RoomName : ");
        RoomName = GUILayout.TextField(RoomName, GUILayout.Width(200));
        GUILayout.EndHorizontal();

        // ルームを作成して入室する
        if (GUILayout.Button("Create Room", GUILayout.Width(150)))
        {
            PhotonNetwork.CreateRoom(RoomName);
        }

        // ルーム一覧を検索
        foreach (var room in RoomList)
        {
            // ルームパラメータの可視化
            string room_param = $"{room.Name}{room.PlayerCount}/{((room.MaxPlayers == 0) ? "-" : room.MaxPlayers.ToString())}";

            // ルームを選択して入室する
            if (GUILayout.Button("Enter Room : " + room_param))
            {
                PhotonNetwork.JoinRoom(room.Name);
            }
        }
    }
    void PlayerNameUIWindow(int window_id)
    {
        // プレイヤー名の入力
        GUILayout.BeginHorizontal();
        GUILayout.Label("PlayerName : ");
        PhotonNetwork.NickName = GUILayout.TextField(
            (PhotonNetwork.NickName == null) ?
                "" :
                PhotonNetwork.NickName, GUILayout.Width(200));
        GUILayout.EndHorizontal();

        // MUNサーバに接続する
        if (GUILayout.Button("Connect Server", GUILayout.Width(150)))
        {
            PhotonNetwork.AutomaticallySyncScene = true;
            PhotonNetwork.ConnectUsingSettings();
        }
    }
    public override void OnCreatedRoom()
    {
        SceneManager.LoadScene("Game");
    }
    void OnGUI()
    {
        // サーバに接続している場合
        if (PhotonNetwork.IsConnected)
        {
            // ロビーに入室してる場合
            if (PhotonNetwork.InLobby)
            {
                // ルームに入室していない場合
                if (!PhotonNetwork.InRoom)
                {
                    GUILayout.Window(1, LobbyGuiRect, RoomNameUIWindow, "");
                }
            }
            else // ロビーに入室してない場合
            {
                // ルームに入室している場合
                if (PhotonNetwork.InRoom)
                {

                }
            }
        }
        // PUNサーバに接続していない場合
        else
        {
            GUILayout.Window(2, LobbyGuiRect, PlayerNameUIWindow, "");
        }
    }
}

注意点

PhotonNetwork.AutomaticallySyncSceneをtrueに設定しているので、マスタークライアントと同じルームに入ったプレイヤーは自動的に同じシーンに移動する事になります。

これによって、OnCreatedRoomで部屋が作られた時のみ、シーンを移動する処理をすればよいです。もし、falseの場合は、OnJoinedRoom(部屋に参加したら呼ばれるコールバック関数)の方を使用しなければなりません。

プレイヤーに各種PhotonViewを追加する

  • PhotonView
  • PhotonTransformViewClassic
  • PhotonAnimatorView

以上の3つのコンポーネントを追加して、以下の画像の様に設定します。

上記の設定の詳しい解説はこちらから

ThirdPersonController.csを書き換える

ハイライトした部分が追加したコードです。

using UnityEngine;
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
using UnityEngine.InputSystem;
#endif
using Photon.Pun;
using Photon.Realtime;
using Cinemachine;

/* 
 * 注:アニメーションは、アニメーターのNULLチェックを使って、キャラクターとカプセルの両方に対して、コントローラー経由で呼び出されます。
 */

namespace StarterAssets
{
	[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
	[RequireComponent(typeof(PlayerInput))]
#endif
	public class ThirdPersonController : MonoBehaviourPun
	{
		[Header("Player")]
		[Tooltip("キャラクターの移動速度(m/s)")]
		public float MoveSpeed = 2.0f;
		[Tooltip("キャラクターのスプリント速度(単位:m/s)")]
		public float SprintSpeed = 5.335f;
		[Tooltip("キャラクターが移動方向を向く速さ")]
		[Range(0.0f, 0.3f)]
		public float RotationSmoothTime = 0.12f;
		[Tooltip("加速と減速")]
		public float SpeedChangeRate = 10.0f;

		[Space(10)]
		[Tooltip("プレイヤーがジャンプできる高さ")]
		public float JumpHeight = 1.2f;
		[Tooltip("キャラクターは、独自の重力値を使用します。エンジンのデフォルトは-9.81f")]
		public float Gravity = -15.0f;

		[Space(10)]
		[Tooltip("再びジャンプできるようになるまでの時間です。00fに設定すると瞬時にジャンプ可能になります")]
		public float JumpTimeout = 0.50f;
		[Tooltip("転倒状態になるまでの経過時間です。階段を下りるときに便利")]
		public float FallTimeout = 0.15f;

		[Header("Player Grounded")]
		[Tooltip("キャラクターが接地されているかどうか。CharacterControllerに内蔵されている接地チェックには含まれない")]
		public bool Grounded = true;
		[Tooltip("不整地での使用に便利")]
		public float GroundedOffset = -0.14f;
		[Tooltip("接地チェックの半径です。CharacterControllerの半径と一致する必要があります。")]
		public float GroundedRadius = 0.28f;
		[Tooltip("キャラクターが地面として使用するレイヤー")]
		public LayerMask GroundLayers;

		[Header("Cinemachine")]
		[Tooltip("Cinemachine Virtual Cameraで設定された、カメラが追従するフォローターゲット")]
		public GameObject CinemachineCameraTarget;
		[Tooltip("どのくらいの角度でカメラを上に動かせるか")]
		public float TopClamp = 70.0f;
		[Tooltip("カメラを何度まで下げられるか")]
		public float BottomClamp = -30.0f;
		[Tooltip("カメラをオーバーライドするための追加のディグレス。ロック時にカメラの位置を微調整するのに便利")]
		public float CameraAngleOverride = 0.0f;
		[Tooltip("カメラの位置を全軸で固定する場合")]
		public bool LockCameraPosition = false;

		// cinemachine
		private float _cinemachineTargetYaw;
		private float _cinemachineTargetPitch;

		// player
		private float _speed;
		private float _animationBlend;
		private float _targetRotation = 0.0f;
		private float _rotationVelocity;
		private float _verticalVelocity;
		private float _terminalVelocity = 53.0f;

		// timeout deltatime
		private float _jumpTimeoutDelta;
		private float _fallTimeoutDelta;

		// animation IDs
		private int _animIDSpeed;
		private int _animIDGrounded;
		private int _animIDJump;
		private int _animIDFreeFall;
		private int _animIDMotionSpeed;

		private Animator _animator;
		private CharacterController _controller;
		private StarterAssetsInputs _input;
		private GameObject _mainCamera;

		private const float _threshold = 0.01f;

		private bool _hasAnimator;

		private PhotonTransformViewClassic _PhotonTransformViewClassic;
		private void Awake()
		{
			if(!photonView.IsMine)
            {
				return;
            }
			// get a reference to our main camera
			if (_mainCamera == null)
			{
				_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
			}
		}

		private void Start()
		{
			if (!photonView.IsMine)
			{
				return;
			}
			GameObject player_follow_camera = GameObject.Find("PlayerFollowCamera");
			var cvc = player_follow_camera.GetComponent<CinemachineVirtualCamera>();
			cvc.Follow = transform.Find("PlayerCameraRoot");
			_PhotonTransformViewClassic = GetComponent<PhotonTransformViewClassic>();
			Cursor.lockState = CursorLockMode.Locked;
			Cursor.visible = false;

			_hasAnimator = TryGetComponent(out _animator);
			_controller = GetComponent<CharacterController>();
			_input = GetComponent<StarterAssetsInputs>();

			AssignAnimationIDs();

			// reset our timeouts on start
			_jumpTimeoutDelta = JumpTimeout;
			_fallTimeoutDelta = FallTimeout;
		}	

		private void Update()
		{
			if (!photonView.IsMine)
			{
				return;
			}
			// カーソルを外す
			if (Input.GetKeyDown(KeyCode.Escape))
			{
				if(Cursor.lockState == CursorLockMode.Locked)
                {
					Cursor.lockState = CursorLockMode.None;
					Cursor.visible = true;
				}
				else if (Cursor.lockState == CursorLockMode.None)
                {
					Cursor.lockState = CursorLockMode.Locked;
					Cursor.visible = false;
				}
			}
			_hasAnimator = TryGetComponent(out _animator);
			
			JumpAndGravity();
			GroundedCheck();
			Move();
		}

		private void LateUpdate()
		{
			if (!photonView.IsMine)
			{
				return;
			}
			CameraRotation();
		}

		private void AssignAnimationIDs()
		{
			_animIDSpeed = Animator.StringToHash("Speed");
			_animIDGrounded = Animator.StringToHash("Grounded");
			_animIDJump = Animator.StringToHash("Jump");
			_animIDFreeFall = Animator.StringToHash("FreeFall");
			_animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
		}

		private void GroundedCheck()
		{
			// set sphere position, with offset
			Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
			Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);

			// update animator if using character
			if (_hasAnimator)
			{
				_animator.SetBool(_animIDGrounded, Grounded);
			}
		}

		private void CameraRotation()
		{
			// 入力があってカメラの位置が固定されていない場合
			if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
			{
				_cinemachineTargetYaw += _input.look.x * Time.deltaTime;
				_cinemachineTargetPitch += _input.look.y * Time.deltaTime;
			}

			// 回転を固定して、値が360度に制限されるようにする。
			_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
			_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

			// Cinemachine will follow this target
			CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride, _cinemachineTargetYaw, 0.0f);
		}

		private void Move()
		{
			// 移動速度、スプリント速度、スプリントが押されたかどうかに基づいて目標速度を設定します。
			float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

			// 削除、交換、反復が簡単にできるようにデザインされたシンプルな加速と減速。

			// Vector2の==演算子は近似値を使用するため、浮動小数点エラーが発生しにくく、magnitudeよりも低コストです。
			// 入力がない場合、目標速度を0にする
			if (_input.move == Vector2.zero) targetSpeed = 0.0f;

			// プレイヤーの現在の水平方向の速度を示す値
			float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

			float speedOffset = 0.1f;
			float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

			// 目標速度までの加速・減速
			if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
			{
				// 直線的な結果ではなく、曲線的な結果となり、より有機的な速度変化が得られます。
				// LerpのTは固定されているので、スピードを固定する必要はありません。
				_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);

				// スピードを小数点以下3桁まで丸める
				_speed = Mathf.Round(_speed * 1000f) / 1000f;
			}
			else
			{
				_speed = targetSpeed;
			}
			_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);

			// 入力方向の正規化
			Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

			// Vector2の != 演算子は近似値を使用しているため、浮動小数点エラーが発生しにくく、Magnitudeよりも低コストです。
			// 移動入力がある場合、プレイヤーが移動しているときにプレイヤーを回転させる
			if (_input.move != Vector2.zero)
			{
				_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg + _mainCamera.transform.eulerAngles.y;
				float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity, RotationSmoothTime);

				// カメラの位置を基準にして入力方向に向かって回転
				transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
			}

			Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

			// move the player
			_controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
			_PhotonTransformViewClassic.SetSynchronizedValues(_controller.velocity, 0);

			// update animator if using character
			if (_hasAnimator)
			{
				_animator.SetFloat(_animIDSpeed, _animationBlend);
				_animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
			}
		}

		private void JumpAndGravity()
		{
			if (Grounded)
			{
				// reset the fall timeout timer
				_fallTimeoutDelta = FallTimeout;

				// update animator if using character
				if (_hasAnimator)
				{
					_animator.SetBool(_animIDJump, false);
					_animator.SetBool(_animIDFreeFall, false);
				}

				// 接地時に速度が無限に落ちるのを防ぐ
				if (_verticalVelocity < 0.0f)
				{
					_verticalVelocity = -2f;
				}

				// Jump
				if (_input.jump && _jumpTimeoutDelta <= 0.0f)
				{
					// H*-2*Gの平方根=目的の高さに到達するために必要な速度
					_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

					// update animator if using character
					if (_hasAnimator)
					{
						_animator.SetBool(_animIDJump, true);
					}
				}

				// jump timeout
				if (_jumpTimeoutDelta >= 0.0f)
				{
					_jumpTimeoutDelta -= Time.deltaTime;
				}
			}
			else
			{
				// reset the jump timeout timer
				_jumpTimeoutDelta = JumpTimeout;

				// fall timeout
				if (_fallTimeoutDelta >= 0.0f)
				{
					_fallTimeoutDelta -= Time.deltaTime;
				}
				else
				{
					// キャラクターを使用している場合はアニメーターを更新
					if (_hasAnimator)
					{
						_animator.SetBool(_animIDFreeFall, true);
					}
				}

				// 地に足がついていなければジャンプしない
				_input.jump = false;
			}

			// terminalの下にある場合、時間をかけて重力をかける(deltaTimeを2回かけると、時間をかけて直線的にスピードアップする)
			if (_verticalVelocity < _terminalVelocity)
			{
				_verticalVelocity += Gravity * Time.deltaTime;
			}
		}

		private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
		{
			if (lfAngle < -360f) lfAngle += 360f;
			if (lfAngle > 360f) lfAngle -= 360f;
			return Mathf.Clamp(lfAngle, lfMin, lfMax);
		}

		private void OnDrawGizmosSelected()
		{
			Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
			Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

			if (Grounded) Gizmos.color = transparentGreen;
			else Gizmos.color = transparentRed;

			// 選択されると、接地されたコライダーの位置と半径に合わせてギズモを描く
			Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius);
		}
	}
}

実行結果

Image from Gyazo

まとめ

新しいStarter Assets Third Person Character ControllerをPhotonCloudPUN2を使ってオンライン対応しました。この新しいStarter Assetsのすごい所はモデルを差し替えるのが非常に簡単にできる事です!外部ファイルからランタイムロードでモデルを差し替えるような処理を行いたい場合は、従来よりも圧倒的に有利になってるのは良いですよね!

アイコンはAI・コンタクトさんからお借りしました。

+4