Unity自学--CreatorKit代码解析 1
Unity初学者项目 Creator Kit Beginner
ExampleScene 样例场景如下
1.附着脚本:(Character)
Character对象是游戏项目的PLayer,即玩家操纵的游戏对象,对于Character来说,需要拥有自己的运行脚本。
附着在Character对象上的运行脚本有如下:
可以看到,除了基础的Transform模块外还存在CapsuleCollider 用于检测碰撞,NavMeshAgent用于自动寻路,Rigidbody用于描述物体的物理性质,官方文档有详细描述。
2.CharacterControl 代码解析
using System.Collections; using System.Collections.Generic; using System.Data; using System.Timers; using CreatorKitCode; using UnityEngine; using UnityEngine.AI; using UnityEngine.EventSystems; using UnityEngine.Serialization; namespace CreatorKitCodeInternal { //继承了基类,实现了攻击,移动的接口 public class CharacterControl : MonoBehaviour, AnimationControllerDispatcher.IAttackFrameReceiver, AnimationControllerDispatcher.IFootstepFrameReceiver { //单例对象 public static CharacterControl Instance { get; protected set; } //移动速度 public float Speed = 10.0f; //角色数据 => 依赖于m_CharacterData 即附着在Character中的CharacterData脚本类 public CharacterData Data => m_CharacterData; //目标数据 => 依赖于m_CurrentTargetCharacterData 即附着在其他对象中的CharacterData脚本类 public CharacterData CurrentTarget => m_CurrentTargetCharacterData; //武器的位置数据等 public Transform WeaponLocator; //[Header] 显示标头 在Unity中显示Audio [Header("Audio")] //音频数据 public AudioClip[] SpurSoundClips; //最后射线的位置 Vector3 m_LastRaycastResult; //动画播放器 Animator m_Animator; //寻路器 NavMeshAgent m_Agent; //用户数据 CharacterData m_CharacterData; //高光类,用于高光渲染 HighlightableObject m_Highlighted; //RaycastHit用于存储RayCast(射线)命中的数据 RaycastHit[] m_RaycastHitCache = new RaycastHit[16]; int m_SpeedParamID; int m_AttackParamID; int m_HitParamID; //眩晕id int m_FaintParamID; //复位,出生ID int m_RespawnParamID; bool m_IsKO = false; float m_KOTimer = 0.0f; //交互层级--层级 int m_InteractableLayer; int m_LevelLayer; //碰撞器 Collider m_TargetCollider; //交互对象 InteractableObject m_TargetInteractable = null; //主视角 Camera m_MainCamera; //自动寻路组件产生的路径,保存在corners中 NavMeshPath m_CalculatedPath; //主角音频 CharacterAudio m_CharacterAudio; //目标层 当前目标数据 int m_TargetLayer; CharacterData m_CurrentTargetCharacterData = null; //this is a flag that tell the controller it need to clear the target once the attack finished. //usefull for when clicking elwswhere mid attack animation, allow to finish the attack and then exit. //当攻击完成后需要清除的标志位,用于攻击动画途中点击某个位置时允许退出攻击状态并离开 bool m_ClearPostAttack = false; //复生点,当经过存在复生点的区域时,设置复生点 SpawnPoint m_CurrentSpawn = null; //枚举状态 默认 受击 攻击 enum State { DEFAULT, HIT, ATTACKING } //当前状态 State m_CurrentState; //初始化实例 获得主摄像机 Camera.main tag标签为MainCamera的摄像机 void Awake() { Instance = this; m_MainCamera = Camera.main; } // Start is called before the first frame update void Start() { //QualitySettings品质接口 此处为不等待垂直同步 (垂直同步防止跳帧,撕裂(关闭会更流畅)) QualitySettings.vSyncCount = 0; //尝试60fps 设置了QualitySettings.vSyncCount之后,targetFrameRate不使用,使用平台默认刷新率 Application.targetFrameRate = 60; //新建路径 m_CalculatedPath = new NavMeshPath(); //获取寻路组件,动画组件 m_Agent = GetComponent<NavMeshAgent>(); m_Animator = GetComponentInChildren<Animator>(); //寻路的速度设为当前移动速度 m_Agent.speed = Speed; //设置最大回转速度 需要转弯的速度 m_Agent.angularSpeed = 360.0f; //射线位置设为当前位置 m_LastRaycastResult = transform.position; //初始化动画ID m_SpeedParamID = Animator.StringToHash("Speed"); m_AttackParamID = Animator.StringToHash("Attack"); m_HitParamID = Animator.StringToHash("Hit"); m_FaintParamID = Animator.StringToHash("Faint"); m_RespawnParamID = Animator.StringToHash("Respawn"); //获得人物数据 m_CharacterData = GetComponent<CharacterData>(); //给人物数据中的武器装备动作提供 m_CharacterData.Equipment.OnEquiped += item => { if (item.Slot == (EquipmentItem.EquipmentSlot)666) { //初始化预制件,父类是当前对象,不在世界坐标系下生成(父类坐标系下生成) var obj = Instantiate(item.WorldObjectPrefab, WeaponLocator, false); //将每一层都变为PlayerEquipment层循环调用 Helpers.RecursiveLayerChange(obj.transform, LayerMask.NameToLayer("PlayerEquipment")); } }; m_CharacterData.Equipment.OnUnequip += item => { if (item.Slot == (EquipmentItem.EquipmentSlot)666) { //摧毁所有的角色 foreach(Transform t in WeaponLocator) Destroy(t.gameObject); } }; //角色数据初始化 m_CharacterData.Init(); //获得层级位置 m_InteractableLayer = 1 << LayerMask.NameToLayer("Interactable"); m_LevelLayer = 1 << LayerMask.NameToLayer("Level"); m_TargetLayer = 1 << LayerMask.NameToLayer("Target"); //设置默认状态 m_CurrentState = State.DEFAULT; //设置音频 m_CharacterAudio = GetComponent<CharacterAudio>(); //受伤时的动作函数 (设置动画,调用Hit函数播放) m_CharacterData.OnDamage += () => { m_Animator.SetTrigger(m_HitParamID); m_CharacterAudio.Hit(transform.position); }; } // Update is called once per frame void Update() { Vector3 pos = transform.position; //判断是否死了,死了设置复活时间m_KOTimer,达到3s后调用GoToRespawn()复活 if (m_IsKO) { m_KOTimer += Time.deltaTime; if (m_KOTimer > 3.0f) { GoToRespawn(); } return; } //The update need to run, so we can check the health here. //Another method would be to add a callback in the CharacterData that get called //when health reach 0, and this class register to the callback in Start //(see CharacterData.OnDamage for an example) //当前生命值为 0时死亡 if (m_CharacterData.Stats.CurrentHealth == 0) { //设置眩晕状态 m_Animator.SetTrigger(m_FaintParamID); //寻路停止 m_Agent.isStopped = true; //清空路径 m_Agent.ResetPath(); //状态值为死亡 m_IsKO = true; //设置复活计时器 m_KOTimer = 0.0f; //调用死亡函数 Data.Death(); //调用死亡音频 m_CharacterAudio.Death(pos); return; } //没死亡的话 获取在鼠标位置射线 Ray screenRay = CameraController.Instance.GameplayCamera.ScreenPointToRay(Input.mousePosition); //交互对象不为空 ,进入交互状态 if (m_TargetInteractable != null) { CheckInteractableRange(); } //目标数据不为空 判断对象是否死亡,是置为空,否则进入攻击状态 if (m_CurrentTargetCharacterData != null) { if (m_CurrentTargetCharacterData.Stats.CurrentHealth == 0) m_CurrentTargetCharacterData = null; else CheckAttack(); } //鼠标滚轮事件 float mouseWheel = Input.GetAxis("Mouse ScrollWheel"); //如果滚动了 if (!Mathf.Approximately(mouseWheel, 0.0f)) { //获取当前的鼠标位置(从屏幕空间变换为视口空间) Vector3 view = m_MainCamera.ScreenToViewportPoint(Input.mousePosition); //如果处在摄像机范围内 if(view.x > 0f && view.x < 1f && view.y > 0f && view.y < 1f) //变换摄像机视角 CameraController.Instance.Zoom(-mouseWheel * Time.deltaTime * 20.0f); } if(Input.GetMouseButtonDown(0)) { //if we click the mouse button, we clear any previously et targets //按下按键但不是攻击状态时,清空所有的对象(攻击对象,交互对象) 否则攻击后清除 if (m_CurrentState != State.ATTACKING) { m_CurrentTargetCharacterData = null; m_TargetInteractable = null; } else { //处于攻击状态时 m_ClearPostAttack = true; } } //EventSystem.current.IsPointerOverGameObject()判断触点是否在对象上而不是UI上 if (!EventSystem.current.IsPointerOverGameObject() && m_CurrentState != State.ATTACKING) { //Raycast to find object currently under the mouse cursor //用射线的方式获取光标位置下的对象 //设置为高光对象 没有 则不设置 ObjectsRaycasts(screenRay); if (Input.GetMouseButton(0)) { //如果按下按键 if (m_TargetInteractable == null && m_CurrentTargetCharacterData == null) { //判断高光对象 InteractableObject obj = m_Highlighted as InteractableObject; if (obj) { InteractWith(obj); } else { CharacterData data = m_Highlighted as CharacterData; if (data != null) { m_CurrentTargetCharacterData = data; } else { //高光对象也为NULL则寻路检查 MoveCheck(screenRay); } } } } } //设置平滑的移动 m_Animator.SetFloat(m_SpeedParamID, m_Agent.velocity.magnitude / m_Agent.speed); //Keyboard shortcuts //按I键获取物品栏 if(Input.GetKeyUp(KeyCode.I)) UISystem.Instance.ToggleInventory(); } void GoToRespawn() { m_Animator.ResetTrigger(m_HitParamID); //设置代理路径 m_Agent.Warp(m_CurrentSpawn.transform.position); //停止寻路 m_Agent.isStopped = true; //清空路径 m_Agent.ResetPath(); //复活 m_IsKO = false; //所有状态重置 m_CurrentTargetCharacterData = null; m_TargetInteractable = null; m_CurrentState = State.DEFAULT; m_Animator.SetTrigger(m_RespawnParamID); //恢复状态 m_CharacterData.Stats.ChangeHealth(m_CharacterData.Stats.stats.health); } void ObjectsRaycasts(Ray screenRay) { //bool值 bool somethingFound = false; //first check for interactable Object //往screenRay方向投射球体,半径为1,m_RaycastHitCache存储命中对象,m_InteractableLayer遮罩层忽略不在层中的对象 int count = Physics.SphereCastNonAlloc(screenRay, 1.0f, m_RaycastHitCache, 1000.0f, m_InteractableLayer); //缓冲区个数不为0时 if (count > 0) { for (int i = 0; i < count; ++i) { //获取每个碰撞体中的可交互对象(附着体) InteractableObject obj = m_RaycastHitCache[0].collider.GetComponentInParent<InteractableObject>(); //不为NULL且可交互时 if (obj != null && obj.IsInteractable) { //调用函数使物体高光 SwitchHighlightedObject(obj); //设置为找到东西,跳出循环 somethingFound = true; break; } } } else { //往screenRay方向投射球体,半径为1,m_RaycastHitCache存储命中对象,m_InteractableLayer遮罩层忽略不在层中的对象 count = Physics.SphereCastNonAlloc(screenRay, 1.0f, m_RaycastHitCache, 1000.0f, m_TargetLayer); if (count > 0) { CharacterData data = m_RaycastHitCache[0].collider.GetComponentInParent<CharacterData>(); //数据不为null时 if (data != null) { SwitchHighlightedObject(data); somethingFound = true; } } } //没找到对象并且高光对象不为NULL时 if (!somethingFound && m_Highlighted != null) { //清除高光对象 SwitchHighlightedObject(null); } } void SwitchHighlightedObject(HighlightableObject obj) { //高光对象不为null时 取消高光 if(m_Highlighted != null) m_Highlighted.Dehighlight(); //设置高光对象 m_Highlighted = obj; //高光对象不为null时 设置高光 if (m_Highlighted != null) m_Highlighted.Highlight(); } //移动检测 void MoveCheck(Ray screenRay) { //判断寻路状态是否在目的地终止 NavMeshPathStatus.PathComplete if ( m_CalculatedPath.status == NavMeshPathStatus.PathComplete) { //设置新路径 m_Agent.SetPath(m_CalculatedPath); //清除路径 m_CalculatedPath.ClearCorners(); } if (Physics.RaycastNonAlloc(screenRay, m_RaycastHitCache, 1000.0f, m_LevelLayer) > 0) { //抛射对象大于0时,点击位置的对象,选择碰到的第一个 //获取第一个对象 Vector3 point = m_RaycastHitCache[0].point; //avoid recomputing path for close enough click //避免重复计算近距离点击(点在上次点的位置附近的就不移动了) if (Vector3.SqrMagnitude(point - m_LastRaycastResult) > 1.0f) { NavMeshHit hit; //NavMesh.SamplePosition判断是否是可行区域 point 目标点 hit 输出最近路径 NavMesh.AllAreas全路段 if (NavMesh.SamplePosition(point, out hit, 0.5f, NavMesh.AllAreas)) {//sample just around where we hit, avoid setting destination outside of navmesh (ie. on building) //是可行区域设置之前的位置 m_LastRaycastResult = point; //m_Agent.SetDestination(hit.position); //计算路径并将其储存到m_CalculatedPath中 m_Agent.CalculatePath(hit.position, m_CalculatedPath); } } } } //交互 void CheckInteractableRange() { //攻击状态,则不交互 if(m_CurrentState == State.ATTACKING) return; //ClosestPointOnBounds与目标碰撞体碰撞的最近的位置 相减得出距离 Vector3 distance = m_TargetCollider.ClosestPointOnBounds(transform.position) - transform.position; //距离小于一个范围 if (distance.sqrMagnitude < 1.5f * 1.5f) { //停止寻路 StopAgent(); //交互 m_TargetInteractable.InteractWith(m_CharacterData); //交互完了,交互对象为NULL,再次点击才会继续交互 m_TargetInteractable = null; } } //停止寻路 void StopAgent() { m_Agent.ResetPath(); //速度为0 立刻停止 m_Agent.velocity = Vector3.zero; } void CheckAttack() { //处于攻击状态,返回 if(m_CurrentState == State.ATTACKING) return; //是否达到攻击范围 if (m_CharacterData.CanAttackReach(m_CurrentTargetCharacterData)) { //达到,停止寻路 StopAgent(); //if the mouse button isn't pressed, we do NOT attack //判断是否按下攻击键 if (Input.GetMouseButton(0)) { //获得两者的距离 Vector3 forward = (m_CurrentTargetCharacterData.transform.position - transform.position); //垂直距离为0 forward.y = 0; //归一化 forward.Normalize(); //设置标准化矢量 transform.forward = forward; //判断是否能够攻击到目标 if (m_CharacterData.CanAttackTarget(m_CurrentTargetCharacterData)) { //能则进入攻击状态 m_CurrentState = State.ATTACKING; //播放动画和音效 m_CharacterData.AttackTriggered(); m_Animator.SetTrigger(m_AttackParamID); } } } else { //没达到则设置攻击目标的位置为寻路位置 m_Agent.SetDestination(m_CurrentTargetCharacterData.transform.position); } } public void AttackFrame() { //攻击帧 攻击人物不存在了 放弃攻击 返回 if (m_CurrentTargetCharacterData == null) { m_ClearPostAttack = false; return; } //if we can't reach the target anymore when it's time to damage, then that attack miss. //攻击范围是否达到了 if (m_CharacterData.CanAttackReach(m_CurrentTargetCharacterData)) { //攻击 m_CharacterData.Attack(m_CurrentTargetCharacterData); //设置攻击位置和播放动画和音频 var attackPos = m_CurrentTargetCharacterData.transform.position + transform.up * 0.5f; VFXManager.PlayVFX(VFXType.Hit, attackPos); SFXManager.PlaySound(m_CharacterAudio.UseType, new SFXManager.PlayData() { Clip = m_CharacterData.Equipment.Weapon.GetHitSound(), PitchMin = 0.8f, PitchMax = 1.2f, Position = attackPos }); } //攻击过了 if(m_ClearPostAttack) { //清空所有对象 m_ClearPostAttack = false; m_CurrentTargetCharacterData = null; m_TargetInteractable = null; } //恢复默认状态 m_CurrentState = State.DEFAULT; } //设置出生点 public void SetNewRespawn(SpawnPoint point) { //设置为死寂状态(enable属性为可用) if(m_CurrentSpawn != null) m_CurrentSpawn.Deactivated(); m_CurrentSpawn = point; //设置为活跃状态 m_CurrentSpawn.Activated(); } //交互函数 public void InteractWith(InteractableObject obj) { if (obj.IsInteractable) { //可交互则获得碰撞器 m_TargetCollider = obj.GetComponentInChildren<Collider>(); //获得交互对象 m_TargetInteractable = obj; //设置寻路目标 m_Agent.SetDestination(obj.transform.position); } } //实现的接口函数 public void FootstepFrame() { Vector3 pos = transform.position; m_CharacterAudio.Step(pos); SFXManager.PlaySound(SFXManager.Use.Player, new SFXManager.PlayData() { Clip = SpurSoundClips[Random.Range(0, SpurSoundClips.Length)], Position = pos, PitchMin = 0.8f, PitchMax = 1.2f, Volume = 0.3f }); VFXManager.PlayVFX(VFXType.StepPuff, pos); } } }
3.整体人物逻辑设计(Update逻辑详解)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律