# Unity_Learn_3D_Kit
## Last Week
## 鏡頭

### 旋轉


[Cinemachine Free Look Camera 官方文檔](https://docs.unity3d.com/Packages/com.unity.cinemachine@2.3/manual/CinemachineFreeLook.html)
## 玩家
### 移動

### 跳躍

### 攻擊

## 取得鍵盤輸入
```csharp=
void Update()
{
m_Movement.Set(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
m_Camera.Set(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
m_Jump = Input.GetButton("Jump");
if (Input.GetButtonDown("Fire1"))
{
if (m_AttackWaitCoroutine != null)
StopCoroutine(m_AttackWaitCoroutine);
m_AttackWaitCoroutine = StartCoroutine(AttackWait());
}
m_Pause = Input.GetButtonDown ("Pause");
}
```
### 為什麼Input.GetAxis("Horizontal")就抓得到鍵盤?

[Input Manager官方文檔](https://docs.unity3d.com/Manual/class-InputManager.html)
### 怎麼看程式碼?
- 查看定義:按F12 / 右鍵Go to definition,來查看變數、class在哪裡宣告的
- 查看引用:按Shft+F12 / 右鍵Find Usage,來看哪些地方有用過這個變數

▲ 對 m_Movement 按F12找到他是Vector2

▲ 對 m_Movement 按Shft+F12找到哪幾行有用到這個變數

▲ 找到 MoveInput 會返回 m_Movement

▲ 對 MoveInput 按Shft+F12找到哪幾行有用到這個變數
→ PlayerController使用了MoveInput來計算移動方向
---
## 操作人物
找到從PlayerInput取得了鍵盤輸入,PlayerController再拿來計算方向。
先全部摺疊起來,從標題看看大致有哪些功能
```csharp=
public class PlayerController : MonoBehaviour, IMessageReceiver
{
......
public void SetCanAttack(bool canAttack)
// 醒來
void Awake()
// 啟用
void OnEnable()
// 停用
void OnDisable()
// 更新
void FixedUpdate()
// Called at the start of FixedUpdate to record the current state of the base layer of the animator.
void CacheAnimatorState()
// Called after the animator state has been cached to determine whether this script should block user input.
void UpdateInputBlocking()
// Called after the animator state has been cached to determine whether or not the staff should be active or not.
bool IsWeaponEquiped()
// Called each physics step with a parameter based on the return value of IsWeaponEquiped.
void EquipMeleeWeapon(bool equip)
// Called each physics step.
void CalculateForwardMovement()
// Called each physics step.
void CalculateVerticalMovement()
// Called each physics step to set the rotation Ellen is aiming to have.
void SetTargetRotation()
// Called each physics step to help determine whether Ellen can turn under player input.
bool IsOrientationUpdated()
// Called each physics step after SetTargetRotation if there is move input and Ellen is in the correct animator state according to IsOrientationUpdated.
void UpdateOrientation()
// Called each physics step to check if audio should be played and if so instruct the relevant random audio player to do so.
void PlayAudio()
// Called each physics step to count up to the point where Ellen considers a random idle.
void TimeoutToIdle()
// Called each physics step (so long as the Animator component is set to Animate Physics) after FixedUpdate to override root motion.
void OnAnimatorMove()
// This is called by an animation event when Ellen swings her staff.
public void MeleeAttackStart(int throwing = 0)
// This is called by an animation event when Ellen finishes swinging her staff.
public void MeleeAttackEnd()
// This is called by Checkpoints to make sure Ellen respawns correctly.
public void SetCheckpoint(Checkpoint checkpoint)
// This is usually called by a state machine behaviour on the animator controller but can be called from anywhere.
public void Respawn()
protected IEnumerator RespawnRoutine()
// Called by a state machine behaviour on Ellen's animator controller.
public void RespawnFinished()
// Called by Ellen's Damageable when she is hurt.
public void OnReceiveMessage(MessageType type, object sender, object data)
// Called by OnReceiveMessage.
void Damaged(Damageable.DamageMessage damageMessage)
// Called by OnReceiveMessage and by DeathVolumes in the scene.
public void Die(Damageable.DamageMessage damageMessage)
}
```
眼睛很花對吧? 表示塞太多東西了。
最好盡量細分功能,一個程式只負責一件事情
腦袋也會比較分的清楚👍
*總歸一句話,不需要給別人知道的東西,就不要公開出去*
## 生命週期

### 常用的事件函数(依執行順序)
1. Awake:醒來(一定會做一次,不會重複執行)
2. OnEnable:啟用
3. Start:開始("啟用後"做一次,不會重複執行)
4. Update:更新(每一幀呼叫一次,啟用後會一直重複)
5. OnDisable:停用
6. OnDestroy:刪除
比喻人來說:起床→有沒有班?→腦袋開機→開始工作->關機->下班
[事件函数的执行顺序](https://docs.unity3d.com/cn/2018.4/Manual/ExecutionOrder.html)
所以主要工作的地方都是在□□□Update
打開FixedUpdate區塊:
```csharp=180
// Called automatically by Unity once every Physics step.
void FixedUpdate()
{
CacheAnimatorState();//儲存動畫狀態
UpdateInputBlocking();//更新輸入偵測狀態
EquipMeleeWeapon(IsWeaponEquiped());//裝備近戰武器
m_Animator.SetFloat(m_HashStateTime, Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f));//設置動畫時間
m_Animator.ResetTrigger(m_HashMeleeAttack);//重置攻擊觸發
if (m_Input.Attack && canAttack)//如果可以攻擊
m_Animator.SetTrigger(m_HashMeleeAttack);//觸發攻擊動畫
CalculateForwardMovement();//計算跑步速度
CalculateVerticalMovement();//計算降落速度
SetTargetRotation();//計算轉向
if (IsOrientationUpdated() && IsMoveInput)//如果動了
UpdateOrientation();//設定轉向
PlayAudio();//播放音效
TimeoutToIdle();//是否播放閒置動畫
m_PreviouslyGrounded = m_IsGrounded;
}
```
看命名找到計算跑步速度函式 → CalculateForwardMovement()
```csharp=253
void CalculateForwardMovement()
{
// Cache the move input and cap it's magnitude at 1.
Vector2 moveInput = m_Input.MoveInput;
if (moveInput.sqrMagnitude > 1f)
moveInput.Normalize();
// Calculate the speed intended by input.
m_DesiredForwardSpeed = moveInput.magnitude * maxForwardSpeed;
// Determine change to speed based on whether there is currently any move input.
float acceleration = IsMoveInput ? k_GroundAcceleration : k_GroundDeceleration;
// Adjust the forward speed towards the desired speed.
m_ForwardSpeed = Mathf.MoveTowards(m_ForwardSpeed, m_DesiredForwardSpeed, acceleration * Time.deltaTime);
// Set the animator parameter to control what animation is being played.
m_Animator.SetFloat(m_HashForwardSpeed, m_ForwardSpeed);
}
```
重要的是輸入從哪裡來?算完被送到哪裡去?
```csharp=253
void CalculateForwardMovement()
{
//從PlayInput取得移動輸入
Vector2 moveInput = m_Input.MoveInput;
(經過一些處理......)
//設定到Animator的"ForwardSpeed"變數
m_Animator.SetFloat(m_HashForwardSpeed, m_ForwardSpeed);
}
```
只有設定動畫人物就會動?!
## BlendTree
BlendTree是一個可以用數值去控制混合動畫的功能

▲ AnimatorController中使用ForwardSpeed的地方

▲ 依ForwardSpeed的大小決定要用走路還是跑步動畫

打開動畫發現有把Root往前(Z軸)移動了5.32 → 這動畫含有Root Motion
[Animator component](https://docs.unity3d.com/2022.3/Documentation/Manual/class-Animator.html)

:::warning
如果遇到EX:怎麼跑起來怪怪的?
:::
## 有效率的Debug方式:直接看到數值,不用自己辛苦推算
1. 顯示到Inspector上
```csharp=
protected float m_ForwardSpeed;// How fast Ellen is currently going along the ground.
public float m_ForwardSpeed;//暫時公開成public
[SerializeField] protected float m_ForwardSpeed;//或是加上[SerializeField]
```
## 11/21
## 取得道具



▲碰到道具之後Can Attack打開了

▲法杖有擺一個碰撞框,碰到Player之後把Can Attack打開
```csharp=8
[RequireComponent(typeof(Collider))] //預先標記依賴的元件(Component)
public class InteractOnTrigger : MonoBehaviour
{
public LayerMask layers; //要偵測的圖層
public UnityEvent OnEnter, OnExit; //自定義事件
new Collider collider; //碰撞框
public InventoryController.InventoryChecker[] inventoryChecks;
void Reset()
{
layers = LayerMask.NameToLayer("Everything");
collider = GetComponent<Collider>();
collider.isTrigger = true;
}
//進入碰撞框
void OnTriggerEnter(Collider other)
{
if (0 != (layers.value & 1 << other.gameObject.layer))//判斷圖層
{
ExecuteOnEnter(other);
}
}
protected virtual void ExecuteOnEnter(Collider other)
{
OnEnter.Invoke();//執行自定義事件
for (var i = 0; i < inventoryChecks.Length; i++)
{
inventoryChecks[i].CheckInventory(other.GetComponentInChildren<InventoryController>());
}
}
//出去碰撞框
void OnTriggerExit(Collider other)
{
if (0 != (layers.value & 1 << other.gameObject.layer))
{
ExecuteOnExit(other);
}
}
protected virtual void ExecuteOnExit(Collider other)
{
OnExit.Invoke();//執行自定義事件
}
void OnDrawGizmos()
{
Gizmos.DrawIcon(transform.position, "InteractionTrigger", false);
}
void OnDrawGizmosSelected()
{
//need to inspect events and draw arrows to relevant gameObjects.
}
}
```
:::info
## 練習:自定義事件
```csharp=
public UnityEvent OnDebug;
public void DebugMessage(string msg)
{
print(msg);
}
```


:::
## 攻擊
```csharp=104
public class PlayerController : MonoBehaviour, IMessageReceiver
{
......
public void SetCanAttack(bool canAttack)
{
this.canAttack = canAttack;
}
}
```

```csharp=177
public class PlayerController : MonoBehaviour, IMessageReceiver
{
......
void FixedUpdate()
{
CacheAnimatorState();
UpdateInputBlocking();
EquipMeleeWeapon(IsWeaponEquiped());//是否要顯示武器
m_Animator.SetFloat(m_HashStateTime, Mathf.Repeat(m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f));
m_Animator.ResetTrigger(m_HashMeleeAttack);
if (m_Input.Attack && canAttack)//如果按下攻擊
m_Animator.SetTrigger(m_HashMeleeAttack);//觸發攻擊動畫
CalculateForwardMovement();
CalculateVerticalMovement();
SetTargetRotation();
......
}
}
```

▲MeleeAttack的Trigger被打開
### 攻擊時才顯示武器

```csharp=226
public class PlayerController : MonoBehaviour, IMessageReceiver
{
......
// Called after the animator state has been cached to determine whether or not the staff should be active or not.
bool IsWeaponEquiped() //是不是正在打Combo ?
{
bool equipped = m_NextStateInfo.shortNameHash == m_HashEllenCombo1 || m_CurrentStateInfo.shortNameHash == m_HashEllenCombo1;
equipped |= m_NextStateInfo.shortNameHash == m_HashEllenCombo2 || m_CurrentStateInfo.shortNameHash == m_HashEllenCombo2;
equipped |= m_NextStateInfo.shortNameHash == m_HashEllenCombo3 || m_CurrentStateInfo.shortNameHash == m_HashEllenCombo3;
equipped |= m_NextStateInfo.shortNameHash == m_HashEllenCombo4 || m_CurrentStateInfo.shortNameHash == m_HashEllenCombo4;
return equipped;
}
// Called each physics step with a parameter based on the return value of IsWeaponEquiped.
void EquipMeleeWeapon(bool equip)
{
meleeWeapon.gameObject.SetActive(equip);
m_InAttack = false;
m_InCombo = equip;
if (!equip)
m_Animator.ResetTrigger(m_HashMeleeAttack);
}
}
```
### 偵測打擊
```csharp=76
public class MeleeWeapon : MonoBehaviour
{
......
public void BeginAttack(bool thowingAttack)
{
if (attackAudio != null)
attackAudio.PlayRandomClip();
throwingHit = thowingAttack;
m_InAttack = true;
m_PreviousPos = new Vector3[attackPoints.Length];
for (int i = 0; i < attackPoints.Length; ++i)
{
Vector3 worldPos = attackPoints[i].attackRoot.position +
attackPoints[i].attackRoot.TransformVector(attackPoints[i].offset);
m_PreviousPos[i] = worldPos;
#if UNITY_EDITOR
attackPoints[i].previousPositions.Clear();
attackPoints[i].previousPositions.Add(m_PreviousPos[i]);
#endif
}
}
public void EndAttack()
{
m_InAttack = false;
#if UNITY_EDITOR
for (int i = 0; i < attackPoints.Length; ++i)
{
attackPoints[i].previousPositions.Clear();
}
#endif
}
private void FixedUpdate()
{
if (m_InAttack)
{
for (int i = 0; i < attackPoints.Length; ++i)
{
AttackPoint pts = attackPoints[i];
Vector3 worldPos = pts.attackRoot.position + pts.attackRoot.TransformVector(pts.offset);
Vector3 attackVector = worldPos - m_PreviousPos[i];
if (attackVector.magnitude < 0.001f)
{
// A zero vector for the sphere cast don't yield any result, even if a collider overlap the "sphere" created by radius.
// so we set a very tiny microscopic forward cast to be sure it will catch anything overlaping that "stationary" sphere cast
attackVector = Vector3.forward * 0.0001f;
}
Ray r = new Ray(worldPos, attackVector.normalized);
int contacts = Physics.SphereCastNonAlloc(r, pts.radius, s_RaycastHitCache, attackVector.magnitude,
~0,
QueryTriggerInteraction.Ignore);
for (int k = 0; k < contacts; ++k)
{
Collider col = s_RaycastHitCache[k].collider;
if (col != null)
CheckDamage(col, pts);
}
m_PreviousPos[i] = worldPos;
#if UNITY_EDITOR
pts.previousPositions.Add(m_PreviousPos[i]);
#endif
}
}
}
}
```

▲動畫裡也是可以呼叫程式碼的

▲攻擊範圍
## Debug攻擊範圍
```csharp
public class MeleeWeapon : MonoBehaviour
{
private void FixedUpdate()
{
if (m_InAttack)
{
debugPoints.Clear();
......
Debug.DrawLine(worldPos, worldPos + attackVector, Color.red, 1);
for (int j = 0; j <= 4; j++)
{
debugPoints.Add(worldPos + attackVector * (j / 4f));
}
}
}
List<Vector3> debugPoints = new();
private void OnDrawGizmosSelected()
{
......
Gizmos.color = Color.yellow;
foreach (var item in debugPoints)
{
Gizmos.DrawWireSphere(item, pts.radius);
}
}
}
```
:::info
## 練習:作一把新武器


▲設定偵測點(刀刃)


▲記得要擺音效
:::
## 造成傷害
```csharp
public class MeleeWeapon : MonoBehaviour
{
private bool CheckDamage(Collider other, AttackPoint pts)
{
Damageable d = other.GetComponent<Damageable>();
if (d == null)
{
return false;
}
if (d.gameObject == m_Owner)
return true; //ignore self harm, but do not end the attack (we don't "bounce" off ourselves)
if ((targetLayers.value & (1 << other.gameObject.layer)) == 0)
{
//hit an object that is not in our layer, this end the attack. we "bounce" off it
return false;
}
播放音效......
Damageable.DamageMessage data;
data.amount = damage;
data.damager = this;
data.direction = m_Direction.normalized;
data.damageSource = m_Owner.transform.position;
data.throwing = m_IsThrowingHit;
data.stopCamera = false;
d.ApplyDamage(data);
播放粒子......
return true;
}
}
```
```csharp
public partial class Damageable : MonoBehaviour
{
......
public void ApplyDamage(DamageMessage data)
{
if (currentHitPoints <= 0)
{//ignore damage if already dead. TODO : may have to change that if we want to detect hit on death...
return;
}
if (isInvulnerable)
{
OnHitWhileInvulnerable.Invoke();
return;
}
Vector3 forward = transform.forward;
forward = Quaternion.AngleAxis(hitForwardRotation, transform.up) * forward;
//we project the direction to damager to the plane formed by the direction of damage
Vector3 positionToDamager = data.damageSource - transform.position;
positionToDamager -= transform.up * Vector3.Dot(transform.up, positionToDamager);
if (Vector3.Angle(forward, positionToDamager) > hitAngle * 0.5f)
return; //不在攻擊角度内不接受傷害
isInvulnerable = true;
currentHitPoints -= data.amount;
if (currentHitPoints <= 0)
schedule += OnDeath.Invoke; //This avoid race condition when objects kill each other.
else
OnReceiveDamage.Invoke();
var messageType = currentHitPoints <= 0 ? MessageType.DEAD : MessageType.DAMAGED;
for (var i = 0; i < onDamageMessageReceivers.Count; ++i)
{
var receiver = onDamageMessageReceivers[i] as IMessageReceiver;
receiver.OnReceiveMessage(messageType, this, data);
}
}
void LateUpdate()
{
if (schedule != null)
{
schedule();
schedule = null;
}
}
}
```

▲怪物是360度都會受傷的
:::info
## 練習:調整傷害接收器
調整血量(Max Hit Points)、偵測範圍(Hit Angle、Hit Forward Rotation)

```csharp
public void Explode()
{
//在事件中加一些奇怪的東西,例如越打越大隻↓
transform.localScale += Vector3.one * 0.5f;
}
```
:::
## 敵人偵測玩家

```csharp=7
//use this class to simply scan & spot the player based on the parameters.
//Used by enemies behaviours.
[System.Serializable]
public class TargetScanner
{
public float heightOffset = 0.0f;
public float detectionRadius = 10;
[Range(0.0f, 360.0f)]
public float detectionAngle = 270;
public float maxHeightDifference = 1.0f;
public LayerMask viewBlockerLayerMask;
/// <summary>
/// 偵測玩家
/// </summary>
public PlayerController Detect(Transform detector, bool useHeightDifference = true)
{
//玩家目前狀態不是正在重生
if (PlayerController.instance == null || PlayerController.instance.respawning)
return null;
Vector3 eyePos = detector.position + Vector3.up * heightOffset; //眼睛位置
Vector3 toPlayer = PlayerController.instance.transform.position - eyePos;//玩家的腳
Vector3 toPlayerTop = PlayerController.instance.transform.position + Vector3.up * 1.5f - eyePos;//玩家的頭
if (useHeightDifference && Mathf.Abs(toPlayer.y + heightOffset) > maxHeightDifference)
{
return null;// 跟玩家高度差太多當作找不到
}
Vector3 toPlayerFlat = toPlayer;
toPlayerFlat.y = 0;// 只取平面旋轉角度
//在偵測距離内
if (toPlayerFlat.sqrMagnitude <= detectionRadius * detectionRadius)
{
//點積:兩個方向完全一樣=1 完全相反=-1 垂直=0
if (Vector3.Dot(toPlayerFlat.normalized, detector.forward) >
Mathf.Cos(detectionAngle * 0.5f * Mathf.Deg2Rad))
{
bool canSee = false;
Debug.DrawRay(eyePos, toPlayer, Color.blue);
Debug.DrawRay(eyePos, toPlayerTop, Color.blue);
canSee |= !Physics.Raycast(eyePos, toPlayer.normalized, detectionRadius,
viewBlockerLayerMask, QueryTriggerInteraction.Ignore);
canSee |= !Physics.Raycast(eyePos, toPlayerTop.normalized, toPlayerTop.magnitude,
viewBlockerLayerMask, QueryTriggerInteraction.Ignore);
if (canSee)
return PlayerController.instance;
}
}
return null;
}
......
}
```
https://www.desmos.com/calculator/vpxgi2xxfh?lang=zh-TW

▲搜索範圍是270度,得出來點積要在-0.707以上

▲黑線是怪物正前方,紅色是玩家位置,兩個方向的點積是-0.207,在偵測範圍內