当前位置:首页 » 《随便一记》 » 正文

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

21 人参与  2022年07月22日 14:42  分类 : 《随便一记》  评论

点击全文阅读


❥ 由于大三期末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);        }    }}


点击全文阅读


本文链接:http://zhangshiyu.com/post/43711.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1