小言_互联网的博客

Unity_Demo | 中世纪风3D-RPG游戏

389人阅读  评论(0)

❥ 由于大三期末N个大课设轮番轰炸,停下了手里的好多事。

故时隔一月余久,我又去继续催化RPG小游戏Demo了。

❥ 此次短暂优化之后,基本的战斗系统、对话系统和背包系统已具雏形,

画面渲染也较为惹眼舒适了。

不知不觉,实习已近一月,在mentor的指导和同事的帮助下,成功接手并完成了一些开发业务单,明天开始为期两周左右的GameJam了,暂且搁置这一Demo探索。

❥ 等新鲜的-科技风-元素塔防出炉之后,再来和大家分享可以试玩的作品。

先有蛋还是先有鸡?反正先发B站才方便插视频URL hh~

RPG小Demo_哔哩哔哩_视频链接

⭐️部分场景展示:


⭐️项目的架构大致如下:

在此次的 Demo 制作中,借用了 Unity Asset Store 的一些免费资源,效果还是不错的

比如下面这个 Free SkyBox,可以呈现一个基础的3D天空场景 

其实还是比较 beautiful 的对不对? 这样的对目前来说其实也够用了

将 Materials 中的 Skybox 拖进 Hierarchy 中即可产生效果,主要是Unity的版本要 > 2019.4.0 


在初步制作的时候,我们需要在基础之上对一些 Bug 进行纠错 (主要是效果展现上的差距和程序上的不完善),最终不断丰富我们的表现效果

要考虑的东西有很多:

⭐️比如如何设计角色移动和攻击方式 (在 Unity 客户端中,可以像我一样利用鼠标响应,点击即立刻前往,点击并拖拽光标能朝着光标拖拽的方向即时丝滑移动。当停止移动并在攻击范围之内,即可点击敌人进行攻击。移动Move() 与 攻击Combat() 的细节逻辑处理也是一个重要的东西,是利用了混合树结合代码逻辑解决的);

⭐️比如死亡的对象要进行销毁,使它不再具有物理意义也要注意不要让死亡的NPC跟随我们的角色移动,避免造成一种混乱的现象。

⭐️比如一个有地势差异的比较大的场景混合各种小场景,如何比较好的处理角色能否移动,这个时候我们就要利用 Bake烘焙 辅助处理,通过控制 Navigation 中 Bake 的属性值来准确控制表现效果,如下图:

NavMesh 与 Bake 具体可以参考下面两篇文章:

Unity | 深入了解NavMeshAgent_米莱虾的博客-CSDN博客_navmeshagent 详解

Unity | Navmesh自动寻路运行报错分析与解决方案_米莱虾的博客-CSDN博客

⭐️比如我们如何将视角绑定在角色身上或者别的想要被绑定的 target 上,这就要用到跟随相机,在 Camera 下挂载 Follow Camera,将 Follow Camera 调整到距离 target 合适的位置上并且与我们的目标绑定(挂载),从而达到一个视角跟随主人公移动的效果,但其实没几行代码...


  
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace RPG.Core
  5. {
  6. public class FollowCamera : MonoBehaviour
  7. {
  8. [ SerializeField] Transform target;
  9. void LateUpdate()
  10. {
  11. transform.position = target.position;
  12. }
  13. }
  14. }

其他一些具体的细节以及优化有机会再和大家分享,下面呈现部分重要的代码

