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


    ❥ 由于大三期末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 currentWeapon;
    25. private void Awake() {
    26. currentWeaponConfig = defaultWeapon;
    27. currentWeapon = new LazyValue(SetupDefaultWeapon);
    28. equipment = GetComponent();
    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().MoveTo(target.transform.position, 1f);
    54. }
    55. else
    56. {
    57. GetComponent().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();
    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 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();
    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().ResetTrigger("stopAttack");
    140. GetComponent().SetTrigger("attack");
    141. }
    142. // Animation Event
    143. void Hit()
    144. {
    145. if(target == null) { return; }
    146. float damage = GetComponent().GetStat(Stat.Damage);
    147. BaseStats targetBaseStats = target.GetComponent();
    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().CanMoveTo(combatTarget.transform.position) &&
    178. !GetIsInRange(combatTarget.transform))
    179. {
    180. return false;
    181. }
    182. Health targetToTest = combatTarget.GetComponent();
    183. return targetToTest != null && !targetToTest.IsDead();
    184. }
    185. public void Attack(GameObject combatTarget)
    186. {
    187. GetComponent().StartAction(this);
    188. target = combatTarget.GetComponent();
    189. }
    190. public void Cancel()
    191. {
    192. StopAttack();
    193. target = null;
    194. GetComponent().Cancel();
    195. }
    196. private void StopAttack()
    197. {
    198. GetComponent().ResetTrigger("attack");
    199. GetComponent().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();
    29. actionStore = GetComponent();
    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();
    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().CanMoveTo(target)) return false;
    110. if (Input.GetMouseButton(0))
    111. {
    112. GetComponent().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. }

  • 相关阅读:
    [Rust笔记] 代码写明 Rust 中的泛型型变
    实验七 团队作业4:团队项目需求建模与系统设计
    三十多年前 钱学森给虚拟现实技术取了一个名
    Maven的生命周期
    [LeetCode304周赛] 两道关于基环树的题 6134. 找到离给定两个节点最近的节点,6135. 图中的最长环
    哈工大操作系统实验环境安装
    chatgpt赋能python:Python操作表格的全面指南
    剑指 Offer 10- II. 青蛙跳台阶问题【力扣】
    opencv 形态学转换
    Spark实现二次排序
  • 原文地址:https://blog.csdn.net/Luoxiaobaia/article/details/125865286