Unity3D 利用FSM设计相机跟随实现

Posted 海洋_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D 利用FSM设计相机跟随实现相关的知识,希望对你有一定的参考价值。

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

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;
	}
}

其中脚本中加粗的函数是有限状态机执行的具体逻辑。。。。。。。另外其他的变量声明和函数实现是根据策划需求添加的,读者只需要关注加粗的函数实现就可以了。
附图如下所示:



以上是关于Unity3D 利用FSM设计相机跟随实现的主要内容,如果未能解决你的问题,请参考以下文章

unity3d相机怎么跟随鼠标移动

[Unity3D/2D]实现相机对人物角色的跟随效果/相机在一定范围内移动/内置插件实现

Unity3d学习 相机的跟随

Unity3d学习 相机的跟随

unity3d里照相机跟随鼠标左右移动的代码怎么写

unity3D:游戏分解之角色移动和相机跟随