❥ 由于大三期末N个大课设轮番轰炸,停下了手里的好多事。
故时隔一月余久,我又去继续催化RPG小游戏Demo了。
❥ 此次短暂优化之后,基本的战斗系统、对话系统和背包系统已具雏形,
画面渲染也较为惹眼舒适了。
❥ 不知不觉,实习已近一月,在mentor的指导和同事的帮助下,成功接手并完成了一些开发业务单,明天开始为期两周左右的GameJam了,暂且搁置这一Demo探索。
❥ 等新鲜的-科技风-元素塔防出炉之后,再来和大家分享可以试玩的作品。
❥ 先有蛋还是先有鸡?反正先发B站才方便插视频URL hh~
⭐️部分场景展示:

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

在此次的 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 Event
- void 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);
- }
- }
- }