⭐️Fighter.cs (主要是我们角色战斗逻辑的一些处理)


  
  1. using UnityEngine;
  2. using RPG.Movement;
  3. using RPG.Core;
  4. using GameDevTV.Saving;
  5. using RPG.Attributes;
  6. using RPG.Stats;
  7. using System.Collections.Generic;
  8. using GameDevTV.Utils;
  9. using System;
  10. using GameDevTV.Inventories;
  11. namespace RPG.Combat
  12. {
  13. public class Fighter : MonoBehaviour, IAction
  14. {
  15. [ SerializeField] float timeBetweenAttacks = 1f;
  16. [ SerializeField] Transform rightHandTransform = null;
  17. [ SerializeField] Transform leftHandTransform = null;
  18. [ SerializeField] WeaponConfig defaultWeapon = null;
  19. [ SerializeField] float autoAttackRange = 4f;
  20. Health target;
  21. Equipment equipment;
  22. float timeSinceLastAttack = Mathf.Infinity;
  23. WeaponConfig currentWeaponConfig;
  24. LazyValue<Weapon> currentWeapon;
  25. private void Awake() {
  26. currentWeaponConfig = defaultWeapon;
  27. currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
  28. equipment = GetComponent<Equipment>();
  29. if (equipment)
  30. {
  31. equipment.equipmentUpdated += UpdateWeapon;
  32. }
  33. }
  34. private Weapon SetupDefaultWeapon()
  35. {
  36. return AttachWeapon(defaultWeapon);
  37. }
  38. private void Start()
  39. {
  40. currentWeapon.ForceInit();
  41. }
  42. private void Update()
  43. {
  44. timeSinceLastAttack += Time.deltaTime;
  45. if (target == null) return;
  46. if (target.IsDead())
  47. {
  48. target = FindNewTargetInRange();
  49. if (target == null) return;
  50. }
  51. if (!GetIsInRange(target.transform))
  52. {
  53. GetComponent<Mover>().MoveTo(target.transform.position, 1f);
  54. }
  55. else
  56. {
  57. GetComponent<Mover>().Cancel();
  58. AttackBehaviour();
  59. }
  60. }
  61. public void EquipWeapon(WeaponConfig weapon)
  62. {
  63. currentWeaponConfig = weapon;
  64. currentWeapon. value = AttachWeapon(weapon);
  65. }
  66. private void UpdateWeapon()
  67. {
  68. var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
  69. if (weapon == null)
  70. {
  71. EquipWeapon(defaultWeapon);
  72. }
  73. else
  74. {
  75. EquipWeapon(weapon);
  76. }
  77. }
  78. private Weapon AttachWeapon(WeaponConfig weapon)
  79. {
  80. Animator animator = GetComponent<Animator>();
  81. return weapon.Spawn(rightHandTransform, leftHandTransform, animator);
  82. }
  83. public Health GetTarget()
  84. {
  85. return target;
  86. }
  87. public Transform GetHandTransform(bool isRightHand)
  88. {
  89. if (isRightHand)
  90. {
  91. return rightHandTransform;
  92. }
  93. else
  94. {
  95. return leftHandTransform;
  96. }
  97. }
  98. private void AttackBehaviour()
  99. {
  100. transform.LookAt(target.transform);
  101. if (timeSinceLastAttack > timeBetweenAttacks)
  102. {
  103. // This will trigger the Hit() event.
  104. TriggerAttack();
  105. timeSinceLastAttack = 0;
  106. }
  107. }
  108. private Health FindNewTargetInRange()
  109. {
  110. Health best = null;
  111. float bestDistance = Mathf.Infinity;
  112. foreach ( var candidate in FindAllTargetsInRange())
  113. {
  114. float candidateDistance = Vector3.Distance(
  115. transform.position, candidate.transform.position);
  116. if (candidateDistance < bestDistance)
  117. {
  118. best = candidate;
  119. bestDistance = candidateDistance;
  120. }
  121. }
  122. return best;
  123. }
  124. private IEnumerable<Health> FindAllTargetsInRange()
  125. {
  126. RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position,
  127. autoAttackRange, Vector3.up);
  128. foreach ( var hit in raycastHits)
  129. {
  130. Health health = hit.transform.GetComponent<Health>();
  131. if (health == null) continue;
  132. if (health.IsDead()) continue;
  133. if (health.gameObject == gameObject) continue;
  134. yield return health;
  135. }
  136. }
  137. private void TriggerAttack()
  138. {
  139. GetComponent<Animator>().ResetTrigger( "stopAttack");
  140. GetComponent<Animator>().SetTrigger( "attack");
  141. }
  142. // Animation Event
  143. void Hit()
  144. {
  145. if(target == null) { return; }
  146. float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
  147. BaseStats targetBaseStats = target.GetComponent<BaseStats>();
  148. if (targetBaseStats != null)
  149. {
  150. float defence = targetBaseStats.GetStat(Stat.Defence);
  151. damage /= 1 + defence / damage;
  152. }
  153. if (currentWeapon. value != null)
  154. {
  155. currentWeapon. value.OnHit();
  156. }
  157. if (currentWeaponConfig.HasProjectile())
  158. {
  159. currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
  160. }
  161. else
  162. {
  163. target.TakeDamage(gameObject, damage);
  164. }
  165. }
  166. void Shoot()
  167. {
  168. Hit();
  169. }
  170. private bool GetIsInRange(Transform targetTransform)
  171. {
  172. return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange();
  173. }
  174. public bool CanAttack(GameObject combatTarget)
  175. {
  176. if (combatTarget == null) { return false; }
  177. if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) &&
  178. !GetIsInRange(combatTarget.transform))
  179. {
  180. return false;
  181. }
  182. Health targetToTest = combatTarget.GetComponent<Health>();
  183. return targetToTest != null && !targetToTest.IsDead();
  184. }
  185. public void Attack(GameObject combatTarget)
  186. {
  187. GetComponent<ActionScheduler>().StartAction( this);
  188. target = combatTarget.GetComponent<Health>();
  189. }
  190. public void Cancel()
  191. {
  192. StopAttack();
  193. target = null;
  194. GetComponent<Mover>().Cancel();
  195. }
  196. private void StopAttack()
  197. {
  198. GetComponent<Animator>().ResetTrigger( "attack");
  199. GetComponent<Animator>().SetTrigger( "stopAttack");
  200. }
  201. }
  202. }

⭐️PlayerController.cs (主要是我们角色控制逻辑的一些处理,包括角色的自动寻路、和UI的交互、技能、和组件的交互、移动的交互、射线投射...)


  
  1. using RPG.Combat;
  2. using RPG.Movement;
  3. using UnityEngine;
  4. using RPG.Attributes;
  5. using System;
  6. using UnityEngine.EventSystems;
  7. using UnityEngine.AI;
  8. using GameDevTV.Inventories;
  9. namespace RPG.Control
  10. {
  11. public class PlayerController : MonoBehaviour
  12. {
  13. Health health;
  14. ActionStore actionStore;
  15. [ System.Serializable]
  16. struct CursorMapping
  17. {
  18. public CursorType type;
  19. public Texture2D texture;
  20. public Vector2 hotspot;
  21. }
  22. [ SerializeField] CursorMapping[] cursorMappings = null;
  23. [ SerializeField] float maxNavMeshProjectionDistance = 1f;
  24. [ SerializeField] float raycastRadius = 1f;
  25. [ SerializeField] int numberOfAbilities = 6;
  26. bool isDraggingUI = false;
  27. private void Awake() {
  28. health = GetComponent<Health>();
  29. actionStore = GetComponent<ActionStore>();
  30. }
  31. private void Update()
  32. {
  33. if (InteractWithUI()) return;
  34. if (health.IsDead())
  35. {
  36. SetCursor(CursorType.None);
  37. return;
  38. }
  39. UseAbilities();
  40. if (InteractWithComponent()) return;
  41. if (InteractWithMovement()) return;
  42. SetCursor(CursorType.None);
  43. }
  44. private bool InteractWithUI()
  45. {
  46. if (Input.GetMouseButtonUp( 0))
  47. {
  48. isDraggingUI = false;
  49. }
  50. if (EventSystem.current.IsPointerOverGameObject())
  51. {
  52. if (Input.GetMouseButtonDown( 0))
  53. {
  54. isDraggingUI = true;
  55. }
  56. SetCursor(CursorType.UI);
  57. return true;
  58. }
  59. if (isDraggingUI)
  60. {
  61. return true;
  62. }
  63. return false;
  64. }
  65. private void UseAbilities()
  66. {
  67. for ( int i = 0; i < numberOfAbilities; i++)
  68. {
  69. if (Input.GetKeyDown(KeyCode.Alpha1 + i))
  70. {
  71. actionStore.Use(i, gameObject);
  72. }
  73. }
  74. }
  75. private bool InteractWithComponent()
  76. {
  77. RaycastHit[] hits = RaycastAllSorted();
  78. foreach (RaycastHit hit in hits)
  79. {
  80. IRaycastable[] raycastables = hit.transform.GetComponents<IRaycastable>();
  81. foreach (IRaycastable raycastable in raycastables)
  82. {
  83. if (raycastable.HandleRaycast( this))
  84. {
  85. SetCursor(raycastable.GetCursorType());
  86. return true;
  87. }
  88. }
  89. }
  90. return false;
  91. }
  92. RaycastHit[] RaycastAllSorted()
  93. {
  94. RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius);
  95. float[] distances = new float[hits.Length];
  96. for ( int i = 0; i < hits.Length; i++)
  97. {
  98. distances[i] = hits[i].distance;
  99. }
  100. Array.Sort(distances, hits);
  101. return hits;
  102. }
  103. private bool InteractWithMovement()
  104. {
  105. Vector3 target;
  106. bool hasHit = RaycastNavMesh( out target);
  107. if (hasHit)
  108. {
  109. if (!GetComponent<Mover>().CanMoveTo(target)) return false;
  110. if (Input.GetMouseButton( 0))
  111. {
  112. GetComponent<Mover>().StartMoveAction(target, 1f);
  113. }
  114. SetCursor(CursorType.Movement);
  115. return true;
  116. }
  117. return false;
  118. }
  119. private bool RaycastNavMesh(out Vector3 target)
  120. {
  121. target = new Vector3();
  122. RaycastHit hit;
  123. bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
  124. if (!hasHit) return false;
  125. NavMeshHit navMeshHit;
  126. bool hasCastToNavMesh = NavMesh.SamplePosition(
  127. hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);
  128. if (!hasCastToNavMesh) return false;
  129. target = navMeshHit.position;
  130. return true;
  131. }
  132. private void SetCursor(CursorType type)
  133. {
  134. CursorMapping mapping = GetCursorMapping(type);
  135. Cursor.SetCursor(mapping.texture, mapping.hotspot, CursorMode.Auto);
  136. }
  137. private CursorMapping GetCursorMapping(CursorType type)
  138. {
  139. foreach (CursorMapping mapping in cursorMappings)
  140. {
  141. if (mapping.type == type)
  142. {
  143. return mapping;
  144. }
  145. }
  146. return cursorMappings[ 0];
  147. }
  148. public static Ray GetMouseRay()
  149. {
  150. return Camera.main.ScreenPointToRay(Input.mousePosition);
  151. }
  152. }
  153. }


转载:https://blog.csdn.net/Luoxiaobaia/article/details/125865286
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场