アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

【Unity】AI Behavior用マニュアル【PhotonPUN2でオンラインゲーム化】

2021年4月15日

こんにちは!ジェイです。以前の記事でAI Behaviorの使い方を解説しましたが、今回はPhotonCloudPUN2でオンラインゲームでAIの使い方を解説します。

記事内広告

Photonを使える準備をする

まだPhotonPUN2の導入が済んでない人はこちらを参考にしてください。

最初にUsingディレクティブを追加しなければならないのですが、AI Behaviorにはコンパイル時間を削減するために、asmdefファイルが定義されています。しかし、これがあるとPhotonで必要なUsingディレクティブが使えないので、削除しましょう。

具体的には、Asset/AIBehavior/Scripts/AIBehavior.asmdefとAsset/AIBehavior/Scripts/Navigation/RunTimeNavMesh/Editor/AIBehaviorNav_Editor.asmdefの2つのファイルを削除してください。そしたら、Photonのusingディレクティブが使用可能になります。

AIにPhotonViewを追加する

AIにPhotonViewとPhotonTransformViewClassicとPhotonAnimatorViewを追加して以下のように設定します。詳しくは以下を参考にしてください。

スクリプトを書き換える

次にAIのスクリプトを書き換えます。まずはAIComponent.csです。変更点は色を変えてます。

using UnityEngine;
using AIBehavior;
using Photon.Pun;
using Photon.Realtime;

namespace AIBehavior
{
    public class AIComponent : MonoBehaviourPun
	{
		protected string displayName = "";

		public AIComponent()
		{
			displayName = DefaultDisplayName ();
		}


		public virtual string DefaultDisplayName()
		{
			return GetType().ToString();
		}


		public virtual string GetDisplayName()
		{
			return displayName;
		}


		public virtual void SetDisplayName(string newDisplayName)
		{
			displayName = newDisplayName;
		}


		// === Save Variables === //
		
		public int saveId = -1;


		// === Save Methods === //
		
		public int GetSaveID()
		{
			saveId = SaveIdDistributor.GetId(saveId);
			return saveId;
		}
	
	
		public void SetSaveID(int id)
		{
			saveId = id;
			SaveIdDistributor.SetId(id);
		}
	}
}

次にAIBehaviors.csです。こちらも変更点は色を変えてます。

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

#if UNITY_EDITOR
using System.Reflection;
#endif

using Random = UnityEngine.Random;


namespace AIBehavior
{
	[RequireComponent(typeof(AIAnimationStates))]
	public class AIBehaviors : AIComponent
	{
		PhotonTransformViewClassic _PhotonTransformViewClassic;
		public bool isActive { get; private set; }

		public bool isDefending = false;

		[SerializeField] protected float damageMultiplier = 1.0f;

		public int stateCount { get { return states.Length; } }

		public Transform aiTransform;
		Vector3 thisPos;
		NavMeshAgent navMeshAgent;

		public bool useSightTransform = false;

		public Transform sightTransform = null;

		public BaseState initialState;

		public BaseState currentState { get; private set; }

		public BaseState previousState { get; private set; }

		public BaseState[] states = new BaseState[0];

		public BaseTrigger[] triggers = new BaseTrigger[0];

		// === General Properties === //

		public TaggedObjectFinder objectFinder;

		/// IE. If this AI is an enemy looking for a player, you should EXCLUDE the player layer from these layers
		public LayerMask raycastLayers = -1;

		public bool useSightFalloff = true;

		public float sightFOV = 180.0f;

		public float sightDistance = 50.0f;

		public Vector3 eyePosition = new Vector3(0.0f, 1.5f, 0.0f);

		public bool keepAgentUpright = true;

		public float health = 100.0f;

		public float maxHealth = 100.0f;

		public GameObject statesGameObject = null;

		// === Animation Callback Info === //

		public Component animationCallbackComponent = null;

		public string animationCallbackMethodName = "";

		public AIAnimationStates animationStates;

		// === Callbacks === //

		public Action<BaseState, BaseState> onStateChanged = null;

		public Action<AIAnimationState> onPlayAnimation = null;

		public Action<Vector3, float, float> externalMove = null;

		// === Targetting and Rotation === //

		public Vector3 currentDestination { get; private set; }
		protected Transform lastKnownRotationTarget;
		protected Vector3 targetRotationPoint;
		protected float rotationSpeed;

		public bool showDebugMessages = true;

		// AI Behaviors custom variables
		public AiBehaviorVariable[] userVariables = new AiBehaviorVariable[0];

		// === Methods === //

		public AIBehaviors()
		{
			objectFinder = CreateObjectFinder();
		}


		protected virtual TaggedObjectFinder CreateObjectFinder()
		{
			return new TaggedObjectFinder();
		}

		void Awake()
		{
#if USE_ASTAR
			Debug.Log("Use Astar!");
#endif
			_PhotonTransformViewClassic = GetComponent<PhotonTransformViewClassic>();
			aiTransform = GetTransform();
			animationStates = aiTransform.GetComponent<AIAnimationStates>();
			navMeshAgent = aiTransform.GetComponent<NavMeshAgent>();

			objectFinder.CacheTransforms(CachePoint.Awake);
			InitGlobalTriggers();

			if ( !useSightTransform || sightTransform == null )
			{
				sightTransform = new GameObject("Sight Transform").transform;
				sightTransform.parent = aiTransform;
				sightTransform.localRotation = Quaternion.identity;
				sightTransform.localPosition = eyePosition;
				useSightTransform = false;
			}

			currentDestination = aiTransform.position;

			SetActive(true);
		}

		void Start()
		{
			ChangeActiveState(initialState);
		}


		public void Update()
		{
			if (PhotonNetwork.InRoom && photonView != null && !photonView.IsMine)
			{
				return;
			}
			if ( isActive && Time.timeScale > 0 )
			{
				objectFinder.CacheTransforms(CachePoint.EveryFrame);

				if ( currentState.RotatesTowardTarget() )
				{
					RotateAgent();
				}

				thisPos = aiTransform.position;

				for ( int i = 0; i < triggers.Length; i++ )
				{
					if ( triggers[i].HandleEvaluate(this) && triggers[i].transitionState != null )
					{
						currentState = triggers[i].transitionState;
					}
				}

				// If the state remained the same, do the action
				if ( currentState.HandleReason(this) )
				{
					currentState.HandleAction(this);
				}
			}
		}

		Transform GetTransform()
		{
			if ( aiTransform == null )
			{
				aiTransform = transform;
			}

			return aiTransform;
		}

		void InitGlobalTriggers ()
		{
			for ( int i = 0; i < triggers.Length; i++ )
			{
				triggers[i].HandleInit(this, objectFinder);
			}
		}

		public void SetActive(bool isActive)
		{
			this.isActive = isActive;
		}


		[System.Obsolete("Use Damage instead.")]
		public void GotHit(float damage)
		{
			Damage(damage);
		}

		public void Damage(float damage)
		{
			// We don't want to enable this state if the FSM is dead
			GotHitState gotHitState = GetState<GotHitState>();

			if ( gotHitState != null && gotHitState.CanGetHit(this) )
			{
				float totalDamage = damage * GetDamageMultiplier ();
				SubtractHealthValue(totalDamage);
				Debug.Log ("Got " + totalDamage + " damage");

				if ( gotHitState.CoolDownFinished() )
				{
					ChangeActiveState(gotHitState);
				}
			}
		}

		public virtual void SetDamageMultiplier (float newDamageMultiplier)
		{
			damageMultiplier = newDamageMultiplier;
		}

		public virtual float GetDamageMultiplier ()
		{
			return damageMultiplier;
		}


		// === GetStates === //

		public BaseState[] GetAllStates()
		{
			return states;
		}

		public BaseState GetStateByIndex(int index)
		{
			if ( index < states.Length )
			{
				return states[index];
			}

			return null;
		}

		public BaseState GetStateByName(string stateName)
		{
			foreach ( BaseState state in states )
			{
				if ( state.name.Equals(stateName) )
				{
					return state;
				}
			}

			return null;
		}


		public T GetState<T> () where T : BaseState
		{
			foreach ( BaseState state in states )
			{
				if ( state is T )
				{
					return state as T;
				}
			}

			return null;
		}

		public BaseState GetState(System.Type type)
		{
			foreach ( BaseState state in states )
			{
				if ( state.GetType() == type )
				{
					return state;
				}
			}

			return null;
		}


		// === GetStates === //

		public T[] GetStates<T> () where T : BaseState
		{
			List<T> stateList = new List<T>();

			foreach ( BaseState state in states )
			{
				if ( state is T )
				{
					stateList.Add(state as T);
				}
			}

			return stateList.ToArray();
		}

		public BaseState[] GetStates(System.Type type)
		{
			List<BaseState> stateList = new List<BaseState>();

			foreach ( BaseState state in states )
			{
				if ( state.GetType() == type )
				{
					stateList.Add(state);
				}
			}

			return stateList.ToArray();
		}


		// === Replace States === //

		public void ReplaceAllStates(BaseState[] newStates)
		{
			states = newStates;
		}

		public void ReplaceStateAtIndex(BaseState newState, int index)
		{
			states[index] = newState;
		}


		// === Change Current Active State === //

		public void ChangeActiveState(BaseState newState)
		{
			if ( newState == null || !newState.CanSwitchToState())
			{
				return;
			}

			objectFinder.CacheTransforms(CachePoint.StateChanged);
			InitGlobalTriggers();

			previousState = currentState;
			previousState?.EndState(this);

			currentState = newState;
			newState.InitState(this);

			onStateChanged?.Invoke(newState, previousState);
		}

		public void ChangeActiveStateByName(string stateName)
		{
			foreach ( BaseState state in states )
			{
				if ( state.name.Equals(stateName) )
				{
					ChangeActiveState(state);
					return;
				}
			}
		}

		public void ChangeActiveStateByIndex(int index)
		{
			if ( index < states.Length )
			{
				ChangeActiveState(states[index]);
			}
		}

		public void MoveAgent(Transform target, float targetSpeed, float rotationSpeed)
		{
			if ( target != null )
			{
				RotateAgent(target, rotationSpeed);
			}

			MoveAgent(target.position, targetSpeed, rotationSpeed);
		}

		public void MoveAgent(Vector3 targetPoint, float targetSpeed, float rotationSpeed, Transform rotationTargetOverride = null)
		{
			bool isNavAgent = navMeshAgent != null;

			currentDestination = targetPoint;
			targetRotationPoint = targetPoint;
			this.rotationSpeed = rotationSpeed;

			if ( isNavAgent && navMeshAgent.enabled )
			{
				Vector3 velocity = navMeshAgent.velocity;
				// 同期するためにベクトルをPhotonTransformViewClassicに渡してやる
				_PhotonTransformViewClassic.SetSynchronizedValues(velocity, 0);

				float velocityMagnitude = velocity.magnitude;
				float rotationMultiplier = Mathf.InverseLerp(0, targetSpeed, velocityMagnitude);

				navMeshAgent.speed = targetSpeed;
				navMeshAgent.angularSpeed = 0;
				navMeshAgent.destination = targetPoint;

				if ( rotationTargetOverride != null )
				{
					velocity = (rotationTargetOverride.position - aiTransform.position).normalized;
				}

				if ( keepAgentUpright )
				{
					velocity.y = 0.0f;
					velocity = velocity.normalized * velocityMagnitude;
				}

				aiTransform.Rotate(Vector3.Cross(aiTransform.forward, velocity) * Time.deltaTime * rotationSpeed * rotationMultiplier);
			}

			externalMove?.Invoke(targetPoint, targetSpeed, rotationSpeed);
		}

		public void RotateAgent(Transform target, float rotationSpeed)
		{
			targetRotationPoint = target.position;
			lastKnownRotationTarget = target;
			this.rotationSpeed = rotationSpeed;
		}


		protected internal virtual void RotateAgent()
		{
			bool isNavAgent = navMeshAgent != null;
			bool updateRot = false;

			Vector3 pos = aiTransform.position;
			Quaternion curRotation = aiTransform.rotation;
			Vector3 sightPosition = GetSightPosition(sightTransform);
			Vector3 targetPos;

			if ( lastKnownRotationTarget != null )
			{
				RaycastHit hit;

				targetPos = lastKnownRotationTarget.position;

				if ( Physics.Linecast(sightPosition, targetPos, out hit, raycastLayers) && hit.transform != lastKnownRotationTarget )
				{
					if ( showDebugMessages )
					{
						Debug.LogWarning("Can't rotate toward target. The object '" + hit.transform.name + "' is in the way of '" + lastKnownRotationTarget.name + "'.");
					}

					lastKnownRotationTarget = null;
				}
				else
				{
					targetRotationPoint = targetPos;
				}
			}

			targetRotationPoint.y = pos.y;
			aiTransform.LookAt(targetRotationPoint);

			if ( isNavAgent )
			{
				updateRot = navMeshAgent.updateRotation;
				navMeshAgent.updateRotation = false;
			}

			if ( rotationSpeed > 0.0f && Time.deltaTime > 0.0f )
			{
				//thisTFM.rotation = Quaternion.RotateTowards(curRotation, thisTFM.rotation, rotationSpeed * Time.deltaTime);
			}

			if ( isNavAgent )
			{
				navMeshAgent.updateRotation = updateRot;
			}
		}

		public void PlayAnimation(AIAnimationState animState)
		{
			if ( onPlayAnimation != null )
			{
				onPlayAnimation?.Invoke(animState);
			}
			else if ( animationCallbackComponent != null && animState != null )
			{
				if ( !string.IsNullOrEmpty(animationCallbackMethodName) )
				{
					animationCallbackComponent.SendMessage(animationCallbackMethodName, animState);
				}
			}
		}

		public float GetHealthValue()
		{
			return health;
		}

		public void SetHealthValue(float healthAmount)
		{
			health = healthAmount;
		}

		public void AddHealthValue(float healthAmount)
		{
			health += healthAmount;
		}

		public void SubtractHealthValue(float healthAmount)
		{
			health -= healthAmount;
		}

		public Transform GetClosestPlayer(Transform[] playerTransforms)
		{
			float sqrDist;
			return GetClosestPlayer(playerTransforms, out sqrDist);
		}

		public Transform GetClosestPlayer(Transform[] playerTransforms, out float squareDistance)
		{
			int closestPlayerIndex = -1;

			squareDistance = Mathf.Infinity;

			for ( int i = 0; i < playerTransforms.Length; i++ )
			{
				Vector3 playerPosition = playerTransforms[i].position;
				Vector3 targetDifference = playerPosition - thisPos;

				// is the target within distance?
				if ( targetDifference.sqrMagnitude < squareDistance )
				{
					squareDistance = targetDifference.sqrMagnitude;
					closestPlayerIndex = i;
				}
			}

			if ( closestPlayerIndex == -1 )
				return null;
			else
				return playerTransforms[closestPlayerIndex];
		}

		public Transform GetClosestPlayerWithinSight(Transform[] playerTransforms)
		{
			return GetClosestPlayerWithinSight(playerTransforms, false);
		}

		public Transform GetClosestPlayerWithinSight(Transform[] playerTransforms, bool includeSightFalloff, Transform sightTransformOverride = null)
		{
			float dist = 0.0f;
			return GetClosestPlayerWithinSight(playerTransforms, out dist, includeSightFalloff, sightTransformOverride);
		}

