1. 程式人生 > >Unity3D 利用FSM設計相機跟隨實現

Unity3D 利用FSM設計相機跟隨實現

筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。

FSM有限狀態機前面已經跟讀者介紹過,使用Unity3D引擎實現了動作狀態以及技能切換,FSM使用的條件是有限個狀態切換,我們可以將FSM應用到相機中,很多人會問在相機中如何使用FSM,不論那種架構其主要目的是將模組之間的耦合性降低,傳統的寫法就是使用一個相機跟隨類,所有的邏輯一股腦的寫在一個類或者兩個類中,這樣一旦邏輯變動,修改起來非常麻煩,可能修改的就不是一個類兩個類的事情,而如果我們採用FSM設計相機跟隨,這樣就容易多了。

接下來就實現FSM有限狀態機,FSM作為一個通用類需要將其設定成模版的方式,具體程式碼如下所示:

using System;
using System.Collections.Generic;

namespace Core
{
    public class FSM
    {
        public class Object<T, K>
            where T : Object<T, K>
        {
            public delegate void Function(T self, float time);

            #region Protected members
            protected TimeSource timeSource = null;
            protected Dictionary<K, State<T, K>> states = new Dictionary<K,State<T,K>>();
            protected State<T, K> state = null;
            protected State<T, K> prevState = null;
            #endregion

            #region Ctors
            public Object()
            {
                timeSource = TimeManager.Instance.MasterSource;
            }

            public Object(TimeSource source)
            {
                timeSource = source;
            }
            #endregion

            #region Public properties
            public K PrevState
            {
                get
                {
                    return prevState.key;
                }
            }

            public K State
            {
                get
                {
                    return state.key;
                }

                set
                {
                    prevState = state;

                    if (prevState != null)
                        prevState.onExit(this as T, timeSource.TotalTime);

                    State<T, K> nextState;
                    if (states.TryGetValue(value, out nextState))
                    {
                        state = nextState;
                        state.onEnter(this as T, timeSource.TotalTime);
                    }
                    else
                    {
                        state = null;
                    }
                }
            }

            public TimeSource TimeSource
            {
                get
                {
                    return timeSource;
                }
                set
                {
                    timeSource = value;
                }
            }
            #endregion

            #region Public methods
            public void AddState(K key, Function onEnter, Function onExec, Function onExit)
            {
                State<T, K> newState = new State<T, K>();

                newState.key = key;
                newState.onEnter = onEnter;
                newState.onExec = onExec;
                newState.onExit = onExit;

                states.Add(key, newState);
            }

            public void Update()
            {
                if (null == state) return;

                state.onExec(this as T, timeSource.TotalTime);
            }
            #endregion
        }

        public class State<T, K>
            where T : Object<T, K>
        {
            public K key;
            public Object<T, K>.Function onEnter;
            public Object<T, K>.Function onExec;
            public Object<T, K>.Function onExit;
        }
    }
}
在這個類中有三部分最重要,第一部分是定義了狀態類,它實現了狀態的切換函式,onEnter,onExec,onExit,這個是作為狀態切換使用的。程式碼如下:
        public class State<T, K>
            where T : Object<T, K>
        {
            public K key;
            public Object<T, K>.Function onEnter;
            public Object<T, K>.Function onExec;
            public Object<T, K>.Function onExit;
        }
另一個類的函式是增加狀態函式,這個需要在Start函式中去執行的,函式程式碼如下所示:
            public void AddState(K key, Function onEnter, Function onExec, Function onExit)
            {
                State<T, K> newState = new State<T, K>();

                newState.key = key;
                newState.onEnter = onEnter;
                newState.onExec = onExec;
                newState.onExit = onExit;

                states.Add(key, newState);
            }
最後一個函式就是Update函式,需要每幀去檢測執行狀態,函式如下所示:
            public void Update()
            {
                if (null == state) return;

                state.onExec(this as T, timeSource.TotalTime);
            }
這三個是最重要的,必須要有的,接下來編寫掛接到物件上的類FiniteStateMachine類指令碼,程式碼如下所示:
using System;
using System.Collections.Generic;
using UnityEngine;
using Core;

public class FiniteStateMachine : MonoBehaviour
{
	public enum UpdateFunction
	{
		Update = 0,
		LateUpdate,
		FixedUpdate
	}
	
	#region Public classes
	public class FSMObject : FSM.Object<FSMObject, int>
	{
		public GameObject go;
		
		public FSMObject(GameObject _go)
		{
			go = _go;
		}
	}
	
	[Serializable]
	public class StateType
	{
		public int id;
		public string onEnterMessage;
		public string onExecMessage;
		public string onExitMessage;
		
		public void onEnter(FSMObject fsmObject, float time)
		{
			fsmObject.go.SendMessage(onEnterMessage, time, SendMessageOptions.RequireReceiver);
		}
		
		public void onExec(FSMObject fsmObject, float time)
		{
			fsmObject.go.SendMessage(onExecMessage, time, SendMessageOptions.RequireReceiver);
		}
		
