Unity3D 利用FSM設計相機跟隨實現
阿新 • • 發佈:2019-01-01
筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。
FSM有限狀態機前面已經跟讀者介紹過,使用Unity3D引擎實現了動作狀態以及技能切換,FSM使用的條件是有限個狀態切換,我們可以將FSM應用到相機中,很多人會問在相機中如何使用FSM,不論那種架構其主要目的是將模組之間的耦合性降低,傳統的寫法就是使用一個相機跟隨類,所有的邏輯一股腦的寫在一個類或者兩個類中,這樣一旦邏輯變動,修改起來非常麻煩,可能修改的就不是一個類兩個類的事情,而如果我們採用FSM設計相機跟隨,這樣就容易多了。
接下來就實現FSM有限狀態機,FSM作為一個通用類需要將其設定成模版的方式,具體程式碼如下所示:
在這個類中有三部分最重要,第一部分是定義了狀態類,它實現了狀態的切換函式,onEnter,onExec,onExit,這個是作為狀態切換使用的。程式碼如下: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; } } }
另一個類的函式是增加狀態函式,這個需要在Start函式中去執行的,函式程式碼如下所示: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; }
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;
}
}
其中指令碼中加粗的函式是有限狀態機執行的具體邏輯。。。。。。。另外其他的變數宣告和函式實現是根據策劃需求新增的,讀者只需要關注加粗的函式實現就可以了。
附圖如下所示: