權重較高網(wǎng)站深圳百度推廣聯(lián)系方式
Root motion動畫可以將角色的根節(jié)點(通常是角色的骨盆或腳部)的運動直接應用到游戲對象上,從而實現(xiàn)角色的自然移動和旋轉,避免出現(xiàn)腳底打滑的現(xiàn)象。采用Root motion動畫的游戲對象,通常是重載了onAnimatorMove函數(shù),在腳本中來設置動畫的速度,從而實現(xiàn)角色的移動。Unity的Navigation系統(tǒng)是一個用于實現(xiàn)游戲世界中的尋路和導航功能的組件。它允許游戲角色在復雜的游戲環(huán)境中自動找到從一點到另一點的最短路徑。如果我們對采用Root motion動畫的游戲對象應用Navigation,就會產(chǎn)生沖突,因為這兩個組件都會嘗試控制游戲對象的移動。有兩個解決方式:
一是讓動畫跟隨Navigation agent,通過獲取agent.velocity來設置root motion的速度,從而大致匹配Agent的移動到動畫的移動。這個方式最簡單,但是可能會出現(xiàn)腳底打滑的現(xiàn)象。
二是讓Agent跟隨動畫,關閉agent的updatePosition和updateRotation,通過計算agent的nextPosition和動畫根節(jié)點的rootPosition的插值來進行控制。這種方式比方式一要復雜,但是效果更好。以下將以一個游戲場景為例子,詳細介紹一下如何實現(xiàn)方式二。
游戲場景
在游戲中,對于NPC角色,當前設置了幾個狀態(tài),分別是漫游Wander,瞄準Aim以及追蹤Chase。NPC剛開始是漫游狀態(tài),在場景中自由地進行移動,這時是通過Root motion來驅動的。當NPC檢測到玩家時,會進入瞄準狀態(tài)。如果玩家進行躲避NPC,則NPC會進入追蹤狀態(tài),自動跑到上一次發(fā)現(xiàn)玩家的位置,這時NPC是由Navigation來驅動,實現(xiàn)自動尋路??梢妼τ贜PC是需要按照不同的場景來用Root motion或Navigation來驅動的。
Animator設置
建立一個名為Enemy的Animator,包含了兩個狀態(tài),分別是Aim和Move,設置如下:
添加兩個Trigger,分別為Aim和Walk,用于切換狀態(tài)。定義一個名為Speed的Float變量,用于控制Root motion的移動速度。
Move狀態(tài)是一個BlendTree,通過Speed來進行Idle,Walk,Run這三種動作的混合,改變Speed的值,可以看到人物動作的改變。
Unity Blendtree動畫
改變Speed的值,可以看到人物的動作的改變。
實現(xiàn)漫游狀態(tài)
現(xiàn)在給游戲對象增加一個名為EnemyAI的腳本文件,實現(xiàn)游戲對象在場景中漫游。代碼如下:
public class EnemyAI : MonoBehaviour
{[Header("Enemy eyeview")]public float eyeviewDistance = 500.0f;public float viewAngle = 120f;public float obstacleRange = 3.0f;[Header("Enemy Property")]public float enemyHeight = 1.8f;public float enemyWidth = 1.2f;public float rotateSpeed = 2.0f;public float maxDetectDistance = 10f;private float _walkSpeed = 1.5f;private float _runSpeed = 3.5f;private Animator _animator;private Transform _transform;private float _currentSpeed;private float _targetSpeed;private float _statusDuration = 1.0f;private bool _isStatusTimerEnds = true;private bool _isDetectTimerEnds = true;[Flags]private enum EnemyStatus {Aim,Shoot,Wander,Chase}private EnemyStatus _enemyStatus;void Start(){_animator = GetComponent<Animator>();_rb = GetComponent<Rigidbody>();_transform = transform;_enemyStatus = EnemyStatus.Wander;rayCastOffset = new Vector3(0f, enemyHeight - 0.6f, 0f);}void Update(){if (_enemyStatus == EnemyStatus.Wander) {Wander();} Detect();}private void OnAnimatorMove() {if (_currentSpeed != _targetSpeed) {if (Mathf.Abs(_currentSpeed - _targetSpeed) > 0.1) {_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);} else {_currentSpeed = _targetSpeed;} }_animator.SetFloat("Speed", _currentSpeed); Vector3 speed = new Vector3(_animator.velocity.x, _rb.velocity.y, _animator.velocity.z);_rb.velocity = speed;}void Wander() {if (_isStatusTimerEnds) {_targetSpeed = UnityEngine.Random.Range(0, 2) == 0 ? 0f : _walkSpeed;_statusDuration = UnityEngine.Random.Range(5f, 10f);_isStatusTimerEnds = false;StartCoroutine(StatusTimer());}}IEnumerator StatusTimer() {float timer = 0;while (timer < _statusDuration) {timer += Time.deltaTime;yield return null; }_isStatusTimerEnds = true;}IEnumerator DetectTimer() {float timer = 0;while (timer < _detectDuration) {timer += Time.deltaTime;yield return null; }_isDetectTimerEnds = true;}float DetectObstacle(float angle) {RaycastHit hit;int layerMask = ~(1 << 8);Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.up);bool hitDetect = Physics.BoxCast(_transform.position + rayCastOffset, new Vector3(enemyWidth/2, rayCastOffset.y/2, 0.2f), rotation * _transform.forward, out hit,transform.rotation * rotation,maxDetectDistance,layerMask);if (hitDetect) {return hit.distance;} else {return 9999.0f;}}void Detect() {if (_isDetectTimerEnds) {_isDetectTimerEnds = false;StartCoroutine(DetectTimer());if (_currentSpeed > 0.2) {float distance = DetectObstacle(0f);if (distance < obstacleRange) {float leftDistance = DetectObstacle(-90f);float rightDistance = DetectObstacle(90f);float startAngle = -45f;float endAngle = -110f;if (rightDistance < obstacleRange && leftDistance < obstacleRange) {startAngle = 180f;endAngle = 180.01f;} else {if (leftDistance < rightDistance) {startAngle *= -1f;endAngle *= -1f;} }_targetAngle = UnityEngine.Random.Range(startAngle, endAngle);_currentAngle = 0f;}}} else {if (Mathf.Abs(_currentAngle - _targetAngle) > 0.1) {_prevAngle = _currentAngle;_currentAngle = Mathf.Lerp(_currentAngle, _targetAngle, rotateSpeed * Time.deltaTime);_transform.Rotate(0, _currentAngle - _prevAngle, 0);} else {_currentAngle = _targetAngle;}}}
}
以上代碼大致邏輯是一開始設置狀態(tài)為漫游狀態(tài),然后通過一個StatusTimer來計時,每次計時器到時就隨機設置一個速度值。在onAnimatorMove函數(shù)中通過插值的方法來平滑改變速度值,并設置Animator的speed值,實現(xiàn)通過root motion動畫來驅動游戲對象。另外還設置一個DetectTimer來計時,定期調(diào)用DetectObstacle函數(shù)來檢測游戲對象行進方向上是否有障礙物,如有則進行隨機轉向。運行場景,可以看到游戲對象在場景中可以自由地進行漫步。
實現(xiàn)瞄準狀態(tài)
現(xiàn)在我們要增加一個檢測玩家的功能,讓游戲對象在漫步過程中能發(fā)現(xiàn)玩家,并且進入瞄準狀態(tài)。對以上代碼做改動
public class EnemyAI : MonoBehaviour
{...void Update(){if (_enemyStatus == EnemyStatus.Aim) {_prevAngle = _currentAngle;_currentAngle = Mathf.Lerp(_currentAngle, _targetAngle, rotateSpeed * Time.deltaTime);_transform.Rotate(0, _currentAngle - _prevAngle, 0);if (Mathf.Abs(_currentAngle - _targetAngle) < 0.5) {_currentAngle = _targetAngle;}} ...}bool DetectPlayer() {bool findPlayer = false;Vector3 position = _transform.position + new Vector3(0f, enemyHeight-0.2f, 0f);_spottedPlayers = Physics.OverlapSphere(position, eyeviewDistance, LayerMask.GetMask("Character"));for (int i=0;i<_spottedPlayers.Length;i++) {Vector3 playerPosition = _spottedPlayers[i].transform.position;float angle = Vector3.SignedAngle(transform.forward, playerPosition - position, Vector3.up);if (angle <= viewAngle/2 && angle >= -viewAngle/2) {RaycastHit info;int layermask = LayerMask.GetMask("Character", "Default");Physics.Raycast(position, playerPosition - position, out info, eyeviewDistance, layermask);if (info.collider == _spottedPlayers[i]) {if (_currentSpeed >= 0.1) {_targetSpeed = 0f;_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.75f);_animator.SetFloat("Speed", _currentSpeed);} else {_prevPlayerPosition = playerPosition;_foundPlayer = true;_enemyStatus = EnemyStatus.Aim;_animator.SetTrigger("Aim");_currentAngle = 0;_targetAngle = angle;_currentSpeed = 0f;findPlayer = true;}}}}return findPlayer;} void Detect() {if (_isDetectTimerEnds) {...DetectPlayer()}...}
}
在原來的Detect代碼中增加一個對檢測玩家的DetectPlayer的調(diào)用,當檢測到玩家時,設置狀態(tài)為Aim,并且設置Animator的Aim觸發(fā)器,播放瞄準動作。
實現(xiàn)追蹤狀態(tài)
當游戲對象檢測到玩家之后,玩家可以躲避游戲對象的瞄準,例如跑到一旁的障礙物隱藏。游戲對象找不到玩家,這時應該跑去之前發(fā)現(xiàn)玩家的地方,進行搜索。要實現(xiàn)這個功能,簡單的一個想法是通過Unity的Navigation自動尋路功能來實現(xiàn),讓游戲對象自行尋路,而不是通過代碼來控制。但是如前面提到的,Navigation和Root motion同時驅動游戲對象就會產(chǎn)生沖突,因此我們可以采取方式二來解決,即讓Navigation agent跟隨動畫來移動。
給游戲對象增加一個Navmesh agent組件,然后對代碼進行如下改動:
public class EnemyAI : MonoBehaviour
{...private NavMeshAgent _agent;Vector2 smoothDeltaPosition = Vector2.zero;Vector2 velocity = Vector2.zero;void Start(){..._agent = GetComponent<NavMeshAgent>();_agent.updatePosition = false;_agent.speed = _runSpeed;}void Update(){...if (_enemyStatus == EnemyStatus.Chase) {Vector3 worldDeltaPosition = _agent.nextPosition - _transform.position;// Map 'worldDeltaPosition' to local spacefloat dx = Vector3.Dot(_transform.right, worldDeltaPosition);float dy = Vector3.Dot(_transform.forward, worldDeltaPosition);Vector2 deltaPosition = new Vector2(dx, dy);// Low-pass filter the deltaMovefloat smooth = Mathf.Min(1.0f, Time.deltaTime/0.15f);smoothDeltaPosition = Vector2.Lerp (smoothDeltaPosition, deltaPosition, smooth);// Update velocity if time advancesif (Time.deltaTime > 1e-5f)velocity = smoothDeltaPosition / Time.deltaTime;//Debug.LogFormat("Chase, speed:{0}", velocity.magnitude);_animator.SetFloat("Speed", velocity.magnitude); _transform.LookAt(_agent.steeringTarget + transform.forward);if (_agent.remainingDistance < _agent.radius) {_enemyStatus = EnemyStatus.Wander;}}Detect()}private void OnAnimatorMove() {if (_enemyStatus == EnemyStatus.Chase) {_transform.position = _agent.nextPosition;}else {if (_currentSpeed != _targetSpeed) {if (Mathf.Abs(_currentSpeed - _targetSpeed) > 0.1) {_currentSpeed = Mathf.Lerp(_currentSpeed, _targetSpeed, 0.5f);} else {_currentSpeed = _targetSpeed;} }_animator.SetFloat("Speed", _currentSpeed); Vector3 speed = new Vector3(_animator.velocity.x, _rb.velocity.y, _animator.velocity.z);_rb.velocity = speed;}}void Detect() {if (_isDetectTimerEnds) {...//DetectPlayer();if (!DetectPlayer()) {if (_foundPlayer) {_foundPlayer = false;_agent.nextPosition = _transform.position;_agent.destination = _prevPlayerPosition;_enemyStatus = EnemyStatus.Chase;_animator.SetTrigger("Walk");_targetSpeed = _runSpeed;}} ...}
}
以上的代碼值得詳細講解一下,在Start函數(shù)中,設置了agent的updatePosition為false,即不讓agent來移動游戲對象,同時設置agent的最大速度不要超過runspeed。在Update函數(shù)中,判斷如果當前是Chase狀態(tài),那么計算agent的nextPosition與當前位置的差值,然后計算在deltaTime時間間隔中,需要以什么速度來移動,并設置animator的speed,使得游戲對象的動作與移動速度保持同步,不會出現(xiàn)腳底打滑的現(xiàn)象。在onAnimatorMove函數(shù)中,通過設置transform的位置為agent的nextPosition來實現(xiàn)移動。在Detect函數(shù)中進行修改,如果之前發(fā)現(xiàn)玩家,但現(xiàn)在沒有發(fā)現(xiàn),則進入Chase狀態(tài), 把之前發(fā)現(xiàn)玩家的位置設置為agent的目的地,讓agent來進行自動尋路。注意在進入Chase狀態(tài)時需要更新一下agent的nextPosition為當前游戲對象的位置,因為我們之前設置了updatePostion為false,所以agent的當前位置并不同步。
實現(xiàn)效果
Root Motion動畫與Navigation結合
FPS教程
另外我之前也寫了一系列文章介紹如何實現(xiàn)FPS游戲,有興趣的可以了解一下
Unity開發(fā)一個FPS游戲_unity 模仿開發(fā)fps 游戲-CSDN博客
Unity開發(fā)一個FPS游戲之二_unity 模仿開發(fā)fps 游戲-CSDN博客
Unity開發(fā)一個FPS游戲之三-CSDN博客
Unity開發(fā)一個FPS游戲之四_unity fps-CSDN博客
Unity開發(fā)一個FPS游戲之五-CSDN博客