		public void onExit(FSMObject fsmObject, float time)
		{
			fsmObject.go.SendMessage(onExitMessage, time, SendMessageOptions.RequireReceiver);
		}
	}
	#endregion
	
	#region Public members
	public bool manualUpdate = false;
	public UpdateFunction updateFunction = UpdateFunction.Update;
	public StateType[] states;
	public int startState;
	#endregion
	
	#region Protected members
	protected FSMObject fsmObject = null;
	#endregion
	
	#region Public properties
	public int PrevState
	{
		get
		{
			return fsmObject.PrevState;
		}
	}
	
	public int State
	{
		get
		{
			return fsmObject.State;
		}
		set
		{
			fsmObject.State = value;
		}
	}
	
	public TimeSource TimeSource
	{
		get
		{
			return fsmObject.TimeSource;
		}
		set
		{
			fsmObject.TimeSource = value;
		}
	}
	#endregion
	
	#region Public methods
	public void ForceUpdate()
	{
		fsmObject.Update();
	}
	#endregion
	
	#region Unity callbacks
	protected void Start()
	{
		fsmObject = new FSMObject(gameObject);
		foreach (StateType state in states)
			fsmObject.AddState(state.id, state.onEnter, state.onExec, state.onExit);
		fsmObject.State = startState;
	}
	
	void Update()
	{
		//Debug.Log ("update");
		if (manualUpdate)
			return;
		
		if (UpdateFunction.Update == updateFunction)
			fsmObject.Update();
	}
	
	void LateUpdate()
	{
		if (manualUpdate)
			return;
		
		if (UpdateFunction.LateUpdate == updateFunction)
			fsmObject.Update();
	}
	
	void FixedUpdate()
	{
		if (manualUpdate)
			return;
		
		if (UpdateFunction.FixedUpdate == updateFunction)
			fsmObject.Update();
	}
	#endregion
}
該函式需要掛接到物件上,效果如下所示:



以上就是我們所封裝的FSM有限狀態機,接下來在專案中使用我們的FSM,先實現最基本的邏輯類如下所示:

using System;
using System.Collections.Generic;
using UnityEngine;
public class FollowCharacter : MonoBehaviour
{
	public GameObject player;
	public Vector3 sourceOffset = new Vector3(0.0f, 2.5f, -3.4f);
	public Vector3 targetOffset = new Vector3(0.0f, 1.7f, 0.0f);
	
	protected bool firstFrame;
	protected float currHeightSmoothing;
	
	protected float groundHeightTest;
	protected bool slideshowActive = false;
	protected float slideshowEnterTime = 0.0f;
	protected float slideshowExitTime = 0.0f;
	
	protected bool oldCameraActive = true;
	
	protected float oldFov = 70.0f;
	protected Vector3 oldCamSourceOffset = new Vector3(0.0f, 8.5f, -4.5f);
	protected Vector3 oldCamTargetOffset = new Vector3(0.0f, 0.9f, 5.3f);
	
	protected int cameraIndex = 3;
	protected float[] cameraFovs = { 55.0f, 60.0f, 55.0f };
	protected Vector3[] cameraSourceOffsets = {
		new Vector3(0.0f, 5.8f, -3.8f),
		new Vector3(0.0f, 6.04f, -4.0f),
		new Vector3(0.0f, 8.5f, -6.7f)
	};
	protected Vector3[] cameraTargetOffsets = {
		new Vector3(0.0f, 2.2f, 2.5f),
		new Vector3(0.0f, 1.35f, 3.36f),
		new Vector3(0.0f, 1.45f, 5.3f)
	};
	protected Vector3 newCamSourceOffset = new Vector3(0.0f, 6.04f, -4.0f);//Camera 2
	protected Vector3 newCamTargetOffset = new Vector3(0.0f, 1.35f, 3.36f);//Camera 2

	protected Vector3 testNewTurboSourceOffset = new Vector3(0.0f, 5.8f, -4.0f);
	protected Vector3 testNewTurboTargetOffset = new Vector3(0.0f, 2.1f, 2.5f);
	protected Vector3 testNewFinalSourceOffset = new Vector3(-6.5f, 5.0f, -5.5f);
	protected Vector3 testNewFinalTargetOffset = new Vector3(-4.5f, 1.7f, 0.0f);

	#region public Classes
	public class ShakeData
	{
		public float duration;
		public float noise;
		public float smoothTime;
		
		public ShakeData(float _duration, float _noise, float _smoothTime)
		{
			duration = _duration;
			noise = _noise;
			smoothTime = _smoothTime;
		}
	}
	#endregion
	
	public void OnFollowCharaEnter(float time)
	{
		prevPlayerPivot = player.transform.position;
		firstFrame = true;
		currHeightSmoothing = heightSmoothing;
		deadTime = -1.0f;
		actionTaken = false;
	}
	
