❥ 由于大三期末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 合适的位置上并且与我们的目标绑定(挂载),从而达到一个视角跟随主人公移动的效果,但其实没几行代码...
using System.Collections;using System.Collections.Generic;using UnityEngine;namespace RPG.Core{ public class FollowCamera : MonoBehaviour { [SerializeField] Transform target; void LateUpdate() { transform.position = target.position; } }}
其他一些具体的细节以及优化有机会再和大家分享,下面呈现部分重要的代码
⭐️Fighter.cs (主要是我们角色战斗逻辑的一些处理)
using UnityEngine;using RPG.Movement;using RPG.Core;using GameDevTV.Saving;using RPG.Attributes;using RPG.Stats;using System.Collections.Generic;using GameDevTV.Utils;using System;using GameDevTV.Inventories;namespace RPG.Combat{ public class Fighter : MonoBehaviour, IAction { [SerializeField] float timeBetweenAttacks = 1f; [SerializeField] Transform rightHandTransform = null; [SerializeField] Transform leftHandTransform = null; [SerializeField] WeaponConfig defaultWeapon = null; [SerializeField] float autoAttackRange = 4f; Health target; Equipment equipment; float timeSinceLastAttack = Mathf.Infinity; WeaponConfig currentWeaponConfig; LazyValue<Weapon> currentWeapon; private void Awake() { currentWeaponConfig = defaultWeapon; currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon); equipment = GetComponent<Equipment>(); if (equipment) { equipment.equipmentUpdated += UpdateWeapon; } } private Weapon SetupDefaultWeapon() { return AttachWeapon(defaultWeapon); } private void Start() { currentWeapon.ForceInit(); } private void Update() { timeSinceLastAttack += Time.deltaTime; if (target == null) return; if (target.IsDead()) { target = FindNewTargetInRange(); if (target == null) return; } if (!GetIsInRange(target.transform)) { GetComponent<Mover>().MoveTo(target.transform.position, 1f); } else { GetComponent<Mover>().Cancel(); AttackBehaviour(); } } public void EquipWeapon(WeaponConfig weapon) { currentWeaponConfig = weapon; currentWeapon.value = AttachWeapon(weapon); } private void UpdateWeapon() { var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig; if (weapon == null) { EquipWeapon(defaultWeapon); } else { EquipWeapon(weapon); } } private Weapon AttachWeapon(WeaponConfig weapon) { Animator animator = GetComponent<Animator>(); return weapon.Spawn(rightHandTransform, leftHandTransform, animator); } public Health GetTarget() { return target; } public Transform GetHandTransform(bool isRightHand) { if (isRightHand) { return rightHandTransform; } else { return leftHandTransform; } } private void AttackBehaviour() { transform.LookAt(target.transform); if (timeSinceLastAttack > timeBetweenAttacks) { // This will trigger the Hit() event. TriggerAttack(); timeSinceLastAttack = 0; } } private Health FindNewTargetInRange() { Health best = null; float bestDistance = Mathf.Infinity; foreach (var candidate in FindAllTargetsInRange()) { float candidateDistance = Vector3.Distance( transform.position, candidate.transform.position); if (candidateDistance < bestDistance) { best = candidate; bestDistance = candidateDistance; } } return best; } private IEnumerable<Health> FindAllTargetsInRange() { RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position, autoAttackRange, Vector3.up); foreach (var hit in raycastHits) { Health health = hit.transform.GetComponent<Health>(); if (health == null) continue; if (health.IsDead()) continue; if (health.gameObject == gameObject) continue; yield return health; } } private void TriggerAttack() { GetComponent<Animator>().ResetTrigger("stopAttack"); GetComponent<Animator>().SetTrigger("attack"); } // Animation Event void Hit() { if(target == null) { return; } float damage = GetComponent<BaseStats>().GetStat(Stat.Damage); BaseStats targetBaseStats = target.GetComponent<BaseStats>(); if (targetBaseStats != null) { float defence = targetBaseStats.GetStat(Stat.Defence); damage /= 1 + defence / damage; } if (currentWeapon.value != null) { currentWeapon.value.OnHit(); } if (currentWeaponConfig.HasProjectile()) { currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage); } else { target.TakeDamage(gameObject, damage); } } void Shoot() { Hit(); } private bool GetIsInRange(Transform targetTransform) { return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange(); } public bool CanAttack(GameObject combatTarget) { if (combatTarget == null) { return false; } if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) && !GetIsInRange(combatTarget.transform)) { return false; } Health targetToTest = combatTarget.GetComponent<Health>(); return targetToTest != null && !targetToTest.IsDead(); } public void Attack(GameObject combatTarget) { GetComponent<ActionScheduler>().StartAction(this); target = combatTarget.GetComponent<Health>(); } public void Cancel() { StopAttack(); target = null; GetComponent<Mover>().Cancel(); } private void StopAttack() { GetComponent<Animator>().ResetTrigger("attack"); GetComponent<Animator>().SetTrigger("stopAttack"); } }}
⭐️PlayerController.cs (主要是我们角色控制逻辑的一些处理,包括角色的自动寻路、和UI的交互、技能、和组件的交互、移动的交互、射线投射...)
using RPG.Combat;using RPG.Movement;using UnityEngine;using RPG.Attributes;using System;using UnityEngine.EventSystems;using UnityEngine.AI;using GameDevTV.Inventories;namespace RPG.Control{ public class PlayerController : MonoBehaviour { Health health; ActionStore actionStore; [System.Serializable] struct CursorMapping { public CursorType type; public Texture2D texture; public Vector2 hotspot; } [SerializeField] CursorMapping[] cursorMappings = null; [SerializeField] float maxNavMeshProjectionDistance = 1f; [SerializeField] float raycastRadius = 1f; [SerializeField] int numberOfAbilities = 6; bool isDraggingUI = false; private void Awake() { health = GetComponent<Health>(); actionStore = GetComponent<ActionStore>(); } private void Update() { if (InteractWithUI()) return; if (health.IsDead()) { SetCursor(CursorType.None); return; } UseAbilities(); if (InteractWithComponent()) return; if (InteractWithMovement()) return; SetCursor(CursorType.None); } private bool InteractWithUI() { if (Input.GetMouseButtonUp(0)) { isDraggingUI = false; } if (EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { isDraggingUI = true; } SetCursor(CursorType.UI); return true; } if (isDraggingUI) { return true; } return false; } private void UseAbilities() { for (int i = 0; i < numberOfAbilities; i++) { if (Input.GetKeyDown(KeyCode.Alpha1 + i)) { actionStore.Use(i, gameObject); } } } private bool InteractWithComponent() { RaycastHit[] hits = RaycastAllSorted(); foreach (RaycastHit hit in hits) { IRaycastable[] raycastables = hit.transform.GetComponents<IRaycastable>(); foreach (IRaycastable raycastable in raycastables) { if (raycastable.HandleRaycast(this)) { SetCursor(raycastable.GetCursorType()); return true; } } } return false; } RaycastHit[] RaycastAllSorted() { RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius); float[] distances = new float[hits.Length]; for (int i = 0; i < hits.Length; i++) { distances[i] = hits[i].distance; } Array.Sort(distances, hits); return hits; } private bool InteractWithMovement() { Vector3 target; bool hasHit = RaycastNavMesh(out target); if (hasHit) { if (!GetComponent<Mover>().CanMoveTo(target)) return false; if (Input.GetMouseButton(0)) { GetComponent<Mover>().StartMoveAction(target, 1f); } SetCursor(CursorType.Movement); return true; } return false; } private bool RaycastNavMesh(out Vector3 target) { target = new Vector3(); RaycastHit hit; bool hasHit = Physics.Raycast(GetMouseRay(), out hit); if (!hasHit) return false; NavMeshHit navMeshHit; bool hasCastToNavMesh = NavMesh.SamplePosition( hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas); if (!hasCastToNavMesh) return false; target = navMeshHit.position; return true; } private void SetCursor(CursorType type) { CursorMapping mapping = GetCursorMapping(type); Cursor.SetCursor(mapping.texture, mapping.hotspot, CursorMode.Auto); } private CursorMapping GetCursorMapping(CursorType type) { foreach (CursorMapping mapping in cursorMappings) { if (mapping.type == type) { return mapping; } } return cursorMappings[0]; } public static Ray GetMouseRay() { return Camera.main.ScreenPointToRay(Input.mousePosition); } }}