❥ 由于大三期末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 currentWeapon;private void Awake() {currentWeaponConfig = defaultWeapon;currentWeapon = new LazyValue(SetupDefaultWeapon);equipment = GetComponent();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().MoveTo(target.transform.position, 1f);}else{GetComponent().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();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 FindAllTargetsInRange(){RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position,autoAttackRange, Vector3.up);foreach (var hit in raycastHits){Health health = hit.transform.GetComponent();if (health == null) continue;if (health.IsDead()) continue;if (health.gameObject == gameObject) continue;yield return health;}}private void TriggerAttack(){GetComponent().ResetTrigger("stopAttack");GetComponent().SetTrigger("attack");}// Animation Eventvoid Hit(){if(target == null) { return; }float damage = GetComponent().GetStat(Stat.Damage);BaseStats targetBaseStats = target.GetComponent();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().CanMoveTo(combatTarget.transform.position) &&!GetIsInRange(combatTarget.transform)) {return false; }Health targetToTest = combatTarget.GetComponent();return targetToTest != null && !targetToTest.IsDead();}public void Attack(GameObject combatTarget){GetComponent().StartAction(this);target = combatTarget.GetComponent();}public void Cancel(){StopAttack();target = null;GetComponent().Cancel();}private void StopAttack(){GetComponent().ResetTrigger("attack");GetComponent().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();actionStore = GetComponent();}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();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().CanMoveTo(target)) return false;if (Input.GetMouseButton(0)){GetComponent().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);}}}