	public void OnFollowCharaExec(float time)
	{
		if (player == null)
			return;

		float dt = Time.fixedDeltaTime;
		float now = TimeManager.Instance.MasterSource.TotalTime;
		Vector3 playerPivot = player.transform.position;
		playerPivot.x = 0.0f;
		playerPivot.y = 0.0f;
		
		float targetHeight = playerPivot.y;
		
		if (firstFrame)
		{
			lastPivotHeight = targetHeight;
			prevPlayerPivot = playerPivot;
			heightVelocity = 0.0f;
			firstFrame = false;
		}
		else
		{
			float targetSmoothTime = 0.1f;
			
			smoothTime = Mathf.MoveTowards(smoothTime, targetSmoothTime, 2.5f * dt);
			
			lastPivotHeight = Mathf.SmoothDamp(lastPivotHeight, targetHeight, ref heightVelocity, smoothTime, 50.0f, dt);
			prevPlayerPivot = playerPivot;
		}
		Vector3 camPivot = new Vector3(prevPlayerPivot.x * 0.8f, lastPivotHeight, prevPlayerPivot.z);

		lastSourceOffset = this.EaseTo(lastSourceOffset, goalSourceOffset, sourceOffset);
		lastTargetOffset = this.EaseTo(lastTargetOffset, goalTargetOffset, targetOffset);
		
		transform.position = camPivot + lastSourceOffset + offset * 0.1f + noise * noiseStrength; // +noise * noiseStrength + noiseTremor * 0.00069f * kinematics.PlayerRigidbody.velocity.z; //PIETRO
		
		transform.LookAt(camPivot + lastTargetOffset + offset * 0.1f, Vector3.up);
		
		if (!TimeManager.Instance.MasterSource.IsPaused)
		{
			//Camera Shake
			if (shakeCameraActive)
				ShakeCamera(dt);
			
			//tremor (always active
			this.UpdateTremor(dt);
		}
		
		//check if is dead
		if (now - deadTime > 3.6f && !actionTaken && deadTime > 0.0f)
		{
			actionTaken = true;
			//Debug.Log("GO TO REWARD");
			LevelRoot.Instance.BroadcastMessage("GoToOffgame");     //GoToReward");
		}
	}
	
	public void OnFollowCharaExit(float time)
	{
	}
	
	void OnReset()
	{
		//Debug.Log("RESET CAM");
		interpolating = false;
		shakeCameraActive = false;
		sourceOffset = defaultSourceOffset;
		targetOffset = defaultTargetOffset;
	}
	
	void ShakeCamera(float deltaTime)
	{
		if (TimeManager.Instance.MasterSource.TotalTime - shakeStartTime <= currentShakeData.duration)
		{
			if (currentShakeData.smoothTime > 0.0f)
				noiseStrength = Mathf.SmoothDamp(noiseStrength, currentShakeData.noise, ref noiseStrengthVel, currentShakeData.smoothTime, 300.0f, deltaTime);
			else
				noiseStrength = currentShakeData.noise; // go directly
			
			if (noiseStrength > 0.0f)
			{
				Vector3 v = UnityEngine.Random.onUnitSphere;
				noise += (v - noise) * deltaTime * 8.0f;
			}
			else
				noise = SRVector3.zero;
		}
		
		if (TimeManager.Instance.MasterSource.TotalTime - shakeStartTime >= currentShakeData.duration)
			StopShakeCamera();
	}
	
	void StopShakeCamera()
	{
		currentShakeData = new ShakeData(0.0f, 0.0f, 0.0f);
		noiseStrength = 0.0f;
		noise = SRVector3.zero;
		shakeCameraActive = false;
	}
	
	public string ChangeCamera()
	{
		string buttonText = "";
		buttonText = cameraIndex == 0 ? "Old camera on" : "camera " + cameraIndex + " on";
		if (cameraIndex == 0)
		{
			gameObject.GetComponent<Camera>().fieldOfView = oldFov;
			lastSourceOffset = defaultSourceOffset = sourceOffset = oldCamSourceOffset;
			lastTargetOffset = defaultTargetOffset = targetOffset = oldCamTargetOffset;
			goalSourceOffset = oldCamSourceOffset;
			goalTargetOffset = oldCamTargetOffset;
		}
		else
		{
			gameObject.GetComponent<Camera>().fieldOfView = cameraFovs[cameraIndex - 1];
			lastSourceOffset = defaultSourceOffset = sourceOffset = cameraSourceOffsets[cameraIndex - 1];
			lastTargetOffset = defaultTargetOffset = targetOffset = cameraTargetOffsets[cameraIndex - 1];
			goalSourceOffset = cameraSourceOffsets[cameraIndex - 1];
			goalTargetOffset = cameraTargetOffsets[cameraIndex - 1];
		}
		
		return buttonText;
	}
}

其中指令碼中加粗的函式是有限狀態機執行的具體邏輯。。。。。。。另外其他的變數宣告和函式實現是根據策劃需求新增的,讀者只需要關注加粗的函式實現就可以了。
附圖如下所示: