| 使用バージョン | ||
|---|---|---|
| Unity Editor | Unity 2022LTS | 2022.3.11f1 |
| Project Template | 3D(URP)コア | |
| Package (Unity Registry) | Netcode for Game Objects | 1.6.0 |
| Asset Store | Starter Assets - Third Person | 1.1.5 |




using System.Collections;
using System.Collections.Generic;
using Unity.Netcode.Components;
using UnityEngine;
public class ClientNetworkTransform : NetworkTransform
{
protected override bool OnIsServerAuthoritative()
{
return false;
}
}
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode.Components;
using UnityEngine;
public class ClientNetworkAnimator : NetworkAnimator
{
protected override bool OnIsServerAuthoritative()
{
return false;
}
}
| マルチプレイ役割モード設定 | 作動状態チェック | |||
|---|---|---|---|---|
| ネットワークマネージャー | 役割 | IsHost | IsClient | IsServer |
| 未起動 | false | false | false | |
| Start Client | プレイメンバとして参加 | false | true | false |
| Start Server | Serverとして参加 | false | false | true |
| Start Host | Serverかつメンバとして参加 | true | true | true |
using Unity.Netcode;
using UnityEngine;
public class UIManager : MonoBehaviour
{
private void Start()
{
#if UNITY_SERVER
//ヘッドレスサーバーで動く場合でも起動と同時にネットワークマネージャーをスタート
NetworkManager.Singleton.StartServer();
#endif
}
void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 300, 300));
if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
{
StartButtons();
}
else
{
StatusLabels();
}
GUILayout.EndArea();
}
static void StartButtons()
{
if (GUILayout.Button("Quit")) QuitApp();
if (GUILayout.Button("Server")) NetworkManager.Singleton.StartServer();
if (GUILayout.Button("Host")) NetworkManager.Singleton.StartHost();
if (GUILayout.Button("Client")) NetworkManager.Singleton.StartClient();
}
static void StatusLabels()
{
if (GUILayout.Button("Quit")) QuitApp();
var mode = NetworkManager.Singleton.IsHost ?
"Host" : NetworkManager.Singleton.IsServer ? "Server" : "Client";
GUILayout.Label("Transport: " +
NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetType().Name);
GUILayout.Label("Mode: " + mode);
}
static void QuitApp()
{
Application.Quit(); //このアプリを終了させる。
#if UNITY_EDITOR //Unityエディターで動いているときに限定 ここから
UnityEditor.EditorApplication.isPlaying = false; //プレビュー再生をOffにする。
#endif //Unityエディターで動いているときに限定 ここまで
}
}




Player InputコンポーネントをOFF
Network Objectコンポーネントを追加
Client Network Transformコンポーネントを追加
Client Network Animatorコンポーネントを追加
アニメーターをリストから自分を選んで追加。
プレファブウィンドウを閉じる。(”<”でシーン編集に戻る)

Assets>Prefabsフォルダで、Create>Netcode>Network Prefab List

作成したNetwork Prefab Listをダブルクリックして、NetworkPlayerArmatureをリストに追加

ヒエラルキーで空のオブジェクトを2つ作成。

名前をNetworkManager、UIManagerにして、それぞれ同じ名前のコンポーネントを追加。

オブジェクト名=NetworkManager
プレイヤープレファブ=NetworkPlayerArmature
ネットワークプレファブリスト=Network Prefab List
Select Transport =Unity Transport


Assets>PrefabsのNetworkPlayerArmatureプレファブをダブルクリックしてインスペクターで開く。
ThirdPersonController.csをダブルクリックしてVisualStudioで開く。