		public Transform GetClosestPlayerWithinSight(Transform[] playerTransforms, out float squareDistance, bool includeSightFalloff, Transform sightTransformOverride = null)
		{
			float sqrSightDistance = sightDistance * sightDistance;
			int closestPlayerIndex = -1;

			squareDistance = Mathf.Infinity;

			if ( sightTransformOverride == null )
			{
				sightTransformOverride = sightTransform;
			}

			for ( int i = 0; i < playerTransforms.Length; i++ )
			{
				Vector3 playerPosition = playerTransforms[i].position;
				Vector3 targetDifference = playerPosition - thisPos;
				float targetSqrMagnitude = targetDifference.sqrMagnitude;
				float angle = Vector3.Angle(targetDifference, sightTransformOverride.forward);
				Vector3 sightPosition = GetSightPosition(sightTransformOverride);

				// If we already have a player in sight
				if ( closestPlayerIndex != -1 )
				{
					// Is the new one closer than the previous closest one?
					if ( targetSqrMagnitude > squareDistance )
					{
						continue;
					}
				}

				// is the target within distance?
				if ( targetSqrMagnitude < sqrSightDistance )
				{
					float halfFOV = sightFOV / 2.0f;

					if ( angle < halfFOV )
					{
						if ( useSightFalloff && includeSightFalloff )
						{
							float anglePercentage = Mathf.InverseLerp(0.0f, halfFOV, angle);

							if ( Random.value < anglePercentage )
							{
								continue;
							}
						}

						RaycastHit hit;

						// Make sure there isn't anything between the player
						if ( !Physics.Linecast(sightPosition, playerPosition, out hit, raycastLayers) || hit.transform == playerTransforms[i] || hit.transform == aiTransform )
						{
							squareDistance = targetSqrMagnitude;
							closestPlayerIndex = i;
						}
					}
				}
			}

			if ( closestPlayerIndex == -1 )
				return null;
			else
				return playerTransforms[closestPlayerIndex];
		}


		public Vector3 GetSightPosition (Transform sightTransformOverride = null)
		{
			if (sightTransformOverride == null)
			{
				if (sightTransform == null)
				{
					return GetTransform().TransformPoint(eyePosition);
				}
				else
				{
					sightTransformOverride = sightTransform;
				}
			}

			return sightTransformOverride.position;
		}


		public Vector3 GetSightDirection (Transform sightTransformOverride = null)
		{
			if (sightTransformOverride == null)
			{
				if (sightTransform == null)
				{
					return GetTransform().forward;
				}
				else
				{
					sightTransformOverride = sightTransform;
				}
			}

			return sightTransformOverride.forward;
		}

		public void PlayAudio()
		{
			AudioSource audioSource;

			if ( currentState.sounds.Length == 0 )
			{
				return;
			}

			audioSource = GetComponent<AudioSource>();

			if ( audioSource == null )
			{
				audioSource = gameObject.AddComponent<AudioSource>();
			}

			int index = Random.Range (0, currentState.sounds.Length);
			audioSource.loop = currentState.sounds[index].loopAudio;
			audioSource.volume = currentState.sounds[index].audioVolume;
			audioSource.pitch = currentState.sounds[index].audioPitch + ((Random.value * currentState.sounds[index].audioPitchRandomness) - (currentState.sounds[index].audioPitchRandomness / 2.0f)) * 2.0f;
			audioSource.clip = currentState.sounds[index].audioClip;
			audioSource.Play();
		}

		public string[] GetVariableNames()
		{
			List<string> varNames = new List<string>();
			for(int i = 0; i < userVariables.Length; i++)
			{
				varNames.Add(userVariables[i].name);
			}
			return varNames.ToArray();
		}
	}
}

以上の変更で、PhotonPUN2で精度の高い同期のAIを実装することができます。まだ攻撃に関しては、同期してないので、次回に攻撃の同期をRPCで行う解説をします。

アイコンは、ぱすてるどーるメーカーを使わせていただきました。→こちら

+1