# Unity_Learn_3D_Kit ## Last Week ## 鏡頭 ![{59DB7D30-447A-48A6-9C8A-110445B1BB75}](https://hackmd.io/_uploads/HygHAAS-ke.png) ### 旋轉 ![{430362E7-E4B8-4BE5-BC27-6A6DBE0150C7}](https://hackmd.io/_uploads/BkWcqkU-ye.png) ![{0DCBF796-8023-4AD4-8B74-B500B71087E7}](https://hackmd.io/_uploads/SyuPpkLWkx.png) [Cinemachine Free Look Camera 官方文檔](https://docs.unity3d.com/Packages/com.unity.cinemachine@2.3/manual/CinemachineFreeLook.html) ## 玩家 ### 移動 ![{27D5B5C7-5542-47C9-80F0-5C6727933DA7}](https://hackmd.io/_uploads/S1G4nRr-ye.png) ### 跳躍 ![{7B40349A-FCAC-41EA-BFE1-0AB50D8A9F1D}](https://hackmd.io/_uploads/Hk-N6CHZke.png) ### 攻擊 ![{DEC4FD08-8C67-4AC4-9D5A-895FCE0E1DAD}](https://hackmd.io/_uploads/Bk7t60rZyx.png) ## 取得鍵盤輸入 ```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")就抓得到鍵盤? ![{3E504221-74E1-4EBD-9056-1E20335D36AB}](https://hackmd.io/_uploads/ryUTHbLWkl.png) [Input Manager官方文檔](https://docs.unity3d.com/Manual/class-InputManager.html) ### 怎麼看程式碼? - 查看定義:按F12 / 右鍵Go to definition,來查看變數、class在哪裡宣告的 - 查看引用:按Shft+F12 / 右鍵Find Usage,來看哪些地方有用過這個變數 ![{EDCA675C-2778-41D6-AD45-8282B9C05ABD}](https://hackmd.io/_uploads/ByCm4lI-1g.png) ▲ 對 m_Movement 按F12找到他是Vector2 ![{D07EEEA1-6D01-42C0-A83F-5C597A516D96}](https://hackmd.io/_uploads/BJwJqg8-1l.png) ▲ 對 m_Movement 按Shft+F12找到哪幾行有用到這個變數 ![{2A6BCCE4-AA85-477F-9727-FB5AF9D41E21}](https://hackmd.io/_uploads/BJ8a9xUWyx.png) ▲ 找到 MoveInput 會返回 m_Movement ![{4D9536C0-B977-4A09-994E-C65D564EA1F2}](https://hackmd.io/_uploads/ryjGse8Wkx.png) ▲ 對 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) } ``` 眼睛很花對吧? 表示塞太多東西了。 最好盡量細分功能,一個程式只負責一件事情 腦袋也會比較分的清楚👍 *總歸一句話,不需要給別人知道的東西,就不要公開出去* ## 生命週期 ![image](https://hackmd.io/_uploads/HkFwlzI-1x.png) ### 常用的事件函数(依執行順序) 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是一個可以用數值去控制混合動畫的功能 ![image](https://hackmd.io/_uploads/H1RXhIDWke.png) ▲ AnimatorController中使用ForwardSpeed的地方 ![image](https://hackmd.io/_uploads/ByNYhUwZJg.png) ▲ 依ForwardSpeed的大小決定要用走路還是跑步動畫 ![image](https://hackmd.io/_uploads/Skj40IPbkl.png) 打開動畫發現有把Root往前(Z軸)移動了5.32 → 這動畫含有Root Motion [Animator component](https://docs.unity3d.com/2022.3/Documentation/Manual/class-Animator.html) ![image](https://hackmd.io/_uploads/rJ6KgPwbyx.png) :::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 ## 取得道具 ![{AC9F122C-E236-4C6A-9649-0B7ACB10FDD7}](https://hackmd.io/_uploads/BkEgaJ3Gkx.png) ![{EC43CD6F-A60D-4EB5-BA79-785EB92541BE}](https://hackmd.io/_uploads/B1M70JhfJg.png) ![{A31C2FDC-F8EB-425B-B254-2A221A292DD9}](https://hackmd.io/_uploads/SJaNRk2fye.png) ▲碰到道具之後Can Attack打開了 ![453453453_proc](https://hackmd.io/_uploads/rJChJlhf1e.jpg) ▲法杖有擺一個碰撞框,碰到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); } ``` ![{9A6FD118-E107-40B9-8BEC-D58E2126D194}](https://hackmd.io/_uploads/HkgpPghGke.png) ![{D8DB91CA-1323-46ED-9159-599E05DF4973}](https://hackmd.io/_uploads/H1jaPlnzke.png) ::: ## 攻擊 ```csharp=104 public class PlayerController : MonoBehaviour, IMessageReceiver { ...... public void SetCanAttack(bool canAttack) { this.canAttack = canAttack; } } ``` ![{1403AB9E-670C-4458-9ADA-48350CA8884D}](https://hackmd.io/_uploads/ryhI7x2f1l.png) ```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(); ...... } } ``` ![image](https://hackmd.io/_uploads/rkSvSenzkx.png) ▲MeleeAttack的Trigger被打開 ### 攻擊時才顯示武器 ![{A0358F8B-8BAD-4A94-9C6F-EE01B51B016F}](https://hackmd.io/_uploads/SydsBgnG1x.png) ```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 } } } } ``` ![{E4EC069D-8946-4F0F-BCEE-131BE62C3BF0}](https://hackmd.io/_uploads/HkKLXM3fyl.png) ▲動畫裡也是可以呼叫程式碼的 ![453453453_proc](https://hackmd.io/_uploads/ry8sZ7hGyg.jpg) ▲攻擊範圍 ## 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 ## 練習:作一把新武器 ![{0A43557C-DEEE-451D-989F-4C3B2488DFA0}](https://hackmd.io/_uploads/rJELKmnGkg.png) ![{350F1338-5C5A-4326-8224-8C48CCB4719C}](https://hackmd.io/_uploads/S1vKtX2Gke.png) ▲設定偵測點(刀刃) ![image](https://hackmd.io/_uploads/H1i0D5H7Jl.png) ![image](https://hackmd.io/_uploads/HkkgO9Smyx.png) ▲記得要擺音效 ::: ## 造成傷害 ```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; } } } ``` ![{B7262028-60D5-4707-BB80-960806A9D8EF}](https://hackmd.io/_uploads/SkVDUm2Gyx.png) ▲怪物是360度都會受傷的 :::info ## 練習:調整傷害接收器 調整血量(Max Hit Points)、偵測範圍(Hit Angle、Hit Forward Rotation) ![螢幕擷取畫面 2024-11-21 114112](https://hackmd.io/_uploads/BJHl27hGke.jpg) ```csharp public void Explode() { //在事件中加一些奇怪的東西,例如越打越大隻↓ transform.localScale += Vector3.one * 0.5f; } ``` ::: ## 敵人偵測玩家 ![985618951](https://hackmd.io/_uploads/SyECtN3zke.jpg) ```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 ![{7A840D60-512E-4028-A066-D0546B703099}](https://hackmd.io/_uploads/rJBFbS2fyl.png) ▲搜索範圍是270度,得出來點積要在-0.707以上 ![{4095D6BB-83D8-4FBB-9428-F3D55085DC61}](https://hackmd.io/_uploads/Bkojbr3zyl.png) ▲黑線是怪物正前方,紅色是玩家位置,兩個方向的點積是-0.207,在偵測範圍內