NetworkPlayerArmatureは、ネットワークマネージャーが出現させるので、
シーンの中でのカメラやインプットの対応設定ができていない。
出現時に、カメラやインプットを接続する。
ClientNetworkTransformで、クライアント側でモデルを動かして結果をサーバーに送って同期するので、
クライアント側でのキャラクター操作の部分はシングルプレーヤーのまま使える。
ThirdPersonController.cs 修正後
using Cinemachine;
using UnityEngine;
using Unity.Netcode;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
/* Note: animations are called via the controller for both the character and capsule using animator null checks
*/
namespace StarterAssets
{
[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM
[RequireComponent(typeof(PlayerInput))]
#endif
public class ThirdPersonController : NetworkBehaviour
{
[Header("Player")]
[Tooltip("Move speed of the character in m/s")]
public float MoveSpeed = 2.0f;
[Tooltip("Sprint speed of the character in m/s")]
public float SprintSpeed = 5.335f;
[Tooltip("How fast the character turns to face movement direction")]
[Range(0.0f, 0.3f)]
public float RotationSmoothTime = 0.12f;
[Tooltip("Acceleration and deceleration")]
public float SpeedChangeRate = 10.0f;
public AudioClip LandingAudioClip;
public AudioClip[] FootstepAudioClips;
[Range(0, 1)] public float FootstepAudioVolume = 0.5f;
[Space(10)]
[Tooltip("The height the player can jump")]
public float JumpHeight = 1.2f;
[Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
public float Gravity = -15.0f;
[Space(10)]
[Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
public float JumpTimeout = 0.50f;
[Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
public float FallTimeout = 0.15f;
[Header("Player Grounded")]
[Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
public bool Grounded = true;
[Tooltip("Useful for rough ground")]
public float GroundedOffset = -0.14f;
[Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
public float GroundedRadius = 0.28f;
[Tooltip("What layers the character uses as ground")]
public LayerMask GroundLayers;
[Header("Cinemachine")]
[Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
public GameObject CinemachineCameraTarget;
[Tooltip("How far in degrees can you move the camera up")]
public float TopClamp = 70.0f;
[Tooltip("How far in degrees can you move the camera down")]
public float BottomClamp = -30.0f;
[Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
public float CameraAngleOverride = 0.0f;
[Tooltip("For locking the camera position on all axis")]
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;
#if ENABLE_INPUT_SYSTEM
private PlayerInput _playerInput;
#endif
private Animator _animator;
private CharacterController _controller;
private StarterAssetsInputs _input;
private GameObject _mainCamera;
private GameObject _PlayerCameraRoot;
private CinemachineVirtualCamera _CinemachineVirtualCamera;
private const float _threshold = 0.01f;
private bool _hasAnimator;
private bool IsCurrentDeviceMouse
{
get
{
#if ENABLE_INPUT_SYSTEM
return _playerInput.currentControlScheme == "KeyboardMouse";
#else
return false;
#endif
}
}
private void Awake()
{
// get a reference to our main camera
if (_mainCamera == null)
{
_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
}
if(_CinemachineVirtualCamera == null)
{
_CinemachineVirtualCamera = FindObjectOfType();
}
}
private void Start()
{
_cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;
_hasAnimator = TryGetComponent(out _animator);
_controller = GetComponent();
_input = GetComponent();
//#if ENABLE_INPUT_SYSTEM
// _playerInput = GetComponent();
//#else
// Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
//#endif
AssignAnimationIDs();
// reset our timeouts on start
_jumpTimeoutDelta = JumpTimeout;
_fallTimeoutDelta = FallTimeout;
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if(IsClient && IsOwner)
{
_playerInput = GetComponent();
_playerInput.enabled = true;
_PlayerCameraRoot = transform.Find("PlayerCameraRoot").gameObject;
_CinemachineVirtualCamera.Follow = _PlayerCameraRoot.transform;
}
}
private void Update()
{
if (IsOwner)
{
_hasAnimator = TryGetComponent(out _animator);
JumpAndGravity();
GroundedCheck();
Move();
}
}
private void LateUpdate()
{
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 there is an input and camera position is not fixed
if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
{
//Don't multiply mouse input by Time.deltaTime;
float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;
_cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
_cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
}
// clamp our rotations so our values are limited 360 degrees
_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()
{
// set target speed based on move speed, sprint speed and if sprint is pressed
float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;
// a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon
// note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is no input, set the target speed to 0
if (_input.move == Vector2.zero) targetSpeed = 0.0f;
// a reference to the players current horizontal velocity
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;
// accelerate or decelerate to target speed
if (currentHorizontalSpeed < targetSpeed - speedOffset ||
currentHorizontalSpeed > targetSpeed + speedOffset)
{
// creates curved result rather than a linear one giving a more organic speed change
// note T in Lerp is clamped, so we don't need to clamp our speed
_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
Time.deltaTime * SpeedChangeRate);
// round speed to 3 decimal places
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = targetSpeed;
}
_animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
if (_animationBlend < 0.01f) _animationBlend = 0f;
// normalise input direction
Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;
// note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
// if there is a move input rotate player when the player is moving
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);
// rotate to face input direction relative to camera position
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);
// 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);
}
// stop our velocity dropping infinitely when grounded
if (_verticalVelocity < 0.0f)
{
_verticalVelocity = -2f;
}
// Jump
if (_input.jump && _jumpTimeoutDelta <= 0.0f)
{
// the square root of H * -2 * G = how much velocity needed to reach desired height
_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
{
// update animator if using character
if (_hasAnimator)
{
_animator.SetBool(_animIDFreeFall, true);
}
}
// if we are not grounded, do not jump
_input.jump = false;
}
// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
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;
// when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
Gizmos.DrawSphere(
new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),
GroundedRadius);
}
private void OnFootstep(AnimationEvent animationEvent)
{
if (animationEvent.animatorClipInfo.weight > 0.5f)
{
if (FootstepAudioClips.Length > 0)
{
var index = Random.Range(0, FootstepAudioClips.Length);
AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);
}
}
}
private void OnLand(AnimationEvent animationEvent)
{
if (animationEvent.animatorClipInfo.weight > 0.5f)
{
AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);
}
}
}
}
BuildSettingsで、スクリーンサイズをWindowed 800x600に変更し、
Buildシーンに編集シーンを追加しBuild and Runを実行。


Unity Starter Asset Third Personを、Netcodeを使用してマルチプレーヤーに変更できた。
◆◆Server(サーバー機能)
起動したのがサーバー状態だった場合は、Serverとして開始する。
◆◆Server(サーバー機能)
//NetworkManagerで、Serverとしてゲームを開始
NetworkManager.Singleton.StartServer();
◆◆Host(サーバー機能+プレーヤー)
//NetworkManagerで、Hostとしてゲームを開始
NetworkManager.Singleton.StartHost();
◆◆Client(プレーヤー)
//NetworkManagerで、Clientとしてゲームを開始
NetworkManager.Singleton.StartClient();
Hostで起動すれば、シングルプレイ状態になる。
同じPCで動くアプリ同士なので、サーバーのアドレスは127.0.0.1(自機)としている。
他機で起動しているサーバーへ接続するにはサーバーのIPアドレスを指定しないといけない。
インターネット経由の接続で固定アドレスは珍しいので、都度IPアドレス調べないと繋げない。
こうした接続を仲介し、インターネットどこでも繋がるようにする。
UnityサービスのRelayやLobbyはインターネットのサーバーで行う仲介サービスで、
インターネット越し連絡してきたデバイスと接続できるようにする。
Unityサービスを使うには、Unityアカウントで組織、プロジェクトのサービスへの登録、セットアップが必要。
一定使用量の無料枠があり、超えた分は従量課金となる。
Relayはデバイス間の通信情報を中継する。ホスト・サーバーが開いた回線のIDコードを入力すれば、
そこに接続できる。
Lobbyは、アプリで指定したサーバー上に開いた回線情報を登録し、それを問合せて参加したり、
自分で開いて接続されるのを待ったりできる。
Lobbyで開設されたルームのリストを見てClientで参加するか、Hostとして新たにルームを作って待つか
選択する。
Hostで起動すればシングルプレイ状態になる。
しばらくすると、他プレーヤーにルームボタンが表示されるので、クライアントとしてジョインできる。


パッケージマネージャーUnityRegistryからRelayとLobbyをインストール

インストールが終わったら、ProjectSettingsでサービスを設定する。
UnityサービスのRelayやLobbyを使用するアプリとして、制作アプリのIDを登録する。
◆◆Server(Serverとしてルーム作成)
◆◆Server(Serverとしてルーム作成)
◆◆Host(Serverとしてルーム作成 プレーヤ起動)
◆◆Client(すでにあるルームに入室 プレーヤ起動)
◆◆Host/Server(ルーム作成)
//UnityサービスへのAuth認証
UnityServices.InitializeAsync();
AuthenticationService.Instance.SignInAnonymouslyAsync();
//RelayServiceでAllocationを作成
RelayService.Instance.CreateAllocationAsync(maxConnections);
//RelayServiceでJoinCode取得
RelayService.Instance.GetJoinCodeAsync(hostAllocationId);
//LobbyServiceでロビーを作成
Lobbies.Instance.CreateLobbyAsync(lobbyName, maxPlayers, createOptions);
//allocationから入手したRelayサーバー情報をNetworkManagerに設定
NetworkManager.Singleton.GetComponent
//NetworkManagerで、Host/Serverとしてゲームを開始
NetworkManager.Singleton.StartHost();
//LobbyにHeartBeatを送って切断をしてないと伝える
Lobbies.Instance.SendHeartbeatPingAsync(lobbyId);
◆◆Client(入室)
ルームリストを習得し、存在しているルームの数だけボタンを表示する
ルームNoボタンを押すと入室する
//UnityサービスへのAuth認証
UnityServices.InitializeAsync();
AuthenticationService.Instance.SignInAnonymouslyAsync();
//Lobbyリスト取得
Lobbies.Instance.QueryLobbiesAsync();
//Lobbyを選択して参加
Lobbies.Instance.JoinLobbyByIdAsync(lobbyId, joinOptions);
//LobbyからJoinCodeを入手して、作成済のRelayServiceへ参加
RelayService.Instance.JoinAllocationAsync(joinCode);
//allocationから入手したRelayサーバー情報をNetworkManagerに設定
NetworkManager.Singleton.GetComponent
//NetworkManagerで、Clientとしてゲームに参加
NetworkManager.Singleton.StartClient();
RelayとLobbyの制御スクリプト
UIManagerオブジェクトの、UIManagerコンポーネント(script)を削除
NetPlayManager.csとNetRoomUIManager.csコンポーネント(script)を追加する。
NetRoomUIManager.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR;
public class NetRoomUIManager : MonoBehaviour
{
static NetPlayManager NPMSingleton;
static private bool findingRoom = false;
static int rooms = 0;
private void Start()
{
NPMSingleton = this.GetComponent
NetPlayManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Relay.Models;
using Unity.Services.Relay;
using Unity.Services.Lobbies.Models;
using Unity.Services.Lobbies;
using UnityEngine;
using UnityEngine.SocialPlatforms;
public class NetPlayManager : MonoBehaviour
{
public static NetPlayManager Singleton;
[SerializeField] string joinCode;
[Range(10, 99)]
[SerializeField] private int ServerId = 10;
public static bool IsConnecting = false;
public static int rooms = 0;
private float _lastLobbyCheck;
public static List
プレーヤー(が所有する)以外のオブジェクトの移動
サーバーでスポーンさせたオブジェクトにトリガーを設定。
Playerタグのついたオブジェクトが近接侵入すると、跳ねるようにした。

近接侵入すると、Player進行方向へ跳ねるように変更。
他のPlayerが重なったとき、またPlayer内部の他のコライダーと干渉しないようレイヤーを分けた。
TouchReaction.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
public class TouchReaction : Unity.Netcode.NetworkBehaviour
{
static private Vector3 DriveDirection = Vector3.zero;
bool IsContact = false;
float forcePower = 5f;
private void OnTriggerEnter(Collider other)
{
if (IsContact) return;
Debug.Log(this.gameObject.name.ToString());
if (other.gameObject.tag == "Player")
{
if(this.IsOwner)
{
Transform transform = other.GetComponent